From 80bb8a108f29331cdb2f2695f6801beee104dc89 Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Thu, 8 Feb 2024 15:14:23 +0000 Subject: [refactor] Move the different packages to the package subdir --- packages/web/app/api/auth/[...nextauth]/route.tsx | 3 + packages/web/app/api/v1/links/[linkId]/route.ts | 32 +++++++ packages/web/app/api/v1/links/route.ts | 49 +++++++++++ .../app/dashboard/bookmarks/components/AddLink.tsx | 67 ++++++++++++++ .../dashboard/bookmarks/components/LinkCard.tsx | 96 +++++++++++++++++++++ .../dashboard/bookmarks/components/LinksGrid.tsx | 21 +++++ packages/web/app/dashboard/bookmarks/page.tsx | 20 +++++ packages/web/app/dashboard/components/Sidebar.tsx | 56 ++++++++++++ packages/web/app/dashboard/layout.tsx | 15 ++++ packages/web/app/favicon.ico | Bin 0 -> 25931 bytes packages/web/app/globals.css | 76 ++++++++++++++++ packages/web/app/layout.tsx | 27 ++++++ packages/web/app/page.tsx | 19 ++++ 13 files changed, 481 insertions(+) create mode 100644 packages/web/app/api/auth/[...nextauth]/route.tsx create mode 100644 packages/web/app/api/v1/links/[linkId]/route.ts create mode 100644 packages/web/app/api/v1/links/route.ts create mode 100644 packages/web/app/dashboard/bookmarks/components/AddLink.tsx create mode 100644 packages/web/app/dashboard/bookmarks/components/LinkCard.tsx create mode 100644 packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx create mode 100644 packages/web/app/dashboard/bookmarks/page.tsx create mode 100644 packages/web/app/dashboard/components/Sidebar.tsx create mode 100644 packages/web/app/dashboard/layout.tsx create mode 100644 packages/web/app/favicon.ico create mode 100644 packages/web/app/globals.css create mode 100644 packages/web/app/layout.tsx create mode 100644 packages/web/app/page.tsx (limited to 'packages/web/app') diff --git a/packages/web/app/api/auth/[...nextauth]/route.tsx b/packages/web/app/api/auth/[...nextauth]/route.tsx new file mode 100644 index 00000000..e722926b --- /dev/null +++ b/packages/web/app/api/auth/[...nextauth]/route.tsx @@ -0,0 +1,3 @@ +import { authHandler } from "@/lib/auth"; + +export { authHandler as GET, authHandler as POST }; diff --git a/packages/web/app/api/v1/links/[linkId]/route.ts b/packages/web/app/api/v1/links/[linkId]/route.ts new file mode 100644 index 00000000..39449d6d --- /dev/null +++ b/packages/web/app/api/v1/links/[linkId]/route.ts @@ -0,0 +1,32 @@ +import { authOptions } from "@/lib/auth"; +import { unbookmarkLink } from "@/lib/services/links"; +import { Prisma } from "@remember/db"; + +import { getServerSession } from "next-auth"; +import { NextRequest } from "next/server"; + +export async function DELETE( + _request: NextRequest, + { params }: { params: { linkId: string } }, +) { + // TODO: We probably should be using an API key here instead of the session; + const session = await getServerSession(authOptions); + if (!session) { + return new Response(null, { status: 401 }); + } + + try { + await unbookmarkLink(params.linkId, session.user.id); + } catch (e: unknown) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === "P2025" // RecordNotFound + ) { + return new Response(null, { status: 404 }); + } else { + throw e; + } + } + + return new Response(null, { status: 201 }); +} diff --git a/packages/web/app/api/v1/links/route.ts b/packages/web/app/api/v1/links/route.ts new file mode 100644 index 00000000..87541634 --- /dev/null +++ b/packages/web/app/api/v1/links/route.ts @@ -0,0 +1,49 @@ +import { authOptions } from "@/lib/auth"; +import { bookmarkLink, getLinks } from "@/lib/services/links"; + +import { + zNewBookmarkedLinkRequestSchema, + ZGetLinksResponse, + ZBookmarkedLink, +} from "@/lib/types/api/links"; +import { getServerSession } from "next-auth"; +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(request: NextRequest) { + // TODO: We probably should be using an API key here instead of the session; + const session = await getServerSession(authOptions); + if (!session) { + return new Response(null, { status: 401 }); + } + + const linkRequest = zNewBookmarkedLinkRequestSchema.safeParse( + await request.json(), + ); + + if (!linkRequest.success) { + return NextResponse.json( + { + error: linkRequest.error.toString(), + }, + { status: 400 }, + ); + } + + const link = await bookmarkLink(linkRequest.data.url, session.user.id); + + let response: ZBookmarkedLink = { ...link }; + return NextResponse.json(response, { status: 201 }); +} + +export async function GET() { + // TODO: We probably should be using an API key here instead of the session; + const session = await getServerSession(authOptions); + if (!session) { + return new Response(null, { status: 401 }); + } + + const links = await getLinks(session.user.id); + + let response: ZGetLinksResponse = { links }; + return NextResponse.json(response); +} diff --git a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx b/packages/web/app/dashboard/bookmarks/components/AddLink.tsx new file mode 100644 index 00000000..fb77786c --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/components/AddLink.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import APIClient from "@/lib/api"; +import { Plus } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useForm, SubmitErrorHandler } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { toast } from "@/components/ui/use-toast"; + +const formSchema = z.object({ + url: z.string().url({ message: "The link must be a valid URL" }), +}); + +export default function AddLink() { + const router = useRouter(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + }); + + async function onSubmit(value: z.infer) { + const [_resp, error] = await APIClient.bookmarkLink(value.url); + if (error) { + toast({ description: error.message, variant: "destructive" }); + return; + } + router.refresh(); + } + + const onError: SubmitErrorHandler> = (errors) => { + toast({ + description: Object.values(errors) + .map((v) => v.message) + .join("\n"), + variant: "destructive", + }); + }; + + return ( +
+ +
+ { + return ( + + + + + + ); + }} + /> + +
+
+ + ); +} diff --git a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx new file mode 100644 index 00000000..da59d9da --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + ImageCard, + ImageCardBody, + ImageCardFooter, + ImageCardTitle, +} from "@/components/ui/imageCard"; +import { useToast } from "@/components/ui/use-toast"; +import APIClient from "@/lib/api"; +import { ZBookmarkedLink } from "@/lib/types/api/links"; +import { MoreHorizontal, Trash2 } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; + +export function LinkOptions({ linkId }: { linkId: string }) { + const { toast } = useToast(); + const router = useRouter(); + + const unbookmarkLink = async () => { + let [_, error] = await APIClient.unbookmarkLink(linkId); + + if (error) { + toast({ + variant: "destructive", + title: "Something went wrong", + description: "There was a problem with your request.", + }); + } else { + toast({ + description: "The link has been deleted!", + }); + } + + router.refresh(); + }; + return ( + + + + + + + + Delete + + + + ); +} + +export default function LinkCard({ link }: { link: ZBookmarkedLink }) { + const parsedUrl = new URL(link.url); + + return ( + + + + {link.details?.title ?? parsedUrl.host} + + + + {link.tags.map((t) => ( + + #{t.name} + + ))} + + +
+
+ + {parsedUrl.host} + +
+ +
+
+
+ ); +} diff --git a/packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx b/packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx new file mode 100644 index 00000000..66f0d766 --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx @@ -0,0 +1,21 @@ +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { authOptions } from "@/lib/auth"; +import { getLinks } from "@/lib/services/links"; +import LinkCard from "./LinkCard"; + +export default async function LinksGrid() { + const session = await getServerSession(authOptions); + if (!session) { + redirect("/"); + } + const links = await getLinks(session.user.id); + + return ( +
+ {links.map((l) => ( + + ))} +
+ ); +} diff --git a/packages/web/app/dashboard/bookmarks/page.tsx b/packages/web/app/dashboard/bookmarks/page.tsx new file mode 100644 index 00000000..b4158893 --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/page.tsx @@ -0,0 +1,20 @@ +import AddLink from "./components/AddLink"; +import LinksGrid from "./components/LinksGrid"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Remember - Bookmarks", +}; + +export default async function Bookmarks() { + return ( +
+
+ +
+
+ +
+
+ ); +} diff --git a/packages/web/app/dashboard/components/Sidebar.tsx b/packages/web/app/dashboard/components/Sidebar.tsx new file mode 100644 index 00000000..0ed87daf --- /dev/null +++ b/packages/web/app/dashboard/components/Sidebar.tsx @@ -0,0 +1,56 @@ +import { Button } from "@/components/ui/button"; +import { authOptions } from "@/lib/auth"; +import { Archive, MoreHorizontal, Star, Tag, Home, Brain} from "lucide-react"; +import { getServerSession } from "next-auth"; +import Link from "next/link"; +import { redirect } from "next/navigation"; + +function SidebarItem({ + name, + logo, + path, +}: { + name: string; + logo: React.ReactNode; + path: string; +}) { + return ( +
  • + + {logo} + {name} + +
  • + ); +} + +export default async function Sidebar() { + const session = await getServerSession(authOptions); + if (!session) { + redirect("/"); + } + + return ( + + ); +} diff --git a/packages/web/app/dashboard/layout.tsx b/packages/web/app/dashboard/layout.tsx new file mode 100644 index 00000000..9b21271e --- /dev/null +++ b/packages/web/app/dashboard/layout.tsx @@ -0,0 +1,15 @@ +import Bookmarks from "@/app/dashboard/bookmarks/page"; +import Sidebar from "@/app/dashboard/components/Sidebar"; + +export default async function Dashboard() { + return ( +
    +
    + +
    +
    + +
    +
    + ); +} diff --git a/packages/web/app/favicon.ico b/packages/web/app/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/packages/web/app/favicon.ico differ diff --git a/packages/web/app/globals.css b/packages/web/app/globals.css new file mode 100644 index 00000000..8abdb15c --- /dev/null +++ b/packages/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/packages/web/app/layout.tsx b/packages/web/app/layout.tsx new file mode 100644 index 00000000..a2d34046 --- /dev/null +++ b/packages/web/app/layout.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import React from "react"; +import { Toaster } from "@/components/ui/toaster"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Remember", + description: "Your AI powered second brain", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + + ); +} diff --git a/packages/web/app/page.tsx b/packages/web/app/page.tsx new file mode 100644 index 00000000..ffc128a5 --- /dev/null +++ b/packages/web/app/page.tsx @@ -0,0 +1,19 @@ +import { LoginButton } from "@/components/auth/login"; +import { LogoutButton } from "@/components/auth/logout"; +import Link from "next/link"; + +export default function Home() { + return ( +
    +
    + +
    +
    + +
    +
    + Bookmarks +
    +
    + ); +} -- cgit v1.2.3-70-g09d2