diff --git a/client/app/(auth)/head.tsx b/client/app/(auth)/head.tsx index cea829b8..6a70b7ad 100644 --- a/client/app/(auth)/head.tsx +++ b/client/app/(auth)/head.tsx @@ -1,5 +1,5 @@ -import PageSeo from "@components/head" +import PageSeo from "@components/page-seo" export default function AuthHead() { - return + return } diff --git a/client/components/home/home.module.css b/client/app/(home)/home.module.css similarity index 100% rename from client/components/home/home.module.css rename to client/app/(home)/home.module.css diff --git a/client/components/home/index.tsx b/client/app/(home)/home.tsx similarity index 99% rename from client/components/home/index.tsx rename to client/app/(home)/home.tsx index 332f8839..42bbbd6e 100644 --- a/client/components/home/index.tsx +++ b/client/app/(home)/home.tsx @@ -1,3 +1,4 @@ +"use client" import ShiftBy from "@components/shift-by" import { Spacer, Tabs, Card, Textarea, Text } from "@geist-ui/core/dist" import Image from "next/image" diff --git a/client/app/page.tsx b/client/app/(home)/page.tsx similarity index 55% rename from client/app/page.tsx rename to client/app/(home)/page.tsx index 56c3c4d7..7fa37be4 100644 --- a/client/app/page.tsx +++ b/client/app/(home)/page.tsx @@ -1,4 +1,5 @@ import { getWelcomeContent } from "pages/api/welcome" +import Home from "./home" const getWelcomeData = async () => { const welcomeContent = await getWelcomeContent() @@ -6,7 +7,7 @@ const getWelcomeData = async () => { } export default async function Page() { - const welcomeData = await getWelcomeData() - - return

{JSON.stringify(welcomeData)}

+ const { content, rendered, title } = await getWelcomeData() + + return } diff --git a/client/app/(posts)/expired/page.tsx b/client/app/(posts)/expired/page.tsx new file mode 100644 index 00000000..f19719e4 --- /dev/null +++ b/client/app/(posts)/expired/page.tsx @@ -0,0 +1,11 @@ +"use client" + +import { Note, Text } from "@geist-ui/core/dist" + +export default function ExpiredPage() { + return ( + + Error: The Drift you're trying to view has expired. + + ) +} diff --git a/client/app/(posts)/new/from/[id]/page.tsx b/client/app/(posts)/new/from/[id]/page.tsx index e520fe66..6db2aa28 100644 --- a/client/app/(posts)/new/from/[id]/page.tsx +++ b/client/app/(posts)/new/from/[id]/page.tsx @@ -4,6 +4,7 @@ import { cookies } from "next/headers" import { TOKEN_COOKIE_NAME } from "@lib/constants" import { getPostWithFiles } from "app/prisma" import { useRedirectIfNotAuthed } from "@lib/server/hooks/use-redirect-if-not-authed" + const NewFromExisting = async ({ params }: { diff --git a/client/app/(posts)/new/layout.tsx b/client/app/(posts)/new/layout.tsx index 479eb04b..35dfebc7 100644 --- a/client/app/(posts)/new/layout.tsx +++ b/client/app/(posts)/new/layout.tsx @@ -1,4 +1,4 @@ export default function NewLayout({ children }: { children: React.ReactNode }) { - // useRedirectIfNotAuthed() - return <>{children}; + // useRedirectIfNotAuthed() + return <>{children} } diff --git a/client/app/(posts)/new/page.tsx b/client/app/(posts)/new/page.tsx index aa0621a3..4bb5e812 100644 --- a/client/app/(posts)/new/page.tsx +++ b/client/app/(posts)/new/page.tsx @@ -1,5 +1,5 @@ import NewPost from "@components/new-post" -import '@styles/react-datepicker.css' +import "@styles/react-datepicker.css" const New = () => diff --git a/client/app/(profiles)/mine/head.tsx b/client/app/(profiles)/mine/head.tsx new file mode 100644 index 00000000..8b9fc207 --- /dev/null +++ b/client/app/(profiles)/mine/head.tsx @@ -0,0 +1,5 @@ +import PageSeo from "@components/page-seo" + +export default function Head() { + return +} diff --git a/client/app/(profiles)/mine/page.tsx b/client/app/(profiles)/mine/page.tsx new file mode 100644 index 00000000..36440abc --- /dev/null +++ b/client/app/(profiles)/mine/page.tsx @@ -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 +} diff --git a/client/app/(profiles)/settings/head.tsx b/client/app/(profiles)/settings/head.tsx new file mode 100644 index 00000000..6404a743 --- /dev/null +++ b/client/app/(profiles)/settings/head.tsx @@ -0,0 +1,5 @@ +import PageSeo from "@components/page-seo" + +export default function Head() { + return +} diff --git a/client/app/(profiles)/settings/page.tsx b/client/app/(profiles)/settings/page.tsx new file mode 100644 index 00000000..ce14b34f --- /dev/null +++ b/client/app/(profiles)/settings/page.tsx @@ -0,0 +1,5 @@ +import SettingsPage from "@components/settings" + +const Settings = () => + +export default Settings diff --git a/client/app/admin/page.tsx b/client/app/admin/page.tsx new file mode 100644 index 00000000..3f3b36bb --- /dev/null +++ b/client/app/admin/page.tsx @@ -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 +} + +export default AdminPage diff --git a/client/app/layout.tsx b/client/app/layout.tsx index 8bc39b88..2d4794af 100644 --- a/client/app/layout.tsx +++ b/client/app/layout.tsx @@ -49,7 +49,9 @@ export default function RootLayout({ children }: RootLayoutProps) { Drift - {children} + + {children} + ) diff --git a/client/app/mine/page.tsx b/client/app/mine/page.tsx deleted file mode 100644 index 2835c15e..00000000 --- a/client/app/mine/page.tsx +++ /dev/null @@ -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 -} diff --git a/client/app/page-wrapper.tsx b/client/app/page-wrapper.tsx deleted file mode 100644 index 86d2f22d..00000000 --- a/client/app/page-wrapper.tsx +++ /dev/null @@ -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 ( - - -
- - - {children} - - ) -} diff --git a/client/app/prisma.ts b/client/app/prisma.ts index 9ef83abe..319b82ba 100644 --- a/client/app/prisma.ts +++ b/client/app/prisma.ts @@ -1,16 +1,10 @@ -import { Post, PrismaClient, File } from "@prisma/client" +import { Post, PrismaClient, File, User } from "@prisma/client" const prisma = new PrismaClient() export default prisma -export type { - User, - AuthTokens, - File, - Post, - PostToAuthors -} from "@prisma/client" +export type { User, AuthTokens, File, Post } from "@prisma/client" export type PostWithFiles = Post & { files: File[] @@ -50,3 +44,74 @@ export const getPostWithFiles = async ( files } } + +export async function getPostsByUser(userId: string): Promise +export async function getPostsByUser( + userId: string, + includeFiles: true +): Promise +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" +} diff --git a/client/app/root-layout-wrapper.tsx b/client/app/root-layout-wrapper.tsx index bd612503..06d71856 100644 --- a/client/app/root-layout-wrapper.tsx +++ b/client/app/root-layout-wrapper.tsx @@ -1,9 +1,10 @@ "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 { SkeletonTheme } from "react-loading-skeleton" -import PageWrapper from "./page-wrapper" +import styles from "@styles/Home.module.css" export function LayoutWrapper({ children }: { children: React.ReactNode }) { const skeletonBaseColor = "var(--light-gray)" @@ -58,7 +59,13 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) { attribute="data-theme" > - {children} + + +
+ + + {children} + diff --git a/client/components/admin/index.tsx b/client/components/admin/index.tsx index 34c7b5b7..569ad857 100644 --- a/client/components/admin/index.tsx +++ b/client/components/admin/index.tsx @@ -1,3 +1,5 @@ +"use client" + import { TOKEN_COOKIE_NAME } from "@lib/constants" import { getCookie } from "cookies-next" import styles from "./admin.module.css" diff --git a/client/components/auth/index.tsx b/client/components/auth/index.tsx index f8c7075e..4dd97337 100644 --- a/client/components/auth/index.tsx +++ b/client/components/auth/index.tsx @@ -1,16 +1,13 @@ "use client" -import { FormEvent, useEffect, useState } from "react" +import { FormEvent, useState } from "react" import styles from "./auth.module.css" import { useRouter } from "next/navigation" import Link from "../link" -import Cookies from "js-cookie" 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 { setCookie } from "cookies-next" +import { Button, Input, Note } from "@geist-ui/core/dist" const NO_EMPTY_SPACE_REGEX = /^\S*$/ const ERROR_MESSAGE = @@ -83,24 +80,26 @@ const Auth = ({
setUsername(event.currentTarget.value)} placeholder="Username" required + minLength={3} /> setPassword(event.currentTarget.value)} placeholder="Password" required + minLength={6} /> {requiresServerPassword && ( @@ -108,10 +107,11 @@ const Auth = ({ } placeholder="Server Password" required + width="100%" /> )} -
diff --git a/client/components/head/index.tsx b/client/components/head/index.tsx deleted file mode 100644 index a586544f..00000000 --- a/client/components/head/index.tsx +++ /dev/null @@ -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 ( - <> - - {title} - {!isPrivate && } - - - ) -} - -export default PageSeo diff --git a/client/components/link/index.tsx b/client/components/link/index.tsx index e130f579..c601a95d 100644 --- a/client/components/link/index.tsx +++ b/client/components/link/index.tsx @@ -3,7 +3,7 @@ import NextLink from "next/link" import styles from "./link.module.css" type LinkProps = { - colored?: boolean, + colored?: boolean children: React.ReactNode } & React.ComponentProps diff --git a/client/components/my-posts/index.tsx b/client/components/my-posts/index.tsx deleted file mode 100644 index 72e8730d..00000000 --- a/client/components/my-posts/index.tsx +++ /dev/null @@ -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 -} - -export default MyPosts diff --git a/client/components/page-seo/index.tsx b/client/components/page-seo/index.tsx index 4083f2c4..9dec0100 100644 --- a/client/components/page-seo/index.tsx +++ b/client/components/page-seo/index.tsx @@ -16,6 +16,7 @@ const PageSeo = ({ <> Drift - {title} {!isPrivate && } + {isPrivate && } ) } diff --git a/client/components/post-list/index.tsx b/client/components/post-list/index.tsx index 2fed3a54..28637d73 100644 --- a/client/components/post-list/index.tsx +++ b/client/components/post-list/index.tsx @@ -1,24 +1,25 @@ +"use client" + import { Button, Input, Text } from "@geist-ui/core/dist" import styles from "./post-list.module.css" import ListItemSkeleton from "./list-item-skeleton" import ListItem from "./list-item" -import { Post } from "@lib/types" -import { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react" +import { ChangeEvent, useCallback, useEffect, useState } from "react" import useDebounce from "@lib/hooks/use-debounce" import Link from "@components/link" import { TOKEN_COOKIE_NAME } from "@lib/constants" import { getCookie } from "cookies-next" +import type { PostWithFiles } from "app/prisma" type Props = { - initialPosts: Post[] - error: boolean + initialPosts: PostWithFiles[] morePosts: boolean } -const PostList = ({ morePosts, initialPosts, error }: Props) => { +const PostList = ({ morePosts, initialPosts }: Props) => { const [search, setSearchValue] = useState("") - const [posts, setPosts] = useState(initialPosts) + const [posts, setPosts] = useState(initialPosts) const [searching, setSearching] = useState(false) const [hasMorePosts, setHasMorePosts] = useState(morePosts) @@ -122,7 +123,7 @@ const PostList = ({ morePosts, initialPosts, error }: Props) => { onChange={handleSearchChange} /> - {error && Failed to load.} + {!posts && Failed to load.} {!posts.length && searching && (
  • @@ -133,7 +134,7 @@ const PostList = ({ morePosts, initialPosts, error }: Props) => {
)} - {posts?.length === 0 && !error && ( + {posts?.length === 0 && posts && ( No posts found. Create one{" "} diff --git a/client/components/post-list/list-item.tsx b/client/components/post-list/list-item.tsx index a5a35e82..0674fbf9 100644 --- a/client/components/post-list/list-item.tsx +++ b/client/components/post-list/list-item.tsx @@ -1,7 +1,13 @@ import NextLink from "next/link" import VisibilityBadge from "../badges/visibility-badge" -import { Text, Card, Tooltip, Divider, Badge, Button } from "@geist-ui/core/dist" -import { File, Post } from "@lib/types" +import { + Text, + Card, + Tooltip, + Divider, + Badge, + Button +} from "@geist-ui/core/dist" import FadeIn from "@components/fade-in" import Trash from "@geist-ui/icons/trash" import ExpirationBadge from "@components/badges/expiration-badge" @@ -11,6 +17,8 @@ import { useRouter } from "next/router" import Parent from "@geist-ui/icons/arrowUpCircle" import styles from "./list-item.module.css" 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 const ListItem = ({ @@ -18,7 +26,7 @@ const ListItem = ({ isOwner = true, deletePost }: { - post: Post + post: PostWithFiles isOwner?: boolean deletePost: () => void }) => { @@ -29,7 +37,7 @@ const ListItem = ({ } const viewParentClick = () => { - router.push(`/post/${post.parent?.id}`) + router.push(`/post/${post.parentId}`) } return ( @@ -48,7 +56,7 @@ const ListItem = ({ {isOwner && ( - {post.parent && ( + {post.parentId && (