Bug fixes, code cleanup, made root dir /
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"root": true,
|
||||
"ignorePatterns": ["node_modules/", "__tests__/"]
|
||||
}
|
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
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": false,
|
||||
"printWidth": 80,
|
||||
"useTabs": true
|
||||
}
|
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 { getRequiresPasscode } from "pages/api/auth/requires-passcode"
|
||||
import { getRequiresPasscode } from "src/pages/api/auth/requires-passcode"
|
||||
import config from "@lib/config"
|
||||
|
||||
const getPasscode = async () => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -14,5 +14,5 @@
|
|||
}
|
||||
|
||||
.labelAndInput {
|
||||
font-size: 1.2rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
|
|
@ -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 = () => <NewPost />
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -22,7 +22,7 @@ export const PostTitle = ({
|
|||
createdAt,
|
||||
expiresAt,
|
||||
loading,
|
||||
authorId
|
||||
// authorId
|
||||
}: TitleProps) => {
|
||||
return (
|
||||
<span className={styles.title}>
|
||||
|
@ -33,10 +33,9 @@ export const PostTitle = ({
|
|||
>
|
||||
{title}{" "}
|
||||
<span style={{ color: "var(--gray)" }}>
|
||||
by{" "}
|
||||
<Link colored href={`/author/${authorId}`}>
|
||||
{displayName || "anonymous"}
|
||||
</Link>
|
||||
by {/* <Link colored href={`/author/${authorId}`}> */}
|
||||
{displayName || "anonymous"}
|
||||
{/* </Link> */}
|
||||
</span>
|
||||
</h1>
|
||||
{!loading && (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -14,9 +14,10 @@ async function PostListWrapper({
|
|||
const data = (await posts).filter((post) => post.visibility === "public")
|
||||
return (
|
||||
<PostList
|
||||
morePosts={false}
|
||||
userId={userId}
|
||||
initialPosts={JSON.stringify(data)}
|
||||
hideSearch
|
||||
hideActions
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -62,9 +63,9 @@ export default async function UserPage({
|
|||
<h1>Public posts by {user?.displayName || "Anonymous"}</h1>
|
||||
<Avatar />
|
||||
</div>
|
||||
<Suspense fallback={<PostList initialPosts={JSON.stringify({})} />}>
|
||||
<Suspense fallback={<PostList hideSearch skeleton initialPosts={[]} />}>
|
||||
{/* @ts-expect-error because TS async JSX support is iffy */}
|
||||
<PostListWrapper posts={posts} userId={id} />
|
||||
<PostListWrapper hideSearch posts={posts} userId={id} />
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -3,12 +3,12 @@ import styles from "./badge.module.css"
|
|||
type BadgeProps = {
|
||||
type: "primary" | "secondary" | "error" | "warning"
|
||||
children: React.ReactNode
|
||||
}
|
||||
} & React.HTMLAttributes<HTMLDivElement>
|
||||
|
||||
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
||||
({ type, children }: BadgeProps, ref) => {
|
||||
({ type, children, ...rest}: BadgeProps, ref) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.container} {...rest}>
|
||||
<div className={`${styles.badge} ${styles[type]}`} ref={ref}>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
<Tooltip content={formattedTime}>
|
||||
<Badge type="secondary">
|
||||
<Badge type="secondary" onClick={onClick}>
|
||||
{" "}
|
||||
<>{time}</>
|
||||
</Badge>
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
|
||||
.header {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
opacity: .5;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.header:not(.loading) {
|
||||
|
|
|
@ -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<PostWithFiles[]>(initialPosts)
|
||||
const [searchValue, setSearchValue] = useState("")
|
||||
const [searching, setSearching] = useState(false)
|
||||
const [hasMorePosts] = useState(morePosts)
|
||||
const [posts, setPosts] = useState<PostWithFiles[]>(initialPosts)
|
||||
const { setToast } = useToasts()
|
||||
|
||||
const loadMoreClick = useCallback(
|
||||
(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]
|
||||
)
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!posts && <p style={{ color: "var(--warning)" }}>Failed to load.</p>}
|
||||
{searching && (
|
||||
{showSkeleton && (
|
||||
<ul>
|
||||
<ListItemSkeleton />
|
||||
<ListItemSkeleton />
|
||||
</ul>
|
||||
)}
|
||||
{!searching && posts?.length === 0 && posts && (
|
||||
<p>
|
||||
No posts found. Create one{" "}
|
||||
<Link colored href="/new">
|
||||
here
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
{!searching && posts?.length > 0 && (
|
||||
{!showSkeleton && posts?.length > 0 && (
|
||||
<div>
|
||||
<ul>
|
||||
{posts.map((post) => {
|
||||
|
@ -168,15 +135,20 @@ const PostList = ({
|
|||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{!searching && hasMorePosts && !setSearchValue && (
|
||||
<div>
|
||||
<Button width={"100%"} onClick={loadMoreClick}>
|
||||
Load more
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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}>
|
||||
<span className={styles.titleText}>
|
||||
<h3 style={{ display: "inline-block", margin: 0 }}>
|
||||
<h4 style={{ display: "inline-block", margin: 0 }}>
|
||||
<Link
|
||||
colored
|
||||
style={{ marginRight: "var(--gap)" }}
|
||||
|
@ -80,7 +80,7 @@ const ListItem = ({
|
|||
>
|
||||
{post.title}
|
||||
</Link>
|
||||
</h3>
|
||||
</h4>
|
||||
<div className={styles.badges}>
|
||||
<VisibilityBadge visibility={post.visibility} />
|
||||
<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 { 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 (
|
||||
// <ServerThemeProvider
|
||||
// enableSystem={true}
|
||||
// disableTransitionOnChange
|
||||
// cookieName={"drift-theme"}
|
||||
// attribute="data-theme"
|
||||
// enableColorScheme={true}
|
||||
// >
|
||||
<html lang="en" className={inter.variable}>
|
||||
<head />
|
||||
<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 { 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 (
|
||||
<PostList
|
||||
userId={userId}
|
||||
morePosts={hasMore}
|
||||
initialPosts={stringifiedPosts}
|
||||
isOwner={true}
|
||||
hideSearch={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const revalidate = 0;
|
|
@ -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() {
|
|||
</Card>
|
||||
<div>
|
||||
<h2>Recent public posts</h2>
|
||||
{/* @ts-expect-error because of async RSC */}
|
||||
<PublicPostList getPostsPromise={getPostsPromise} />
|
||||
<Suspense
|
||||
fallback={<PostList skeleton hideSearch initialPosts={JSON.stringify({})} />}
|
||||
>
|
||||
{/* @ts-expect-error because of async RSC */}
|
||||
<PublicPostList getPostsPromise={getPostsPromise} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -80,25 +85,16 @@ async function PublicPostList({
|
|||
}) {
|
||||
try {
|
||||
const posts = await getPostsPromise
|
||||
|
||||
if (posts.length === 0) {
|
||||
return <NoPostsFound />
|
||||
}
|
||||
|
||||
return (
|
||||
<PostList
|
||||
userId={undefined}
|
||||
morePosts={false}
|
||||
initialPosts={JSON.stringify(posts)}
|
||||
hideActions
|
||||
hideSearch
|
||||
/>
|
||||
<PostList initialPosts={JSON.stringify(posts)} hideActions hideSearch />
|
||||
)
|
||||
} catch (error) {
|
||||
return (
|
||||
<PostList
|
||||
userId={undefined}
|
||||
morePosts={false}
|
||||
initialPosts={[]}
|
||||
hideActions
|
||||
hideSearch
|
||||
/>
|
||||
)
|
||||
return <NoPostsFound />
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<T> = {
|
||||
[P in keyof T]: T[P] extends Date ? string : T[P]
|
||||
}
|
||||
|
||||
type SerializedApiToken = ConvertDateToString<ApiToken>
|
||||
|
||||
// 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<SerializedApiToken[]>(
|
||||
"/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<boolean>(false)
|
||||
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>) => {
|
||||
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 (
|
||||
<>
|
||||
{!hasError && (
|
||||
|
@ -103,31 +62,32 @@ const APIKeys = ({ tokens: initialTokens }: { tokens?: SerializedApiToken[] }) =
|
|||
API keys allow you to access the API from 3rd party tools.
|
||||
</Note>
|
||||
)}
|
||||
{hasError && <Note type="error">{error?.message || errorText}</Note>}
|
||||
|
||||
{hasError && <Note type="error">{error?.message}</Note>}
|
||||
<form className={styles.form}>
|
||||
<h3>Create new</h3>
|
||||
<Input
|
||||
type="text"
|
||||
value={newToken}
|
||||
onChange={onChangeNewToken}
|
||||
aria-label="API Key name"
|
||||
placeholder="Name"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={createToken}
|
||||
loading={submitting}
|
||||
disabled={!newToken}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<h5>Create new</h5>
|
||||
<fieldset className={styles.fieldset}>
|
||||
<Input
|
||||
type="text"
|
||||
value={newToken}
|
||||
onChange={onChangeNewToken}
|
||||
aria-label="API Key name"
|
||||
placeholder="Name"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onCreateTokenClick}
|
||||
loading={submitting}
|
||||
disabled={!newToken}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<div className={styles.tokens}>
|
||||
{data ? (
|
||||
data?.length ? (
|
||||
<table width={'100%'}>
|
||||
<table width={"100%"}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
|
@ -144,7 +104,6 @@ const APIKeys = ({ tokens: initialTokens }: { tokens?: SerializedApiToken[] }) =
|
|||
<Button
|
||||
type="button"
|
||||
onClick={() => expireToken(token.id)}
|
||||
loading={submitting}
|
||||
>
|
||||
Revoke
|
||||
</Button>
|
||||
|
|
|
@ -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<boolean>(false)
|
||||
const { setToast } = useToasts()
|
||||
|
||||
useEffect(() => {
|
||||
if (!name) {
|
||||
setName(session?.user.name || "")
|
||||
}
|
||||
}, [name, session])
|
||||
|
||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(e.target.value)
|
||||
}
|
||||
|
@ -71,7 +77,7 @@ const Profile = () => {
|
|||
return (
|
||||
<>
|
||||
<Note type="warning">
|
||||
This information will be publicly available on your profile.
|
||||
Your display name is publicly available on your profile.
|
||||
</Note>
|
||||
<form onSubmit={onSubmit} className={styles.form}>
|
||||
<div>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -175,4 +175,43 @@ textarea {
|
|||
textarea:focus {
|
||||
border-color: var(--light-gray);
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
setupFiles: ["<rootDir>/test/setup-tests.ts"],
|
||||
moduleNameMapper: {
|
||||
"@lib/(.*)": "<rootDir>/src/lib/$1",
|
||||
"@routes/(.*)": "<rootDir>/src/routes/$1"
|
||||
},
|
||||
testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/dist/"]
|
||||
}
|
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
5
src/next-env.d.ts
vendored
|
@ -1,5 +0,0 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
@ -1,31 +0,0 @@
|
|||
import bundleAnalyzer from "@next/bundle-analyzer"
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
// esmExternals: true,
|
||||
appDir: true,
|
||||
serverComponentsExternalPackages: ["prisma", "@prisma/client"]
|
||||
},
|
||||
output: "standalone",
|
||||
rewrites() {
|
||||
return [
|
||||
{
|
||||
source: "/file/raw/:id",
|
||||
destination: `/api/raw/:id`
|
||||
},
|
||||
{
|
||||
source: "/home",
|
||||
destination: "/"
|
||||
}
|
||||
]
|
||||
},
|
||||
images: {
|
||||
domains: ["avatars.githubusercontent.com"]
|
||||
}
|
||||
}
|
||||
|
||||
export default bundleAnalyzer({ enabled: process.env.ANALYZE === "true" })(
|
||||
nextConfig
|
||||
)
|
|
@ -1,86 +0,0 @@
|
|||
{
|
||||
"name": "drift",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3000",
|
||||
"build": "next build",
|
||||
"start": "next start --port 3000",
|
||||
"lint": "next lint && prettier --list-different --config .prettierrc '{components,lib,app,pages}/**/*.{ts,tsx}' --write",
|
||||
"analyze": "cross-env ANALYZE=true next build",
|
||||
"find:unused": "next-unused",
|
||||
"prisma": "prisma",
|
||||
"jest": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@next-auth/prisma-adapter": "^1.0.5",
|
||||
"@next/eslint-plugin-next": "13.1.2-canary.0",
|
||||
"@next/font": "13.1.2-canary.0",
|
||||
"@prisma/client": "^4.8.0",
|
||||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.1",
|
||||
"@radix-ui/react-popover": "^1.0.2",
|
||||
"@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",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"next": "13.1.2-canary.0",
|
||||
"next-auth": "^4.18.6",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "18.2.0",
|
||||
"react-datepicker": "4.8.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-dropzone": "14.2.3",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-hot-toast": "2.4.0-beta.0",
|
||||
"server-only": "^0.0.1",
|
||||
"swr": "^2.0.0",
|
||||
"textarea-markdown-editor": "1.0.4",
|
||||
"ts-jest": "^29.0.3",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "13.0.7-canary.4",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "17.0.23",
|
||||
"@types/react": "18.0.9",
|
||||
"@types/react-datepicker": "4.4.1",
|
||||
"@types/react-dom": "18.0.3",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||
"@typescript-eslint/parser": "^5.46.1",
|
||||
"clsx": "^1.2.1",
|
||||
"cross-env": "7.0.3",
|
||||
"csstype": "^3.1.1",
|
||||
"eslint": "8.27.0",
|
||||
"eslint-config-next": "13.0.3",
|
||||
"next-unused": "0.0.6",
|
||||
"prettier": "2.6.2",
|
||||
"prisma": "^4.8.0",
|
||||
"typescript": "4.6.4",
|
||||
"typescript-plugin-css-modules": "3.4.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sharp": "^0.31.2"
|
||||
},
|
||||
"next-unused": {
|
||||
"alias": {
|
||||
"@components": "components/",
|
||||
"@lib": "lib/",
|
||||
"@styles": "styles/"
|
||||
},
|
||||
"include": [
|
||||
"components",
|
||||
"lib"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
}
|
||||
}
|
|
@ -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]"
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -95,7 +95,6 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<unknown>) {
|
|||
.catch((error) => {
|
||||
return res.status(500).json(error)
|
||||
})
|
||||
|
||||
return res.json(post)
|
||||
} catch (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 { 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)
|
||||
|
|
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 { 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"
|
||||
|
||||
|
|
7280
src/pnpm-lock.yaml
generated
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 3.6 KiB |
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
Before Width: | Height: | Size: 653 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 930 B |
|
@ -1,19 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="72" height="72"><svg width="72" height="72" version="1.1" viewBox="0 0 19.05 19.05" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<clipPath id="SvgjsClipPath1001">
|
||||
<circle cx="115.27" cy="135.33" r="9.1406"></circle>
|
||||
</clipPath>
|
||||
<clipPath id="SvgjsClipPath1000">
|
||||
<circle cx="115.27" cy="135.33" r="9.1406"></circle>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g transform="translate(-106.13 -126.19)">
|
||||
<rect transform="matrix(1.0421 0 0 1.0421 -4.4639 -5.3074)" x="106.13" y="126.19" width="18.281" height="18.281" clip-path="url(#clipPath7864)" fill="#1b1b1b"></rect>
|
||||
<g transform="matrix(1.0421 0 0 1.0421 -4.4639 -5.3074)" clip-path="url(#clipPath7860)" stroke-width=".95964">
|
||||
<path d="m132.15 142c-10.707-9.0354-17.374-8.908-17.374-8.908s2.3881 3.4829 0.94799 7.4545c-1.4401 3.9716-7.6664 7.7467-7.6664 7.7467z" fill="#fff"></path>
|
||||
<path d="m108.89 148.35s6.2294-3.8135 7.6695-7.7851c1.4401-3.9716-1.7851-7.4745-1.7851-7.4745s1.2204 3.091-1.0184 6.7752c-2.239 3.6841-8.9226 4.9787-8.9226 4.9787z" fill="#e7e7e7"></path>
|
||||
<path d="m105.93 146.03s6.6058-2.1876 8.8448-5.8717c2.2387-3.6841-5e-3 -7.0674-5e-3 -7.0674s-1.3628 3.2487-4.9368 5.2561-10.125 2.2728-10.125 2.2728z" fill="#c6c6c6"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
|
||||
</style></svg>
|
Before Width: | Height: | Size: 1.4 KiB |
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="72" height="72" version="1.1" viewBox="0 0 19.05 19.05" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<clipPath id="clipPath7860">
|
||||
<circle cx="115.27" cy="135.33" r="9.1406" />
|
||||
</clipPath>
|
||||
<clipPath id="clipPath7864">
|
||||
<circle cx="115.27" cy="135.33" r="9.1406" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g transform="translate(-106.13 -126.19)">
|
||||
<rect transform="matrix(1.0421 0 0 1.0421 -4.4639 -5.3074)" x="106.13" y="126.19" width="18.281" height="18.281" clip-path="url(#clipPath7864)" fill="#1b1b1b" />
|
||||
<g transform="matrix(1.0421 0 0 1.0421 -4.4639 -5.3074)" clip-path="url(#clipPath7860)" stroke-width=".95964">
|
||||
<path d="m132.15 142c-10.707-9.0354-17.374-8.908-17.374-8.908s2.3881 3.4829 0.94799 7.4545c-1.4401 3.9716-7.6664 7.7467-7.6664 7.7467z" fill="#fff" />
|
||||
<path d="m108.89 148.35s6.2294-3.8135 7.6695-7.7851c1.4401-3.9716-1.7851-7.4745-1.7851-7.4745s1.2204 3.091-1.0184 6.7752c-2.239 3.6841-8.9226 4.9787-8.9226 4.9787z" fill="#e7e7e7" />
|
||||
<path d="m105.93 146.03s6.6058-2.1876 8.8448-5.8717c2.2387-3.6841-5e-3 -7.0674-5e-3 -7.0674s-1.3628 3.2487-4.9368 5.2561-10.125 2.2728-10.125 2.2728z" fill="#c6c6c6" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 23 KiB |
|
@ -1,124 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="72.000008"
|
||||
height="72"
|
||||
viewBox="0 0 19.05 19.05"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:export-filename="/home/reese/git/github.com/maxleiter/drift/logo.png"
|
||||
inkscape:export-xdpi="682.66669"
|
||||
inkscape:export-ydpi="682.66669"
|
||||
inkscape:version="1.1.2 (1:1.1+202202050950+0a00cf5339)"
|
||||
sodipodi:docname="logo.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:document-units="px"
|
||||
showgrid="false"
|
||||
showguides="false"
|
||||
inkscape:zoom="13.877295"
|
||||
inkscape:cx="10.448722"
|
||||
inkscape:cy="34.444753"
|
||||
inkscape:current-layer="g3632"
|
||||
units="px"
|
||||
viewbox-width="19.05" />
|
||||
<defs
|
||||
id="defs2">
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath7860">
|
||||
<circle
|
||||
style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.93688;stroke-linecap:round"
|
||||
id="circle7862"
|
||||
cx="115.27311"
|
||||
cy="135.3275"
|
||||
r="9.1405506" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath7864">
|
||||
<circle
|
||||
style="display:inline;opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.93688;stroke-linecap:round"
|
||||
id="circle7866"
|
||||
cx="115.27311"
|
||||
cy="135.3275"
|
||||
r="9.1405506" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="source strokes"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:none"
|
||||
transform="translate(-106.13256,-126.18696)">
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 114.7741,133.0871 c 0,0 2.24373,3.38322 0.005,7.06735 -2.23896,3.68413 -8.84476,5.87171 -8.84476,5.87171"
|
||||
id="path1824"
|
||||
sodipodi:nodetypes="csc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 99.71221,140.61603 c 0,0 6.55112,-0.26544 10.1251,-2.27285 3.57398,-2.00741 4.93679,-5.25608 4.93679,-5.25608"
|
||||
id="path857"
|
||||
sodipodi:nodetypes="czc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 114.7741,133.0871 c 0,0 3.22515,3.50294 1.78507,7.47454 -1.44009,3.97159 -7.66948,7.78507 -7.66948,7.78507"
|
||||
id="path949"
|
||||
sodipodi:nodetypes="czc" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 114.7741,133.0871 c 0,0 6.66681,-0.12736 17.37373,8.90799"
|
||||
id="path1345"
|
||||
sodipodi:nodetypes="cc" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Layer 1 copy"
|
||||
inkscape:groupmode="layer"
|
||||
id="g3632"
|
||||
transform="translate(-106.13256,-126.18696)">
|
||||
<rect
|
||||
style="display:inline;fill:#1b1b1b;fill-opacity:1;stroke:none;stroke-width:2.81834;stroke-linecap:round"
|
||||
id="rect6284"
|
||||
width="18.28112"
|
||||
height="18.28112"
|
||||
x="106.13255"
|
||||
y="126.18695"
|
||||
clip-path="url(#clipPath7864)"
|
||||
transform="matrix(1.0420598,0,0,1.0420598,-4.4639102,-5.3073932)" />
|
||||
<g
|
||||
id="g937"
|
||||
inkscape:label="drift"
|
||||
clip-path="url(#clipPath7860)"
|
||||
mask="none"
|
||||
style="display:inline;stroke-width:0.959638"
|
||||
transform="matrix(1.0420598,0,0,1.0420598,-4.4639102,-5.3073932)">
|
||||
<path
|
||||
id="path935"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.253904px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 132.14783,141.99509 c -10.70692,-9.03535 -17.37373,-8.90799 -17.37373,-8.90799 0,0 2.38807,3.48286 0.94799,7.45446 -1.44009,3.97159 -7.66636,7.74668 -7.66636,7.74668 z"
|
||||
sodipodi:nodetypes="csccc" />
|
||||
<path
|
||||
id="path931"
|
||||
style="fill:#e7e7e7;fill-opacity:1;stroke:none;stroke-width:0.253904px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 108.88969,148.34671 c 0,0 6.22939,-3.81348 7.66948,-7.78507 1.44008,-3.9716 -1.78507,-7.47454 -1.78507,-7.47454 0,0 1.22037,3.09102 -1.01836,6.77515 -2.23896,3.68413 -8.92258,4.9787 -8.92258,4.9787 z"
|
||||
sodipodi:nodetypes="cscccc" />
|
||||
<path
|
||||
id="path933"
|
||||
style="fill:#c6c6c6;fill-opacity:1;stroke:none;stroke-width:0.253904px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 105.93434,146.02616 c 0,0 6.6058,-2.18758 8.84476,-5.87171 2.23873,-3.68413 -0.005,-7.06735 -0.005,-7.06735 0,0 -1.36281,3.24867 -4.93679,5.25608 -3.57398,2.00741 -10.1251,2.27285 -10.1251,2.27285 z"
|
||||
sodipodi:nodetypes="csccc" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 2.5 KiB |
|
@ -1,26 +0,0 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M3327 6994 c-1 -1 -47 -4 -103 -8 -55 -4 -110 -8 -122 -11 -11 -2
|
||||
-45 -6 -74 -9 -29 -4 -65 -8 -80 -11 -16 -3 -39 -7 -53 -9 -337 -59 -654 -165
|
||||
-970 -326 -365 -185 -635 -383 -918 -670 -436 -443 -735 -969 -893 -1570 -14
|
||||
-52 -27 -106 -30 -120 -2 -14 -7 -36 -10 -50 -10 -41 -12 -53 -29 -166 -24
|
||||
-164 -24 -162 -32 -264 -9 -119 -9 -456 0 -574 7 -80 20 -189 32 -257 3 -14 7
|
||||
-41 9 -60 3 -19 14 -77 26 -129 63 -285 145 -523 272 -790 269 -564 718 -1071
|
||||
1248 -1412 411 -264 845 -433 1323 -513 200 -33 355 -45 584 -45 177 0 267 6
|
||||
453 26 93 10 308 53 450 90 728 191 1398 640 1872 1254 78 101 216 311 282
|
||||
430 182 325 342 781 391 1112 21 148 23 167 32 263 10 115 10 547 0 645 -39
|
||||
380 -126 724 -270 1065 -45 107 -180 361 -253 476 -196 310 -443 594 -709 814
|
||||
-33 28 -62 53 -65 56 -12 14 -202 150 -280 201 -248 161 -564 313 -805 388
|
||||
-22 7 -51 17 -64 22 -36 13 -320 85 -366 92 -36 6 -70 12 -129 22 -44 8 -176
|
||||
24 -251 30 -71 6 -462 13 -468 8z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.4 KiB |
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"name": "Drift",
|
||||
"short_name": "Drift",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"plugins": [
|
||||
{
|
||||
"name": "typescript-plugin-css-modules"
|
||||
},
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"target": "es2020",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@components/*": [
|
||||
"app/components/*"
|
||||
],
|
||||
"@lib/*": [
|
||||
"lib/*"
|
||||
],
|
||||
"@styles/*": [
|
||||
"app/styles/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
33
src/types/next-auth.d.ts
vendored
|
@ -1,33 +0,0 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import type { User } from "next-auth"
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import type { JWT } from "next-auth/jwt"
|
||||
|
||||
type UserId = string
|
||||
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
id: UserId
|
||||
role: string
|
||||
sessionToken: string
|
||||
}
|
||||
}
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
user: User & {
|
||||
id: UserId
|
||||
role: string
|
||||
sessionToken: string
|
||||
}
|
||||
}
|
||||
|
||||
// override user
|
||||
interface User {
|
||||
username?: string | null
|
||||
email?: string | null
|
||||
role?: string | null
|
||||
id: UserId
|
||||
token?: string
|
||||
}
|
||||
}
|