aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-02-06 12:26:29 +0000
committerMohamedBassem <me@mbassem.com>2024-02-06 12:26:29 +0000
commit083ea5bd19172381bb53e95aaf98eaabef839c54 (patch)
tree4d1c041aab7cd8d15382330db08caeb9c5a12c63 /web
parentb792121977ade8bdd3fb62704b65a9fcbd1436b9 (diff)
downloadkarakeep-083ea5bd19172381bb53e95aaf98eaabef839c54.tar.zst
Move the web app into a subdir
Diffstat (limited to 'web')
-rw-r--r--web/.env.sample8
-rw-r--r--web/.eslintrc.json3
-rw-r--r--web/.gitignore39
-rw-r--r--web/Dockerfile57
-rw-r--r--web/README.md36
-rw-r--r--web/app/api/auth/[...nextauth]/route.tsx3
-rw-r--r--web/app/api/v1/links/route.ts60
-rw-r--r--web/app/favicon.icobin0 -> 25931 bytes
-rw-r--r--web/app/globals.css76
-rw-r--r--web/app/layout.tsx22
-rw-r--r--web/app/page.tsx15
-rwxr-xr-xweb/bun.lockbbin0 -> 152334 bytes
-rw-r--r--web/components.json17
-rw-r--r--web/components/auth/login.tsx17
-rw-r--r--web/components/auth/logout.tsx12
-rw-r--r--web/lib/auth.ts25
-rw-r--r--web/lib/config.ts20
-rw-r--r--web/lib/prisma.ts5
-rw-r--r--web/lib/types/api/links.ts26
-rw-r--r--web/lib/types/next-auth.d.ts12
-rw-r--r--web/lib/utils.ts6
-rw-r--r--web/next.config.mjs4
-rw-r--r--web/package.json36
-rw-r--r--web/postcss.config.js6
-rw-r--r--web/prisma/migrations/20240205153748_add_users/migration.sql56
-rw-r--r--web/prisma/migrations/20240206000813_add_links/migration.sql43
-rw-r--r--web/prisma/migrations/migration_lock.toml3
-rw-r--r--web/prisma/schema.prisma105
-rw-r--r--web/public/next.svg1
-rw-r--r--web/public/vercel.svg1
-rw-r--r--web/tailwind.config.ts80
-rw-r--r--web/tsconfig.json26
32 files changed, 820 insertions, 0 deletions
diff --git a/web/.env.sample b/web/.env.sample
new file mode 100644
index 00000000..a48054f0
--- /dev/null
+++ b/web/.env.sample
@@ -0,0 +1,8 @@
+DATABASE_URL="file:./dev.db"
+NEXTAUTH_URL=
+NEXTAUTH_SECRET=
+
+# Oauth
+AUTHENTIK_ID=
+AUTHENTIK_SECRET=
+AUTHENTIK_ISSUER=
diff --git a/web/.eslintrc.json b/web/.eslintrc.json
new file mode 100644
index 00000000..bffb357a
--- /dev/null
+++ b/web/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/web/.gitignore b/web/.gitignore
new file mode 100644
index 00000000..5bcde103
--- /dev/null
+++ b/web/.gitignore
@@ -0,0 +1,39 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# The sqlite database
+prisma/*dev.db*
diff --git a/web/Dockerfile b/web/Dockerfile
new file mode 100644
index 00000000..30a46bd3
--- /dev/null
+++ b/web/Dockerfile
@@ -0,0 +1,57 @@
+FROM oven/bun:1.0-alpine AS base
+
+# Install dependencies only when needed
+FROM base AS deps
+# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
+# RUN apk add --no-cache libc6-compat
+WORKDIR /app
+
+# Install dependencies based on the preferred package manager
+COPY package.json bun.lockb ./
+RUN bun install --frozen-lockfile
+
+# Rebuild the source code only when needed
+FROM base AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+
+ENV NODE_ENV production
+
+ENV NEXT_TELEMETRY_DISABLED 1
+
+RUN bun run build
+
+# Production image, copy all the files and run next
+FROM base AS runner
+WORKDIR /app
+
+ENV NODE_ENV production
+
+ENV NEXT_TELEMETRY_DISABLED 1
+
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+
+COPY --from=builder /app/public ./public
+
+# Set the correct permission for prerender cache
+RUN mkdir .next
+RUN chown nextjs:nodejs .next
+
+# Automatically leverage output traces to reduce image size
+# https://nextjs.org/docs/advanced-features/output-file-tracing
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+
+USER nextjs
+
+EXPOSE 3000
+
+ENV PORT 3000
+# set hostname to localhost
+ENV HOSTNAME "0.0.0.0"
+
+# server.js is created by next build from the standalone output
+# https://nextjs.org/docs/pages/api-reference/next-config-js/output
+CMD ["node", "server.js"]
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 00000000..c4033664
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/web/app/api/auth/[...nextauth]/route.tsx b/web/app/api/auth/[...nextauth]/route.tsx
new file mode 100644
index 00000000..bfcda516
--- /dev/null
+++ b/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/web/app/api/v1/links/route.ts b/web/app/api/v1/links/route.ts
new file mode 100644
index 00000000..5be1018e
--- /dev/null
+++ b/web/app/api/v1/links/route.ts
@@ -0,0 +1,60 @@
+import { authOptions } from "@/lib/auth";
+import prisma from "@/lib/prisma";
+import { ZNewBookmarkedLinkRequest, 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 = ZNewBookmarkedLinkRequest.safeParse(await request.json());
+
+ if (!linkRequest.success) {
+ return NextResponse.json({
+ error: linkRequest.error.toString(),
+ }, { status: 400 });
+ }
+
+ const link = await prisma.bookmarkedLink.create({
+ data: {
+ url: linkRequest.data.url,
+ userId: 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 prisma.bookmarkedLink.findMany({
+ where: {
+ userId: session.user.id,
+ },
+ select: {
+ id: true,
+ url: true,
+ createdAt: true,
+ details: {
+ select: {
+ title: true,
+ description: true,
+ imageUrl: true,
+ }
+ },
+ }
+ });
+
+ let response: ZGetLinksResponse = { links };
+ return NextResponse.json(response);
+}
diff --git a/web/app/favicon.ico b/web/app/favicon.ico
new file mode 100644
index 00000000..718d6fea
--- /dev/null
+++ b/web/app/favicon.ico
Binary files differ
diff --git a/web/app/globals.css b/web/app/globals.css
new file mode 100644
index 00000000..6a757250
--- /dev/null
+++ b/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;
+ }
+} \ No newline at end of file
diff --git a/web/app/layout.tsx b/web/app/layout.tsx
new file mode 100644
index 00000000..3314e478
--- /dev/null
+++ b/web/app/layout.tsx
@@ -0,0 +1,22 @@
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
+import "./globals.css";
+
+const inter = Inter({ subsets: ["latin"] });
+
+export const metadata: Metadata = {
+ title: "Create Next App",
+ description: "Generated by create next app",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+ <html lang="en">
+ <body className={inter.className}>{children}</body>
+ </html>
+ );
+}
diff --git a/web/app/page.tsx b/web/app/page.tsx
new file mode 100644
index 00000000..2df40508
--- /dev/null
+++ b/web/app/page.tsx
@@ -0,0 +1,15 @@
+import { LoginButton } from "../components/auth/login";
+import { LogoutButton } from "../components/auth/logout";
+
+export default function Home() {
+ return (
+ <main className="flex min-h-screen flex-col items-center justify-between p-24">
+ <div>
+ <LoginButton />
+ <br />
+ <br />
+ <LogoutButton />
+ </div>
+ </main>
+ );
+}
diff --git a/web/bun.lockb b/web/bun.lockb
new file mode 100755
index 00000000..bfd618dc
--- /dev/null
+++ b/web/bun.lockb
Binary files differ
diff --git a/web/components.json b/web/components.json
new file mode 100644
index 00000000..15f2b025
--- /dev/null
+++ b/web/components.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "app/globals.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils"
+ }
+} \ No newline at end of file
diff --git a/web/components/auth/login.tsx b/web/components/auth/login.tsx
new file mode 100644
index 00000000..4cd55546
--- /dev/null
+++ b/web/components/auth/login.tsx
@@ -0,0 +1,17 @@
+"use client";
+import { signIn } from "next-auth/react";
+
+export const LoginButton = () => {
+ return (
+ <button
+ className="btn btn-primary"
+ onClick={() =>
+ signIn(undefined, {
+ callbackUrl: "/",
+ })
+ }
+ >
+ Sign in
+ </button>
+ );
+};
diff --git a/web/components/auth/logout.tsx b/web/components/auth/logout.tsx
new file mode 100644
index 00000000..87391c84
--- /dev/null
+++ b/web/components/auth/logout.tsx
@@ -0,0 +1,12 @@
+"use client";
+import { signOut } from "next-auth/react";
+
+export const LogoutButton = () => {
+ return (
+ <button className="btn btn-ghost normal-case" onClick={() => signOut({
+ callbackUrl: "/",
+ })}>
+ Sign Out
+ </button>
+ );
+};
diff --git a/web/lib/auth.ts b/web/lib/auth.ts
new file mode 100644
index 00000000..9b21e605
--- /dev/null
+++ b/web/lib/auth.ts
@@ -0,0 +1,25 @@
+import NextAuth, { NextAuthOptions } from "next-auth"
+import { PrismaAdapter } from "@next-auth/prisma-adapter"
+import AuthentikProvider from "next-auth/providers/authentik";
+import serverConfig from "@/lib/config";
+import prisma from "@/lib/prisma";
+
+let providers = [];
+
+if (serverConfig.auth.authentik) {
+ providers.push(AuthentikProvider(serverConfig.auth.authentik));
+}
+
+export const authOptions: NextAuthOptions = {
+ // Configure one or more authentication providers
+ adapter: PrismaAdapter(prisma),
+ providers: providers,
+ callbacks: {
+ session({ session, token, user }) {
+ session.user = { ...user };
+ return session;
+ }
+ }
+};
+
+export const authHandler = NextAuth(authOptions);
diff --git a/web/lib/config.ts b/web/lib/config.ts
new file mode 100644
index 00000000..ef86cb5a
--- /dev/null
+++ b/web/lib/config.ts
@@ -0,0 +1,20 @@
+function buildAuthentikConfig() {
+ let {id, secret, issuer} = process.env;
+ if (!id || !secret || !issuer) {
+ return undefined;
+ }
+
+ return {
+ clientId: id,
+ clientSecret: secret,
+ issuer: issuer,
+ };
+}
+
+const serverConfig = {
+ auth: {
+ authentik: buildAuthentikConfig(),
+ }
+};
+
+export default serverConfig;
diff --git a/web/lib/prisma.ts b/web/lib/prisma.ts
new file mode 100644
index 00000000..d73ba5f2
--- /dev/null
+++ b/web/lib/prisma.ts
@@ -0,0 +1,5 @@
+import { PrismaClient } from '@prisma/client'
+
+const prisma = new PrismaClient();
+
+export default prisma;
diff --git a/web/lib/types/api/links.ts b/web/lib/types/api/links.ts
new file mode 100644
index 00000000..81cde053
--- /dev/null
+++ b/web/lib/types/api/links.ts
@@ -0,0 +1,26 @@
+import { z } from "zod";
+
+export const ZBookmarkedLink = z.object({
+ id: z.string(),
+ url: z.string().url(),
+ createdAt: z.coerce.date(),
+
+ details: z.object({
+ title: z.string(),
+ description: z.string(),
+ imageUrl: z.string().url(),
+ }).nullish(),
+
+});
+export type ZBookmarkedLink = z.infer<typeof ZBookmarkedLink>;
+
+
+// POST /v1/links
+export const ZNewBookmarkedLinkRequest = ZBookmarkedLink.pick({ url: true });
+
+
+// GET /v1/links
+export const ZGetLinksResponse = z.object({
+ links: z.array(ZBookmarkedLink),
+});
+export type ZGetLinksResponse = z.infer<typeof ZGetLinksResponse>;
diff --git a/web/lib/types/next-auth.d.ts b/web/lib/types/next-auth.d.ts
new file mode 100644
index 00000000..bdd3bd03
--- /dev/null
+++ b/web/lib/types/next-auth.d.ts
@@ -0,0 +1,12 @@
+import NextAuth, { DefaultSession } from "next-auth"
+
+declare module "next-auth" {
+ /**
+ * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
+ */
+ interface Session {
+ user: {
+ id: string;
+ } & DefaultSession["user"];
+ }
+}
diff --git a/web/lib/utils.ts b/web/lib/utils.ts
new file mode 100644
index 00000000..d084ccad
--- /dev/null
+++ b/web/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/web/next.config.mjs b/web/next.config.mjs
new file mode 100644
index 00000000..4678774e
--- /dev/null
+++ b/web/next.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {};
+
+export default nextConfig;
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 00000000..1323e456
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "remember",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@next-auth/prisma-adapter": "^1.0.7",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.0",
+ "lucide-react": "^0.322.0",
+ "next": "14.1.0",
+ "next-auth": "^4.24.5",
+ "prisma": "^5.9.1",
+ "react": "^18",
+ "react-dom": "^18",
+ "tailwind-merge": "^2.2.1",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "typescript": "^5",
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "autoprefixer": "^10.0.1",
+ "postcss": "^8",
+ "tailwindcss": "^3.3.0",
+ "eslint": "^8",
+ "eslint-config-next": "14.1.0"
+ }
+}
diff --git a/web/postcss.config.js b/web/postcss.config.js
new file mode 100644
index 00000000..12a703d9
--- /dev/null
+++ b/web/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/web/prisma/migrations/20240205153748_add_users/migration.sql b/web/prisma/migrations/20240205153748_add_users/migration.sql
new file mode 100644
index 00000000..cbf47073
--- /dev/null
+++ b/web/prisma/migrations/20240205153748_add_users/migration.sql
@@ -0,0 +1,56 @@
+-- CreateTable
+CREATE TABLE "Account" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "userId" TEXT NOT NULL,
+ "type" TEXT NOT NULL,
+ "provider" TEXT NOT NULL,
+ "providerAccountId" TEXT NOT NULL,
+ "refresh_token" TEXT,
+ "access_token" TEXT,
+ "expires_at" INTEGER,
+ "token_type" TEXT,
+ "scope" TEXT,
+ "id_token" TEXT,
+ "session_state" TEXT,
+ CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "Session" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "sessionToken" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "expires" DATETIME NOT NULL,
+ CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "User" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "name" TEXT,
+ "email" TEXT,
+ "emailVerified" DATETIME,
+ "image" TEXT
+);
+
+-- CreateTable
+CREATE TABLE "VerificationToken" (
+ "identifier" TEXT NOT NULL,
+ "token" TEXT NOT NULL,
+ "expires" DATETIME NOT NULL
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
diff --git a/web/prisma/migrations/20240206000813_add_links/migration.sql b/web/prisma/migrations/20240206000813_add_links/migration.sql
new file mode 100644
index 00000000..38c8d938
--- /dev/null
+++ b/web/prisma/migrations/20240206000813_add_links/migration.sql
@@ -0,0 +1,43 @@
+-- CreateTable
+CREATE TABLE "BookmarkedLink" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "url" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "userId" TEXT NOT NULL,
+ CONSTRAINT "BookmarkedLink_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "BookmarkedLinkDetails" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "title" TEXT NOT NULL,
+ "description" TEXT NOT NULL,
+ "imageUrl" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT "BookmarkedLinkDetails_id_fkey" FOREIGN KEY ("id") REFERENCES "BookmarkedLink" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "BookmarkTags" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "name" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "userId" TEXT NOT NULL,
+ CONSTRAINT "BookmarkTags_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateTable
+CREATE TABLE "TagsOnLinks" (
+ "linkId" TEXT NOT NULL,
+ "tagId" TEXT NOT NULL,
+ "attachedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "bookmarkTagsId" TEXT NOT NULL,
+ CONSTRAINT "TagsOnLinks_linkId_fkey" FOREIGN KEY ("linkId") REFERENCES "BookmarkedLink" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT "TagsOnLinks_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "BookmarkTags" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "BookmarkTags_name_key" ON "BookmarkTags"("name");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "TagsOnLinks_linkId_tagId_key" ON "TagsOnLinks"("linkId", "tagId");
diff --git a/web/prisma/migrations/migration_lock.toml b/web/prisma/migrations/migration_lock.toml
new file mode 100644
index 00000000..e5e5c470
--- /dev/null
+++ b/web/prisma/migrations/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "sqlite" \ No newline at end of file
diff --git a/web/prisma/schema.prisma b/web/prisma/schema.prisma
new file mode 100644
index 00000000..54be3eae
--- /dev/null
+++ b/web/prisma/schema.prisma
@@ -0,0 +1,105 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "sqlite"
+ url = env("DATABASE_URL")
+}
+
+model Account {
+ id String @id @default(cuid())
+ userId String
+ type String
+ provider String
+ providerAccountId String
+ refresh_token String?
+ access_token String?
+ expires_at Int?
+ token_type String?
+ scope String?
+ id_token String?
+ session_state String?
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([provider, providerAccountId])
+}
+
+model Session {
+ id String @id @default(cuid())
+ sessionToken String @unique
+ userId String
+ expires DateTime
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+}
+
+model User {
+ id String @id @default(cuid())
+ name String?
+ email String? @unique
+ emailVerified DateTime?
+ image String?
+ accounts Account[]
+ sessions Session[]
+ links BookmarkedLink[]
+ tags BookmarkTags[]
+}
+
+model VerificationToken {
+ identifier String
+ token String @unique
+ expires DateTime
+
+ @@unique([identifier, token])
+}
+
+model BookmarkedLink {
+ id String @id @default(cuid())
+ url String
+ createdAt DateTime @default(now())
+
+ userId String
+
+ details BookmarkedLinkDetails?
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ tags TagsOnLinks[]
+}
+
+model BookmarkedLinkDetails {
+ id String @id
+ title String
+ description String
+ imageUrl String
+ createdAt DateTime @default(now())
+
+ link BookmarkedLink @relation(fields: [id], references: [id], onDelete: Cascade)
+}
+
+model BookmarkTags {
+ id String @id @default(cuid())
+ name String @unique
+ createdAt DateTime @default(now())
+
+ userId String
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ attachedLinks TagsOnLinks[]
+}
+
+model TagsOnLinks {
+ link BookmarkedLink @relation(fields: [linkId], references: [id], onDelete: Cascade)
+ linkId String
+
+ tag BookmarkTags @relation(fields: [tagId], references: [id], onDelete: Cascade)
+ tagId String
+
+ attachedAt DateTime @default(now())
+ bookmarkTagsId String
+
+ @@unique([linkId, tagId])
+}
diff --git a/web/public/next.svg b/web/public/next.svg
new file mode 100644
index 00000000..5174b28c
--- /dev/null
+++ b/web/public/next.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> \ No newline at end of file
diff --git a/web/public/vercel.svg b/web/public/vercel.svg
new file mode 100644
index 00000000..d2f84222
--- /dev/null
+++ b/web/public/vercel.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg> \ No newline at end of file
diff --git a/web/tailwind.config.ts b/web/tailwind.config.ts
new file mode 100644
index 00000000..84287e82
--- /dev/null
+++ b/web/tailwind.config.ts
@@ -0,0 +1,80 @@
+import type { Config } from "tailwindcss"
+
+const config = {
+ darkMode: ["class"],
+ content: [
+ './pages/**/*.{ts,tsx}',
+ './components/**/*.{ts,tsx}',
+ './app/**/*.{ts,tsx}',
+ './src/**/*.{ts,tsx}',
+ ],
+ prefix: "",
+ theme: {
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ keyframes: {
+ "accordion-down": {
+ from: { height: "0" },
+ to: { height: "var(--radix-accordion-content-height)" },
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: "0" },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
+ },
+ },
+ },
+ plugins: [require("tailwindcss-animate")],
+} satisfies Config
+
+export default config \ No newline at end of file
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 00000000..e7ff90fd
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}