diff --git a/src/.eslintrc.json b/.eslintrc.json similarity index 100% rename from src/.eslintrc.json rename to .eslintrc.json diff --git a/.gitignore b/.gitignore index 57650435..3000161b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# production env +.env + +# vercel .vercel -drift.sqlite \ No newline at end of file + +# typescript +*.tsbuildinfo diff --git a/src/.prettierrc b/.prettierrc similarity index 100% rename from src/.prettierrc rename to .prettierrc diff --git a/.vscode/settings.json b/.vscode/settings.json index e0442780..8f5be19e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "typescript.tsdk": "src/node_modules/.pnpm/typescript@4.6.4/node_modules/typescript/lib", + "typescript.tsdk": "node_modules/.pnpm/typescript@4.6.4/node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, "dotenv.enableAutocloaking": false -} +} \ No newline at end of file diff --git a/src/jest.config.js b/jest.config.js similarity index 100% rename from src/jest.config.js rename to jest.config.js diff --git a/src/next-env.d.ts b/next-env.d.ts similarity index 100% rename from src/next-env.d.ts rename to next-env.d.ts diff --git a/src/next.config.mjs b/next.config.mjs similarity index 100% rename from src/next.config.mjs rename to next.config.mjs diff --git a/src/package.json b/package.json similarity index 96% rename from src/package.json rename to package.json index d0d94d75..0080dba7 100644 --- a/src/package.json +++ b/package.json @@ -23,7 +23,6 @@ "@radix-ui/react-tabs": "^1.0.1", "@radix-ui/react-tooltip": "^1.0.2", "@wcj/markdown-to-html": "^2.1.2", - "@wits/next-themes": "0.2.14", "client-only": "^0.0.1", "client-zip": "2.2.1", "jest": "^29.3.1", @@ -71,7 +70,7 @@ "next-unused": { "alias": { "@components": "components/", - "@lib": "lib/", + "@lib": "src/lib/", "@styles": "styles/" }, "include": [ @@ -79,6 +78,9 @@ "lib" ] }, + "prisma": { + "schema": "src/prisma/schema.prisma" + }, "overrides": { "react": "18.2.0", "react-dom": "18.2.0" diff --git a/src/pnpm-lock.yaml b/pnpm-lock.yaml similarity index 99% rename from src/pnpm-lock.yaml rename to pnpm-lock.yaml index 663de351..00317774 100644 --- a/src/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,6 @@ specifiers: '@typescript-eslint/eslint-plugin': ^5.46.1 '@typescript-eslint/parser': ^5.46.1 '@wcj/markdown-to-html': ^2.1.2 - '@wits/next-themes': 0.2.14 client-only: ^0.0.1 client-zip: 2.2.1 clsx: ^1.2.1 @@ -63,7 +62,6 @@ dependencies: '@radix-ui/react-tabs': 1.0.1_biqbaboplfbrettd7655fr4n2y '@radix-ui/react-tooltip': 1.0.2_jbvntnid6ohjelon6ccj5dhg2u '@wcj/markdown-to-html': 2.1.2 - '@wits/next-themes': 0.2.14_3tcywg5dy5qhcmsv6sy2pt6lua client-only: 0.0.1 client-zip: 2.2.1 jest: 29.3.1_@types+node@17.0.23 @@ -1805,18 +1803,6 @@ packages: - supports-color dev: false - /@wits/next-themes/0.2.14_3tcywg5dy5qhcmsv6sy2pt6lua: - resolution: {integrity: sha512-fHKb/tRcWbYNblGHZtfvAQztDhzUB9d7ZkYOny0BisSPh6EABcsqxKB48ABUQztcmKywlp2zEMkLcSRj/PQBSw==} - peerDependencies: - next: '*' - react: '*' - react-dom: '*' - dependencies: - next: 13.1.2-canary.0_biqbaboplfbrettd7655fr4n2y - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - dev: false - /acorn-jsx/5.3.2_acorn@8.8.1: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: diff --git a/src/public/assets/android-chrome-192x192.png b/public/assets/android-chrome-192x192.png similarity index 100% rename from src/public/assets/android-chrome-192x192.png rename to public/assets/android-chrome-192x192.png diff --git a/src/public/assets/android-chrome-512x512.png b/public/assets/android-chrome-512x512.png similarity index 100% rename from src/public/assets/android-chrome-512x512.png rename to public/assets/android-chrome-512x512.png diff --git a/src/public/assets/apple-touch-icon.png b/public/assets/apple-touch-icon.png similarity index 100% rename from src/public/assets/apple-touch-icon.png rename to public/assets/apple-touch-icon.png diff --git a/src/public/assets/browserconfig.xml b/public/assets/browserconfig.xml similarity index 100% rename from src/public/assets/browserconfig.xml rename to public/assets/browserconfig.xml diff --git a/src/public/assets/favicon-16x16.png b/public/assets/favicon-16x16.png similarity index 100% rename from src/public/assets/favicon-16x16.png rename to public/assets/favicon-16x16.png diff --git a/src/public/assets/favicon-32x32.png b/public/assets/favicon-32x32.png similarity index 100% rename from src/public/assets/favicon-32x32.png rename to public/assets/favicon-32x32.png diff --git a/src/public/assets/favicon.ico b/public/assets/favicon.ico similarity index 100% rename from src/public/assets/favicon.ico rename to public/assets/favicon.ico diff --git a/src/public/assets/favicon.png b/public/assets/favicon.png similarity index 100% rename from src/public/assets/favicon.png rename to public/assets/favicon.png diff --git a/src/public/assets/favicon.svg b/public/assets/favicon.svg similarity index 100% rename from src/public/assets/favicon.svg rename to public/assets/favicon.svg diff --git a/src/public/assets/logo-optimized.svg b/public/assets/logo-optimized.svg similarity index 100% rename from src/public/assets/logo-optimized.svg rename to public/assets/logo-optimized.svg diff --git a/src/public/assets/logo.png b/public/assets/logo.png similarity index 100% rename from src/public/assets/logo.png rename to public/assets/logo.png diff --git a/src/public/assets/logo.svg b/public/assets/logo.svg similarity index 100% rename from src/public/assets/logo.svg rename to public/assets/logo.svg diff --git a/src/public/assets/mstile-144x144.png b/public/assets/mstile-144x144.png similarity index 100% rename from src/public/assets/mstile-144x144.png rename to public/assets/mstile-144x144.png diff --git a/src/public/assets/mstile-150x150.png b/public/assets/mstile-150x150.png similarity index 100% rename from src/public/assets/mstile-150x150.png rename to public/assets/mstile-150x150.png diff --git a/src/public/assets/mstile-310x150.png b/public/assets/mstile-310x150.png similarity index 100% rename from src/public/assets/mstile-310x150.png rename to public/assets/mstile-310x150.png diff --git a/src/public/assets/mstile-310x310.png b/public/assets/mstile-310x310.png similarity index 100% rename from src/public/assets/mstile-310x310.png rename to public/assets/mstile-310x310.png diff --git a/src/public/assets/mstile-70x70.png b/public/assets/mstile-70x70.png similarity index 100% rename from src/public/assets/mstile-70x70.png rename to public/assets/mstile-70x70.png diff --git a/src/public/assets/safari-pinned-tab.svg b/public/assets/safari-pinned-tab.svg similarity index 100% rename from src/public/assets/safari-pinned-tab.svg rename to public/assets/safari-pinned-tab.svg diff --git a/src/public/site.webmanifest b/public/site.webmanifest similarity index 100% rename from src/public/site.webmanifest rename to public/site.webmanifest diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index 3000161b..00000000 --- a/src/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# production env -.env - -# vercel -.vercel - -# typescript -*.tsbuildinfo diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json deleted file mode 100644 index e11793fd..00000000 --- a/src/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "typescript.tsdk": "node_modules/.pnpm/typescript@4.6.4/node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true -} \ No newline at end of file diff --git a/src/README.md b/src/README.md deleted file mode 100644 index c87e0421..00000000 --- a/src/README.md +++ /dev/null @@ -1,34 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. - -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. - -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 6c392da0..dc82bbc7 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -1,5 +1,5 @@ import Auth from "../components" -import { getRequiresPasscode } from "pages/api/auth/requires-passcode" +import { getRequiresPasscode } from "src/pages/api/auth/requires-passcode" import config from "@lib/config" const getPasscode = async () => { diff --git a/src/app/(posts)/components/file-dropdown/index.tsx b/src/app/(posts)/components/file-dropdown/index.tsx index 62f47b8d..7c028157 100644 --- a/src/app/(posts)/components/file-dropdown/index.tsx +++ b/src/app/(posts)/components/file-dropdown/index.tsx @@ -1,7 +1,7 @@ import { Popover } from "@components/popover" import { codeFileExtensions } from "@lib/constants" import clsx from "clsx" -import type { PostWithFiles } from "lib/server/prisma" +import type { PostWithFiles } from "src/lib/server/prisma" import styles from "./dropdown.module.css" import buttonStyles from "@components/button/button.module.css" import { ChevronDown, Code, File as FileIcon } from "react-feather" diff --git a/src/app/(posts)/components/tabs/index.tsx b/src/app/(posts)/components/tabs/index.tsx index 449c1925..6f5a0f43 100644 --- a/src/app/(posts)/components/tabs/index.tsx +++ b/src/app/(posts)/components/tabs/index.tsx @@ -1,7 +1,7 @@ "use client" import * as RadixTabs from "@radix-ui/react-tabs" -import FormattingIcons from "app/(posts)/new/components/edit-document-list/edit-document/formatting-icons" +import FormattingIcons from "src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons" import { ChangeEvent, useRef } from "react" import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor" import Preview, { StaticPreview } from "../preview" diff --git a/src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons/index.tsx b/src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons/index.tsx index 62ef1718..9dda28e7 100644 --- a/src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons/index.tsx +++ b/src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons/index.tsx @@ -11,7 +11,6 @@ import styles from "./formatting-icons.module.css" import { TextareaMarkdownRef } from "textarea-markdown-editor" import Tooltip from "@components/tooltip" import Button from "@components/button" -import ButtonGroup from "@components/button-group" import clsx from "clsx" // TODO: clean up diff --git a/src/app/(posts)/new/components/edit-document-list/edit-document/index.tsx b/src/app/(posts)/new/components/edit-document-list/edit-document/index.tsx index 942d07b0..cea1b5c8 100644 --- a/src/app/(posts)/new/components/edit-document-list/edit-document/index.tsx +++ b/src/app/(posts)/new/components/edit-document-list/edit-document/index.tsx @@ -2,7 +2,7 @@ import { ChangeEvent, useCallback } from "react" import styles from "./document.module.css" import Button from "@components/button" import Input from "@components/input" -import DocumentTabs from "app/(posts)/components/tabs" +import DocumentTabs from "src/app/(posts)/components/tabs" import { Trash } from "react-feather" type Props = { diff --git a/src/app/(posts)/new/components/title/title.module.css b/src/app/(posts)/new/components/title/title.module.css index 09668462..4d1ec8f6 100644 --- a/src/app/(posts)/new/components/title/title.module.css +++ b/src/app/(posts)/new/components/title/title.module.css @@ -14,5 +14,5 @@ } .labelAndInput { - font-size: 1.2rem; + font-size: 1.2rem; } diff --git a/src/app/(posts)/new/page.tsx b/src/app/(posts)/new/page.tsx index 35ce0707..8bd3712c 100644 --- a/src/app/(posts)/new/page.tsx +++ b/src/app/(posts)/new/page.tsx @@ -1,4 +1,4 @@ -import NewPost from "app/(posts)/new/components/new" +import NewPost from "src/app/(posts)/new/components/new" import "./react-datepicker.css" const New = () => diff --git a/src/app/(posts)/post/[id]/components/header/post-buttons/index.tsx b/src/app/(posts)/post/[id]/components/header/post-buttons/index.tsx index 0b30ea54..55ff885c 100644 --- a/src/app/(posts)/post/[id]/components/header/post-buttons/index.tsx +++ b/src/app/(posts)/post/[id]/components/header/post-buttons/index.tsx @@ -2,7 +2,7 @@ import Button from "@components/button" import ButtonGroup from "@components/button-group" -import FileDropdown from "app/(posts)/components/file-dropdown" +import FileDropdown from "src/app/(posts)/components/file-dropdown" import { Edit, ArrowUpCircle, Archive } from "react-feather" import styles from "./post-buttons.module.css" import { useRouter } from "next/navigation" diff --git a/src/app/(posts)/post/[id]/components/header/title/index.tsx b/src/app/(posts)/post/[id]/components/header/title/index.tsx index fa3be480..ec2bb776 100644 --- a/src/app/(posts)/post/[id]/components/header/title/index.tsx +++ b/src/app/(posts)/post/[id]/components/header/title/index.tsx @@ -22,7 +22,7 @@ export const PostTitle = ({ createdAt, expiresAt, loading, - authorId + // authorId }: TitleProps) => { return ( @@ -33,10 +33,9 @@ export const PostTitle = ({ > {title}{" "} - by{" "} - - {displayName || "anonymous"} - + by {/* */} + {displayName || "anonymous"} + {/* */} {!loading && ( diff --git a/src/app/(posts)/post/[id]/components/header/title/title.module.css b/src/app/(posts)/post/[id]/components/header/title/title.module.css index e8a1c24f..c9f11ef9 100644 --- a/src/app/(posts)/post/[id]/components/header/title/title.module.css +++ b/src/app/(posts)/post/[id]/components/header/title/title.module.css @@ -3,6 +3,7 @@ flex-direction: row; justify-content: space-between; align-items: center; + margin-bottom: var(--gap); } .title .badges { @@ -11,12 +12,6 @@ flex-wrap: wrap; } -.title h3 { - margin: 0; - padding: 0; - display: inline-block; -} - .titleWithDropdown { display: flex; flex-direction: row; diff --git a/src/app/(posts)/post/[id]/components/post-files/view-document/index.tsx b/src/app/(posts)/post/[id]/components/post-files/view-document/index.tsx index a175819a..c6eac4e2 100644 --- a/src/app/(posts)/post/[id]/components/post-files/view-document/index.tsx +++ b/src/app/(posts)/post/[id]/components/post-files/view-document/index.tsx @@ -4,7 +4,7 @@ import Button from "@components/button" import ButtonGroup from "@components/button-group" import Skeleton from "@components/skeleton" import Tooltip from "@components/tooltip" -import DocumentTabs from "app/(posts)/components/tabs" +import DocumentTabs from "src/app/(posts)/components/tabs" import Link from "next/link" import { memo } from "react" import { Download, ExternalLink } from "react-feather" diff --git a/src/app/author/[username]/page.tsx b/src/app/author/[username]/page.tsx index e816028a..215ba208 100644 --- a/src/app/author/[username]/page.tsx +++ b/src/app/author/[username]/page.tsx @@ -14,9 +14,10 @@ async function PostListWrapper({ const data = (await posts).filter((post) => post.visibility === "public") return ( ) } @@ -62,9 +63,9 @@ export default async function UserPage({

