Bug fixes, code cleanup, made root dir /
35
.gitignore
vendored
|
@ -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
|
.vercel
|
||||||
drift.sqlite
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
2
.vscode/settings.json
vendored
|
@ -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,
|
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||||
"dotenv.enableAutocloaking": false
|
"dotenv.enableAutocloaking": false
|
||||||
}
|
}
|
0
src/next-env.d.ts → next-env.d.ts
vendored
|
@ -23,7 +23,6 @@
|
||||||
"@radix-ui/react-tabs": "^1.0.1",
|
"@radix-ui/react-tabs": "^1.0.1",
|
||||||
"@radix-ui/react-tooltip": "^1.0.2",
|
"@radix-ui/react-tooltip": "^1.0.2",
|
||||||
"@wcj/markdown-to-html": "^2.1.2",
|
"@wcj/markdown-to-html": "^2.1.2",
|
||||||
"@wits/next-themes": "0.2.14",
|
|
||||||
"client-only": "^0.0.1",
|
"client-only": "^0.0.1",
|
||||||
"client-zip": "2.2.1",
|
"client-zip": "2.2.1",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
|
@ -71,7 +70,7 @@
|
||||||
"next-unused": {
|
"next-unused": {
|
||||||
"alias": {
|
"alias": {
|
||||||
"@components": "components/",
|
"@components": "components/",
|
||||||
"@lib": "lib/",
|
"@lib": "src/lib/",
|
||||||
"@styles": "styles/"
|
"@styles": "styles/"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
|
@ -79,6 +78,9 @@
|
||||||
"lib"
|
"lib"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"prisma": {
|
||||||
|
"schema": "src/prisma/schema.prisma"
|
||||||
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0"
|
"react-dom": "18.2.0"
|
|
@ -21,7 +21,6 @@ specifiers:
|
||||||
'@typescript-eslint/eslint-plugin': ^5.46.1
|
'@typescript-eslint/eslint-plugin': ^5.46.1
|
||||||
'@typescript-eslint/parser': ^5.46.1
|
'@typescript-eslint/parser': ^5.46.1
|
||||||
'@wcj/markdown-to-html': ^2.1.2
|
'@wcj/markdown-to-html': ^2.1.2
|
||||||
'@wits/next-themes': 0.2.14
|
|
||||||
client-only: ^0.0.1
|
client-only: ^0.0.1
|
||||||
client-zip: 2.2.1
|
client-zip: 2.2.1
|
||||||
clsx: ^1.2.1
|
clsx: ^1.2.1
|
||||||
|
@ -63,7 +62,6 @@ dependencies:
|
||||||
'@radix-ui/react-tabs': 1.0.1_biqbaboplfbrettd7655fr4n2y
|
'@radix-ui/react-tabs': 1.0.1_biqbaboplfbrettd7655fr4n2y
|
||||||
'@radix-ui/react-tooltip': 1.0.2_jbvntnid6ohjelon6ccj5dhg2u
|
'@radix-ui/react-tooltip': 1.0.2_jbvntnid6ohjelon6ccj5dhg2u
|
||||||
'@wcj/markdown-to-html': 2.1.2
|
'@wcj/markdown-to-html': 2.1.2
|
||||||
'@wits/next-themes': 0.2.14_3tcywg5dy5qhcmsv6sy2pt6lua
|
|
||||||
client-only: 0.0.1
|
client-only: 0.0.1
|
||||||
client-zip: 2.2.1
|
client-zip: 2.2.1
|
||||||
jest: 29.3.1_@types+node@17.0.23
|
jest: 29.3.1_@types+node@17.0.23
|
||||||
|
@ -1805,18 +1803,6 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
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:
|
/acorn-jsx/5.3.2_acorn@8.8.1:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 653 B After Width: | Height: | Size: 653 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 930 B After Width: | Height: | Size: 930 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
35
src/.gitignore
vendored
|
@ -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
|
|
4
src/.vscode/settings.json
vendored
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"typescript.tsdk": "node_modules/.pnpm/typescript@4.6.4/node_modules/typescript/lib",
|
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
|
||||||
}
|
|
|
@ -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.
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Auth from "../components"
|
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"
|
import config from "@lib/config"
|
||||||
|
|
||||||
const getPasscode = async () => {
|
const getPasscode = async () => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Popover } from "@components/popover"
|
import { Popover } from "@components/popover"
|
||||||
import { codeFileExtensions } from "@lib/constants"
|
import { codeFileExtensions } from "@lib/constants"
|
||||||
import clsx from "clsx"
|
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 styles from "./dropdown.module.css"
|
||||||
import buttonStyles from "@components/button/button.module.css"
|
import buttonStyles from "@components/button/button.module.css"
|
||||||
import { ChevronDown, Code, File as FileIcon } from "react-feather"
|
import { ChevronDown, Code, File as FileIcon } from "react-feather"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as RadixTabs from "@radix-ui/react-tabs"
|
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 { ChangeEvent, useRef } from "react"
|
||||||
import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor"
|
import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor"
|
||||||
import Preview, { StaticPreview } from "../preview"
|
import Preview, { StaticPreview } from "../preview"
|
||||||
|
|
|
@ -11,7 +11,6 @@ import styles from "./formatting-icons.module.css"
|
||||||
import { TextareaMarkdownRef } from "textarea-markdown-editor"
|
import { TextareaMarkdownRef } from "textarea-markdown-editor"
|
||||||
import Tooltip from "@components/tooltip"
|
import Tooltip from "@components/tooltip"
|
||||||
import Button from "@components/button"
|
import Button from "@components/button"
|
||||||
import ButtonGroup from "@components/button-group"
|
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
// TODO: clean up
|
// TODO: clean up
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { ChangeEvent, useCallback } from "react"
|
||||||
import styles from "./document.module.css"
|
import styles from "./document.module.css"
|
||||||
import Button from "@components/button"
|
import Button from "@components/button"
|
||||||
import Input from "@components/input"
|
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"
|
import { Trash } from "react-feather"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
|
@ -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"
|
import "./react-datepicker.css"
|
||||||
|
|
||||||
const New = () => <NewPost />
|
const New = () => <NewPost />
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import Button from "@components/button"
|
import Button from "@components/button"
|
||||||
import ButtonGroup from "@components/button-group"
|
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 { Edit, ArrowUpCircle, Archive } from "react-feather"
|
||||||
import styles from "./post-buttons.module.css"
|
import styles from "./post-buttons.module.css"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const PostTitle = ({
|
||||||
createdAt,
|
createdAt,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
loading,
|
loading,
|
||||||
authorId
|
// authorId
|
||||||
}: TitleProps) => {
|
}: TitleProps) => {
|
||||||
return (
|
return (
|
||||||
<span className={styles.title}>
|
<span className={styles.title}>
|
||||||
|
@ -33,10 +33,9 @@ export const PostTitle = ({
|
||||||
>
|
>
|
||||||
{title}{" "}
|
{title}{" "}
|
||||||
<span style={{ color: "var(--gray)" }}>
|
<span style={{ color: "var(--gray)" }}>
|
||||||
by{" "}
|
by {/* <Link colored href={`/author/${authorId}`}> */}
|
||||||
<Link colored href={`/author/${authorId}`}>
|
|
||||||
{displayName || "anonymous"}
|
{displayName || "anonymous"}
|
||||||
</Link>
|
{/* </Link> */}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
{!loading && (
|
{!loading && (
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
margin-bottom: var(--gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title .badges {
|
.title .badges {
|
||||||
|
@ -11,12 +12,6 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title h3 {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.titleWithDropdown {
|
.titleWithDropdown {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Button from "@components/button"
|
||||||
import ButtonGroup from "@components/button-group"
|
import ButtonGroup from "@components/button-group"
|
||||||
import Skeleton from "@components/skeleton"
|
import Skeleton from "@components/skeleton"
|
||||||
import Tooltip from "@components/tooltip"
|
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 Link from "next/link"
|
||||||
import { memo } from "react"
|
import { memo } from "react"
|
||||||
import { Download, ExternalLink } from "react-feather"
|
import { Download, ExternalLink } from "react-feather"
|
||||||
|
|
|
@ -14,9 +14,10 @@ async function PostListWrapper({
|
||||||
const data = (await posts).filter((post) => post.visibility === "public")
|
const data = (await posts).filter((post) => post.visibility === "public")
|
||||||
return (
|
return (
|
||||||
<PostList
|
<PostList
|
||||||
morePosts={false}
|
|
||||||
userId={userId}
|
userId={userId}
|
||||||
initialPosts={JSON.stringify(data)}
|
initialPosts={JSON.stringify(data)}
|
||||||
|
hideSearch
|
||||||
|
hideActions
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -62,9 +63,9 @@ export default async function UserPage({
|
||||||
<h1>Public posts by {user?.displayName || "Anonymous"}</h1>
|
<h1>Public posts by {user?.displayName || "Anonymous"}</h1>
|
||||||
<Avatar />
|
<Avatar />
|
||||||
</div>
|
</div>
|
||||||
<Suspense fallback={<PostList initialPosts={JSON.stringify({})} />}>
|
<Suspense fallback={<PostList hideSearch skeleton initialPosts={[]} />}>
|
||||||
{/* @ts-expect-error because TS async JSX support is iffy */}
|
{/* @ts-expect-error because TS async JSX support is iffy */}
|
||||||
<PostListWrapper posts={posts} userId={id} />
|
<PostListWrapper hideSearch posts={posts} userId={id} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,12 +3,12 @@ import styles from "./badge.module.css"
|
||||||
type BadgeProps = {
|
type BadgeProps = {
|
||||||
type: "primary" | "secondary" | "error" | "warning"
|
type: "primary" | "secondary" | "error" | "warning"
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
} & React.HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||||
({ type, children }: BadgeProps, ref) => {
|
({ type, children, ...rest}: BadgeProps, ref) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container} {...rest}>
|
||||||
<div className={`${styles.badge} ${styles[type]}`} ref={ref}>
|
<div className={`${styles.badge} ${styles[type]}`} ref={ref}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
import { useToasts } from "@components/toasts"
|
||||||
import Tooltip from "@components/tooltip"
|
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 { useMemo, useState, useEffect } from "react"
|
||||||
import Badge from "../badge"
|
import Badge from "../badge"
|
||||||
|
|
||||||
|
@ -8,6 +10,7 @@ const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
|
||||||
const createdDate = useMemo(() => new Date(createdAt), [createdAt])
|
const createdDate = useMemo(() => new Date(createdAt), [createdAt])
|
||||||
const [time, setTimeAgo] = useState(timeAgo(createdDate))
|
const [time, setTimeAgo] = useState(timeAgo(createdDate))
|
||||||
|
|
||||||
|
const { setToast } = useToasts()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setTimeAgo(timeAgo(createdDate))
|
setTimeAgo(timeAgo(createdDate))
|
||||||
|
@ -15,11 +18,19 @@ const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [createdDate])
|
}, [createdDate])
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
copyToClipboard(createdDate.toISOString())
|
||||||
|
setToast({
|
||||||
|
message: "Copied to clipboard",
|
||||||
|
type: "success"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const formattedTime = `${createdDate.toLocaleDateString()} ${createdDate.toLocaleTimeString()}`
|
const formattedTime = `${createdDate.toLocaleDateString()} ${createdDate.toLocaleTimeString()}`
|
||||||
return (
|
return (
|
||||||
// TODO: investigate tooltip not showing
|
// TODO: investigate tooltip not showing
|
||||||
<Tooltip content={formattedTime}>
|
<Tooltip content={formattedTime}>
|
||||||
<Badge type="secondary">
|
<Badge type="secondary" onClick={onClick}>
|
||||||
{" "}
|
{" "}
|
||||||
<>{time}</>
|
<>{time}</>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Tooltip from "@components/tooltip"
|
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 { useEffect, useMemo, useState } from "react"
|
||||||
import Badge from "../badge"
|
import Badge from "../badge"
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,11 @@ const VisibilityControl = ({ authorId, postId, visibility: postVisibility }: Pro
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
setVisibility(json.visibility)
|
setVisibility(json.visibility)
|
||||||
router.refresh()
|
router.refresh()
|
||||||
|
setToast({
|
||||||
|
message: "Visibility updated",
|
||||||
|
type: "success"
|
||||||
|
})
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
setToast({
|
setToast({
|
||||||
message: "An error occurred",
|
message: "An error occurred",
|
||||||
|
@ -47,7 +52,7 @@ const VisibilityControl = ({ authorId, postId, visibility: postVisibility }: Pro
|
||||||
setPasswordModalVisible(false)
|
setPasswordModalVisible(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[postId, setToast, setVisibility]
|
[postId, router, setToast]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
opacity: .5;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header:not(.loading) {
|
.header:not(.loading) {
|
||||||
|
|
|
@ -3,66 +3,42 @@
|
||||||
import styles from "./post-list.module.css"
|
import styles from "./post-list.module.css"
|
||||||
import ListItem from "./list-item"
|
import ListItem from "./list-item"
|
||||||
import { ChangeEvent, useCallback, useState } from "react"
|
import { ChangeEvent, useCallback, useState } from "react"
|
||||||
import Link from "@components/link"
|
|
||||||
import type { PostWithFiles } from "@lib/server/prisma"
|
import type { PostWithFiles } from "@lib/server/prisma"
|
||||||
import Input from "@components/input"
|
import Input from "@components/input"
|
||||||
import Button from "@components/button"
|
|
||||||
import { useToasts } from "@components/toasts"
|
import { useToasts } from "@components/toasts"
|
||||||
import { ListItemSkeleton } from "./list-item-skeleton"
|
import { ListItemSkeleton } from "./list-item-skeleton"
|
||||||
|
import Link from "@components/link"
|
||||||
import debounce from "lodash.debounce"
|
import debounce from "lodash.debounce"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialPosts: string | PostWithFiles[]
|
initialPosts: string | PostWithFiles[]
|
||||||
morePosts?: boolean
|
morePosts?: boolean
|
||||||
userId?: string
|
|
||||||
hideSearch?: boolean
|
hideSearch?: boolean
|
||||||
hideActions?: boolean
|
hideActions?: boolean
|
||||||
isOwner?: boolean
|
isOwner?: boolean
|
||||||
|
skeleton?: boolean
|
||||||
|
searchValue?: string
|
||||||
|
userId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const PostList = ({
|
const PostList = ({
|
||||||
morePosts,
|
|
||||||
initialPosts: initialPostsMaybeJSON,
|
initialPosts: initialPostsMaybeJSON,
|
||||||
userId,
|
|
||||||
hideSearch,
|
hideSearch,
|
||||||
hideActions,
|
hideActions,
|
||||||
isOwner
|
isOwner,
|
||||||
|
skeleton,
|
||||||
|
userId
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const initialPosts =
|
const initialPosts =
|
||||||
typeof initialPostsMaybeJSON === "string"
|
typeof initialPostsMaybeJSON === "string"
|
||||||
? JSON.parse(initialPostsMaybeJSON)
|
? JSON.parse(initialPostsMaybeJSON)
|
||||||
: initialPostsMaybeJSON
|
: initialPostsMaybeJSON
|
||||||
const [search, setSearchValue] = useState("")
|
const [searchValue, setSearchValue] = useState("")
|
||||||
const [posts, setPosts] = useState<PostWithFiles[]>(initialPosts)
|
|
||||||
const [searching, setSearching] = useState(false)
|
const [searching, setSearching] = useState(false)
|
||||||
const [hasMorePosts] = useState(morePosts)
|
const [posts, setPosts] = useState<PostWithFiles[]>(initialPosts)
|
||||||
const { setToast } = useToasts()
|
const { setToast } = useToasts()
|
||||||
|
|
||||||
const loadMoreClick = useCallback(
|
const showSkeleton = skeleton || searching
|
||||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
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]
|
|
||||||
)
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: address this
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- TODO: address this
|
||||||
const onSearch = useCallback(
|
const onSearch = useCallback(
|
||||||
|
@ -131,27 +107,18 @@ const PostList = ({
|
||||||
disabled={!posts}
|
disabled={!posts}
|
||||||
style={{ maxWidth: 300 }}
|
style={{ maxWidth: 300 }}
|
||||||
aria-label="Search"
|
aria-label="Search"
|
||||||
value={search}
|
value={searchValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!posts && <p style={{ color: "var(--warning)" }}>Failed to load.</p>}
|
{!posts && <p style={{ color: "var(--warning)" }}>Failed to load.</p>}
|
||||||
{searching && (
|
{showSkeleton && (
|
||||||
<ul>
|
<ul>
|
||||||
<ListItemSkeleton />
|
<ListItemSkeleton />
|
||||||
<ListItemSkeleton />
|
<ListItemSkeleton />
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
{!searching && posts?.length === 0 && posts && (
|
{!showSkeleton && posts?.length > 0 && (
|
||||||
<p>
|
|
||||||
No posts found. Create one{" "}
|
|
||||||
<Link colored href="/new">
|
|
||||||
here
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{!searching && posts?.length > 0 && (
|
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
{posts.map((post) => {
|
{posts.map((post) => {
|
||||||
|
@ -168,15 +135,20 @@ const PostList = ({
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!searching && hasMorePosts && !setSearchValue && (
|
|
||||||
<div>
|
|
||||||
<Button width={"100%"} onClick={loadMoreClick}>
|
|
||||||
Load more
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PostList
|
export default PostList
|
||||||
|
|
||||||
|
export function NoPostsFound() {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
No posts found. Create one{" "}
|
||||||
|
<Link colored href="/new">
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ const ListItem = ({
|
||||||
<>
|
<>
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
<span className={styles.titleText}>
|
<span className={styles.titleText}>
|
||||||
<h3 style={{ display: "inline-block", margin: 0 }}>
|
<h4 style={{ display: "inline-block", margin: 0 }}>
|
||||||
<Link
|
<Link
|
||||||
colored
|
colored
|
||||||
style={{ marginRight: "var(--gap)" }}
|
style={{ marginRight: "var(--gap)" }}
|
||||||
|
@ -80,7 +80,7 @@ const ListItem = ({
|
||||||
>
|
>
|
||||||
{post.title}
|
{post.title}
|
||||||
</Link>
|
</Link>
|
||||||
</h3>
|
</h4>
|
||||||
<div className={styles.badges}>
|
<div className={styles.badges}>
|
||||||
<VisibilityBadge visibility={post.visibility} />
|
<VisibilityBadge visibility={post.visibility} />
|
||||||
<Badge type="secondary">
|
<Badge type="secondary">
|
||||||
|
|
73
src/app/hooks/swr/use-api-tokens.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { ApiToken } from "@prisma/client"
|
||||||
|
import useSWR from "swr"
|
||||||
|
|
||||||
|
type ConvertDateToString<T> = {
|
||||||
|
[P in keyof T]: T[P] extends Date ? string : T[P]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SerializedApiToken = ConvertDateToString<ApiToken>
|
||||||
|
|
||||||
|
type UseApiTokens = {
|
||||||
|
userId?: string
|
||||||
|
initialTokens?: SerializedApiToken[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOKENS_ENDPOINT = "/api/user/tokens"
|
||||||
|
|
||||||
|
export function useApiTokens({ userId, initialTokens }: UseApiTokens) {
|
||||||
|
const { data, mutate, error, isLoading } = useSWR<SerializedApiToken[]>(
|
||||||
|
"/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
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import "@styles/globals.css"
|
import "@styles/globals.css"
|
||||||
import { Providers } from "./providers"
|
import { Providers } from "./providers"
|
||||||
// import { ServerThemeProvider } from "@wits/next-themes"
|
|
||||||
import Page from "@components/page"
|
import Page from "@components/page"
|
||||||
import { Toasts } from "@components/toasts"
|
import { Toasts } from "@components/toasts"
|
||||||
import Header from "@components/header"
|
import Header from "@components/header"
|
||||||
|
@ -14,13 +13,6 @@ interface RootLayoutProps {
|
||||||
|
|
||||||
export default async function RootLayout({ children }: RootLayoutProps) {
|
export default async function RootLayout({ children }: RootLayoutProps) {
|
||||||
return (
|
return (
|
||||||
// <ServerThemeProvider
|
|
||||||
// enableSystem={true}
|
|
||||||
// disableTransitionOnChange
|
|
||||||
// cookieName={"drift-theme"}
|
|
||||||
// attribute="data-theme"
|
|
||||||
// enableColorScheme={true}
|
|
||||||
// >
|
|
||||||
<html lang="en" className={inter.variable}>
|
<html lang="en" className={inter.variable}>
|
||||||
<head />
|
<head />
|
||||||
<body>
|
<body>
|
||||||
|
|
3
src/app/lib/copy-to-clipboard.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export async function copyToClipboard(text: string ) {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
}
|
17
src/app/lib/use-session.ts
Normal file
|
@ -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 }
|
||||||
|
}
|
|
@ -1,13 +1,8 @@
|
||||||
import { redirect } from "next/navigation"
|
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 PostList from "@components/post-list"
|
||||||
import { getCurrentUser } from "@lib/server/session"
|
import { getCurrentUser } from "@lib/server/session"
|
||||||
import { authOptions } from "@lib/server/auth"
|
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() {
|
export default async function Mine() {
|
||||||
const userId = (await getCurrentUser())?.id
|
const userId = (await getCurrentUser())?.id
|
||||||
|
@ -16,16 +11,17 @@ export default async function Mine() {
|
||||||
return redirect(authOptions.pages?.signIn || "/new")
|
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)
|
const stringifiedPosts = JSON.stringify(posts)
|
||||||
return (
|
return (
|
||||||
<PostList
|
<PostList
|
||||||
userId={userId}
|
userId={userId}
|
||||||
morePosts={hasMore}
|
|
||||||
initialPosts={stringifiedPosts}
|
initialPosts={stringifiedPosts}
|
||||||
isOwner={true}
|
isOwner={true}
|
||||||
|
hideSearch={false}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const revalidate = 0;
|
|
@ -1,9 +1,10 @@
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Card from "@components/card"
|
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 DocumentTabs from "./(posts)/components/tabs"
|
||||||
import { getAllPosts, Post } from "@lib/server/prisma"
|
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 getWelcomeData = async () => {
|
||||||
const welcomeContent = await getWelcomeContent()
|
const welcomeContent = await getWelcomeContent()
|
||||||
|
@ -66,8 +67,12 @@ export default async function Page() {
|
||||||
</Card>
|
</Card>
|
||||||
<div>
|
<div>
|
||||||
<h2>Recent public posts</h2>
|
<h2>Recent public posts</h2>
|
||||||
|
<Suspense
|
||||||
|
fallback={<PostList skeleton hideSearch initialPosts={JSON.stringify({})} />}
|
||||||
|
>
|
||||||
{/* @ts-expect-error because of async RSC */}
|
{/* @ts-expect-error because of async RSC */}
|
||||||
<PublicPostList getPostsPromise={getPostsPromise} />
|
<PublicPostList getPostsPromise={getPostsPromise} />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -80,25 +85,16 @@ async function PublicPostList({
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const posts = await getPostsPromise
|
const posts = await getPostsPromise
|
||||||
|
|
||||||
|
if (posts.length === 0) {
|
||||||
|
return <NoPostsFound />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PostList
|
<PostList initialPosts={JSON.stringify(posts)} hideActions hideSearch />
|
||||||
userId={undefined}
|
|
||||||
morePosts={false}
|
|
||||||
initialPosts={JSON.stringify(posts)}
|
|
||||||
hideActions
|
|
||||||
hideSearch
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return (
|
return <NoPostsFound />
|
||||||
<PostList
|
|
||||||
userId={undefined}
|
|
||||||
morePosts={false}
|
|
||||||
initialPosts={[]}
|
|
||||||
hideActions
|
|
||||||
hideSearch
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,36 +2,18 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap);
|
gap: var(--gap);
|
||||||
max-width: 300px;
|
max-width: 350px;
|
||||||
margin-top: var(--gap);
|
margin-top: var(--gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload {
|
/* fieldset contains an input and button. I want button to be small next to input */
|
||||||
position: relative;
|
.form .fieldset {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
gap: var(--gap);
|
gap: var(--gap);
|
||||||
max-width: 300px;
|
border: none;
|
||||||
margin-top: var(--gap);
|
padding: 0;
|
||||||
cursor: pointer;
|
margin: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tokens {
|
.tokens {
|
||||||
|
|
|
@ -5,97 +5,56 @@ import Input from "@components/input"
|
||||||
import Note from "@components/note"
|
import Note from "@components/note"
|
||||||
import { Spinner } from "@components/spinner"
|
import { Spinner } from "@components/spinner"
|
||||||
import { useToasts } from "@components/toasts"
|
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 { useSession } from "next-auth/react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import useSWR from "swr"
|
|
||||||
import styles from "./api-keys.module.css"
|
import styles from "./api-keys.module.css"
|
||||||
|
|
||||||
type ConvertDateToString<T> = {
|
|
||||||
[P in keyof T]: T[P] extends Date ? string : T[P]
|
|
||||||
}
|
|
||||||
|
|
||||||
type SerializedApiToken = ConvertDateToString<ApiToken>
|
|
||||||
|
|
||||||
// need to pass in the accessToken
|
// need to pass in the accessToken
|
||||||
const APIKeys = ({ tokens: initialTokens }: { tokens?: SerializedApiToken[] }) => {
|
const APIKeys = ({
|
||||||
|
tokens: initialTokens
|
||||||
|
}: {
|
||||||
|
tokens?: SerializedApiToken[]
|
||||||
|
}) => {
|
||||||
const session = useSession()
|
const session = useSession()
|
||||||
const { setToast } = useToasts()
|
const { setToast } = useToasts()
|
||||||
const { data, error, mutate } = useSWR<SerializedApiToken[]>(
|
const { data, error, createToken, expireToken } = useApiTokens({
|
||||||
"/api/user/tokens?userId=" + session?.data?.user?.id,
|
userId: session.data?.user.id,
|
||||||
{
|
initialTokens
|
||||||
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 [submitting, setSubmitting] = useState<boolean>(false)
|
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||||
const [newToken, setNewToken] = useState<string>("")
|
const [newToken, setNewToken] = useState<string>("")
|
||||||
const [errorText, setError] = useState<string>()
|
|
||||||
|
|
||||||
const createToken = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
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<HTMLInputElement>) => {
|
const onChangeNewToken = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setNewToken(e.target.value)
|
setNewToken(e.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasError = Boolean(error || errorText)
|
const onCreateTokenClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{!hasError && (
|
{!hasError && (
|
||||||
|
@ -103,10 +62,10 @@ const APIKeys = ({ tokens: initialTokens }: { tokens?: SerializedApiToken[] }) =
|
||||||
API keys allow you to access the API from 3rd party tools.
|
API keys allow you to access the API from 3rd party tools.
|
||||||
</Note>
|
</Note>
|
||||||
)}
|
)}
|
||||||
{hasError && <Note type="error">{error?.message || errorText}</Note>}
|
{hasError && <Note type="error">{error?.message}</Note>}
|
||||||
|
|
||||||
<form className={styles.form}>
|
<form className={styles.form}>
|
||||||
<h3>Create new</h3>
|
<h5>Create new</h5>
|
||||||
|
<fieldset className={styles.fieldset}>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={newToken}
|
value={newToken}
|
||||||
|
@ -116,18 +75,19 @@ const APIKeys = ({ tokens: initialTokens }: { tokens?: SerializedApiToken[] }) =
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={createToken}
|
onClick={onCreateTokenClick}
|
||||||
loading={submitting}
|
loading={submitting}
|
||||||
disabled={!newToken}
|
disabled={!newToken}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className={styles.tokens}>
|
<div className={styles.tokens}>
|
||||||
{data ? (
|
{data ? (
|
||||||
data?.length ? (
|
data?.length ? (
|
||||||
<table width={'100%'}>
|
<table width={"100%"}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
@ -144,7 +104,6 @@ const APIKeys = ({ tokens: initialTokens }: { tokens?: SerializedApiToken[] }) =
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => expireToken(token.id)}
|
onClick={() => expireToken(token.id)}
|
||||||
loading={submitting}
|
|
||||||
>
|
>
|
||||||
Revoke
|
Revoke
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Input from "@components/input"
|
||||||
import Note from "@components/note"
|
import Note from "@components/note"
|
||||||
import { useToasts } from "@components/toasts"
|
import { useToasts } from "@components/toasts"
|
||||||
import { useSession } from "next-auth/react"
|
import { useSession } from "next-auth/react"
|
||||||
import { useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import styles from "./profile.module.css"
|
import styles from "./profile.module.css"
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
|
@ -14,6 +14,12 @@ const Profile = () => {
|
||||||
const [submitting, setSubmitting] = useState<boolean>(false)
|
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||||
const { setToast } = useToasts()
|
const { setToast } = useToasts()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!name) {
|
||||||
|
setName(session?.user.name || "")
|
||||||
|
}
|
||||||
|
}, [name, session])
|
||||||
|
|
||||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setName(e.target.value)
|
setName(e.target.value)
|
||||||
}
|
}
|
||||||
|
@ -71,7 +77,7 @@ const Profile = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Note type="warning">
|
<Note type="warning">
|
||||||
This information will be publicly available on your profile.
|
Your display name is publicly available on your profile.
|
||||||
</Note>
|
</Note>
|
||||||
<form onSubmit={onSubmit} className={styles.form}>
|
<form onSubmit={onSubmit} className={styles.form}>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -11,7 +11,8 @@ export default function SettingsLayout({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "var(--gap)",
|
gap: "var(--gap)",
|
||||||
marginBottom: "var(--gap)"
|
marginBottom: "var(--gap)",
|
||||||
|
marginTop: "var(--gap)"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import SettingsGroup from "../components/settings-group"
|
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"
|
import APIKeys from "./components/sections/api-keys"
|
||||||
|
|
||||||
export default async function SettingsPage() {
|
export default async function SettingsPage() {
|
||||||
|
|
|
@ -176,3 +176,42 @@ textarea:focus {
|
||||||
border-color: var(--light-gray);
|
border-color: var(--light-gray);
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
9
src/lib/revalidate-page.ts
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -119,6 +119,7 @@ const providers = () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log("New user created")
|
||||||
return newUser
|
return newUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,6 +117,7 @@ export async function getPostsByUser(userId: User["id"], withFiles?: boolean) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
console.log(posts)
|
||||||
|
|
||||||
return posts
|
return posts
|
||||||
}
|
}
|
||||||
|
@ -272,6 +273,7 @@ export const searchPosts = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
authorId: userId,
|
||||||
visibility: publicOnly ? "public" : undefined
|
visibility: publicOnly ? "public" : undefined
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -46,9 +46,18 @@ const parseAndCheckAuthToken = async (req: NextApiRequest) => {
|
||||||
token
|
token
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
userId: true
|
userId: true,
|
||||||
|
expiresAt: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.expiresAt < new Date()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return user?.userId
|
return user?.userId
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { withMethods } from "@lib/api-middleware/with-methods"
|
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
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 { getSession } from "next-auth/react"
|
||||||
import { deleteUser } from "../user/[id]"
|
import { deleteUser } from "../user/[id]"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
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"
|
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
|
|
||||||
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
import { getPostById } from "@lib/server/prisma"
|
import { getPostById } from "@lib/server/prisma"
|
||||||
import type { NextApiRequest, NextApiResponse } from "next"
|
import type { NextApiRequest, NextApiResponse } from "next"
|
||||||
import { getSession } from "next-auth/react"
|
import { getSession } from "next-auth/react"
|
||||||
import { prisma } from "lib/server/prisma"
|
import { prisma } from "src/lib/server/prisma"
|
||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
|
|
@ -95,7 +95,6 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<unknown>) {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
return res.status(500).json(error)
|
return res.status(500).json(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json(post)
|
return res.json(post)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(500).json(error)
|
return res.status(500).json(error)
|
||||||
|
|
|
@ -2,12 +2,12 @@ import { withMethods } from "@lib/api-middleware/with-methods"
|
||||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
import { searchPosts, ServerPostWithFiles } from "@lib/server/prisma"
|
import { searchPosts, ServerPostWithFiles } from "@lib/server/prisma"
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
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 handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
const { q, userId } = req.query
|
const { q, userId } = req.query
|
||||||
|
|
||||||
const session = await getSession()
|
const session = await unstable_getServerSession()
|
||||||
|
|
||||||
const query = parseQueryParam(q)
|
const query = parseQueryParam(q)
|
||||||
const user = parseQueryParam(userId)
|
const user = parseQueryParam(userId)
|
||||||
|
|
28
src/pages/api/revalidate.ts
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
import { getUserById } from "@lib/server/prisma"
|
import { getUserById } from "@lib/server/prisma"
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
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 { withMethods } from "@lib/api-middleware/with-methods"
|
||||||
import { getSession } from "next-auth/react"
|
import { getSession } from "next-auth/react"
|
||||||
|
|
||||||
|
|
|
@ -39,13 +39,13 @@
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@components/*": [
|
"@components/*": [
|
||||||
"app/components/*"
|
"src/app/components/*"
|
||||||
],
|
],
|
||||||
"@lib/*": [
|
"@lib/*": [
|
||||||
"lib/*"
|
"src/lib/*"
|
||||||
],
|
],
|
||||||
"@styles/*": [
|
"@styles/*": [
|
||||||
"app/styles/*"
|
"src/app/styles/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|