convert admin, run lint
This commit is contained in:
parent
cf7d89eb20
commit
8b0b172f7d
64 changed files with 410 additions and 581 deletions
|
@ -1,5 +1,5 @@
|
||||||
import PageSeo from "@components/head"
|
import PageSeo from "@components/page-seo"
|
||||||
|
|
||||||
export default function AuthHead() {
|
export default function AuthHead() {
|
||||||
return <PageSeo title="Sign In" />
|
return <PageSeo title="Sign In" />
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"use client"
|
||||||
import ShiftBy from "@components/shift-by"
|
import ShiftBy from "@components/shift-by"
|
||||||
import { Spacer, Tabs, Card, Textarea, Text } from "@geist-ui/core/dist"
|
import { Spacer, Tabs, Card, Textarea, Text } from "@geist-ui/core/dist"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
|
@ -1,4 +1,5 @@
|
||||||
import { getWelcomeContent } from "pages/api/welcome"
|
import { getWelcomeContent } from "pages/api/welcome"
|
||||||
|
import Home from "./home"
|
||||||
|
|
||||||
const getWelcomeData = async () => {
|
const getWelcomeData = async () => {
|
||||||
const welcomeContent = await getWelcomeContent()
|
const welcomeContent = await getWelcomeContent()
|
||||||
|
@ -6,7 +7,7 @@ const getWelcomeData = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const welcomeData = await getWelcomeData()
|
const { content, rendered, title } = await getWelcomeData()
|
||||||
|
|
||||||
return <h1>{JSON.stringify(welcomeData)}</h1>
|
return <Home rendered={rendered} introContent={content} introTitle={title} />
|
||||||
}
|
}
|
11
client/app/(posts)/expired/page.tsx
Normal file
11
client/app/(posts)/expired/page.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Note, Text } from "@geist-ui/core/dist"
|
||||||
|
|
||||||
|
export default function ExpiredPage() {
|
||||||
|
return (
|
||||||
|
<Note type="error" label={false}>
|
||||||
|
<Text h4>Error: The Drift you're trying to view has expired.</Text>
|
||||||
|
</Note>
|
||||||
|
)
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import { cookies } from "next/headers"
|
||||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||||
import { getPostWithFiles } from "app/prisma"
|
import { getPostWithFiles } from "app/prisma"
|
||||||
import { useRedirectIfNotAuthed } from "@lib/server/hooks/use-redirect-if-not-authed"
|
import { useRedirectIfNotAuthed } from "@lib/server/hooks/use-redirect-if-not-authed"
|
||||||
|
|
||||||
const NewFromExisting = async ({
|
const NewFromExisting = async ({
|
||||||
params
|
params
|
||||||
}: {
|
}: {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export default function NewLayout({ children }: { children: React.ReactNode }) {
|
export default function NewLayout({ children }: { children: React.ReactNode }) {
|
||||||
// useRedirectIfNotAuthed()
|
// useRedirectIfNotAuthed()
|
||||||
return <>{children}</>;
|
return <>{children}</>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import NewPost from "@components/new-post"
|
import NewPost from "@components/new-post"
|
||||||
import '@styles/react-datepicker.css'
|
import "@styles/react-datepicker.css"
|
||||||
|
|
||||||
const New = () => <NewPost />
|
const New = () => <NewPost />
|
||||||
|
|
||||||
|
|
5
client/app/(profiles)/mine/head.tsx
Normal file
5
client/app/(profiles)/mine/head.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import PageSeo from "@components/page-seo"
|
||||||
|
|
||||||
|
export default function Head() {
|
||||||
|
return <PageSeo title="Drift - Your profile" isPrivate />
|
||||||
|
}
|
18
client/app/(profiles)/mine/page.tsx
Normal file
18
client/app/(profiles)/mine/page.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { USER_COOKIE_NAME } from "@lib/constants"
|
||||||
|
import { notFound, useRouter } from "next/navigation"
|
||||||
|
import { cookies } from "next/headers"
|
||||||
|
import { getPostsByUser } from "app/prisma"
|
||||||
|
import PostList from "@components/post-list"
|
||||||
|
export default async function Mine() {
|
||||||
|
// TODO: fix router usage
|
||||||
|
// const router = useRouter()
|
||||||
|
const userId = cookies().get(USER_COOKIE_NAME)?.value
|
||||||
|
if (!userId) {
|
||||||
|
// return router.push("/signin")
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts = await getPostsByUser(userId, true)
|
||||||
|
const hasMore = false
|
||||||
|
return <PostList morePosts={hasMore} initialPosts={posts} />
|
||||||
|
}
|
5
client/app/(profiles)/settings/head.tsx
Normal file
5
client/app/(profiles)/settings/head.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import PageSeo from "@components/page-seo"
|
||||||
|
|
||||||
|
export default function Head() {
|
||||||
|
return <PageSeo title="Drift - Settings" isPrivate />
|
||||||
|
}
|
5
client/app/(profiles)/settings/page.tsx
Normal file
5
client/app/(profiles)/settings/page.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import SettingsPage from "@components/settings"
|
||||||
|
|
||||||
|
const Settings = () => <SettingsPage />
|
||||||
|
|
||||||
|
export default Settings
|
22
client/app/admin/page.tsx
Normal file
22
client/app/admin/page.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import Admin from "@components/admin"
|
||||||
|
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||||
|
import { isUserAdmin } from "app/prisma"
|
||||||
|
import { cookies } from "next/headers"
|
||||||
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
|
const AdminPage = async () => {
|
||||||
|
const driftToken = cookies().get(TOKEN_COOKIE_NAME)?.value
|
||||||
|
if (!driftToken) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdmin = await isUserAdmin(driftToken)
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Admin />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminPage
|
|
@ -49,7 +49,9 @@ export default function RootLayout({ children }: RootLayoutProps) {
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<title>Drift</title>
|
<title>Drift</title>
|
||||||
</head>
|
</head>
|
||||||
<body><LayoutWrapper>{children}</LayoutWrapper></body>
|
<body>
|
||||||
|
<LayoutWrapper>{children}</LayoutWrapper>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
</ServerThemeProvider>
|
</ServerThemeProvider>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import styles from "@styles/Home.module.css"
|
|
||||||
|
|
||||||
import MyPosts from "@components/my-posts"
|
|
||||||
import type { GetServerSideProps } from "next"
|
|
||||||
import { Post } from "@lib/types"
|
|
||||||
import { Page } from "@geist-ui/core/dist"
|
|
||||||
import { getCookie } from "cookies-next"
|
|
||||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
import { cookies } from "next/headers"
|
|
||||||
export default function Mine() {
|
|
||||||
const router = useRouter()
|
|
||||||
const driftToken = cookies().get(TOKEN_COOKIE_NAME)
|
|
||||||
if (!driftToken) {
|
|
||||||
return router.push("/signin")
|
|
||||||
}
|
|
||||||
|
|
||||||
// const posts = await fetch(process.env.API_URL + `/posts/mine`, {
|
|
||||||
// method: "GET",
|
|
||||||
// headers: {
|
|
||||||
// "Content-Type": "application/json",
|
|
||||||
// Authorization: `Bearer ${driftToken}`,
|
|
||||||
// "x-secret-key": process.env.SECRET_KEY || ""
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
if (!posts.ok) {
|
|
||||||
return router.push("/signin")
|
|
||||||
}
|
|
||||||
|
|
||||||
const { posts, error, hasMore } = await posts.json()
|
|
||||||
|
|
||||||
return <MyPosts morePosts={hasMore} error={error} posts={posts} />
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
"use client"
|
|
||||||
|
|
||||||
import Header from "@components/header"
|
|
||||||
import { Page } from "@geist-ui/core/dist"
|
|
||||||
import styles from "@styles/Home.module.css"
|
|
||||||
|
|
||||||
export default function PageWrapper({
|
|
||||||
children
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Page width={"100%"}>
|
|
||||||
<Page.Header>
|
|
||||||
<Header />
|
|
||||||
</Page.Header>
|
|
||||||
|
|
||||||
<Page.Content className={styles.main}>{children}</Page.Content>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,16 +1,10 @@
|
||||||
import { Post, PrismaClient, File } from "@prisma/client"
|
import { Post, PrismaClient, File, User } from "@prisma/client"
|
||||||
|
|
||||||
const prisma = new PrismaClient()
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
export default prisma
|
export default prisma
|
||||||
|
|
||||||
export type {
|
export type { User, AuthTokens, File, Post } from "@prisma/client"
|
||||||
User,
|
|
||||||
AuthTokens,
|
|
||||||
File,
|
|
||||||
Post,
|
|
||||||
PostToAuthors
|
|
||||||
} from "@prisma/client"
|
|
||||||
|
|
||||||
export type PostWithFiles = Post & {
|
export type PostWithFiles = Post & {
|
||||||
files: File[]
|
files: File[]
|
||||||
|
@ -50,3 +44,74 @@ export const getPostWithFiles = async (
|
||||||
files
|
files
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getPostsByUser(userId: string): Promise<Post[]>
|
||||||
|
export async function getPostsByUser(
|
||||||
|
userId: string,
|
||||||
|
includeFiles: true
|
||||||
|
): Promise<PostWithFiles[]>
|
||||||
|
export async function getPostsByUser(userId: User["id"], withFiles?: boolean) {
|
||||||
|
const sharedOptions = {
|
||||||
|
take: 20,
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc" as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts = await prisma.post.findMany({
|
||||||
|
where: {
|
||||||
|
authorId: userId
|
||||||
|
},
|
||||||
|
...sharedOptions
|
||||||
|
})
|
||||||
|
|
||||||
|
if (withFiles) {
|
||||||
|
return Promise.all(
|
||||||
|
posts.map(async (post) => {
|
||||||
|
const files = await prisma.file.findMany({
|
||||||
|
where: {
|
||||||
|
postId: post.id
|
||||||
|
},
|
||||||
|
...sharedOptions
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...post,
|
||||||
|
files
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserById = async (userId: User["id"]) => {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: userId
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
email: true,
|
||||||
|
displayName: true,
|
||||||
|
role: true,
|
||||||
|
username: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isUserAdmin = async (userId: User["id"]) => {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: userId
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
role: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return user?.role?.toLowerCase() === "admin"
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { CssBaseline, GeistProvider, Themes } from "@geist-ui/core/dist"
|
import Header from "@components/header"
|
||||||
|
import { CssBaseline, GeistProvider, Page, Themes } from "@geist-ui/core/dist"
|
||||||
import { ThemeProvider } from "next-themes"
|
import { ThemeProvider } from "next-themes"
|
||||||
import { SkeletonTheme } from "react-loading-skeleton"
|
import { SkeletonTheme } from "react-loading-skeleton"
|
||||||
import PageWrapper from "./page-wrapper"
|
import styles from "@styles/Home.module.css"
|
||||||
|
|
||||||
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||||
const skeletonBaseColor = "var(--light-gray)"
|
const skeletonBaseColor = "var(--light-gray)"
|
||||||
|
@ -58,7 +59,13 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||||
attribute="data-theme"
|
attribute="data-theme"
|
||||||
>
|
>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<PageWrapper>{children}</PageWrapper>
|
<Page width={"100%"}>
|
||||||
|
<Page.Header>
|
||||||
|
<Header />
|
||||||
|
</Page.Header>
|
||||||
|
|
||||||
|
<Page.Content className={styles.main}>{children}</Page.Content>
|
||||||
|
</Page>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</SkeletonTheme>
|
</SkeletonTheme>
|
||||||
</GeistProvider>
|
</GeistProvider>
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||||
import { getCookie } from "cookies-next"
|
import { getCookie } from "cookies-next"
|
||||||
import styles from "./admin.module.css"
|
import styles from "./admin.module.css"
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { FormEvent, useEffect, useState } from "react"
|
import { FormEvent, useState } from "react"
|
||||||
import styles from "./auth.module.css"
|
import styles from "./auth.module.css"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import Link from "../link"
|
import Link from "../link"
|
||||||
import Cookies from "js-cookie"
|
|
||||||
import useSignedIn from "@lib/hooks/use-signed-in"
|
import useSignedIn from "@lib/hooks/use-signed-in"
|
||||||
import Input from "@components/input"
|
|
||||||
import Button from "@components/button"
|
|
||||||
import Note from "@components/note"
|
|
||||||
import { USER_COOKIE_NAME } from "@lib/constants"
|
import { USER_COOKIE_NAME } from "@lib/constants"
|
||||||
import { setCookie } from "cookies-next"
|
import { setCookie } from "cookies-next"
|
||||||
|
import { Button, Input, Note } from "@geist-ui/core/dist"
|
||||||
|
|
||||||
const NO_EMPTY_SPACE_REGEX = /^\S*$/
|
const NO_EMPTY_SPACE_REGEX = /^\S*$/
|
||||||
const ERROR_MESSAGE =
|
const ERROR_MESSAGE =
|
||||||
|
@ -83,24 +80,26 @@ const Auth = ({
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
htmlType="text"
|
||||||
id="username"
|
id="username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(event) => setUsername(event.currentTarget.value)}
|
onChange={(event) => setUsername(event.currentTarget.value)}
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
required
|
required
|
||||||
|
minLength={3}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
htmlType="password"
|
||||||
id="password"
|
id="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(event) => setPassword(event.currentTarget.value)}
|
onChange={(event) => setPassword(event.currentTarget.value)}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
required
|
required
|
||||||
|
minLength={6}
|
||||||
/>
|
/>
|
||||||
{requiresServerPassword && (
|
{requiresServerPassword && (
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
htmlType="password"
|
||||||
id="server-password"
|
id="server-password"
|
||||||
value={serverPassword}
|
value={serverPassword}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
|
@ -108,10 +107,11 @@ const Auth = ({
|
||||||
}
|
}
|
||||||
placeholder="Server Password"
|
placeholder="Server Password"
|
||||||
required
|
required
|
||||||
|
width="100%"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button buttonType="primary" type="submit">
|
<Button width={"100%"} htmlType="submit">
|
||||||
{signingIn ? "Sign In" : "Sign Up"}
|
{signingIn ? "Sign In" : "Sign Up"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import Head from "next/head"
|
|
||||||
import React from "react"
|
|
||||||
|
|
||||||
type PageSeoProps = {
|
|
||||||
title?: string
|
|
||||||
description?: string
|
|
||||||
isLoading?: boolean
|
|
||||||
isPrivate?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const PageSeo = ({
|
|
||||||
title = "Drift",
|
|
||||||
description = "A self-hostable clone of GitHub Gist",
|
|
||||||
isPrivate = false
|
|
||||||
}: PageSeoProps) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>{title}</title>
|
|
||||||
{!isPrivate && <meta name="description" content={description} />}
|
|
||||||
</Head>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PageSeo
|
|
|
@ -3,7 +3,7 @@ import NextLink from "next/link"
|
||||||
import styles from "./link.module.css"
|
import styles from "./link.module.css"
|
||||||
|
|
||||||
type LinkProps = {
|
type LinkProps = {
|
||||||
colored?: boolean,
|
colored?: boolean
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
} & React.ComponentProps<typeof NextLink>
|
} & React.ComponentProps<typeof NextLink>
|
||||||
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
import type { Post } from "@lib/types"
|
|
||||||
import PostList from "../post-list"
|
|
||||||
|
|
||||||
const MyPosts = ({
|
|
||||||
posts,
|
|
||||||
error,
|
|
||||||
morePosts
|
|
||||||
}: {
|
|
||||||
posts: Post[]
|
|
||||||
error: boolean
|
|
||||||
morePosts: boolean
|
|
||||||
}) => {
|
|
||||||
return <PostList morePosts={morePosts} initialPosts={posts} error={error} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MyPosts
|
|
|
@ -16,6 +16,7 @@ const PageSeo = ({
|
||||||
<>
|
<>
|
||||||
<title>Drift - {title}</title>
|
<title>Drift - {title}</title>
|
||||||
{!isPrivate && <meta name="description" content={description} />}
|
{!isPrivate && <meta name="description" content={description} />}
|
||||||
|
{isPrivate && <meta name="robots" content="noindex" />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
import { Button, Input, Text } from "@geist-ui/core/dist"
|
import { Button, Input, Text } from "@geist-ui/core/dist"
|
||||||
|
|
||||||
import styles from "./post-list.module.css"
|
import styles from "./post-list.module.css"
|
||||||
import ListItemSkeleton from "./list-item-skeleton"
|
import ListItemSkeleton from "./list-item-skeleton"
|
||||||
import ListItem from "./list-item"
|
import ListItem from "./list-item"
|
||||||
import { Post } from "@lib/types"
|
import { ChangeEvent, useCallback, useEffect, useState } from "react"
|
||||||
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react"
|
|
||||||
import useDebounce from "@lib/hooks/use-debounce"
|
import useDebounce from "@lib/hooks/use-debounce"
|
||||||
import Link from "@components/link"
|
import Link from "@components/link"
|
||||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||||
import { getCookie } from "cookies-next"
|
import { getCookie } from "cookies-next"
|
||||||
|
import type { PostWithFiles } from "app/prisma"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialPosts: Post[]
|
initialPosts: PostWithFiles[]
|
||||||
error: boolean
|
|
||||||
morePosts: boolean
|
morePosts: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const PostList = ({ morePosts, initialPosts, error }: Props) => {
|
const PostList = ({ morePosts, initialPosts }: Props) => {
|
||||||
const [search, setSearchValue] = useState("")
|
const [search, setSearchValue] = useState("")
|
||||||
const [posts, setPosts] = useState<Post[]>(initialPosts)
|
const [posts, setPosts] = useState(initialPosts)
|
||||||
const [searching, setSearching] = useState(false)
|
const [searching, setSearching] = useState(false)
|
||||||
const [hasMorePosts, setHasMorePosts] = useState(morePosts)
|
const [hasMorePosts, setHasMorePosts] = useState(morePosts)
|
||||||
|
|
||||||
|
@ -122,7 +123,7 @@ const PostList = ({ morePosts, initialPosts, error }: Props) => {
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && <Text type="error">Failed to load.</Text>}
|
{!posts && <Text type="error">Failed to load.</Text>}
|
||||||
{!posts.length && searching && (
|
{!posts.length && searching && (
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
@ -133,7 +134,7 @@ const PostList = ({ morePosts, initialPosts, error }: Props) => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
{posts?.length === 0 && !error && (
|
{posts?.length === 0 && posts && (
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
No posts found. Create one{" "}
|
No posts found. Create one{" "}
|
||||||
<Link colored href="/new">
|
<Link colored href="/new">
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import NextLink from "next/link"
|
import NextLink from "next/link"
|
||||||
import VisibilityBadge from "../badges/visibility-badge"
|
import VisibilityBadge from "../badges/visibility-badge"
|
||||||
import { Text, Card, Tooltip, Divider, Badge, Button } from "@geist-ui/core/dist"
|
import {
|
||||||
import { File, Post } from "@lib/types"
|
Text,
|
||||||
|
Card,
|
||||||
|
Tooltip,
|
||||||
|
Divider,
|
||||||
|
Badge,
|
||||||
|
Button
|
||||||
|
} from "@geist-ui/core/dist"
|
||||||
import FadeIn from "@components/fade-in"
|
import FadeIn from "@components/fade-in"
|
||||||
import Trash from "@geist-ui/icons/trash"
|
import Trash from "@geist-ui/icons/trash"
|
||||||
import ExpirationBadge from "@components/badges/expiration-badge"
|
import ExpirationBadge from "@components/badges/expiration-badge"
|
||||||
|
@ -11,6 +17,8 @@ import { useRouter } from "next/router"
|
||||||
import Parent from "@geist-ui/icons/arrowUpCircle"
|
import Parent from "@geist-ui/icons/arrowUpCircle"
|
||||||
import styles from "./list-item.module.css"
|
import styles from "./list-item.module.css"
|
||||||
import Link from "@components/link"
|
import Link from "@components/link"
|
||||||
|
import { PostWithFiles, File } from "app/prisma"
|
||||||
|
import { PostVisibility } from "@lib/types"
|
||||||
|
|
||||||
// TODO: isOwner should default to false so this can be used generically
|
// TODO: isOwner should default to false so this can be used generically
|
||||||
const ListItem = ({
|
const ListItem = ({
|
||||||
|
@ -18,7 +26,7 @@ const ListItem = ({
|
||||||
isOwner = true,
|
isOwner = true,
|
||||||
deletePost
|
deletePost
|
||||||
}: {
|
}: {
|
||||||
post: Post
|
post: PostWithFiles
|
||||||
isOwner?: boolean
|
isOwner?: boolean
|
||||||
deletePost: () => void
|
deletePost: () => void
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -29,7 +37,7 @@ const ListItem = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewParentClick = () => {
|
const viewParentClick = () => {
|
||||||
router.push(`/post/${post.parent?.id}`)
|
router.push(`/post/${post.parentId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -48,7 +56,7 @@ const ListItem = ({
|
||||||
</Link>
|
</Link>
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<span className={styles.buttons}>
|
<span className={styles.buttons}>
|
||||||
{post.parent && (
|
{post.parentId && (
|
||||||
<Tooltip text={"View parent"} hideArrow>
|
<Tooltip text={"View parent"} hideArrow>
|
||||||
<Button
|
<Button
|
||||||
auto
|
auto
|
||||||
|
@ -74,7 +82,7 @@ const ListItem = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.badges}>
|
<div className={styles.badges}>
|
||||||
<VisibilityBadge visibility={post.visibility} />
|
<VisibilityBadge visibility={post.visibility as PostVisibility} />
|
||||||
<CreatedAgoBadge createdAt={post.createdAt} />
|
<CreatedAgoBadge createdAt={post.createdAt} />
|
||||||
<Badge type="secondary">
|
<Badge type="secondary">
|
||||||
{post.files?.length === 1
|
{post.files?.length === 1
|
||||||
|
|
|
@ -5,7 +5,13 @@ import styles from "./post-page.module.css"
|
||||||
import homeStyles from "@styles/Home.module.css"
|
import homeStyles from "@styles/Home.module.css"
|
||||||
|
|
||||||
import type { File, Post, PostVisibility } from "@lib/types"
|
import type { File, Post, PostVisibility } from "@lib/types"
|
||||||
import { Page, Button, Text, ButtonGroup, useMediaQuery } from "@geist-ui/core/dist"
|
import {
|
||||||
|
Page,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
ButtonGroup,
|
||||||
|
useMediaQuery
|
||||||
|
} from "@geist-ui/core/dist"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import Archive from "@geist-ui/icons/archive"
|
import Archive from "@geist-ui/icons/archive"
|
||||||
import Edit from "@geist-ui/icons/edit"
|
import Edit from "@geist-ui/icons/edit"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||||
import { getCookie } from "cookies-next"
|
import { getCookie } from "cookies-next"
|
||||||
import Cookies from "js-cookie"
|
|
||||||
import { memo, useEffect, useState } from "react"
|
import { memo, useEffect, useState } from "react"
|
||||||
import styles from "./preview.module.css"
|
import styles from "./preview.module.css"
|
||||||
|
|
||||||
|
@ -36,9 +36,8 @@ const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${getCookie("drift-token")}`
|
Authorization: `Bearer ${getCookie(TOKEN_COOKIE_NAME)}`
|
||||||
},
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
import Password from "./sections/password"
|
import Password from "./sections/password"
|
||||||
import Profile from "./sections/profile"
|
import Profile from "./sections/profile"
|
||||||
import SettingsGroup from "../settings-group"
|
import SettingsGroup from "../settings-group"
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
import { Input, Button, useToasts } from "@geist-ui/core/dist"
|
import { Input, Button, useToasts } from "@geist-ui/core/dist"
|
||||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||||
import { getCookie } from "cookies-next"
|
import { getCookie } from "cookies-next"
|
||||||
import Cookies from "js-cookie"
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
const Password = () => {
|
const Password = () => {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
import { Note, Input, Textarea, Button, useToasts } from "@geist-ui/core/dist"
|
import { Note, Input, Textarea, Button, useToasts } from "@geist-ui/core/dist"
|
||||||
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||||
import useUserData from "@lib/hooks/use-user-data"
|
import useUserData from "@lib/hooks/use-user-data"
|
||||||
import { getCookie } from "cookies-next"
|
import { getCookie } from "cookies-next"
|
||||||
import Cookies from "js-cookie"
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import styles from "./document.module.css"
|
||||||
import Download from "@geist-ui/icons/download"
|
import Download from "@geist-ui/icons/download"
|
||||||
import ExternalLink from "@geist-ui/icons/externalLink"
|
import ExternalLink from "@geist-ui/icons/externalLink"
|
||||||
import Skeleton from "react-loading-skeleton"
|
import Skeleton from "react-loading-skeleton"
|
||||||
import Link from 'next/link';
|
import Link from "next/link"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
|
@ -80,10 +80,9 @@ export const config = (env: Environment): Config => {
|
||||||
registration_password: env.REGISTRATION_PASSWORD ?? "",
|
registration_password: env.REGISTRATION_PASSWORD ?? "",
|
||||||
welcome_content: env.WELCOME_CONTENT ?? "",
|
welcome_content: env.WELCOME_CONTENT ?? "",
|
||||||
welcome_title: env.WELCOME_TITLE ?? "",
|
welcome_title: env.WELCOME_TITLE ?? "",
|
||||||
url: 'http://localhost:3000'
|
url: "http://localhost:3000"
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default config(process.env)
|
export default config(process.env)
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default function generateUUID() {
|
||||||
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))
|
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))
|
||||||
).toString(16)
|
).toString(16)
|
||||||
}
|
}
|
||||||
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, callback);
|
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, callback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let timestamp = new Date().getTime()
|
let timestamp = new Date().getTime()
|
||||||
|
@ -35,5 +35,5 @@ export default function generateUUID() {
|
||||||
perforNow = Math.floor(perforNow / 16)
|
perforNow = Math.floor(perforNow / 16)
|
||||||
}
|
}
|
||||||
return (c === "x" ? random : (random & 0x3) | 0x8).toString(16)
|
return (c === "x" ? random : (random & 0x3) | 0x8).toString(16)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||||
import { getCookie, setCookie } from "cookies-next"
|
import { getCookie, setCookie } from "cookies-next"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import useSharedState from "./use-shared-state"
|
import useSharedState from "./use-shared-state"
|
||||||
|
|
||||||
const useSignedIn = () => {
|
const useSignedIn = () => {
|
||||||
const token = getCookie("drift-token")
|
const token = getCookie(TOKEN_COOKIE_NAME)
|
||||||
|
|
||||||
const [signedIn, setSignedIn] = useSharedState(
|
const [signedIn, setSignedIn] = useSharedState(
|
||||||
"signedIn",
|
"signedIn",
|
||||||
|
@ -13,7 +14,7 @@ const useSignedIn = () => {
|
||||||
const signin = (token: string) => {
|
const signin = (token: string) => {
|
||||||
setSignedIn(true)
|
setSignedIn(true)
|
||||||
// TODO: investigate SameSite / CORS cookie security
|
// TODO: investigate SameSite / CORS cookie security
|
||||||
setCookie("drift-token", token)
|
setCookie(TOKEN_COOKIE_NAME, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -24,6 +25,8 @@ const useSignedIn = () => {
|
||||||
}
|
}
|
||||||
}, [setSignedIn, token])
|
}, [setSignedIn, token])
|
||||||
|
|
||||||
|
console.log("signed in", signedIn)
|
||||||
|
|
||||||
return { signedIn, signin, token }
|
return { signedIn, signin, token }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
|
import { TOKEN_COOKIE_NAME } from "@lib/constants"
|
||||||
import { User } from "@lib/types"
|
import { User } from "@lib/types"
|
||||||
import { deleteCookie, getCookie } from "cookies-next"
|
import { deleteCookie, getCookie } from "cookies-next"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
const useUserData = () => {
|
const useUserData = () => {
|
||||||
const cookie = getCookie("drift-token")
|
const cookie = getCookie(TOKEN_COOKIE_NAME)
|
||||||
const [authToken, setAuthToken] = useState<string>(
|
const [authToken, setAuthToken] = useState<string>(
|
||||||
cookie ? String(cookie) : ""
|
cookie ? String(cookie) : ""
|
||||||
)
|
)
|
||||||
const [user, setUser] = useState<User>()
|
const [user, setUser] = useState<User>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = getCookie("drift-token")
|
const token = getCookie(TOKEN_COOKIE_NAME)
|
||||||
if (token) {
|
if (token) {
|
||||||
setAuthToken(String(token))
|
setAuthToken(String(token))
|
||||||
}
|
}
|
||||||
|
@ -29,9 +30,10 @@ const useUserData = () => {
|
||||||
const user = await response.json()
|
const user = await response.json()
|
||||||
setUser(user)
|
setUser(user)
|
||||||
} else {
|
} else {
|
||||||
deleteCookie("drift-token")
|
// deleteCookie("drift-token")
|
||||||
setAuthToken("")
|
// setAuthToken("")
|
||||||
router.push("/")
|
// router.push("/")
|
||||||
|
console.log("not ok")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchUser()
|
fetchUser()
|
||||||
|
|
|
@ -20,21 +20,18 @@ import Link from "next/link"
|
||||||
const renderer = new marked.Renderer()
|
const renderer = new marked.Renderer()
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
renderer.heading = (text, level, _, slugger) => {
|
// renderer.heading = (text, level, raw, slugger) => {
|
||||||
const id = slugger.slug(text)
|
// const id = slugger.slug(text)
|
||||||
const Component = `h${level}`
|
// const Component = `h${level}`
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<h1>
|
// <Component>
|
||||||
<Link
|
// <Link href={`#${id}`} id={id}>
|
||||||
href={`#${id}`}
|
// {text}
|
||||||
id={id}
|
// </Link>
|
||||||
style={{ color: "inherit" }}
|
// </Component>
|
||||||
dangerouslySetInnerHTML={{ __html: text }}
|
// )
|
||||||
></Link>
|
// }
|
||||||
</h1>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderer.link = (href, _, text) => {
|
// renderer.link = (href, _, text) => {
|
||||||
// const isHrefLocal = href?.startsWith('/') || href?.startsWith('#')
|
// const isHrefLocal = href?.startsWith('/') || href?.startsWith('#')
|
||||||
|
|
|
@ -9,11 +9,11 @@ export async function generateAndExpireAccessToken(userId: User["id"]) {
|
||||||
await prisma.authTokens.create({
|
await prisma.authTokens.create({
|
||||||
data: {
|
data: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
token: token,
|
token: token
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: set expiredReason?
|
// TODO: set expiredReason?
|
||||||
prisma.authTokens.deleteMany({
|
prisma.authTokens.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
|
|
@ -3,7 +3,10 @@ import type { File } from "app/prisma"
|
||||||
/**
|
/**
|
||||||
* returns rendered HTML from a Drift file
|
* returns rendered HTML from a Drift file
|
||||||
*/
|
*/
|
||||||
function getHtmlFromFile({ content, title }: Pick<File, "content" | "title">) {
|
export function getHtmlFromFile({
|
||||||
|
content,
|
||||||
|
title
|
||||||
|
}: Pick<File, "content" | "title">) {
|
||||||
const renderAsMarkdown = [
|
const renderAsMarkdown = [
|
||||||
"markdown",
|
"markdown",
|
||||||
"md",
|
"md",
|
||||||
|
@ -35,5 +38,3 @@ ${content}
|
||||||
const html = markdown(contentToRender)
|
const html = markdown(contentToRender)
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getHtmlFromFile
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from "next/navigation"
|
||||||
import { isSignedIn } from "../is-signed-in"
|
import { isSignedIn } from "../is-signed-in"
|
||||||
|
|
||||||
export const useRedirectIfNotAuthed = (to = '/signin') => {
|
export const useRedirectIfNotAuthed = (to = "/signin") => {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
const signedIn = isSignedIn();
|
const signedIn = isSignedIn()
|
||||||
|
|
||||||
if (!signedIn) {
|
if (!signedIn) {
|
||||||
router.push(to);
|
router.push(to)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { TOKEN_COOKIE_NAME, USER_COOKIE_NAME } from "@lib/constants"
|
||||||
import { cookies } from "next/headers"
|
import { cookies } from "next/headers"
|
||||||
|
|
||||||
export const isSignedIn = () => {
|
export const isSignedIn = () => {
|
||||||
const cookieList = cookies()
|
const cookieList = cookies()
|
||||||
return cookieList.has("drift-token") && cookieList.has("drift-userid")
|
return cookieList.has(TOKEN_COOKIE_NAME) && cookieList.has(USER_COOKIE_NAME)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ export async function withJwt(
|
||||||
if (token == null) return res.status(401).send("Unauthorized")
|
if (token == null) return res.status(401).send("Unauthorized")
|
||||||
|
|
||||||
const authToken = await prisma.authTokens.findUnique({
|
const authToken = await prisma.authTokens.findUnique({
|
||||||
|
// @ts-ignore
|
||||||
where: { id: token }
|
where: { id: token }
|
||||||
})
|
})
|
||||||
if (authToken == null) {
|
if (authToken == null) {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* Parses a URL query string from string | string[] | ...
|
* Parses a URL query string from string | string[] | ...
|
||||||
* to string | undefined. If it's an array, we return the last item.
|
* to string | undefined. If it's an array, we return the last item.
|
||||||
*/
|
*/
|
||||||
export function parseUrlQuery(query: string | string[] | undefined) {
|
export function parseQueryParam(query: string | string[] | undefined) {
|
||||||
if (typeof query === "string") {
|
if (typeof query === "string") {
|
||||||
return query
|
return query
|
||||||
} else if (Array.isArray(query)) {
|
} else if (Array.isArray(query)) {
|
|
@ -1,6 +1,7 @@
|
||||||
import { NextFetchEvent, NextResponse } from "next/server"
|
import { NextFetchEvent, NextResponse } from "next/server"
|
||||||
import type { NextRequest } from "next/server"
|
import type { NextRequest } from "next/server"
|
||||||
import { TOKEN_COOKIE_NAME, USER_COOKIE_NAME } from "@lib/constants"
|
import { TOKEN_COOKIE_NAME, USER_COOKIE_NAME } from "@lib/constants"
|
||||||
|
import serverConfig from "@lib/config"
|
||||||
|
|
||||||
const PUBLIC_FILE = /\.(.*)$/
|
const PUBLIC_FILE = /\.(.*)$/
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ export function middleware(req: NextRequest, event: NextFetchEvent) {
|
||||||
resp.cookies.delete(TOKEN_COOKIE_NAME)
|
resp.cookies.delete(TOKEN_COOKIE_NAME)
|
||||||
resp.cookies.delete(USER_COOKIE_NAME)
|
resp.cookies.delete(USER_COOKIE_NAME)
|
||||||
const signoutPromise = new Promise((resolve) => {
|
const signoutPromise = new Promise((resolve) => {
|
||||||
fetch(`${process.env.API_URL}/auth/signout`, {
|
fetch(`${serverConfig.url}/auth/signout`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
@ -65,7 +66,7 @@ export function middleware(req: NextRequest, event: NextFetchEvent) {
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
match: [
|
match: [
|
||||||
"/signout",
|
// "/signout",
|
||||||
// "/",
|
// "/",
|
||||||
"/signin",
|
"/signin",
|
||||||
"/signup",
|
"/signup",
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
"dev": "next dev --port 3000",
|
"dev": "next dev --port 3000",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start --port 3000",
|
"start": "next start --port 3000",
|
||||||
"lint": "next lint && prettier --list-different --config .prettierrc '{components,lib,pages}/**/*.{ts,tsx}' --write",
|
"lint": "next lint && prettier --list-different --config .prettierrc '{components,lib,app}/**/*.{ts,tsx}' --write",
|
||||||
"analyze": "cross-env ANALYZE=true next build",
|
"analyze": "cross-env ANALYZE=true next build",
|
||||||
"find:unused": "next-unused",
|
"find:unused": "next-unused",
|
||||||
"prisma": "prisma"
|
"prisma": "prisma"
|
||||||
|
@ -32,6 +32,7 @@
|
||||||
"react-dropzone": "14.2.3",
|
"react-dropzone": "14.2.3",
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
"react-loading-skeleton": "3.1.0",
|
"react-loading-skeleton": "3.1.0",
|
||||||
|
"showdown": "^2.1.0",
|
||||||
"swr": "1.3.0",
|
"swr": "1.3.0",
|
||||||
"textarea-markdown-editor": "0.1.13",
|
"textarea-markdown-editor": "0.1.13",
|
||||||
"zod": "^3.19.1"
|
"zod": "^3.19.1"
|
||||||
|
@ -45,6 +46,7 @@
|
||||||
"@types/react": "18.0.9",
|
"@types/react": "18.0.9",
|
||||||
"@types/react-datepicker": "4.4.1",
|
"@types/react-datepicker": "4.4.1",
|
||||||
"@types/react-dom": "18.0.3",
|
"@types/react-dom": "18.0.3",
|
||||||
|
"@types/showdown": "^2.0.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.27.0",
|
"eslint": "8.27.0",
|
||||||
"eslint-config-next": "13.0.2",
|
"eslint-config-next": "13.0.2",
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
import styles from "@styles/Home.module.css"
|
|
||||||
|
|
||||||
import { Page } from "@geist-ui/core/dist"
|
|
||||||
import { useEffect } from "react"
|
|
||||||
import Admin from "@components/admin"
|
|
||||||
import useSignedIn from "@lib/hooks/use-signed-in"
|
|
||||||
import { useRouter } from "next/router"
|
|
||||||
import { GetServerSideProps } from "next"
|
|
||||||
import cookie from "cookie"
|
|
||||||
|
|
||||||
const AdminPage = () => {
|
|
||||||
const { signedIn } = useSignedIn()
|
|
||||||
const router = useRouter()
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window === "undefined") return
|
|
||||||
if (!signedIn) {
|
|
||||||
router.push("/")
|
|
||||||
}
|
|
||||||
}, [router, signedIn])
|
|
||||||
return (
|
|
||||||
<Page className={styles.wrapper}>
|
|
||||||
<Page.Content className={styles.main}>
|
|
||||||
<Admin />
|
|
||||||
</Page.Content>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
|
|
||||||
const driftToken = cookie.parse(req.headers.cookie || "")[`drift-token`]
|
|
||||||
const res = await fetch(`${process.env.API_URL}/admin/is-admin`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${driftToken}`,
|
|
||||||
"x-secret-key": process.env.SECRET_KEY || ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
signedIn: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AdminPage
|
|
|
@ -1,32 +0,0 @@
|
||||||
import config from "@lib/config"
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
|
|
||||||
const handleRequiresPasscode = async (
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) => {
|
|
||||||
const requiresPasscode = Boolean(config.registration_password)
|
|
||||||
return res.json({ requiresPasscode })
|
|
||||||
}
|
|
||||||
|
|
||||||
const PATH_TO_HANDLER = {
|
|
||||||
"requires-passcode": handleRequiresPasscode
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-anonymous-default-export
|
|
||||||
export default (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
const { slug } = req.query
|
|
||||||
|
|
||||||
if (!slug || Array.isArray(slug)) {
|
|
||||||
return res.status(400).json({ error: "Missing param" })
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (req.method) {
|
|
||||||
case "GET":
|
|
||||||
if (PATH_TO_HANDLER[slug as keyof typeof PATH_TO_HANDLER]) {
|
|
||||||
return PATH_TO_HANDLER[slug as keyof typeof PATH_TO_HANDLER](req, res)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return res.status(405).json({ error: "Method not allowed" })
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +1,13 @@
|
||||||
import config from "@lib/config"
|
import config from "@lib/config"
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
|
|
||||||
export const getRequiresPasscode = async () => {
|
export const getRequiresPasscode = async () => {
|
||||||
const requiresPasscode = Boolean(config.registration_password)
|
const requiresPasscode = Boolean(config.registration_password)
|
||||||
return requiresPasscode
|
return requiresPasscode
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRequiresPasscode = async (
|
const handleRequiresPasscode = async (
|
||||||
req: NextApiRequest,
|
_: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse
|
||||||
) => {
|
) => {
|
||||||
return res.json({ requiresPasscode: await getRequiresPasscode() })
|
return res.json({ requiresPasscode: await getRequiresPasscode() })
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import getHtmlFromFile from "@lib/server/get-html-from-drift-file"
|
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
|
||||||
import { parseUrlQuery } from "@lib/server/parse-url-query"
|
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
import prisma from "app/prisma"
|
import prisma from "app/prisma"
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
|
@ -10,9 +10,9 @@ export default async function handler(
|
||||||
switch (req.method) {
|
switch (req.method) {
|
||||||
case "GET":
|
case "GET":
|
||||||
const query = req.query
|
const query = req.query
|
||||||
const fileId = parseUrlQuery(query.fileId)
|
const fileId = parseQueryParam(query.fileId)
|
||||||
const content = parseUrlQuery(query.content)
|
const content = parseQueryParam(query.content)
|
||||||
const title = parseUrlQuery(query.title)
|
const title = parseQueryParam(query.title)
|
||||||
|
|
||||||
if (fileId && (content || title)) {
|
if (fileId && (content || title)) {
|
||||||
return res.status(400).json({ error: "Too many arguments" })
|
return res.status(400).json({ error: "Too many arguments" })
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import prisma from "app/prisma"
|
|
||||||
|
|
||||||
export const getPostsByUser = async (userId: number) => {
|
|
||||||
const posts = await prisma.post.findMany({
|
|
||||||
where: {
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return posts
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import config from "@lib/config"
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
|
||||||
|
|
||||||
const handleSelf = async (req: NextApiRequest, res: NextApiResponse) => {}
|
|
||||||
|
|
||||||
const PATH_TO_HANDLER = {
|
|
||||||
self: handleRequiresPasscode
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-anonymous-default-export
|
|
||||||
export default (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
const { slug } = req.query
|
|
||||||
|
|
||||||
if (!slug || Array.isArray(slug)) {
|
|
||||||
return res.status(400).json({ error: "Missing param" })
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (req.method) {
|
|
||||||
case "GET":
|
|
||||||
if (PATH_TO_HANDLER[slug as keyof typeof PATH_TO_HANDLER]) {
|
|
||||||
return PATH_TO_HANDLER[slug as keyof typeof PATH_TO_HANDLER](req, res)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return res.status(405).json({ error: "Method not allowed" })
|
|
||||||
}
|
|
||||||
}
|
|
21
client/pages/api/user/posts.ts
Normal file
21
client/pages/api/user/posts.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||||
|
import { getPostsByUser } from "app/prisma"
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
|
export default async function handle(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
switch (req.method) {
|
||||||
|
case "GET":
|
||||||
|
const userId = parseQueryParam(req.query.userId)
|
||||||
|
if (!userId) {
|
||||||
|
return res.status(400).json({ error: "Missing userId" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts = await getPostsByUser(userId)
|
||||||
|
return res.json(posts)
|
||||||
|
default:
|
||||||
|
return res.status(405).end()
|
||||||
|
}
|
||||||
|
}
|
59
client/pages/api/user/self.ts
Normal file
59
client/pages/api/user/self.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// user.get("/self", jwt, async (req: UserJwtRequest, res, next) => {
|
||||||
|
// const error = () =>
|
||||||
|
// res.status(401).json({
|
||||||
|
// message: "Unauthorized"
|
||||||
|
// })
|
||||||
|
|
||||||
|
import { USER_COOKIE_NAME } from "@lib/constants"
|
||||||
|
import prisma, { getUserById } from "app/prisma"
|
||||||
|
import { getCookie } from "cookies-next"
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// if (!req.user) {
|
||||||
|
// return error()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const user = await User.findByPk(req.user?.id, {
|
||||||
|
// attributes: {
|
||||||
|
// exclude: ["password"]
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// if (!user) {
|
||||||
|
// return error()
|
||||||
|
// }
|
||||||
|
// res.json(user)
|
||||||
|
// } catch (error) {
|
||||||
|
// next(error)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
): Promise<any> {
|
||||||
|
const error = () =>
|
||||||
|
res.status(401).json({
|
||||||
|
message: "Unauthorized"
|
||||||
|
})
|
||||||
|
|
||||||
|
const userId = String(getCookie(USER_COOKIE_NAME, {
|
||||||
|
req, res
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return error()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await getUserById(userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return error()
|
||||||
|
}
|
||||||
|
return res.json(user)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`/api/user/self:`, e)
|
||||||
|
return error()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,33 +1,31 @@
|
||||||
// a nextjs api handerl
|
// a nextjs api handerl
|
||||||
|
|
||||||
import config from "@lib/config"
|
import config from "@lib/config"
|
||||||
import markdown from "@lib/render-markdown"
|
import renderMarkdown from "@lib/render-markdown"
|
||||||
import { NextApiRequest, NextApiResponse } from "next"
|
import { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
|
||||||
export const getWelcomeContent = async () => {
|
export const getWelcomeContent = async () => {
|
||||||
const introContent = config.welcome_content
|
const introContent = config.welcome_content
|
||||||
const introTitle = config.welcome_title
|
const introTitle = config.welcome_title
|
||||||
// if (!introContent || !introTitle) {
|
|
||||||
// return {}
|
|
||||||
// }
|
|
||||||
|
|
||||||
console.log(introContent)
|
|
||||||
|
|
||||||
|
console.log(renderMarkdown(introContent))
|
||||||
return {
|
return {
|
||||||
title: introTitle,
|
title: introTitle,
|
||||||
content: introContent,
|
content: introContent,
|
||||||
rendered: markdown(introContent)
|
rendered: renderMarkdown(introContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
_: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse
|
||||||
) {
|
) {
|
||||||
const welcomeContent = await getWelcomeContent()
|
const welcomeContent = await getWelcomeContent()
|
||||||
if (!welcomeContent) {
|
if (!welcomeContent) {
|
||||||
return res.status(500).json({ error: "Missing welcome content" })
|
return res.status(500).json({ error: "Missing welcome content" })
|
||||||
}
|
}
|
||||||
|
console.log(welcomeContent.title)
|
||||||
|
|
||||||
return res.json(welcomeContent)
|
return res.json(welcomeContent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { Note, Page, Text } from "@geist-ui/core/dist"
|
|
||||||
import styles from "@styles/Home.module.css"
|
|
||||||
|
|
||||||
const Expired = () => {
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<Page.Content className={styles.main}>
|
|
||||||
<Note type="error" label={false}>
|
|
||||||
<Text h4>
|
|
||||||
Error: The Drift you're trying to view has expired.
|
|
||||||
</Text>
|
|
||||||
</Note>
|
|
||||||
</Page.Content>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Expired
|
|
|
@ -1,74 +0,0 @@
|
||||||
import styles from "@styles/Home.module.css"
|
|
||||||
import PageSeo from "@components/page-seo"
|
|
||||||
import HomeComponent from "@components/home"
|
|
||||||
import { Page, Text } from "@geist-ui/core/dist"
|
|
||||||
import type { GetStaticProps } from "next"
|
|
||||||
import { InferGetStaticPropsType } from "next"
|
|
||||||
type Props =
|
|
||||||
| {
|
|
||||||
introContent: string
|
|
||||||
introTitle: string
|
|
||||||
rendered: string
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
error: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = async () => {
|
|
||||||
try {
|
|
||||||
const resp = await fetch(process.env.API_URL + `/welcome`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-secret-key": process.env.SECRET_KEY || ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { title, content, rendered } = await resp.json()
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
introContent: content || null,
|
|
||||||
rendered: rendered || null,
|
|
||||||
introTitle: title || null
|
|
||||||
},
|
|
||||||
// Next.js will attempt to re-generate the page:
|
|
||||||
// - When a request comes in
|
|
||||||
// - At most every 60 seconds
|
|
||||||
revalidate: 60 // In seconds
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// If there was an error, it's likely due to the server not running, so we attempt to regenerate the page
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
error: true
|
|
||||||
},
|
|
||||||
revalidate: 10 // In seconds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: fix props type
|
|
||||||
const Home = ({
|
|
||||||
rendered,
|
|
||||||
introContent,
|
|
||||||
introTitle,
|
|
||||||
error
|
|
||||||
}: InferGetStaticPropsType<typeof getStaticProps>) => {
|
|
||||||
return (
|
|
||||||
<Page className={styles.wrapper}>
|
|
||||||
<PageSeo />
|
|
||||||
<Page.Content className={styles.main}>
|
|
||||||
{error && <Text>Something went wrong. Is the server running?</Text>}
|
|
||||||
{!error && (
|
|
||||||
<HomeComponent
|
|
||||||
rendered={rendered}
|
|
||||||
introContent={introContent}
|
|
||||||
introTitle={introTitle}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Page.Content>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Home
|
|
|
@ -1,78 +0,0 @@
|
||||||
import styles from "@styles/Home.module.css"
|
|
||||||
import NewPost from "@components/new-post"
|
|
||||||
import Header from "@components/header"
|
|
||||||
import PageSeo from "@components/page-seo"
|
|
||||||
import { Page } from "@geist-ui/core/dist"
|
|
||||||
import Head from "next/head"
|
|
||||||
import { GetServerSideProps } from "next"
|
|
||||||
import { Post } from "@lib/types"
|
|
||||||
import cookie from "cookie"
|
|
||||||
|
|
||||||
const NewFromExisting = ({
|
|
||||||
post,
|
|
||||||
parentId
|
|
||||||
}: {
|
|
||||||
post: Post
|
|
||||||
parentId: string
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Page className={styles.wrapper}>
|
|
||||||
<PageSeo title="Create a new Drift" />
|
|
||||||
<Head>
|
|
||||||
{/* TODO: solve this. */}
|
|
||||||
{/* eslint-disable-next-line @next/next/no-css-tags */}
|
|
||||||
<link rel="stylesheet" href="/css/react-datepicker.css" />
|
|
||||||
</Head>
|
|
||||||
<Page.Content className={styles.main}>
|
|
||||||
<NewPost initialPost={post} newPostParent={parentId} />
|
|
||||||
</Page.Content>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async ({
|
|
||||||
req,
|
|
||||||
params
|
|
||||||
}) => {
|
|
||||||
const id = params?.id
|
|
||||||
const redirect = {
|
|
||||||
redirect: {
|
|
||||||
destination: "/new",
|
|
||||||
permanent: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
return redirect
|
|
||||||
}
|
|
||||||
|
|
||||||
const driftToken = cookie.parse(req.headers.cookie || "")[`drift-token`]
|
|
||||||
|
|
||||||
const post = await fetch(`${process.env.API_URL}/posts/${id}`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${driftToken}`,
|
|
||||||
"x-secret-key": process.env.SECRET_KEY || ""
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!post.ok) {
|
|
||||||
return redirect
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await post.json()
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return redirect
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
post: data,
|
|
||||||
parentId: id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NewFromExisting
|
|
|
@ -1,24 +0,0 @@
|
||||||
import styles from "@styles/Home.module.css"
|
|
||||||
import NewPost from "@components/new-post"
|
|
||||||
import Header from "@components/header"
|
|
||||||
import PageSeo from "@components/page-seo"
|
|
||||||
import { Page } from "@geist-ui/core/dist"
|
|
||||||
import Head from "next/head"
|
|
||||||
|
|
||||||
const New = () => {
|
|
||||||
return (
|
|
||||||
<Page className={styles.wrapper}>
|
|
||||||
<PageSeo title="Create a new Drift" />
|
|
||||||
<Head>
|
|
||||||
{/* TODO: solve this. */}
|
|
||||||
{/* eslint-disable-next-line @next/next/no-css-tags */}
|
|
||||||
<link rel="stylesheet" href="/css/react-datepicker.css" />
|
|
||||||
</Head>
|
|
||||||
<Page.Content className={styles.main}>
|
|
||||||
<NewPost />
|
|
||||||
</Page.Content>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default New
|
|
|
@ -1,27 +0,0 @@
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
Text,
|
|
||||||
Fieldset,
|
|
||||||
Input,
|
|
||||||
Page,
|
|
||||||
Note,
|
|
||||||
Textarea
|
|
||||||
} from "@geist-ui/core/dist"
|
|
||||||
import PageSeo from "@components/page-seo"
|
|
||||||
import styles from "@styles/Home.module.css"
|
|
||||||
import SettingsPage from "@components/settings"
|
|
||||||
|
|
||||||
const Settings = () => (
|
|
||||||
<Page width={"100%"}>
|
|
||||||
<PageSeo title="Drift - Settings" />
|
|
||||||
<Page.Content
|
|
||||||
className={styles.main}
|
|
||||||
style={{ gap: "var(--gap)", display: "flex", flexDirection: "column" }}
|
|
||||||
>
|
|
||||||
<SettingsPage />
|
|
||||||
</Page.Content>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default Settings
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { Page } from "@geist-ui/core/dist"
|
|
||||||
import PageSeo from "@components/page-seo"
|
|
||||||
import Auth from "@components/auth"
|
|
||||||
import styles from "@styles/Home.module.css"
|
|
||||||
const SignIn = () => (
|
|
||||||
<Page width={"100%"}>
|
|
||||||
<PageSeo title="Drift - Sign In" />
|
|
||||||
<Page.Content className={styles.main}>
|
|
||||||
<Auth page="signin" />
|
|
||||||
</Page.Content>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default SignIn
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { Page } from "@geist-ui/core/dist"
|
|
||||||
import Auth from "@components/auth"
|
|
||||||
import PageSeo from "@components/page-seo"
|
|
||||||
import styles from "@styles/Home.module.css"
|
|
||||||
|
|
||||||
const SignUp = () => (
|
|
||||||
<Page width="100%">
|
|
||||||
<PageSeo title="Drift - Sign Up" />
|
|
||||||
<Page.Content className={styles.main}>
|
|
||||||
<Auth page="signup" />
|
|
||||||
</Page.Content>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default SignUp
|
|
|
@ -12,6 +12,7 @@ specifiers:
|
||||||
'@types/react': 18.0.9
|
'@types/react': 18.0.9
|
||||||
'@types/react-datepicker': 4.4.1
|
'@types/react-datepicker': 4.4.1
|
||||||
'@types/react-dom': 18.0.3
|
'@types/react-dom': 18.0.3
|
||||||
|
'@types/showdown': ^2.0.0
|
||||||
bcrypt: ^5.1.0
|
bcrypt: ^5.1.0
|
||||||
client-zip: 2.2.1
|
client-zip: 2.2.1
|
||||||
clsx: ^1.2.1
|
clsx: ^1.2.1
|
||||||
|
@ -36,6 +37,7 @@ specifiers:
|
||||||
react-hot-toast: ^2.4.0
|
react-hot-toast: ^2.4.0
|
||||||
react-loading-skeleton: 3.1.0
|
react-loading-skeleton: 3.1.0
|
||||||
sharp: ^0.31.2
|
sharp: ^0.31.2
|
||||||
|
showdown: ^2.1.0
|
||||||
swr: 1.3.0
|
swr: 1.3.0
|
||||||
textarea-markdown-editor: 0.1.13
|
textarea-markdown-editor: 0.1.13
|
||||||
typescript: 4.6.4
|
typescript: 4.6.4
|
||||||
|
@ -63,6 +65,7 @@ dependencies:
|
||||||
react-dropzone: 14.2.3_react@18.2.0
|
react-dropzone: 14.2.3_react@18.2.0
|
||||||
react-hot-toast: 2.4.0_biqbaboplfbrettd7655fr4n2y
|
react-hot-toast: 2.4.0_biqbaboplfbrettd7655fr4n2y
|
||||||
react-loading-skeleton: 3.1.0_react@18.2.0
|
react-loading-skeleton: 3.1.0_react@18.2.0
|
||||||
|
showdown: 2.1.0
|
||||||
swr: 1.3.0_react@18.2.0
|
swr: 1.3.0_react@18.2.0
|
||||||
textarea-markdown-editor: 0.1.13_biqbaboplfbrettd7655fr4n2y
|
textarea-markdown-editor: 0.1.13_biqbaboplfbrettd7655fr4n2y
|
||||||
zod: 3.19.1
|
zod: 3.19.1
|
||||||
|
@ -79,6 +82,7 @@ devDependencies:
|
||||||
'@types/react': 18.0.9
|
'@types/react': 18.0.9
|
||||||
'@types/react-datepicker': 4.4.1_biqbaboplfbrettd7655fr4n2y
|
'@types/react-datepicker': 4.4.1_biqbaboplfbrettd7655fr4n2y
|
||||||
'@types/react-dom': 18.0.3
|
'@types/react-dom': 18.0.3
|
||||||
|
'@types/showdown': 2.0.0
|
||||||
cross-env: 7.0.3
|
cross-env: 7.0.3
|
||||||
eslint: 8.27.0
|
eslint: 8.27.0
|
||||||
eslint-config-next: 13.0.2_hsmo2rtalirsvadpuxki35bq2i
|
eslint-config-next: 13.0.2_hsmo2rtalirsvadpuxki35bq2i
|
||||||
|
@ -469,6 +473,10 @@ packages:
|
||||||
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
|
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/showdown/2.0.0:
|
||||||
|
resolution: {integrity: sha512-70xBJoLv+oXjB5PhtA8vo7erjLDp9/qqI63SRHm4REKrwuPOLs8HhXwlZJBJaB4kC18cCZ1UUZ6Fb/PLFW4TCA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@typescript-eslint/parser/5.42.1_hsmo2rtalirsvadpuxki35bq2i:
|
/@typescript-eslint/parser/5.42.1_hsmo2rtalirsvadpuxki35bq2i:
|
||||||
resolution: {integrity: sha512-kAV+NiNBWVQDY9gDJDToTE/NO8BHi4f6b7zTsVAJoTkmB/zlfOpiEVBzHOKtlgTndCKe8vj9F/PuolemZSh50Q==}
|
resolution: {integrity: sha512-kAV+NiNBWVQDY9gDJDToTE/NO8BHi4f6b7zTsVAJoTkmB/zlfOpiEVBzHOKtlgTndCKe8vj9F/PuolemZSh50Q==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
@ -944,6 +952,11 @@ packages:
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/commander/9.4.1:
|
||||||
|
resolution: {integrity: sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==}
|
||||||
|
engines: {node: ^12.20.0 || >=14}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/commondir/1.0.1:
|
/commondir/1.0.1:
|
||||||
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
|
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -3546,6 +3559,13 @@ packages:
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/showdown/2.1.0:
|
||||||
|
resolution: {integrity: sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
commander: 9.4.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/side-channel/1.0.4:
|
/side-channel/1.0.4:
|
||||||
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
|
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `PostAuthors` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- Added the required column `authorId` to the `Posts` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropTable
|
||||||
|
PRAGMA foreign_keys=off;
|
||||||
|
DROP TABLE "PostAuthors";
|
||||||
|
PRAGMA foreign_keys=on;
|
||||||
|
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Posts" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"visibility" TEXT NOT NULL,
|
||||||
|
"password" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
"deletedAt" DATETIME,
|
||||||
|
"expiresAt" DATETIME,
|
||||||
|
"parentId" TEXT,
|
||||||
|
"description" TEXT,
|
||||||
|
"authorId" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Posts" ("createdAt", "deletedAt", "description", "expiresAt", "id", "parentId", "password", "title", "updatedAt", "visibility") SELECT "createdAt", "deletedAt", "description", "expiresAt", "id", "parentId", "password", "title", "updatedAt", "visibility" FROM "Posts";
|
||||||
|
DROP TABLE "Posts";
|
||||||
|
ALTER TABLE "new_Posts" RENAME TO "Posts";
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
|
@ -68,8 +68,7 @@ model Post {
|
||||||
expiresAt DateTime?
|
expiresAt DateTime?
|
||||||
parentId String?
|
parentId String?
|
||||||
description String?
|
description String?
|
||||||
// files File[]
|
authorId String
|
||||||
// postToAuthors PostToAuthors[]
|
|
||||||
|
|
||||||
@@map("Posts")
|
@@map("Posts")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue