improvements

pull/150/head
Max Leiter 3 years ago
parent 6917b813b5
commit b488e3fdd6

@ -3,5 +3,6 @@
"trailingComma": "none",
"singleQuote": false,
"printWidth": 80,
"useTabs": true
"useTabs": true,
"plugins": ["prettier-plugin-tailwindcss"]
}

@ -11,6 +11,10 @@ const nextConfig = {
{
source: "/file/raw/:id",
destination: `/api/raw/:id`
},
{
source: "/signout",
destination: `/api/auth/signout`
}
]
},

@ -16,6 +16,7 @@
"@next-auth/prisma-adapter": "^1.0.6",
"@next/eslint-plugin-next": "13.4.4-canary.0",
"@prisma/client": "^4.14.1",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-navigation-menu": "^1.1.3",
@ -80,6 +81,7 @@
"postcss-nested": "^6.0.1",
"postcss-preset-env": "^8.4.1",
"prettier": "2.8.7",
"prettier-plugin-tailwindcss": "^0.3.0",
"prisma": "^4.12.0",
"tailwindcss": "^3.3.2",
"typescript": "5.0.4",

@ -5,6 +5,7 @@ specifiers:
'@next/bundle-analyzer': 13.4.5-canary.2
'@next/eslint-plugin-next': 13.4.4-canary.0
'@prisma/client': ^4.14.1
'@radix-ui/react-alert-dialog': ^1.0.4
'@radix-ui/react-dialog': ^1.0.3
'@radix-ui/react-dropdown-menu': ^2.0.4
'@radix-ui/react-navigation-menu': ^1.1.3
@ -51,6 +52,7 @@ specifiers:
postcss-nested: ^6.0.1
postcss-preset-env: ^8.4.1
prettier: 2.8.7
prettier-plugin-tailwindcss: ^0.3.0
prisma: ^4.12.0
react: 18.2.0
react-cookie: ^4.1.1
@ -76,6 +78,7 @@ dependencies:
'@next-auth/prisma-adapter': 1.0.6_afrt2cjoubnivixcsmhfowwetm
'@next/eslint-plugin-next': 13.4.4-canary.0
'@prisma/client': 4.14.1_prisma@4.12.0
'@radix-ui/react-alert-dialog': 1.0.4_jlpdlnmgz6kfq77dq7ypembf5a
'@radix-ui/react-dialog': 1.0.3_7geocmx3442bocz6x6zwg5lxxi
'@radix-ui/react-dropdown-menu': 2.0.4_7geocmx3442bocz6x6zwg5lxxi
'@radix-ui/react-navigation-menu': 1.1.3_jlpdlnmgz6kfq77dq7ypembf5a
@ -143,6 +146,7 @@ devDependencies:
postcss-nested: 6.0.1_postcss@8.4.21
postcss-preset-env: 8.4.1_postcss@8.4.21
prettier: 2.8.7
prettier-plugin-tailwindcss: 0.3.0_prettier@2.8.7
prisma: 4.12.0
tailwindcss: 3.3.2
typescript: 5.0.4
@ -1352,6 +1356,32 @@ packages:
'@babel/runtime': 7.21.0
dev: false
/@radix-ui/react-alert-dialog/1.0.4_jlpdlnmgz6kfq77dq7ypembf5a:
resolution: {integrity: sha512-jbfBCRlKYlhbitueOAv7z74PXYeIQmWpKwm3jllsdkw7fGWNkxqP3v0nY9WmOzcPqpQuoorNtvViBgL46n5gVg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.21.0
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1_yuz6bkerhkjfjuf6zeb7j6ybc4
'@radix-ui/react-context': 1.0.1_yuz6bkerhkjfjuf6zeb7j6ybc4
'@radix-ui/react-dialog': 1.0.4_jlpdlnmgz6kfq77dq7ypembf5a
'@radix-ui/react-primitive': 1.0.3_jlpdlnmgz6kfq77dq7ypembf5a
'@radix-ui/react-slot': 1.0.2_yuz6bkerhkjfjuf6zeb7j6ybc4
'@types/react': 18.0.35
'@types/react-dom': 18.0.11
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/@radix-ui/react-arrow/1.0.2_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==}
peerDependencies:
@ -1503,6 +1533,40 @@ packages:
- '@types/react'
dev: false
/@radix-ui/react-dialog/1.0.4_jlpdlnmgz6kfq77dq7ypembf5a:
resolution: {integrity: sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.21.0
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1_yuz6bkerhkjfjuf6zeb7j6ybc4
'@radix-ui/react-context': 1.0.1_yuz6bkerhkjfjuf6zeb7j6ybc4
'@radix-ui/react-dismissable-layer': 1.0.4_jlpdlnmgz6kfq77dq7ypembf5a
'@radix-ui/react-focus-guards': 1.0.1_yuz6bkerhkjfjuf6zeb7j6ybc4
'@radix-ui/react-focus-scope': 1.0.3_jlpdlnmgz6kfq77dq7ypembf5a
'@radix-ui/react-id': 1.0.1_yuz6bkerhkjfjuf6zeb7j6ybc4
'@radix-ui/react-portal': 1.0.3_jlpdlnmgz6kfq77dq7ypembf5a
'@radix-ui/react-presence': 1.0.1_jlpdlnmgz6kfq77dq7ypembf5a
'@radix-ui/react-primitive': 1.0.3_jlpdlnmgz6kfq77dq7ypembf5a
'@radix-ui/react-slot': 1.0.2_yuz6bkerhkjfjuf6zeb7j6ybc4
'@radix-ui/react-use-controllable-state': 1.0.1_yuz6bkerhkjfjuf6zeb7j6ybc4
'@types/react': 18.0.35
'@types/react-dom': 18.0.11
aria-hidden: 1.2.2_yuz6bkerhkjfjuf6zeb7j6ybc4
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-remove-scroll: 2.5.5_yuz6bkerhkjfjuf6zeb7j6ybc4
dev: false
/@radix-ui/react-direction/1.0.0_react@18.2.0:
resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==}
peerDependencies:
@ -1612,6 +1676,20 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-focus-guards/1.0.1_yuz6bkerhkjfjuf6zeb7j6ybc4:
resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.21.0
'@types/react': 18.0.35
react: 18.2.0
dev: false
/@radix-ui/react-focus-scope/1.0.0_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==}
peerDependencies:
@ -1640,6 +1718,29 @@ packages:
react-dom: 18.2.0_react@18.2.0
dev: false
/@radix-ui/react-focus-scope/1.0.3_jlpdlnmgz6kfq77dq7ypembf5a:
resolution: {integrity: sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.21.0
'@radix-ui/react-compose-refs': 1.0.1_yuz6bkerhkjfjuf6zeb7j6ybc4
'@radix-ui/react-primitive': 1.0.3_jlpdlnmgz6kfq77dq7ypembf5a
'@radix-ui/react-use-callback-ref': 1.0.1_yuz6bkerhkjfjuf6zeb7j6ybc4
'@types/react': 18.0.35
'@types/react-dom': 18.0.11
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/@radix-ui/react-id/1.0.0_react@18.2.0:
resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==}
peerDependencies:
@ -1805,6 +1906,27 @@ packages:
react-dom: 18.2.0_react@18.2.0
dev: false
/@radix-ui/react-portal/1.0.3_jlpdlnmgz6kfq77dq7ypembf5a:
resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.21.0
'@radix-ui/react-primitive': 1.0.3_jlpdlnmgz6kfq77dq7ypembf5a
'@types/react': 18.0.35
'@types/react-dom': 18.0.11
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/@radix-ui/react-presence/1.0.0_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==}
peerDependencies:
@ -7209,6 +7331,61 @@ packages:
engines: {node: '>= 0.8.0'}
dev: true
/prettier-plugin-tailwindcss/0.3.0_prettier@2.8.7:
resolution: {integrity: sha512-009/Xqdy7UmkcTBpwlq7jsViDqXAYSOMLDrHAdTMlVZOrKfM2o9Ci7EMWTMZ7SkKBFTG04UM9F9iM2+4i6boDA==}
engines: {node: '>=12.17.0'}
peerDependencies:
'@ianvs/prettier-plugin-sort-imports': '*'
'@prettier/plugin-pug': '*'
'@shopify/prettier-plugin-liquid': '*'
'@shufo/prettier-plugin-blade': '*'
'@trivago/prettier-plugin-sort-imports': '*'
prettier: '>=2.2.0'
prettier-plugin-astro: '*'
prettier-plugin-css-order: '*'
prettier-plugin-import-sort: '*'
prettier-plugin-jsdoc: '*'
prettier-plugin-marko: '*'
prettier-plugin-organize-attributes: '*'
prettier-plugin-organize-imports: '*'
prettier-plugin-style-order: '*'
prettier-plugin-svelte: '*'
prettier-plugin-twig-melody: '*'
peerDependenciesMeta:
'@ianvs/prettier-plugin-sort-imports':
optional: true
'@prettier/plugin-pug':
optional: true
'@shopify/prettier-plugin-liquid':
optional: true
'@shufo/prettier-plugin-blade':
optional: true
'@trivago/prettier-plugin-sort-imports':
optional: true
prettier-plugin-astro:
optional: true
prettier-plugin-css-order:
optional: true
prettier-plugin-import-sort:
optional: true
prettier-plugin-jsdoc:
optional: true
prettier-plugin-marko:
optional: true
prettier-plugin-organize-attributes:
optional: true
prettier-plugin-organize-imports:
optional: true
prettier-plugin-style-order:
optional: true
prettier-plugin-svelte:
optional: true
prettier-plugin-twig-melody:
optional: true
dependencies:
prettier: 2.8.7
dev: true
/prettier/2.8.7:
resolution: {integrity: sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==}
engines: {node: '>=10.13.0'}