Public posts by {user?.displayName || "Anonymous"}

- }> + }> {/* @ts-expect-error because TS async JSX support is iffy */} - + ) diff --git a/src/app/components/badges/badge.tsx b/src/app/components/badges/badge.tsx index a24e0868..ac92b6ec 100644 --- a/src/app/components/badges/badge.tsx +++ b/src/app/components/badges/badge.tsx @@ -3,12 +3,12 @@ import styles from "./badge.module.css" type BadgeProps = { type: "primary" | "secondary" | "error" | "warning" children: React.ReactNode -} +} & React.HTMLAttributes const Badge = React.forwardRef( - ({ type, children }: BadgeProps, ref) => { + ({ type, children, ...rest}: BadgeProps, ref) => { return ( -
+
{children}
diff --git a/src/app/components/badges/created-ago-badge/index.tsx b/src/app/components/badges/created-ago-badge/index.tsx index a03b643e..2237eed6 100644 --- a/src/app/components/badges/created-ago-badge/index.tsx +++ b/src/app/components/badges/created-ago-badge/index.tsx @@ -1,6 +1,8 @@ "use client" +import { useToasts } from "@components/toasts" import Tooltip from "@components/tooltip" -import { timeAgo } from "@lib/time-ago" +import { copyToClipboard } from "src/app/lib/copy-to-clipboard" +import { timeAgo } from "src/app/lib/time-ago" import { useMemo, useState, useEffect } from "react" import Badge from "../badge" @@ -8,6 +10,7 @@ const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => { const createdDate = useMemo(() => new Date(createdAt), [createdAt]) const [time, setTimeAgo] = useState(timeAgo(createdDate)) + const { setToast } = useToasts() useEffect(() => { const interval = setInterval(() => { setTimeAgo(timeAgo(createdDate)) @@ -15,11 +18,19 @@ const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => { return () => clearInterval(interval) }, [createdDate]) + function onClick() { + copyToClipboard(createdDate.toISOString()) + setToast({ + message: "Copied to clipboard", + type: "success" + }) + } + const formattedTime = `${createdDate.toLocaleDateString()} ${createdDate.toLocaleTimeString()}` return ( // TODO: investigate tooltip not showing - + {" "} <>{time} diff --git a/src/app/components/badges/expiration-badge/index.tsx b/src/app/components/badges/expiration-badge/index.tsx index caa7965e..570496ec 100644 --- a/src/app/components/badges/expiration-badge/index.tsx +++ b/src/app/components/badges/expiration-badge/index.tsx @@ -1,7 +1,7 @@ "use client" import Tooltip from "@components/tooltip" -import { timeUntil } from "@lib/time-ago" +import { timeUntil } from "src/app/lib/time-ago" import { useEffect, useMemo, useState } from "react" import Badge from "../badge" diff --git a/src/app/components/badges/visibility-control/index.tsx b/src/app/components/badges/visibility-control/index.tsx index a8d16299..893c246d 100644 --- a/src/app/components/badges/visibility-control/index.tsx +++ b/src/app/components/badges/visibility-control/index.tsx @@ -39,6 +39,11 @@ const VisibilityControl = ({ authorId, postId, visibility: postVisibility }: Pro const json = await res.json() setVisibility(json.visibility) router.refresh() + setToast({ + message: "Visibility updated", + type: "success" + }) + } else { setToast({ message: "An error occurred", @@ -47,7 +52,7 @@ const VisibilityControl = ({ authorId, postId, visibility: postVisibility }: Pro setPasswordModalVisible(false) } }, - [postId, setToast, setVisibility] + [postId, router, setToast] ) const onSubmit = useCallback( diff --git a/src/app/components/header/header.module.css b/src/app/components/header/header.module.css index 2f69ef6b..fdc30150 100644 --- a/src/app/components/header/header.module.css +++ b/src/app/components/header/header.module.css @@ -38,7 +38,7 @@ .header { transition: opacity 0.2s ease-in-out; - opacity: .5; + opacity: 0; } .header:not(.loading) { diff --git a/src/app/components/post-list/index.tsx b/src/app/components/post-list/index.tsx index cbb8103e..d83871b9 100644 --- a/src/app/components/post-list/index.tsx +++ b/src/app/components/post-list/index.tsx @@ -3,66 +3,42 @@ import styles from "./post-list.module.css" import ListItem from "./list-item" import { ChangeEvent, useCallback, useState } from "react" -import Link from "@components/link" import type { PostWithFiles } from "@lib/server/prisma" import Input from "@components/input" -import Button from "@components/button" import { useToasts } from "@components/toasts" import { ListItemSkeleton } from "./list-item-skeleton" +import Link from "@components/link" import debounce from "lodash.debounce" type Props = { initialPosts: string | PostWithFiles[] morePosts?: boolean - userId?: string hideSearch?: boolean hideActions?: boolean isOwner?: boolean + skeleton?: boolean + searchValue?: string + userId?: string } const PostList = ({ - morePosts, initialPosts: initialPostsMaybeJSON, - userId, hideSearch, hideActions, - isOwner + isOwner, + skeleton, + userId }: Props) => { const initialPosts = typeof initialPostsMaybeJSON === "string" ? JSON.parse(initialPostsMaybeJSON) : initialPostsMaybeJSON - const [search, setSearchValue] = useState("") - const [posts, setPosts] = useState(initialPosts) + const [searchValue, setSearchValue] = useState("") const [searching, setSearching] = useState(false) - const [hasMorePosts] = useState(morePosts) + const [posts, setPosts] = useState(initialPosts) const { setToast } = useToasts() - const loadMoreClick = useCallback( - (e: React.MouseEvent) => { - e.preventDefault() - if (hasMorePosts) { - // eslint-disable-next-line no-inner-declarations - async function fetchPosts() { - // const res = await fetch(`/api/posts/mine`, { - // method: "GET", - // headers: { - // "Content-Type": "application/json", - // "x-page": `${posts.length / 10 + 1}` - // }, - // next: { - // revalidate: 10 - // } - // }) - // const json = await res.json() - // setPosts([...posts, ...json.posts]) - // setHasMorePosts(json.morePosts) - } - fetchPosts() - } - }, - [hasMorePosts] - ) + const showSkeleton = skeleton || searching // eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: address this const onSearch = useCallback( @@ -131,27 +107,18 @@ const PostList = ({ disabled={!posts} style={{ maxWidth: 300 }} aria-label="Search" - value={search} + value={searchValue} />
)} {!posts &&

Failed to load.

} - {searching && ( + {showSkeleton && (
)} - {!searching && posts?.length === 0 && posts && ( -

- No posts found. Create one{" "} - - here - - . -

- )} - {!searching && posts?.length > 0 && ( + {!showSkeleton && posts?.length > 0 && (
    {posts.map((post) => { @@ -168,15 +135,20 @@ const PostList = ({
)} - {!searching && hasMorePosts && !setSearchValue && ( -
- -
- )}
) } export default PostList + +export function NoPostsFound() { + return ( +

+ No posts found. Create one{" "} + + here + + . +

+ ) +} diff --git a/src/app/components/post-list/list-item.tsx b/src/app/components/post-list/list-item.tsx index 9e7353b7..7e1dd25d 100644 --- a/src/app/components/post-list/list-item.tsx +++ b/src/app/components/post-list/list-item.tsx @@ -72,7 +72,7 @@ const ListItem = ({ <>
-

+

{post.title} -

+
diff --git a/src/app/hooks/swr/use-api-tokens.ts b/src/app/hooks/swr/use-api-tokens.ts new file mode 100644 index 00000000..aed8d1c0 --- /dev/null +++ b/src/app/hooks/swr/use-api-tokens.ts @@ -0,0 +1,73 @@ +import { ApiToken } from "@prisma/client" +import useSWR from "swr" + +type ConvertDateToString = { + [P in keyof T]: T[P] extends Date ? string : T[P] +} + +export type SerializedApiToken = ConvertDateToString + +type UseApiTokens = { + userId?: string + initialTokens?: SerializedApiToken[] +} + +const TOKENS_ENDPOINT = "/api/user/tokens" + +export function useApiTokens({ userId, initialTokens }: UseApiTokens) { + const { data, mutate, error, isLoading } = useSWR( + "/api/user/tokens?userId=" + userId, + async (url: string) => { + return fetch(url).then(async (res) => { + const data = await res.json() + if (data.error) { + throw new Error(data.error) + } + + return data + }) + }, + { + refreshInterval: 10000, + fallbackData: initialTokens + } + ) + + async function createToken(newToken: string) { + if (!newToken) { + throw new Error("Token name is required") + } + + const res = await fetch( + `${TOKENS_ENDPOINT}?userId=${userId}&name=${newToken}`, + { + method: "POST" + } + ) + + const response = await res.json() + if (response.error) { + throw new Error(response.error) + return + } + + mutate([...(data || []), response]) + + return response as SerializedApiToken + } + + const expireToken = async (id: string) => { + await fetch(`${TOKENS_ENDPOINT}?userId=${userId}&tokenId=${id}`, { + method: "DELETE" + }) + mutate(data?.filter((token) => token.id !== id)) + } + + return { + data, + isLoading, + error, + createToken, + expireToken + } +} diff --git a/src/lib/hooks/use-debounce.ts b/src/app/hooks/use-debounce.ts similarity index 100% rename from src/lib/hooks/use-debounce.ts rename to src/app/hooks/use-debounce.ts diff --git a/src/lib/hooks/use-trace-route.ts b/src/app/hooks/use-trace-route.ts similarity index 100% rename from src/lib/hooks/use-trace-route.ts rename to src/app/hooks/use-trace-route.ts diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6d05d0d2..8c35e5cf 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,5 @@ import "@styles/globals.css" import { Providers } from "./providers" -// import { ServerThemeProvider } from "@wits/next-themes" import Page from "@components/page" import { Toasts } from "@components/toasts" import Header from "@components/header" @@ -14,13 +13,6 @@ interface RootLayoutProps { export default async function RootLayout({ children }: RootLayoutProps) { return ( - // diff --git a/src/app/lib/copy-to-clipboard.ts b/src/app/lib/copy-to-clipboard.ts new file mode 100644 index 00000000..670e47af --- /dev/null +++ b/src/app/lib/copy-to-clipboard.ts @@ -0,0 +1,3 @@ +export async function copyToClipboard(text: string ) { + await navigator.clipboard.writeText(text) +} diff --git a/src/lib/time-ago.ts b/src/app/lib/time-ago.ts similarity index 100% rename from src/lib/time-ago.ts rename to src/app/lib/time-ago.ts diff --git a/src/app/lib/use-session.ts b/src/app/lib/use-session.ts new file mode 100644 index 00000000..03472c16 --- /dev/null +++ b/src/app/lib/use-session.ts @@ -0,0 +1,17 @@ +// A wrapper around next-auth/use-session that refreshes the page if the session changes + +import { useEffect } from "react" +import { useSession as useSessionOriginal } from "next-auth/react" +import { useRouter } from "next/navigation" + + +export function useSession() { + const { data: session, status } = useSessionOriginal() + const router = useRouter(); + + useEffect(() => { + router.refresh(); + }, [router, status]) + + return { session, status } +} diff --git a/src/app/mine/page.tsx b/src/app/mine/page.tsx index 5a2af622..6db4da9f 100644 --- a/src/app/mine/page.tsx +++ b/src/app/mine/page.tsx @@ -1,13 +1,8 @@ import { redirect } from "next/navigation" -import { getPostsByUser, User } from "@lib/server/prisma" +import { getPostsByUser } from "@lib/server/prisma" import PostList from "@components/post-list" import { getCurrentUser } from "@lib/server/session" import { authOptions } from "@lib/server/auth" -import { cache } from "react" - -const cachedGetPostsByUser = cache( - async (userId: User["id"]) => await getPostsByUser(userId, true) -) export default async function Mine() { const userId = (await getCurrentUser())?.id @@ -16,16 +11,17 @@ export default async function Mine() { return redirect(authOptions.pages?.signIn || "/new") } - const posts = await cachedGetPostsByUser(userId) + const posts = await getPostsByUser(userId, true) - const hasMore = false const stringifiedPosts = JSON.stringify(posts) return ( ) } + +export const revalidate = 0; \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 882669bd..943fe4fd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,9 +1,10 @@ import Image from "next/image" import Card from "@components/card" -import { getWelcomeContent } from "pages/api/welcome" +import { getWelcomeContent } from "src/pages/api/welcome" import DocumentTabs from "./(posts)/components/tabs" import { getAllPosts, Post } from "@lib/server/prisma" -import PostList from "@components/post-list" +import PostList, { NoPostsFound } from "@components/post-list" +import { Suspense } from "react" const getWelcomeData = async () => { const welcomeContent = await getWelcomeContent() @@ -66,8 +67,12 @@ export default async function Page() {

Recent public posts

- {/* @ts-expect-error because of async RSC */} - + } + > + {/* @ts-expect-error because of async RSC */} + +
) @@ -80,25 +85,16 @@ async function PublicPostList({ }) { try { const posts = await getPostsPromise + + if (posts.length === 0) { + return + } + return ( - + ) } catch (error) { - return ( - - ) + return } } diff --git a/src/app/settings/components/sections/api-keys.module.css b/src/app/settings/components/sections/api-keys.module.css index 0422d473..36cbffc5 100644 --- a/src/app/settings/components/sections/api-keys.module.css +++ b/src/app/settings/components/sections/api-keys.module.css @@ -2,36 +2,18 @@ display: flex; flex-direction: column; gap: var(--gap); - max-width: 300px; + max-width: 350px; margin-top: var(--gap); } -.upload { - position: relative; +/* fieldset contains an input and button. I want button to be small next to input */ +.form .fieldset { display: flex; - flex-direction: column; + flex-direction: row; gap: var(--gap); - max-width: 300px; - margin-top: var(--gap); - cursor: pointer; -} - -.uploadInput { - position: absolute; - opacity: 0; - cursor: pointer; - width: 300px; - height: 37px; - cursor: pointer; -} - -.uploadButton { - width: 100%; -} - -/* hover should affect button */ -.uploadInput:hover + button { - border: 1px solid var(--fg); + border: none; + padding: 0; + margin: 0; } .tokens { diff --git a/src/app/settings/components/sections/api-keys.tsx b/src/app/settings/components/sections/api-keys.tsx index b8022460..b5372312 100644 --- a/src/app/settings/components/sections/api-keys.tsx +++ b/src/app/settings/components/sections/api-keys.tsx @@ -5,97 +5,56 @@ import Input from "@components/input" import Note from "@components/note" import { Spinner } from "@components/spinner" import { useToasts } from "@components/toasts" -import { ApiToken } from "@prisma/client" +import { SerializedApiToken, useApiTokens } from "src/app/hooks/swr/use-api-tokens" +import { copyToClipboard } from "src/app/lib/copy-to-clipboard" import { useSession } from "next-auth/react" import { useState } from "react" -import useSWR from "swr" import styles from "./api-keys.module.css" -type ConvertDateToString = { - [P in keyof T]: T[P] extends Date ? string : T[P] -} - -type SerializedApiToken = ConvertDateToString - // need to pass in the accessToken -const APIKeys = ({ tokens: initialTokens }: { tokens?: SerializedApiToken[] }) => { +const APIKeys = ({ + tokens: initialTokens +}: { + tokens?: SerializedApiToken[] +}) => { const session = useSession() const { setToast } = useToasts() - const { data, error, mutate } = useSWR( - "/api/user/tokens?userId=" + session?.data?.user?.id, - { - fetcher: async (url: string) => { - if (session.status === "loading") return initialTokens - - return fetch(url).then(async (res) => { - const data = await res.json() - if (data.error) { - setError(data.error) - return - } else { - setError(undefined) - } - - return data - }) - }, - fallbackData: initialTokens - } - ) + const { data, error, createToken, expireToken } = useApiTokens({ + userId: session.data?.user.id, + initialTokens + }) const [submitting, setSubmitting] = useState(false) const [newToken, setNewToken] = useState("") - const [errorText, setError] = useState() - - const createToken = async (e: React.MouseEvent) => { - e.preventDefault() - if (!newToken) { - return - } - setSubmitting(true) - - const res = await fetch( - `/api/user/tokens?userId=${session.data?.user.id}&name=${newToken}`, - { - method: "POST", - } - ) - - const response = await res.json() - if (response.error) { - setError(response.error) - return - } else { - setError(undefined) - } - - setSubmitting(false) - navigator.clipboard.writeText(response.token) - mutate([...(data || []), response]) - setNewToken("") - setToast({ - message: "Copied to clipboard!", - type: "success" - }) - } - - const expireToken = async (id: string) => { - setSubmitting(true) - await fetch(`/api/user/tokens?userId=${session.data?.user.id}&tokenId=${id}`, { - method: "DELETE", - headers: { - Authorization: "Bearer " + session?.data?.user.sessionToken - } - }) - setSubmitting(false) - mutate(data?.filter((token) => token.id !== id)) - } const onChangeNewToken = (e: React.ChangeEvent) => { setNewToken(e.target.value) } - const hasError = Boolean(error || errorText) + const onCreateTokenClick = async (e: React.MouseEvent) => { + e.preventDefault() + setSubmitting(true) + try { + const createdToken = await createToken(newToken) + setNewToken("") + await copyToClipboard(createdToken?.token || "") + setToast({ + message: "Your new API key has been copied to your clipboard.", + type: "success" + }) + setSubmitting(false) + } catch (e) { + if (e instanceof Error) { + setToast({ + message: e.message, + type: "error" + }) + } + setSubmitting(false) + } + } + + const hasError = Boolean(error) return ( <> {!hasError && ( @@ -103,31 +62,32 @@ const APIKeys = ({ tokens: initialTokens }: { tokens?: SerializedApiToken[] }) = API keys allow you to access the API from 3rd party tools. )} - {hasError && {error?.message || errorText}} - + {hasError && {error?.message}}
-

Create new

- - +
Create new
+
+ + +
{data ? ( data?.length ? ( - +
@@ -144,7 +104,6 @@ const APIKeys = ({ tokens: initialTokens }: { tokens?: SerializedApiToken[] }) = diff --git a/src/app/settings/components/sections/profile.tsx b/src/app/settings/components/sections/profile.tsx index 283bdac0..23c76452 100644 --- a/src/app/settings/components/sections/profile.tsx +++ b/src/app/settings/components/sections/profile.tsx @@ -5,7 +5,7 @@ import Input from "@components/input" import Note from "@components/note" import { useToasts } from "@components/toasts" import { useSession } from "next-auth/react" -import { useState } from "react" +import { useEffect, useState } from "react" import styles from "./profile.module.css" const Profile = () => { @@ -14,6 +14,12 @@ const Profile = () => { const [submitting, setSubmitting] = useState(false) const { setToast } = useToasts() + useEffect(() => { + if (!name) { + setName(session?.user.name || "") + } + }, [name, session]) + const handleNameChange = (e: React.ChangeEvent) => { setName(e.target.value) } @@ -71,7 +77,7 @@ const Profile = () => { return ( <> - This information will be publicly available on your profile. + Your display name is publicly available on your profile.
diff --git a/src/app/settings/layout.tsx b/src/app/settings/layout.tsx index 1119c418..0d66c911 100644 --- a/src/app/settings/layout.tsx +++ b/src/app/settings/layout.tsx @@ -11,7 +11,8 @@ export default function SettingsLayout({ display: "flex", flexDirection: "column", gap: "var(--gap)", - marginBottom: "var(--gap)" + marginBottom: "var(--gap)", + marginTop: "var(--gap)" }} > {children} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index e05c9b96..cec5953b 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,5 +1,5 @@ import SettingsGroup from "../components/settings-group" -import Profile from "app/settings/components/sections/profile" +import Profile from "src/app/settings/components/sections/profile" import APIKeys from "./components/sections/api-keys" export default async function SettingsPage() { diff --git a/src/app/styles/globals.css b/src/app/styles/globals.css index 7f8ad8eb..7212db86 100644 --- a/src/app/styles/globals.css +++ b/src/app/styles/globals.css @@ -175,4 +175,43 @@ textarea { textarea:focus { border-color: var(--light-gray); outline: none; -} \ No newline at end of file +} + +h1 { + font-size: 2.5rem; + margin: 0; +} + +h2 { + font-size: 2rem; + margin: 0; +} + +h3 { + font-size: 1.5rem; + margin: 0; +} + +h4 { + font-size: 1.25rem; + margin: 0; +} + +h5 { + font-size: 1rem; + margin: 0; +} + +h6 { + font-size: 0.875rem; + margin: 0; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 700; +} diff --git a/src/lib/revalidate-page.ts b/src/lib/revalidate-page.ts new file mode 100644 index 00000000..aa17661e --- /dev/null +++ b/src/lib/revalidate-page.ts @@ -0,0 +1,9 @@ +import config from "./config" + +export const revalidatePage = async (path: string) => { + const res = await fetch( + `${process.env.DRIFT_URL}/api/revalidate?secret=${config.nextauth_secret}&path=${path}` + ) + const json = await res.json() + return json +} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 4b473d12..b1a45e38 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -119,6 +119,7 @@ const providers = () => { } }) + console.log("New user created") return newUser } } diff --git a/src/lib/server/prisma.ts b/src/lib/server/prisma.ts index 782a86c7..5dd7ec6b 100644 --- a/src/lib/server/prisma.ts +++ b/src/lib/server/prisma.ts @@ -117,6 +117,7 @@ export async function getPostsByUser(userId: User["id"], withFiles?: boolean) { }) } }) + console.log(posts) return posts } @@ -272,6 +273,7 @@ export const searchPosts = async ( } } }, + authorId: userId, visibility: publicOnly ? "public" : undefined } ] diff --git a/src/lib/server/verify-api-user.ts b/src/lib/server/verify-api-user.ts index 1d72eaa2..10193d1d 100644 --- a/src/lib/server/verify-api-user.ts +++ b/src/lib/server/verify-api-user.ts @@ -46,9 +46,18 @@ const parseAndCheckAuthToken = async (req: NextApiRequest) => { token }, select: { - userId: true + userId: true, + expiresAt: true } }) + if (!user) { + return null + } + + if (user.expiresAt < new Date()) { + return null + } + return user?.userId } diff --git a/src/pages/api/admin/index.ts b/src/pages/api/admin/index.ts index 3a0023e5..63c4d45b 100644 --- a/src/pages/api/admin/index.ts +++ b/src/pages/api/admin/index.ts @@ -1,7 +1,7 @@ import { withMethods } from "@lib/api-middleware/with-methods" import { parseQueryParam } from "@lib/server/parse-query-param" import { NextApiRequest, NextApiResponse } from "next" -import { prisma } from "lib/server/prisma" +import { prisma } from "src/lib/server/prisma" import { getSession } from "next-auth/react" import { deleteUser } from "../user/[id]" diff --git a/src/pages/api/file/raw/[id].ts b/src/pages/api/file/raw/[id].ts index 3715b912..ec2dd564 100644 --- a/src/pages/api/file/raw/[id].ts +++ b/src/pages/api/file/raw/[id].ts @@ -1,5 +1,5 @@ import { NextApiRequest, NextApiResponse } from "next" -import { prisma } from "lib/server/prisma" +import { prisma } from "src/lib/server/prisma" import { parseQueryParam } from "@lib/server/parse-query-param" const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/post/[id].ts b/src/pages/api/post/[id].ts index d95db352..65aac540 100644 --- a/src/pages/api/post/[id].ts +++ b/src/pages/api/post/[id].ts @@ -3,7 +3,7 @@ import { parseQueryParam } from "@lib/server/parse-query-param" import { getPostById } from "@lib/server/prisma" import type { NextApiRequest, NextApiResponse } from "next" import { getSession } from "next-auth/react" -import { prisma } from "lib/server/prisma" +import { prisma } from "src/lib/server/prisma" import * as crypto from "crypto" const handler = async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/src/pages/api/post/index.ts b/src/pages/api/post/index.ts index 7738301e..6f03182a 100644 --- a/src/pages/api/post/index.ts +++ b/src/pages/api/post/index.ts @@ -95,7 +95,6 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) { .catch((error) => { return res.status(500).json(error) }) - return res.json(post) } catch (error) { return res.status(500).json(error) diff --git a/src/pages/api/post/search.ts b/src/pages/api/post/search.ts index 550ee8e2..1b4f74b1 100644 --- a/src/pages/api/post/search.ts +++ b/src/pages/api/post/search.ts @@ -2,12 +2,12 @@ import { withMethods } from "@lib/api-middleware/with-methods" import { parseQueryParam } from "@lib/server/parse-query-param" import { searchPosts, ServerPostWithFiles } from "@lib/server/prisma" import { NextApiRequest, NextApiResponse } from "next" -import { getSession } from "next-auth/react" +import { unstable_getServerSession } from "next-auth" const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { q, userId } = req.query - const session = await getSession() + const session = await unstable_getServerSession() const query = parseQueryParam(q) const user = parseQueryParam(userId) diff --git a/src/pages/api/revalidate.ts b/src/pages/api/revalidate.ts new file mode 100644 index 00000000..7001ed62 --- /dev/null +++ b/src/pages/api/revalidate.ts @@ -0,0 +1,28 @@ +// https://beta.nextjs.org/docs/data-fetching/revalidating#on-demand-revalidation + +import config from "@lib/config" +import { parseQueryParam } from "@lib/server/parse-query-param" +import type { NextApiRequest, NextApiResponse } from "next" + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + // TODO: create a new secret? + if (req.query.secret !== config.nextauth_secret) { + return res.status(401).json({ message: "Invalid token" }) + } + + const path = parseQueryParam(req.query.path) + + try { + if (path) { + await res.revalidate(path) + return res.json({ revalidated: true }) + } + } catch (err) { + // If there was an error, Next.js will continue + // to show the last successfully generated page + return res.status(500).send("Error revalidating") + } +} diff --git a/src/pages/api/user/[id].ts b/src/pages/api/user/[id].ts index 8ea31dfa..c60faf03 100644 --- a/src/pages/api/user/[id].ts +++ b/src/pages/api/user/[id].ts @@ -1,7 +1,7 @@ import { parseQueryParam } from "@lib/server/parse-query-param" import { getUserById } from "@lib/server/prisma" import { NextApiRequest, NextApiResponse } from "next" -import { prisma } from "lib/server/prisma" +import { prisma } from "src/lib/server/prisma" import { withMethods } from "@lib/api-middleware/with-methods" import { getSession } from "next-auth/react" diff --git a/src/tsconfig.json b/tsconfig.json similarity index 93% rename from src/tsconfig.json rename to tsconfig.json index 071e1e7a..eb09d3d7 100644 --- a/src/tsconfig.json +++ b/tsconfig.json @@ -39,13 +39,13 @@ "baseUrl": ".", "paths": { "@components/*": [ - "app/components/*" + "src/app/components/*" ], "@lib/*": [ - "lib/*" + "src/lib/*" ], "@styles/*": [ - "app/styles/*" + "src/app/styles/*" ] } }, diff --git a/src/types/next-auth.d.ts b/types/next-auth.d.ts similarity index 100% rename from src/types/next-auth.d.ts rename to types/next-auth.d.ts
Name