diff options
| author | MohamedBassem <me@mbassem.com> | 2024-03-13 21:43:44 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2024-03-14 16:40:45 +0000 |
| commit | 04572a8e5081b1e4871e273cde9dbaaa44c52fe0 (patch) | |
| tree | 8e993acb732a50d1306d4d6953df96c165c57f57 /apps/web/app | |
| parent | 2df08ed08c065e8b91bc8df0266bd4bcbb062be4 (diff) | |
| download | karakeep-04572a8e5081b1e4871e273cde9dbaaa44c52fe0.tar.zst | |
structure: Create apps dir and copy tooling dir from t3-turbo repo
Diffstat (limited to 'apps/web/app')
23 files changed, 741 insertions, 0 deletions
diff --git a/apps/web/app/api/auth/[...nextauth]/route.tsx b/apps/web/app/api/auth/[...nextauth]/route.tsx new file mode 100644 index 00000000..2f7f1cb0 --- /dev/null +++ b/apps/web/app/api/auth/[...nextauth]/route.tsx @@ -0,0 +1,3 @@ +import { authHandler } from "@/server/auth"; + +export { authHandler as GET, authHandler as POST }; diff --git a/apps/web/app/api/trpc/[trpc]/route.ts b/apps/web/app/api/trpc/[trpc]/route.ts new file mode 100644 index 00000000..02ca966d --- /dev/null +++ b/apps/web/app/api/trpc/[trpc]/route.ts @@ -0,0 +1,36 @@ +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; +import { appRouter } from "@hoarder/trpc/routers/_app"; +import { createContext } from "@/server/api/client"; +import { authenticateApiKey } from "@hoarder/trpc/auth"; +import { db } from "@hoarder/db"; + +const handler = (req: Request) => + fetchRequestHandler({ + endpoint: "/api/trpc", + req, + router: appRouter, + onError: ({ path, error }) => { + if (process.env.NODE_ENV === "development") { + console.error(`❌ tRPC failed on ${path}`); + } + console.error(error); + }, + + createContext: async (opts) => { + // TODO: This is a hack until we offer a proper REST API instead of the trpc based one. + // Check if the request has an Authorization token, if it does, assume that API key authentication is requested. + const authorizationHeader = opts.req.headers.get("Authorization"); + if (authorizationHeader && authorizationHeader.startsWith("Bearer ")) { + const token = authorizationHeader.split(" ")[1]!; + try { + const user = await authenticateApiKey(token); + return { user, db }; + } catch (e) { + // Fallthrough to cookie-based auth + } + } + + return createContext(); + }, + }); +export { handler as GET, handler as POST }; diff --git a/apps/web/app/dashboard/admin/page.tsx b/apps/web/app/dashboard/admin/page.tsx new file mode 100644 index 00000000..6babdd79 --- /dev/null +++ b/apps/web/app/dashboard/admin/page.tsx @@ -0,0 +1,203 @@ +"use client"; + +import { ActionButton } from "@/components/ui/action-button"; +import LoadingSpinner from "@/components/ui/spinner"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { toast } from "@/components/ui/use-toast"; +import { api } from "@/lib/trpc"; +import { keepPreviousData } from "@tanstack/react-query"; +import { Trash } from "lucide-react"; +import { useSession } from "next-auth/react"; +import { useRouter } from "next/navigation"; + +function ActionsSection() { + const { mutate: recrawlLinks, isPending: isRecrawlPending } = + api.admin.recrawlAllLinks.useMutation({ + onSuccess: () => { + toast({ + description: "Recrawl enqueued", + }); + }, + onError: (e) => { + toast({ + variant: "destructive", + description: e.message, + }); + }, + }); + + const { mutate: reindexBookmarks, isPending: isReindexPending } = + api.admin.reindexAllBookmarks.useMutation({ + onSuccess: () => { + toast({ + description: "Reindex enqueued", + }); + }, + onError: (e) => { + toast({ + variant: "destructive", + description: e.message, + }); + }, + }); + + return ( + <> + <p className="text-xl">Actions</p> + <ActionButton + className="w-1/2" + variant="destructive" + loading={isRecrawlPending} + onClick={() => recrawlLinks()} + > + Recrawl All Links + </ActionButton> + <ActionButton + className="w-1/2" + variant="destructive" + loading={isReindexPending} + onClick={() => reindexBookmarks()} + > + Reindex All Bookmarks + </ActionButton> + </> + ); +} + +function ServerStatsSection() { + const { data: serverStats } = api.admin.stats.useQuery(undefined, { + refetchInterval: 1000, + placeholderData: keepPreviousData, + }); + + if (!serverStats) { + return <LoadingSpinner />; + } + + return ( + <> + <p className="text-xl">Server Stats</p> + <Table className="w-1/2"> + <TableBody> + <TableRow> + <TableCell className="w-2/3">Num Users</TableCell> + <TableCell>{serverStats.numUsers}</TableCell> + </TableRow> + <TableRow> + <TableCell>Num Bookmarks</TableCell> + <TableCell>{serverStats.numBookmarks}</TableCell> + </TableRow> + </TableBody> + </Table> + <hr /> + <p className="text-xl">Background Jobs</p> + <Table className="w-1/2"> + <TableBody> + <TableRow> + <TableCell className="w-2/3">Pending Crawling Jobs</TableCell> + <TableCell>{serverStats.pendingCrawls}</TableCell> + </TableRow> + <TableRow> + <TableCell>Pending Indexing Jobs</TableCell> + <TableCell>{serverStats.pendingIndexing}</TableCell> + </TableRow> + <TableRow> + <TableCell>Pending OpenAI Jobs</TableCell> + <TableCell>{serverStats.pendingOpenai}</TableCell> + </TableRow> + </TableBody> + </Table> + </> + ); +} + +function UsersSection() { + const { data: session } = useSession(); + const invalidateUserList = api.useUtils().users.list.invalidate; + const { data: users } = api.users.list.useQuery(); + const { mutate: deleteUser, isPending: isDeletionPending } = + api.users.delete.useMutation({ + onSuccess: () => { + toast({ + description: "User deleted", + }); + invalidateUserList(); + }, + onError: (e) => { + toast({ + variant: "destructive", + description: `Something went wrong: ${e.message}`, + }); + }, + }); + + if (!users) { + return <LoadingSpinner />; + } + + return ( + <> + <p className="text-xl">Users</p> + <Table> + <TableHeader> + <TableHead>Name</TableHead> + <TableHead>Email</TableHead> + <TableHead>Role</TableHead> + <TableHead>Action</TableHead> + </TableHeader> + <TableBody> + {users.users.map((u) => ( + <TableRow key={u.id}> + <TableCell>{u.name}</TableCell> + <TableCell>{u.email}</TableCell> + <TableCell>{u.role}</TableCell> + <TableCell> + <ActionButton + variant="destructive" + onClick={() => deleteUser({ userId: u.id })} + loading={isDeletionPending} + disabled={session!.user.id == u.id} + > + <Trash /> + </ActionButton> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </> + ); +} + +export default function AdminPage() { + const router = useRouter(); + const { data: session, status } = useSession(); + + if (status == "loading") { + return <LoadingSpinner />; + } + + if (!session || session.user.role != "admin") { + router.push("/"); + return; + } + + return ( + <div className="m-4 flex flex-col gap-5 rounded-md border bg-white p-4"> + <p className="text-2xl">Admin</p> + <hr /> + <ServerStatsSection /> + <hr /> + <UsersSection /> + <hr /> + <ActionsSection /> + </div> + ); +} diff --git a/apps/web/app/dashboard/archive/page.tsx b/apps/web/app/dashboard/archive/page.tsx new file mode 100644 index 00000000..69559185 --- /dev/null +++ b/apps/web/app/dashboard/archive/page.tsx @@ -0,0 +1,9 @@ +import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; + +export default async function ArchivedBookmarkPage() { + return ( + <div className="continer mt-4"> + <Bookmarks title="🗄️ Archive" archived={true} showDivider={true} /> + </div> + ); +} diff --git a/apps/web/app/dashboard/bookmarks/layout.tsx b/apps/web/app/dashboard/bookmarks/layout.tsx new file mode 100644 index 00000000..71ee143b --- /dev/null +++ b/apps/web/app/dashboard/bookmarks/layout.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import TopNav from "@/components/dashboard/bookmarks/TopNav"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Hoarder - Bookmarks", +}; + +export default function BookmarksLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + <div className="flex h-full flex-col"> + <div> + <TopNav /> + </div> + <hr /> + <div className="my-4 flex-1 pb-4">{children}</div> + </div> + ); +} diff --git a/apps/web/app/dashboard/bookmarks/loading.tsx b/apps/web/app/dashboard/bookmarks/loading.tsx new file mode 100644 index 00000000..4e56c3c4 --- /dev/null +++ b/apps/web/app/dashboard/bookmarks/loading.tsx @@ -0,0 +1,11 @@ +import Spinner from "@/components/ui/spinner"; + +export default function Loading() { + return ( + <div className="flex size-full"> + <div className="m-auto"> + <Spinner /> + </div> + </div> + ); +} diff --git a/apps/web/app/dashboard/bookmarks/page.tsx b/apps/web/app/dashboard/bookmarks/page.tsx new file mode 100644 index 00000000..c9391d85 --- /dev/null +++ b/apps/web/app/dashboard/bookmarks/page.tsx @@ -0,0 +1,5 @@ +import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; + +export default async function BookmarksPage() { + return <Bookmarks title="Bookmarks" archived={false} />; +} diff --git a/apps/web/app/dashboard/error.tsx b/apps/web/app/dashboard/error.tsx new file mode 100644 index 00000000..556e59a3 --- /dev/null +++ b/apps/web/app/dashboard/error.tsx @@ -0,0 +1,9 @@ +"use client"; + +export default function Error() { + return ( + <div className="flex size-full"> + <div className="m-auto text-3xl">Something went wrong</div> + </div> + ); +} diff --git a/apps/web/app/dashboard/favourites/page.tsx b/apps/web/app/dashboard/favourites/page.tsx new file mode 100644 index 00000000..de17461d --- /dev/null +++ b/apps/web/app/dashboard/favourites/page.tsx @@ -0,0 +1,14 @@ +import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; + +export default async function FavouritesBookmarkPage() { + return ( + <div className="continer mt-4"> + <Bookmarks + title="⭐️ Favourites" + archived={false} + favourited={true} + showDivider={true} + /> + </div> + ); +} diff --git a/apps/web/app/dashboard/layout.tsx b/apps/web/app/dashboard/layout.tsx new file mode 100644 index 00000000..31d592fb --- /dev/null +++ b/apps/web/app/dashboard/layout.tsx @@ -0,0 +1,24 @@ +import { Separator } from "@/components/ui/separator"; +import MobileSidebar from "@/components/dashboard/sidebar/ModileSidebar"; +import Sidebar from "@/components/dashboard/sidebar/Sidebar"; + +export default async function Dashboard({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + <div className="flex min-h-screen w-screen flex-col sm:h-screen sm:flex-row"> + <div className="hidden flex-none sm:flex"> + <Sidebar /> + </div> + <main className="flex-1 bg-gray-100 sm:overflow-y-auto"> + <div className="block w-full sm:hidden"> + <MobileSidebar /> + <Separator /> + </div> + {children} + </main> + </div> + ); +} diff --git a/apps/web/app/dashboard/lists/[listId]/page.tsx b/apps/web/app/dashboard/lists/[listId]/page.tsx new file mode 100644 index 00000000..006fd3ad --- /dev/null +++ b/apps/web/app/dashboard/lists/[listId]/page.tsx @@ -0,0 +1,44 @@ +import { api } from "@/server/api/client"; +import { getServerAuthSession } from "@/server/auth"; +import { TRPCError } from "@trpc/server"; +import { notFound, redirect } from "next/navigation"; +import ListView from "@/components/dashboard/lists/ListView"; +import DeleteListButton from "@/components/dashboard/lists/DeleteListButton"; + +export default async function ListPage({ + params, +}: { + params: { listId: string }; +}) { + const session = await getServerAuthSession(); + if (!session) { + redirect("/"); + } + + let list; + try { + list = await api.lists.get({ listId: params.listId }); + } catch (e) { + if (e instanceof TRPCError) { + if (e.code == "NOT_FOUND") { + notFound(); + } + } + throw e; + } + + const bookmarks = await api.bookmarks.getBookmarks({ ids: list.bookmarks }); + + return ( + <div className="container flex flex-col gap-3"> + <div className="flex justify-between"> + <span className="pt-4 text-2xl"> + {list.icon} {list.name} + </span> + <DeleteListButton list={list} /> + </div> + <hr /> + <ListView list={list} bookmarks={bookmarks.bookmarks} /> + </div> + ); +} diff --git a/apps/web/app/dashboard/lists/page.tsx b/apps/web/app/dashboard/lists/page.tsx new file mode 100644 index 00000000..88eeda47 --- /dev/null +++ b/apps/web/app/dashboard/lists/page.tsx @@ -0,0 +1,14 @@ +import { api } from "@/server/api/client"; +import AllListsView from "@/components/dashboard/lists/AllListsView"; + +export default async function ListsPage() { + const lists = await api.lists.list(); + + return ( + <div className="container mt-4 flex flex-col gap-3"> + <p className="text-2xl">📋 All Lists</p> + <hr /> + <AllListsView initialData={lists.lists} /> + </div> + ); +} diff --git a/apps/web/app/dashboard/not-found.tsx b/apps/web/app/dashboard/not-found.tsx new file mode 100644 index 00000000..64df220c --- /dev/null +++ b/apps/web/app/dashboard/not-found.tsx @@ -0,0 +1,7 @@ +export default function NotFound() { + return ( + <div className="flex size-full"> + <div className="m-auto text-3xl">Not Found :(</div> + </div> + ); +} diff --git a/apps/web/app/dashboard/preview/[bookmarkId]/page.tsx b/apps/web/app/dashboard/preview/[bookmarkId]/page.tsx new file mode 100644 index 00000000..707d2b69 --- /dev/null +++ b/apps/web/app/dashboard/preview/[bookmarkId]/page.tsx @@ -0,0 +1,14 @@ +import { api } from "@/server/api/client"; +import BookmarkPreview from "@/components/dashboard/bookmarks/BookmarkPreview"; + +export default async function BookmarkPreviewPage({ + params, +}: { + params: { bookmarkId: string }; +}) { + const bookmark = await api.bookmarks.getBookmark({ + bookmarkId: params.bookmarkId, + }); + + return <BookmarkPreview initialData={bookmark} />; +} diff --git a/apps/web/app/dashboard/search/page.tsx b/apps/web/app/dashboard/search/page.tsx new file mode 100644 index 00000000..602f6aa0 --- /dev/null +++ b/apps/web/app/dashboard/search/page.tsx @@ -0,0 +1,41 @@ +"use client"; + +import BookmarksGrid from "@/components/dashboard/bookmarks/BookmarksGrid"; +import Loading from "../bookmarks/loading"; +import { Suspense, useRef } from "react"; +import { SearchInput } from "@/components/dashboard/search/SearchInput"; +import { useBookmarkSearch } from "@/lib/hooks/bookmark-search"; + +function SearchComp() { + const { data, isPending, isPlaceholderData } = useBookmarkSearch(); + + const inputRef: React.MutableRefObject<HTMLInputElement | null> = + useRef<HTMLInputElement | null>(null); + + return ( + <div className="container flex flex-col gap-3 p-4"> + <SearchInput + ref={inputRef} + autoFocus={true} + loading={isPending || isPlaceholderData} + /> + <hr /> + {data ? ( + <BookmarksGrid + query={{ ids: data.bookmarks.map((b) => b.id) }} + bookmarks={data.bookmarks} + /> + ) : ( + <Loading /> + )} + </div> + ); +} + +export default function SearchPage() { + return ( + <Suspense> + <SearchComp /> + </Suspense> + ); +} diff --git a/apps/web/app/dashboard/settings/page.tsx b/apps/web/app/dashboard/settings/page.tsx new file mode 100644 index 00000000..38091e6c --- /dev/null +++ b/apps/web/app/dashboard/settings/page.tsx @@ -0,0 +1,9 @@ +import ApiKeySettings from "@/components/dashboard/settings/ApiKeySettings"; +export default async function Settings() { + return ( + <div className="m-4 flex flex-col space-y-2 rounded-md border bg-white p-4"> + <p className="text-2xl">Settings</p> + <ApiKeySettings /> + </div> + ); +} diff --git a/apps/web/app/dashboard/tags/[tagName]/page.tsx b/apps/web/app/dashboard/tags/[tagName]/page.tsx new file mode 100644 index 00000000..c978b86a --- /dev/null +++ b/apps/web/app/dashboard/tags/[tagName]/page.tsx @@ -0,0 +1,55 @@ +import { getServerAuthSession } from "@/server/auth"; +import { db } from "@hoarder/db"; +import { notFound, redirect } from "next/navigation"; +import BookmarksGrid from "@/components/dashboard/bookmarks/BookmarksGrid"; +import { api } from "@/server/api/client"; +import { bookmarkTags, tagsOnBookmarks } from "@hoarder/db/schema"; +import { and, eq } from "drizzle-orm"; + +export default async function TagPage({ + params, +}: { + params: { tagName: string }; +}) { + const session = await getServerAuthSession(); + if (!session) { + redirect("/"); + } + const tagName = decodeURIComponent(params.tagName); + const tag = await db.query.bookmarkTags.findFirst({ + where: and( + eq(bookmarkTags.userId, session.user.id), + eq(bookmarkTags.name, tagName), + ), + columns: { + id: true, + }, + }); + + if (!tag) { + // TODO: Better error message when the tag is not there + notFound(); + } + + const bookmarkIds = await db.query.tagsOnBookmarks.findMany({ + where: eq(tagsOnBookmarks.tagId, tag.id), + columns: { + bookmarkId: true, + }, + }); + + const query = { + ids: bookmarkIds.map((b) => b.bookmarkId), + archived: false, + }; + + const bookmarks = await api.bookmarks.getBookmarks(query); + + return ( + <div className="container flex flex-col gap-3"> + <span className="pt-4 text-2xl">{tagName}</span> + <hr /> + <BookmarksGrid query={query} bookmarks={bookmarks.bookmarks} /> + </div> + ); +} diff --git a/apps/web/app/dashboard/tags/page.tsx b/apps/web/app/dashboard/tags/page.tsx new file mode 100644 index 00000000..44c164e1 --- /dev/null +++ b/apps/web/app/dashboard/tags/page.tsx @@ -0,0 +1,56 @@ +import { Separator } from "@/components/ui/separator"; +import { getServerAuthSession } from "@/server/auth"; +import { db } from "@hoarder/db"; +import { bookmarkTags, tagsOnBookmarks } from "@hoarder/db/schema"; +import { count, eq } from "drizzle-orm"; +import Link from "next/link"; +import { redirect } from "next/navigation"; + +function TagPill({ name, count }: { name: string; count: number }) { + return ( + <Link + className="text-foreground hover:bg-foreground hover:text-background flex gap-2 rounded-md border border-gray-200 bg-white px-2 py-1" + href={`/dashboard/tags/${name}`} + > + {name} <Separator orientation="vertical" /> {count} + </Link> + ); +} + +export default async function TagsPage() { + const session = await getServerAuthSession(); + if (!session) { + redirect("/"); + } + + let tags = await db + .select({ + id: tagsOnBookmarks.tagId, + name: bookmarkTags.name, + count: count(), + }) + .from(tagsOnBookmarks) + .where(eq(bookmarkTags.userId, session.user.id)) + .groupBy(tagsOnBookmarks.tagId) + .innerJoin(bookmarkTags, eq(bookmarkTags.id, tagsOnBookmarks.tagId)); + + // Sort tags by usage desc + tags = tags.sort((a, b) => b.count - a.count); + + let tagPill; + if (tags.length) { + tagPill = tags.map((t) => ( + <TagPill key={t.id} name={t.name} count={t.count} /> + )); + } else { + tagPill = "No Tags"; + } + + return ( + <div className="container mt-2 space-y-3"> + <span className="text-2xl">All Tags</span> + <hr /> + <div className="flex flex-wrap gap-3">{tagPill}</div> + </div> + ); +} diff --git a/apps/web/app/favicon.ico b/apps/web/app/favicon.ico Binary files differnew file mode 100644 index 00000000..750e3c04 --- /dev/null +++ b/apps/web/app/favicon.ico diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css new file mode 100644 index 00000000..8abdb15c --- /dev/null +++ b/apps/web/app/globals.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx new file mode 100644 index 00000000..b1790a1f --- /dev/null +++ b/apps/web/app/layout.tsx @@ -0,0 +1,51 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import React from "react"; +import { Toaster } from "@/components/ui/toaster"; +import Providers from "@/lib/providers"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { getServerAuthSession } from "@/server/auth"; +import type { Viewport } from "next"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Hoarder", + applicationName: "Hoarder", + description: "Your AI powered second brain", + manifest: "/manifest.json", + appleWebApp: { + capable: true, + title: "Hoarder", + }, + formatDetection: { + telephone: false, + }, +}; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, +}; + +export default async function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const session = await getServerAuthSession(); + return ( + <html lang="en"> + <body className={inter.className}> + <Providers session={session}> + {children} + <ReactQueryDevtools initialIsOpen={false} /> + </Providers> + <Toaster /> + </body> + </html> + ); +} diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx new file mode 100644 index 00000000..f467b64b --- /dev/null +++ b/apps/web/app/page.tsx @@ -0,0 +1,12 @@ +import { getServerAuthSession } from "@/server/auth"; +import { redirect } from "next/navigation"; + +export default async function Home() { + // TODO: Home currently just redirects between pages until we build a proper landing page + const session = await getServerAuthSession(); + if (!session) { + redirect("/signin"); + } + + redirect("/dashboard/bookmarks"); +} diff --git a/apps/web/app/signin/page.tsx b/apps/web/app/signin/page.tsx new file mode 100644 index 00000000..fed71b62 --- /dev/null +++ b/apps/web/app/signin/page.tsx @@ -0,0 +1,25 @@ +import { PackageOpen } from "lucide-react"; +import SignInForm from "@/components/signin/SignInForm"; +import { redirect } from "next/dist/client/components/navigation"; +import { getServerAuthSession } from "@/server/auth"; + +export default async function SignInPage() { + const session = await getServerAuthSession(); + if (session) { + redirect("/"); + } + + return ( + <div className="grid min-h-screen grid-rows-6 justify-center"> + <div className="row-span-2 flex w-96 items-center justify-center space-x-2"> + <span> + <PackageOpen size="60" className="" /> + </span> + <p className="text-6xl">Hoarder</p> + </div> + <div className="row-span-4 px-3"> + <SignInForm /> + </div> + </div> + ); +} |