@ -12,6 +12,7 @@ import { useRouter } from "next/navigation"
import Note from "@components/note"
import { ErrorQueryParamsHandler } from "./query-handler"
import { AuthProviders } from "@lib/server/auth-props"
import { TypographyH1 } from "@components/typography"
function Auth({
page,
@ -75,7 +76,7 @@ function Auth({
<ErrorQueryParamsHandler />
<div className={styles.form}>
<div className={styles.formContentSpace}>
<h1>Sign {signText}</h1>
<h1 className="text-3xl font-bold">Sign {signText}</h1>
</div>
<form onSubmit={handleSubmit}>
<div className={styles.formGroup}>

@ -2,6 +2,7 @@ import { Input } from "@components/input"
import { ChangeEvent } from "react"
import styles from "../post.module.css"
import clsx from "clsx"
type props = {
onChange: (e: ChangeEvent<HTMLInputElement>) => void
@ -10,7 +11,7 @@ type props = {
function Description({ onChange, description }: props) {
return (
<div className={styles.description}>
<div className={clsx(styles.description, "pb-4")}>
<Input
value={description || ""}
onChange={onChange}

@ -8,18 +8,6 @@
margin-top: var(--gap-double);
}
.dropzone {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 18px;
border-radius: 2px;
border: 2px dashed var(--border);
outline: none;
cursor: pointer;
}
.dropzone:focus {
border-color: var(--gray);
}

@ -82,12 +82,11 @@ function FileDropzone({ setDocs }: { setDocs: (docs: Document[]) => void }) {
return (
<div className={styles.container}>
<div {...getRootProps()} className={styles.dropzone}>
<div {...getRootProps()}>
<input {...getInputProps()} />
{!isDragActive && (
<p className="text-muted-foreground text-center text-sm">
Drag some files here, or <span className={styles.verb} /> to select
files
<p className="text-muted-foreground text-sm cursor-pointer select-none border-2 border-dashed rounded-md p-4">
Drag and drop files here, or click to select
</p>
)}
{isDragActive && <p>Release to drop the files here</p>}

@ -9,7 +9,7 @@ import {
import { RefObject, useMemo } from "react"
import styles from "./formatting-icons.module.css"
import { TextareaMarkdownRef } from "textarea-markdown-editor"
import Tooltip from "@components/tooltip"
import { Tooltip } from "@components/tooltip"
import { Button } from "@components/button"
import clsx from "clsx"
import React from "react"

@ -4,6 +4,7 @@ import { Button } from "@components/button"
import { Input } from "@components/input"
import DocumentTabs from "src/app/(drift)/(posts)/components/document-tabs"
import { Trash } from "react-feather"
import { Card, CardContent, CardHeader } from "@components/card"
type Props = {
title?: string
@ -49,8 +50,8 @@ function Document({
)
return (
<>
<div className={styles.card}>
<Card className="min-h-[512px]">
<CardHeader>
<div className={styles.fileNameContainer}>
<Input
placeholder="MyFile.md"
@ -70,21 +71,21 @@ function Document({
</Button>
)}
</div>
<div className={styles.documentContainer}>
<DocumentTabs
isEditing={true}
defaultTab={defaultTab}
handleOnContentChange={handleOnContentChange}
// TODO: solve types
// @ts-expect-error Type 'HTMLDivElement' is missing the following properties from type 'HTMLTextAreaElement': autocomplete, cols, defaultValue, dirName, and 26 more
onPaste={onPaste}
title={title}
>
{content}
</DocumentTabs>
</div>
</div>
</>
</CardHeader>
<CardContent>
<DocumentTabs
isEditing={true}
defaultTab={defaultTab}
handleOnContentChange={handleOnContentChange}
// TODO: solve types
// @ts-expect-error Type 'HTMLDivElement' is missing the following properties from type 'HTMLTextAreaElement': autocomplete, cols, defaultValue, dirName, and 26 more
onPaste={onPaste}
title={title}
>
{content}
</DocumentTabs>
</CardContent>
</Card>
)
}

@ -7,7 +7,7 @@ import styles from "./post.module.css"
import EditDocumentList from "./edit-document-list"
import { ChangeEvent } from "react"
import getTitleForPostCopy from "src/app/lib/get-title-for-post-copy"
import Description from "./description"
// import Description from "./description"
import { PostWithFiles } from "@lib/server/prisma"
import PasswordModal from "../../../../components/password-modal"
import Title from "./title"
@ -19,16 +19,11 @@ import { fetchWithUser } from "src/app/lib/fetch-with-user"
import dynamic from "next/dynamic"
import ButtonDropdown from "@components/button-dropdown"
import clsx from "clsx"
import { Spinner } from "@components/spinner"
const DatePicker = dynamic(() => import("react-datepicker"), {
ssr: false,
loading: () => (
<Input
placeholder="Won't expire"
width="100%"
height="40px"
/>
)
loading: () => <Input placeholder="Won't expire" width="100%" height="40px" />
})
const emptyDoc = {
@ -250,8 +245,7 @@ function Post({
return (
<div className={styles.root}>
<Title title={title} onChange={onChangeTitle} />
<Description description={description} onChange={onChangeDescription} />
<FileDropzone setDocs={uploadDocs} />
{/* <Description description={description} onChange={onChangeDescription} /> */}
<EditDocumentList
onPaste={onPaste}
docs={docs}
@ -259,6 +253,8 @@ function Post({
updateDocContent={updateDocContent}
removeDoc={removeDoc}
/>
<FileDropzone setDocs={uploadDocs} />
<div className={styles.buttons}>
<Button
onClick={() => {
@ -308,7 +304,7 @@ function Post({
)}
onClick={() => onSubmit("unlisted")}
>
Create Unlisted
{isSubmitting ? <Spinner /> : "Create Unlisted"}
</span>
<span
className={clsx("w-full cursor-pointer")}

@ -2,7 +2,7 @@
padding-bottom: 200px;
display: flex;
flex-direction: column;
gap: var(--gap);
gap: var(--gap-half);
}
.buttons {

@ -21,7 +21,7 @@ type props = {
function Title({ onChange, title }: props) {
return (
<div>
<div className="pt-4">
<Input
placeholder={placeholder}
value={title}

@ -1,9 +1,18 @@
import { getMetadata } from "src/app/lib/metadata"
import NewPost from "src/app/(drift)/(posts)/new/components/new"
import "./components/react-datepicker.css"
import { PageTitle } from "@components/page-title"
import { PageWrapper } from "@components/page-wrapper"
export default function New() {
return <NewPost />
return (
<>
<PageTitle>New Post</PageTitle>
<PageWrapper>
<NewPost />
</PageWrapper>
</>
)
}
export const metadata = getMetadata({

@ -48,7 +48,7 @@ export const PostButtons = ({
return (
<span className={styles.buttons}>
<ButtonGroup verticalIfMobile>
<Button variant={"secondary"} onClick={editACopy}>
<Button variant={"secondary"} onClick={editACopy} className="border-r">
Edit a Copy
</Button>
{parentId && (
@ -56,7 +56,7 @@ export const PostButtons = ({
View Parent
</Button>
)}
<Button variant={"secondary"} onClick={downloadClick}>
<Button variant={"secondary"} onClick={downloadClick} className="border-r">
Download as ZIP Archive
</Button>
<FileDropdown loading={loading} files={files || []} />

@ -17,11 +17,7 @@ export const PostTitle = ({ post, loading }: TitleProps) => {
const displayName = author?.displayName
return (
<span className={styles.title}>
<h1
style={{
fontSize: "1.175rem"
}}
>
<h1 className="text-3xl font-bold">
{title}{" "}
<span style={{ color: "var(--gray)" }}>
by {/* <Link colored href={`/author/${authorId}`}> */}

@ -1,7 +1,7 @@
import { Button } from "@components/button"
import ButtonGroup from "@components/button-group"
import Skeleton from "@components/skeleton"
import Tooltip from "@components/tooltip"
import { Tooltip } from "@components/tooltip"
import DocumentTabs from "src/app/(drift)/(posts)/components/document-tabs"
import Link from "next/link"
import { memo } from "react"
@ -46,7 +46,7 @@ const DownloadButtons = ({
aria-label="Download"
style={{ border: "none", background: "transparent" }}
>
<Download color="var(--fg)" className="mr-2 h-4 w-4" />
<Download color="var(--fg)" className="w-4 h-4" />
</Button>
</Link>
</Tooltip>
@ -54,10 +54,11 @@ const DownloadButtons = ({
<Tooltip content="Open raw in new tab">
<Link href={rawLink || ""} target="_blank" rel="noopener noreferrer">
<Button
iconLeft={<ExternalLink color="var(--fg)" />}
aria-label="Open raw file in new tab"
style={{ border: "none", background: "transparent" }}
/>
>
<ExternalLink color="var(--fg)" className="w-4 h-4" />
</Button>
</Link>
</Tooltip>
) : null}
@ -65,10 +66,11 @@ const DownloadButtons = ({
<Tooltip content="Open as webpage">
<Link href={siteLink || ""} target="_blank" rel="noopener noreferrer">
<Button
iconLeft={<Globe color="var(--fg)" />}
aria-label="Open as webpage"
style={{ border: "none", background: "transparent" }}
/>
>
<Globe color="var(--fg)" className="w-4 h-4" />
</Button>
</Link>
</Tooltip>
) : null}

@ -28,11 +28,7 @@ export default async function PostLayout({
{post.visibility !== "protected" && <PostButtons post={clientPost} />}
{post.visibility !== "protected" && <PostTitle post={clientPost} />}
</div>
{post.description && (
<div>
<p>{post.description}</p>
</div>
)}
{/* {post.description && <p className="pb-4 text-lg">{post.description}</p>} */}
<ScrollToTop />
{children}
</div>

@ -1,12 +1,13 @@
import { TypographyH1, TypographyH2 } from "@components/typography"
import { PostTable, UserTable } from "./components/tables"
export default function AdminLoading() {
return (
<div>
<h1>Admin</h1>
<h2>Users</h2>
<TypographyH1>Admin</TypographyH1>
<TypographyH2>Users</TypographyH2>
<UserTable />
<h2>Posts</h2>
<TypographyH2>Posts</TypographyH2>
<PostTable />
</div>
)

@ -6,6 +6,7 @@ import {
ServerPostWithFiles
} from "@lib/server/prisma"
import { PostTable, UserTable } from "./components/tables"
import { TypographyH1, TypographyH2 } from "@components/typography"
export default async function AdminPage() {
const usersPromise = getAllUsers({
@ -44,11 +45,11 @@ export default async function AdminPage() {
return (
<div>
<h1>Admin</h1>
<h2>Users</h2>
<TypographyH1>Admin</TypographyH1>
<TypographyH2>Users</TypographyH2>
{/* @ts-expect-error Type 'unknown' is not assignable to type */}
<UserTable users={serializedUsers as unknown} />
<h2>Posts</h2>
<TypographyH2>Posts</TypographyH2>
<PostTable posts={serializedPosts} />
</div>
)

@ -1,4 +1,5 @@
import PostList from "@components/post-list"
import { TypographyH1 } from "@components/typography"
import {
getPostsByUser,
getUserById,
@ -59,7 +60,7 @@ export default async function UserPage({
justifyContent: "space-between"
}}
>
<h1>Public posts by {user?.displayName || "Anonymous"}</h1>
<TypographyH1>Public posts by {user?.displayName || "Anonymous"}</TypographyH1>
<Avatar />
</div>
<Suspense fallback={<PostList hideSearch skeleton initialPosts={[]} />}>

@ -6,6 +6,7 @@ import Header from "@components/header"
import { Inter } from "next/font/google"
import { getMetadata } from "src/app/lib/metadata"
import dynamic from "next/dynamic"
import clsx from "clsx"
const inter = Inter({ subsets: ["latin"], variable: "--inter-font" })
const CmdK = dynamic(() => import("@components/cmdk"), { ssr: false })
@ -17,14 +18,14 @@ export default async function RootLayout({
}) {
return (
// suppressHydrationWarning is required because of next-themes
<html lang="en" className={inter.variable} suppressHydrationWarning>
<html lang="en" className={clsx(inter.variable, "mx-auto w-[var(--main-content)]")} suppressHydrationWarning>
<body>
<Toasts />
<Providers>
<Layout>
<CmdK />
<Header />
<main className="w-[var(--main-content)] mx-auto mt-4">{children}</main>
<main>{children}</main>
</Layout>
</Providers>
</body>

@ -1,7 +1,15 @@
"use client"
import { PageTitle } from "@components/page-title"
import { PageWrapper } from "@components/page-wrapper"
import PostList from "@components/post-list"
export default function Loading() {
return <PostList skeleton={true} initialPosts={[]} />
return (
<>
<PageTitle>Your Posts</PageTitle>
<PageWrapper></PageWrapper>
<PostList skeleton={true} initialPosts={[]} />
</>
)
}

@ -5,6 +5,8 @@ import { Suspense } from "react"
import ErrorBoundary from "@components/error/fallback"
import { getMetadata } from "src/app/lib/metadata"
import { redirect } from "next/navigation"
import { PageTitle } from "@components/page-title"
import { PageWrapper } from "@components/page-wrapper"
export default async function Mine() {
const userId = (await getCurrentUser())?.id
@ -16,16 +18,20 @@ export default async function Mine() {
const posts = (await getPostsByUser(userId, true)).map(serverPostToClientPost)
return (
<ErrorBoundary>
<Suspense fallback={<PostList skeleton={true} initialPosts={[]} />}>
<PostList
userId={userId}
initialPosts={posts}
isOwner={true}
hideSearch={false}
/>
</Suspense>
</ErrorBoundary>
<>
<PageTitle>Your Posts</PageTitle>
<PageWrapper></PageWrapper>
<ErrorBoundary>
<Suspense fallback={<PostList skeleton={true} initialPosts={[]} />}>
<PostList
userId={userId}
initialPosts={posts}
isOwner={true}
hideSearch={false}
/>
</Suspense>
</ErrorBoundary>
</>
)
}

@ -8,8 +8,8 @@ import {
import PostList, { NoPostsFound } from "@components/post-list"
import { cache, Suspense } from "react"
import ErrorBoundary from "@components/error/fallback"
import { Stack } from "@components/stack"
import DocumentTabs from "src/app/(drift)/(posts)/components/document-tabs"
import { PageWrapper } from "@components/page-wrapper"
export const revalidate = 300
const getWelcomeData = cache(async () => {
@ -19,7 +19,7 @@ const getWelcomeData = cache(async () => {
export default async function Page() {
return (
<Stack direction="column">
<PageWrapper>
{/* @ts-expect-error because of async RSC */}
<WelcomePost />
<h2 className="text-2xl font-bold mt-4">Recent Public Posts</h2>
@ -33,7 +33,7 @@ export default async function Page() {
<PublicPostList />
</Suspense>
</ErrorBoundary>
</Stack>
</PageWrapper>
)
}

@ -13,6 +13,7 @@ import { copyToClipboard } from "src/app/lib/copy-to-clipboard"
import { useState } from "react"
import styles from "./api-keys.module.css"
import { useSessionSWR } from "@lib/use-session-swr"
import { TypographyH4 } from "@components/typography"
// need to pass in the accessToken
const APIKeys = ({
@ -75,7 +76,7 @@ const APIKeys = ({
)}
{hasError && <Note type="error">{error?.message}</Note>}
<form className={styles.form}>
<h5>Create new</h5>
<TypographyH4>Create new</TypographyH4>
<fieldset className={styles.fieldset}>
<Input
type="text"
@ -85,11 +86,11 @@ const APIKeys = ({
placeholder="Name"
/>
<Button
type="button"
onClick={onCreateTokenClick}
disabled={!newToken}
loading={submitting}
>
{submitting ? <Spinner /> : "Submit"}
Submit
</Button>
</fieldset>
</form>
@ -120,7 +121,7 @@ const APIKeys = ({
</tbody>
</table>
) : (
<p>You have no API keys.</p>
<p className="p-4 text-center text-muted-foreground">No API keys found.</p>
)
) : (
<div style={{ marginTop: "var(--gap-quarter)" }}>

@ -143,8 +143,13 @@ function Profile() {
</div>
</TooltipComponent>
</div> */}
<Button type="submit">{submitting ? <Spinner /> : "Submit"}</Button>
<Button
type="submit"
disabled={!name}
loading={submitting}
>
Submit
</Button>
</form>
</>
)

@ -1,22 +0,0 @@
export default function SettingsLayout({
children
}: {
children: React.ReactNode
}) {
return (
<>
<h1>Settings</h1>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "var(--gap)",
marginBottom: "var(--gap)",
marginTop: "var(--gap)"
}}
>
{children}
</div>
</>
)
}

@ -1,5 +1,15 @@
import { PageTitle } from "@components/page-title"
import { PageWrapper } from "@components/page-wrapper"
import SettingsGroup from "@components/settings-group"
export default function SettingsLoading() {
return <SettingsGroup skeleton />
return (
<>
<PageTitle>Settings</PageTitle>
<PageWrapper>
<SettingsGroup skeleton />
<SettingsGroup skeleton />
</PageWrapper>
</>
)
}

@ -2,16 +2,21 @@ import { getMetadata } from "src/app/lib/metadata"
import SettingsGroup from "../../components/settings-group"
import APIKeys from "./components/sections/api-keys"
import Profile from "./components/sections/profile"
import { PageTitle } from "@components/page-title"
import { PageWrapper } from "@components/page-wrapper"
export default async function SettingsPage() {
return (
<>
<SettingsGroup title="Profile">
<Profile />
</SettingsGroup>
<SettingsGroup title="API Keys">
<APIKeys />
</SettingsGroup>
<PageTitle>Settings</PageTitle>
<PageWrapper>
<SettingsGroup title="Profile">
<Profile />
</SettingsGroup>
<SettingsGroup title="API Keys">
<APIKeys />
</SettingsGroup>
</PageWrapper>
</>
)
}

@ -0,0 +1,6 @@
import { authOptions } from "@lib/server/auth"
import NextAuth from "next-auth"
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

@ -0,0 +1,150 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@lib/cn"
import { buttonVariants } from "@components/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = ({
className,
children,
...props
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
{children}
</div>
</AlertDialogPrimitive.Portal>
)
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-opacity animate-in fade-in",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full max-w-lg scale-100 gap-4 border bg-background p-6 opacity-100 shadow-lg animate-in fade-in-90 slide-in-from-bottom-10 sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0 md:w-full",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel
}

@ -3,33 +3,33 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@lib/cn"
const badgeVariants = cva(
"inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
secondary:
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
destructive:
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
"inline-flex items-center border rounded-full font-medium px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
secondary:
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
destructive:
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
outline: "text-foreground"
}
},
defaultVariants: {
variant: "default"
}
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

@ -1,6 +1,6 @@
"use client"
import { useToasts } from "@components/toasts"
import Tooltip from "@components/tooltip"
import { Tooltip } from "@components/tooltip"
import { copyToClipboard } from "src/app/lib/copy-to-clipboard"
import { timeAgo } from "src/app/lib/time-ago"
import { useMemo, useState, useEffect } from "react"
@ -30,7 +30,7 @@ const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
return (
// TODO: investigate tooltip not showing
<Tooltip content={formattedTime}>
<Badge onClick={onClick}>
<Badge onClick={onClick} variant={"outline"}>
{" "}
<>{time}</>
</Badge>

@ -1,6 +1,6 @@
"use client"
import Tooltip from "@components/tooltip"
import { Tooltip } from "@components/tooltip"
import { timeUntil } from "src/app/lib/time-ago"
import { useEffect, useMemo, useState } from "react"
import { Badge } from "../badge"

@ -5,7 +5,7 @@ type Props = {
}
const VisibilityBadge = ({ visibility }: Props) => {
return <Badge>{visibility}</Badge>
return <Badge variant={"outline"}>{visibility}</Badge>
}
export default VisibilityBadge

@ -98,30 +98,36 @@ function VisibilityControl({
>
<Button
disabled={visibility === "private"}
variant={"outline"}
onClick={() => onSubmit("private")}
loading={isSubmitting === "private"}
>
{isSubmitting === "private" ? <Spinner /> : "Make Private"}
Make Private
</Button>
<Button
disabled={visibility === "public"}
variant={"outline"}
onClick={() => onSubmit("public")}
loading={isSubmitting === "public"}
>
{isSubmitting === "public" ? <Spinner /> : "Make Public"}
Make Public
</Button>
<Button
disabled={visibility === "unlisted"}
variant={"outline"}
onClick={() => onSubmit("unlisted")}
loading={isSubmitting === "unlisted"}
>
{isSubmitting === "unlisted" ? <Spinner /> : "Make Unlisted"}
Make Unlisted
</Button>
<Button onClick={() => onSubmit("protected")}>
{isSubmitting === "protected" ? (
<Spinner />
) : visibility === "protected" ? (
"Change Password"
) : (
"Protect with Password"
)}
<Button
onClick={() => onSubmit("protected")}
variant={"outline"}
loading={isSubmitting === "protected"}
>
{visibility === "protected"
? "Change Password"
: "Protect with Password"}
</Button>
</ButtonGroup>
<PasswordModal

@ -3,6 +3,7 @@ import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@lib/cn"
import { Spinner } from "@components/spinner"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
@ -28,7 +29,7 @@ const buttonVariants = cva(
defaultVariants: {
variant: "default",
size: "default"
},
}
}
)
@ -36,11 +37,27 @@ export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
loading?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
({ className, variant, size, loading, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
if (loading) {
return (
<Comp
className={cn(
buttonVariants({ variant: "outline", size, className })
)}
ref={ref}
{...props}
>
<Spinner />
</Comp>
)
}
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
@ -50,6 +67,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

@ -3,6 +3,7 @@
import { Button } from "@components/button"
import Link from "@components/link"
import Note from "@components/note"
import { TypographyH3 } from "@components/typography"
import { useRouter } from "next/navigation"
// an error fallback for react-error-boundary
@ -15,7 +16,7 @@ import {
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<Note type="error" style={{ width: "100%" }}>
<h3>Something went wrong:</h3>
<TypographyH3>Something went wrong:</TypographyH3>
<pre>{error.message}</pre>
<Link href="https://github.com/MaxLeiter/Drift/issues/new">
<Button>Report an issue</Button>

@ -1,4 +1,5 @@
// https://www.joshwcomeau.com/snippets/react-components/fade-in/
import React from "react"
import styles from "./fade.module.css"
function FadeIn({
@ -11,8 +12,18 @@ function FadeIn({
duration?: number
delay?: number
children: React.ReactNode
as?: React.ElementType
as?: React.ElementType | JSX.Element
} & React.HTMLAttributes<HTMLElement>) {
if (as !== null && typeof as === 'object') {
return React.cloneElement(as, {
className: styles.fadeIn,
style: {
...(as.props.style || {}),
animationDuration: duration + "ms",
animationDelay: delay + "ms"
}
})
}
const Element = as || "div"
return (
<Element

@ -1,16 +1,14 @@
"use client"
import { Stack } from "@components/stack"
import Link from "next/link"
import Image from "next/image"
import { cn } from "@lib/cn"
import { DropdownMenu } from "@components/dropdown-menu"
import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@radix-ui/react-dropdown-menu"
import { ArrowDownCircle } from "react-feather"
import { useSessionSWR } from "@lib/use-session-swr"
import { PropsWithChildren, useEffect, useState } from "react"
import { useSelectedLayoutSegments } from "next/navigation"
import Image from "next/image"
import FadeIn from "@components/fade-in"
import { useTheme } from "next-themes"
// <NavButton
// key="home"
@ -87,57 +85,111 @@ import { ArrowDownCircle } from "react-feather"
// />
export default function Header() {
const { isAdmin, isAuthenticated } = useSessionSWR()
const { resolvedTheme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
const toggleTheme = () => {
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}
return (
<header className="container mx-auto flex items-center justify-between px-4 py-4">
<div className="flex items-center">
<header className="flex items-center h-16 mt-4">
<Link href="/" className="flex items-center mr-4">
<Image
src={"/assets/logo.svg"}
width={48}
height={48}
width={32}
height={32}
alt=""
priority
/>
<span className="text-lg ml-2">Drift</span>
</div>
<nav className="flex items-center justify-center">
<ul className="flex space-x-4">
<Link
href="/home"
className="text-sm font-medium transition-colors hover:text-primary"
>
Home
</Link>
<Link
href="/new"
className="text-sm font-medium transition-colors hover:text-primary"
>
<span className="pl-4 text-lg font-bold bg-transparent">Drift</span>
</Link>
<nav className="flex space-x-4 lg:space-x-6">
<ul className="flex justify-center space-x-4">
<NavLink href="/home">Home</NavLink>
<NavLink href="/new" disabled={!isAuthenticated}>
New
</Link>
<Link
href="/mine"
className="text-sm font-medium transition-colors hover:text-primary"
>
</NavLink>
<NavLink href="/mine" disabled={!isAuthenticated}>
Yours
</Link>
<Link
href="/settings"
className="text-sm font-medium transition-colors hover:text-primary"
>
</NavLink>
<NavLink href="/settings" disabled={!isAuthenticated}>
Settings
</Link>
</NavLink>
{isAdmin && <NavLink href="/admin">Admin</NavLink>}
{isAuthenticated === true && (
<NavLink href="/signout">Sign Out</NavLink>
)}
{isAuthenticated === false && (
<NavLink href="/signin">Sign In</NavLink>
)}
{/* {!mounted && (
<NavButton
name="Theme"
icon={<Circle opacity={0.3} />}
value="dark"
href=""
key="theme"
/>
)}
{mounted && (
<NavButton
name="Theme"
icon={
<FadeIn>{resolvedTheme === "dark" ? <Sun /> : <Moon />}</FadeIn>
}
value="dark"
onClick={() => {
setTheme(resolvedTheme === "dark" ? "light" : "dark")
}}
key="theme"
/>
)} */}
{mounted && isAuthenticated !== undefined && (
<span
aria-hidden
className="text-sm font-medium transition-colors cursor-pointer text-muted-foreground hover:text-primary"
onClick={toggleTheme}
>
<FadeIn>{resolvedTheme === "dark" ? "Light" : "Dark"}</FadeIn>
</span>
)}
</ul>
</nav>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ArrowDownCircle />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Link href="/signin">Sign In</Link>
</DropdownMenuItem>
<DropdownMenuItem>Change Theme</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</header>
)
}
type NavLinkProps = PropsWithChildren<{
href: string
disabled?: boolean
active?: boolean
}>
function NavLink({ href, disabled, children }: NavLinkProps) {
const baseClasses =
"text-sm text-muted-foreground font-medium transition-colors hover:text-primary"
const activeClasses = "text-primary border-primary"
const disabledClasses = "text-gray-400 hover:text-gray-400 cursor-default"
const segments = useSelectedLayoutSegments()
const activeSegment = segments[segments.length - 1]
const isActive =
activeSegment === href.slice(1) ||
// special case / because it's an alias of /home/page.tsx
(!activeSegment && href === "/home")
return (
<Link
href={href}
className={cn(
baseClasses,
isActive && activeClasses,
disabled && disabledClasses
)}
>
{children}
</Link>
)
}

@ -1,5 +1,4 @@
.page {
max-width: 70rem;
min-height: 100vh;
box-sizing: border-box;
position: relative;

@ -10,7 +10,7 @@ const Note = ({
type: "info" | "warning" | "error"
children: React.ReactNode
} & React.ComponentProps<"div">) => (
<div className={clsx(className, styles.note, styles[type])} {...props}>
<div className={clsx(className, styles.note, styles[type], "text-sm")} {...props}>
{children}
</div>
)

@ -0,0 +1,14 @@
import { cn } from "@lib/cn"
import { PropsWithChildren } from "react"
export function PageTitle({
children,
className,
...props
}: PropsWithChildren<React.HTMLProps<HTMLHeadingElement>>) {
return (
<h1 className={cn("text-4xl font-bold pb-2 pt-2", className)} {...props}>
{children}
</h1>
)
}

@ -0,0 +1,14 @@
import { cn } from "@lib/cn"
import { PropsWithChildren } from "react"
export function PageWrapper({
children,
className,
...props
}: PropsWithChildren<React.HTMLProps<HTMLDivElement>>) {
return (
<div className={cn("flex flex-col gap-4 mb-4 mt-4", className)} {...props}>
{children}
</div>
)
}

@ -1,9 +1,18 @@
import { Button } from "@components/button"
import { Input } from "@components/input"
import Note from "@components/note"
import * as Dialog from "@radix-ui/react-dialog"
import { useState } from "react"
import styles from "./modal.module.css"
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogDescription,
AlertDialogTitle,
AlertDialogAction,
AlertDialogCancel,
AlertDialogFooter
} from "@components/alert-dialog"
type Props = {
creating: boolean
@ -22,7 +31,8 @@ const PasswordModal = ({
const [confirmPassword, setConfirmPassword] = useState<string>("")
const [error, setError] = useState<string>()
const onSubmit = () => {
const onSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault()
if (!password || (creating && !confirmPassword)) {
setError("Please enter a password")
return
@ -39,60 +49,57 @@ const PasswordModal = ({
return (
<>
{
<Dialog.Root
<AlertDialog
open={isOpen}
onOpenChange={(open) => {
if (!open) onClose()
}}
>
<Dialog.Portal>
<Dialog.Overlay className={styles.overlay} />
<Dialog.Content
className={styles.content}
onEscapeKeyDown={onClose}
>
<Dialog.Title>
{/* <AlertDialogOverlay className={styles.overlay} /> */}
<AlertDialogContent onEscapeKeyDown={onClose}>
<AlertDialogHeader>
<AlertDialogTitle>
{creating ? "Add a password" : "Enter password"}
</Dialog.Title>
<Dialog.Description>
</AlertDialogTitle>
<AlertDialogDescription>
{creating
? "Enter a password to protect your post"
: "Enter the password to access the post"}
</Dialog.Description>
<fieldset className={styles.fieldset}>
{!error && creating && (
<Note type="warning">
This doesn&apos;t protect your post from the server
administrator.
</Note>
)}
{error && <Note type="error">{error}</Note>}
</AlertDialogDescription>
</AlertDialogHeader>
<fieldset className={styles.fieldset}>
{!error && creating && (
<Note type="warning">
This doesn&apos;t protect your post from the server
administrator.
</Note>
)}
{error && <Note type="error">{error}</Note>}
<Input
width={"100%"}
label="Password"
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
/>
{creating && (
<Input
width={"100%"}
label="Password"
label="Confirm"
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
placeholder="Confirm Password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.currentTarget.value)}
/>
{creating && (
<Input
width={"100%"}
label="Confirm"
type="password"
placeholder="Confirm Password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.currentTarget.value)}
/>
)}
</fieldset>
<footer className={styles.footer}>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={onSubmit}>Submit</Button>
</footer>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)}
</fieldset>
<AlertDialogFooter>
<AlertDialogCancel onClick={onClose}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onSubmit}>Submit</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
}
</>
)

@ -10,11 +10,6 @@
flex-wrap: wrap;
}
.buttons {
display: flex;
gap: var(--gap-half);
}
.oneline {
white-space: nowrap;
overflow: hidden;

@ -6,7 +6,7 @@ import { useRouter } from "next/navigation"
import styles from "./list-item.module.css"
import Link from "@components/link"
import type { PostWithFiles } from "@lib/server/prisma"
import Tooltip from "@components/tooltip"
import { Tooltip } from "@components/tooltip"
import { Badge } from "@components/badges/badge"
import {
Card,
@ -17,15 +17,17 @@ import {
} from "@components/card"
import { Button } from "@components/button"
import {
ArrowUpCircle,
Code,
Database,
Edit,
FileText,
MoreVertical,
Terminal,
Trash
} from "react-feather"
import { codeFileExtensions } from "@lib/constants"
import { DropdownMenu, DropdownMenuItem, DropdownMenuTrigger } from "@components/dropdown-menu"
import { DropdownMenuContent } from "@radix-ui/react-dropdown-menu"
// TODO: isOwner should default to false so this can be used generically
const ListItem = ({
@ -76,7 +78,7 @@ const ListItem = ({
{/* <Card style={{ overflowY: "scroll" }}> */}
<Card className="overflow-y-scroll h-42">
<CardHeader>
<CardTitle className={styles.title}>
<CardTitle className="flex items-center justify-between gap-2">
<span className={styles.titleText}>
<h4 style={{ display: "inline-block", margin: 0 }}>
<Link
@ -89,7 +91,7 @@ const ListItem = ({
</h4>
<div className={styles.badges}>
<VisibilityBadge visibility={post.visibility} />
<Badge type="secondary">
<Badge variant={"outline"}>
{post.files?.length === 1
? "1 file"
: `${post.files?.length || 0} files`}
@ -99,28 +101,41 @@ const ListItem = ({
</div>
</span>
{!hideActions ? (
<span className={styles.buttons}>
<span className="flex gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<MoreVertical />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
onSelect={() => {
editACopy()
}}
>
Edit a copy
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{post.parentId && (
<Tooltip content={"View parent"}>
<Button
// iconRight={<ArrowUpCircle />}
onClick={viewParentClick}
// TODO: not perfect on mobile
variant={"secondary"}
/>
</Tooltip>
)}
<Tooltip content={"Make a copy"}>
<Button
// iconRight={<Edit />}
onClick={editACopy}
/>
<Button onClick={editACopy} variant={"secondary"}>
<Edit height={18} />
</Button>
</Tooltip>
{isOwner && (
<Tooltip content={"Delete"}>
<Button
// iconRight={<Trash />}
onClick={deletePost}
/>
<Button onClick={deletePost} variant={"secondary"}>
<Trash height={18} />
</Button>
</Tooltip>
)}
</span>

@ -1,7 +1,7 @@
"use client"
import { Button } from "@components/button"
import Tooltip from "@components/tooltip"
import { Tooltip } from "@components/tooltip"
import { useEffect, useState } from "react"
import { ChevronUp } from "react-feather"
import styles from "./scroll.module.css"

@ -1,9 +1,11 @@
"use client"
import * as RadixTooltip from "@radix-ui/react-tooltip"
import styles from "./tooltip.module.css"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
const Tooltip = ({
import { cn } from "@lib/cn"
const TooltipWrapper = ({
children,
content,
className,
@ -12,18 +14,42 @@ const Tooltip = ({
children: React.ReactNode
content: React.ReactNode
className?: string
} & RadixTooltip.TooltipProps) => {
} & TooltipPrimitive.TooltipProps) => {
return (
<RadixTooltip.Root {...props}>
<RadixTooltip.Trigger asChild className={className}>
{children}
</RadixTooltip.Trigger>
<RadixTooltip.Content>
<div className={styles.tooltip}>{content}</div>
</RadixTooltip.Content>
</RadixTooltip.Root>
<TooltipProvider>
<Tooltip {...props}>
<TooltipTrigger asChild className={className}>
{children}
</TooltipTrigger>
<TooltipContent>{content}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
export default Tooltip
// export TooltipWrapper as Tooltip
export { TooltipWrapper as Tooltip }
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in duration-75 fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName

@ -0,0 +1,227 @@
import { PropsWithChildren } from "react"
import { cn } from "@lib/cn"
type ClassProp = { className?: string }
export function TypographyH1({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<h1
className={cn(
"scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl",
className
)}
>
{children}
</h1>
)
}
export function TypographyH2({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<h2
className={cn(
"scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0",
className
)}
>
{children}
</h2>
)
}
export function TypographyH3({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<h3
className={cn(
"scroll-m-20 text-2xl font-semibold tracking-tight",
className
)}
>
{children}
</h3>
)
}
export function TypographyH4({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<h4
className={cn(
"scroll-m-20 text-xl font-semibold tracking-tight",
className
)}
>
{children}
</h4>
)
}
export function TypographyP({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<p className={cn("leading-7 [&:not(:first-child)]:mt-6", className)}>
{children}
</p>
)
}
export function TypographyBlockquote({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<blockquote className={cn("mt-6 border-l-2 pl-6 italic", className)}>
{children}
</blockquote>
)
}
export function TypographyTable({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<div className={cn("my-6 w-full overflow-y-auto", className)}>
<table className="w-full">{children}</table>
</div>
)
}
export function TableHead({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<thead className={className}>
<tr className="m-0 border-t p-0 even:bg-muted">{children}</tr>
</thead>
)
}
export function TableBody({
children,
className
}: PropsWithChildren<ClassProp>) {
return <tbody className={className}>{children}</tbody>
}
export function TableHeader({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<th
className={cn(
"border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right",
className
)}
>
{children}
</th>
)
}
export function TableRow({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<tr className={cn("m-0 border-t p-0 even:bg-muted", className)}>
{children}
</tr>
)
}
export function TableCell({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<td
className={cn(
"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right",
className
)}
>
{children}
</td>
)
}
export function TypographyList({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<ul className={cn("my-6 ml-6 list-disc [&>li]:mt-2", className)}>
{children}
</ul>
)
}
export function TypographyInlineCode({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<code
className={cn(
"relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold",
className
)}
>
{children}
</code>
)
}
export function TypographyLead({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<p className={cn("text-xl text-muted-foreground", className)}>{children}</p>
)
}
export function TypographyLarge({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<div className={cn("text-lg font-semibold", className)}>{children}</div>
)
}
export function TypographySmall({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<small className={cn("text-sm font-medium leading-none", className)}>
{children}
</small>
)
}
export function TypographyMuted({
children,
className
}: PropsWithChildren<ClassProp>) {
return (
<p className={cn("text-sm text-muted-foreground", className)}>{children}</p>
)
}

@ -1,4 +0,0 @@
import { authOptions } from "@lib/server/auth"
import NextAuth from "next-auth"
export default NextAuth(authOptions)

@ -1,36 +0,0 @@
import config from "@lib/config"
import { NextApiRequest, NextApiResponse } from "next"
export const getRequiresPasscode = async () => {
const requiresPasscode = Boolean(config.registration_password)
return requiresPasscode
}
const handleRequiresPasscode = async (
_: NextApiRequest,
res: NextApiResponse
) => {
return res.json({ requiresPasscode: await getRequiresPasscode() })
}
export default async function requiresPasscode(
req: NextApiRequest,
res: NextApiResponse
) {
const { slug } = req.query
if (!slug || Array.isArray(slug)) {
return res.status(400).json({ error: "Missing param" })
}
switch (req.method) {
case "GET":
if (slug === "requires-passcode") {
return handleRequiresPasscode(req, res)
}
return res.status(404).json({ error: "Not found" })
default:
return res.status(405).json({ error: "Method not allowed" })
}
}
Loading…
Cancel
Save