From 95286ac169c456e51f6d8580fa884d3c3f8b4d66 Mon Sep 17 00:00:00 2001 From: NitinTheGreat Date: Fri, 13 Jun 2025 19:50:02 +0530 Subject: [PATCH 01/29] feat: add hero section --- devboard/.gitignore | 41 + devboard/README.md | 36 + devboard/app/favicon.ico | Bin 0 -> 25931 bytes devboard/app/globals.css | 26 + devboard/app/layout.tsx | 34 + devboard/app/page.tsx | 10 + devboard/components/Hero/index.tsx | 129 ++ devboard/next.config.ts | 7 + devboard/package-lock.json | 1763 ++++++++++++++++++++++++++++ devboard/package.json | 25 + devboard/postcss.config.mjs | 5 + devboard/public/file.svg | 1 + devboard/public/globe.svg | 1 + devboard/public/next.svg | 1 + devboard/public/vercel.svg | 1 + devboard/public/window.svg | 1 + devboard/tsconfig.json | 27 + 17 files changed, 2108 insertions(+) create mode 100644 devboard/.gitignore create mode 100644 devboard/README.md create mode 100644 devboard/app/favicon.ico create mode 100644 devboard/app/globals.css create mode 100644 devboard/app/layout.tsx create mode 100644 devboard/app/page.tsx create mode 100644 devboard/components/Hero/index.tsx create mode 100644 devboard/next.config.ts create mode 100644 devboard/package-lock.json create mode 100644 devboard/package.json create mode 100644 devboard/postcss.config.mjs create mode 100644 devboard/public/file.svg create mode 100644 devboard/public/globe.svg create mode 100644 devboard/public/next.svg create mode 100644 devboard/public/vercel.svg create mode 100644 devboard/public/window.svg create mode 100644 devboard/tsconfig.json diff --git a/devboard/.gitignore b/devboard/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/devboard/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/devboard/README.md b/devboard/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/devboard/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/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/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## 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/app/building-your-application/deploying) for more details. diff --git a/devboard/app/favicon.ico b/devboard/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/devboard/app/globals.css b/devboard/app/globals.css new file mode 100644 index 0000000..a2dc41e --- /dev/null +++ b/devboard/app/globals.css @@ -0,0 +1,26 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} diff --git a/devboard/app/layout.tsx b/devboard/app/layout.tsx new file mode 100644 index 0000000..f7fa87e --- /dev/null +++ b/devboard/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + 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 ( + + + {children} + + + ); +} diff --git a/devboard/app/page.tsx b/devboard/app/page.tsx new file mode 100644 index 0000000..f427e97 --- /dev/null +++ b/devboard/app/page.tsx @@ -0,0 +1,10 @@ +import Image from "next/image"; +import HeroSection from "@/components/Hero"; + +export default function Home() { + return ( + <> + + + ); +} diff --git a/devboard/components/Hero/index.tsx b/devboard/components/Hero/index.tsx new file mode 100644 index 0000000..345d0c3 --- /dev/null +++ b/devboard/components/Hero/index.tsx @@ -0,0 +1,129 @@ +"use client" + +import { useEffect, useRef } from "react" +import { Poppins } from "next/font/google" +import { ArrowRight } from "lucide-react" + +// Initialize the Poppins font +const poppins = Poppins({ + subsets: ["latin"], + weight: ["400", "500", "600", "700"], + variable: "--font-poppins", +}) + +export default function HeroSection() { + const canvasRef = useRef(null) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + + const ctx = canvas.getContext("2d") + if (!ctx) return + + // Set canvas dimensions + const setCanvasDimensions = () => { + canvas.width = window.innerWidth + canvas.height = window.innerHeight + } + + setCanvasDimensions() + window.addEventListener("resize", setCanvasDimensions) + + // Create floating shapes + const shapes: Shape[] = [] + const colors = ["#3F1469"] + + for (let i = 0; i < 15; i++) { + shapes.push({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + radius: Math.random() * 80 + 20, + dx: (Math.random() - 0.5) * 0.5, + dy: (Math.random() - 0.5) * 0.5, + color: colors[Math.floor(Math.random() * colors.length)], + opacity: Math.random() * 0.3 + 0.1, + }) + } + + // Animation loop + const animate = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height) + + // Draw shapes + shapes.forEach((shape) => { + ctx.beginPath() + ctx.arc(shape.x, shape.y, shape.radius, 0, Math.PI * 2) + ctx.fillStyle = `${shape.color}${Math.floor(shape.opacity * 255) + .toString(16) + .padStart(2, "0")}` + ctx.fill() + + // Update position + shape.x += shape.dx + shape.y += shape.dy + + // Bounce off edges with some buffer + if (shape.x < -shape.radius || shape.x > canvas.width + shape.radius) { + shape.dx = -shape.dx + } + + if (shape.y < -shape.radius || shape.y > canvas.height + shape.radius) { + shape.dy = -shape.dy + } + }) + + requestAnimationFrame(animate) + } + + animate() + + return () => { + window.removeEventListener("resize", setCanvasDimensions) + } + }, []) + + return ( +
+ {/* Background canvas for floating shapes */} + + + {/* Logo */} +
+

DevBoard

+
+ + {/* Hero content */} +
+

+ Your Dev Dashboard. +

+

+ Built Your Way. +

+ +

+ lorem ipsum, lorem ipsum.lorem ipsum,lorem ipsum, lorem ipsum. lorem ipsum. lorem ipsum. lorem ipsum.lorem + ipsum.lorem ipsum. lorem ipsum. +

+ + +
+
+ ) +} + +// Types +interface Shape { + x: number + y: number + radius: number + dx: number + dy: number + color: string + opacity: number +} diff --git a/devboard/next.config.ts b/devboard/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/devboard/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/devboard/package-lock.json b/devboard/package-lock.json new file mode 100644 index 0000000..53a29fe --- /dev/null +++ b/devboard/package-lock.json @@ -0,0 +1,1763 @@ +{ + "name": "devboard", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "devboard", + "version": "0.1.0", + "dependencies": { + "lucide-react": "^0.515.0", + "next": "15.3.3", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz", + "integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz", + "integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", + "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", + "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", + "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", + "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", + "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", + "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", + "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", + "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz", + "integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz", + "integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz", + "integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz", + "integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz", + "integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz", + "integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz", + "integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz", + "integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz", + "integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz", + "integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.3.tgz", + "integrity": "sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.3.tgz", + "integrity": "sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz", + "integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz", + "integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz", + "integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz", + "integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz", + "integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz", + "integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz", + "integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.10.tgz", + "integrity": "sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.10" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.10.tgz", + "integrity": "sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.10", + "@tailwindcss/oxide-darwin-arm64": "4.1.10", + "@tailwindcss/oxide-darwin-x64": "4.1.10", + "@tailwindcss/oxide-freebsd-x64": "4.1.10", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.10", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.10", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.10", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.10", + "@tailwindcss/oxide-linux-x64-musl": "4.1.10", + "@tailwindcss/oxide-wasm32-wasi": "4.1.10", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.10", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.10" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.10.tgz", + "integrity": "sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.10.tgz", + "integrity": "sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.10.tgz", + "integrity": "sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.10.tgz", + "integrity": "sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.10.tgz", + "integrity": "sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.10.tgz", + "integrity": "sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.10.tgz", + "integrity": "sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.10.tgz", + "integrity": "sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.10.tgz", + "integrity": "sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.10.tgz", + "integrity": "sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.10", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz", + "integrity": "sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.10.tgz", + "integrity": "sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.10.tgz", + "integrity": "sha512-B+7r7ABZbkXJwpvt2VMnS6ujcDoR2OOcFaqrLIo1xbcdxje4Vf+VgJdBzNNbrAjBj/rLZ66/tlQ1knIGNLKOBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.10", + "@tailwindcss/oxide": "4.1.10", + "postcss": "^8.4.41", + "tailwindcss": "4.1.10" + } + }, + "node_modules/@types/node": { + "version": "20.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz", + "integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001723", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz", + "integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-react": { + "version": "0.515.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.515.0.tgz", + "integrity": "sha512-Sy7bY0MeicRm2pzrnoHm2h6C1iVoeHyBU2fjdQDsXGP51fhkhau1/ZV/dzrcxEmAKsxYb6bGaIsMnGHuQ5s0dw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/next/-/next-15.3.3.tgz", + "integrity": "sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw==", + "license": "MIT", + "dependencies": { + "@next/env": "15.3.3", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.3.3", + "@next/swc-darwin-x64": "15.3.3", + "@next/swc-linux-arm64-gnu": "15.3.3", + "@next/swc-linux-arm64-musl": "15.3.3", + "@next/swc-linux-x64-gnu": "15.3.3", + "@next/swc-linux-x64-musl": "15.3.3", + "@next/swc-win32-arm64-msvc": "15.3.3", + "@next/swc-win32-x64-msvc": "15.3.3", + "sharp": "^0.34.1" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", + "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz", + "integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.2", + "@img/sharp-darwin-x64": "0.34.2", + "@img/sharp-libvips-darwin-arm64": "1.1.0", + "@img/sharp-libvips-darwin-x64": "1.1.0", + "@img/sharp-libvips-linux-arm": "1.1.0", + "@img/sharp-libvips-linux-arm64": "1.1.0", + "@img/sharp-libvips-linux-ppc64": "1.1.0", + "@img/sharp-libvips-linux-s390x": "1.1.0", + "@img/sharp-libvips-linux-x64": "1.1.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", + "@img/sharp-libvips-linuxmusl-x64": "1.1.0", + "@img/sharp-linux-arm": "0.34.2", + "@img/sharp-linux-arm64": "0.34.2", + "@img/sharp-linux-s390x": "0.34.2", + "@img/sharp-linux-x64": "0.34.2", + "@img/sharp-linuxmusl-arm64": "0.34.2", + "@img/sharp-linuxmusl-x64": "0.34.2", + "@img/sharp-wasm32": "0.34.2", + "@img/sharp-win32-arm64": "0.34.2", + "@img/sharp-win32-ia32": "0.34.2", + "@img/sharp-win32-x64": "0.34.2" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz", + "integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/devboard/package.json b/devboard/package.json new file mode 100644 index 0000000..ce7fe46 --- /dev/null +++ b/devboard/package.json @@ -0,0 +1,25 @@ +{ + "name": "devboard", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "lucide-react": "^0.515.0", + "next": "15.3.3", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/devboard/postcss.config.mjs b/devboard/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/devboard/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/devboard/public/file.svg b/devboard/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/devboard/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/devboard/public/globe.svg b/devboard/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/devboard/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/devboard/public/next.svg b/devboard/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/devboard/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/devboard/public/vercel.svg b/devboard/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/devboard/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/devboard/public/window.svg b/devboard/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/devboard/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/devboard/tsconfig.json b/devboard/tsconfig.json new file mode 100644 index 0000000..d8b9323 --- /dev/null +++ b/devboard/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "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"] +} From 25e6b416c9f0aab7cf6d99ff0b8cd165336fef97 Mon Sep 17 00:00:00 2001 From: NitinTheGreat Date: Fri, 13 Jun 2025 20:08:06 +0530 Subject: [PATCH 02/29] feat: add login page --- devboard/app/login/page.tsx | 165 ++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 devboard/app/login/page.tsx diff --git a/devboard/app/login/page.tsx b/devboard/app/login/page.tsx new file mode 100644 index 0000000..61d96bc --- /dev/null +++ b/devboard/app/login/page.tsx @@ -0,0 +1,165 @@ +"use client" + +import { useEffect, useRef } from "react" +import { Poppins } from "next/font/google" +import Link from "next/link" + +// Initialize the Poppins font +const poppins = Poppins({ + subsets: ["latin"], + weight: ["400", "500", "600", "700"], + variable: "--font-poppins", +}) + +export default function LoginPage() { + const canvasRef = useRef(null) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + + const ctx = canvas.getContext("2d") + if (!ctx) return + + // Set canvas dimensions + const setCanvasDimensions = () => { + canvas.width = window.innerWidth + canvas.height = window.innerHeight + } + + setCanvasDimensions() + window.addEventListener("resize", setCanvasDimensions) + + // Create floating shapes + const shapes: Shape[] = [] + const colors = ["#3F1469"] + + for (let i = 0; i < 15; i++) { + shapes.push({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + radius: Math.random() * 80 + 20, + dx: (Math.random() - 0.5) * 0.5, + dy: (Math.random() - 0.5) * 0.5, + color: colors[Math.floor(Math.random() * colors.length)], + opacity: Math.random() * 0.3 + 0.1, + }) + } + + // Animation loop + const animate = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height) + + // Draw shapes + shapes.forEach((shape) => { + ctx.beginPath() + ctx.arc(shape.x, shape.y, shape.radius, 0, Math.PI * 2) + ctx.fillStyle = `${shape.color}${Math.floor(shape.opacity * 255) + .toString(16) + .padStart(2, "0")}` + ctx.fill() + + // Update position + shape.x += shape.dx + shape.y += shape.dy + + // Bounce off edges with some buffer + if (shape.x < -shape.radius || shape.x > canvas.width + shape.radius) { + shape.dx = -shape.dx + } + + if (shape.y < -shape.radius || shape.y > canvas.height + shape.radius) { + shape.dy = -shape.dy + } + }) + + requestAnimationFrame(animate) + } + + animate() + + return () => { + window.removeEventListener("resize", setCanvasDimensions) + } + }, []) + + return ( +
+ {/* Background canvas for floating shapes */} + + + {/* Logo */} +
+

DevBoard

+
+ + {/* Login Card */} +
+
+ {/* Main card content */} +
+
+

Sign In

+

Welcome back! Please sign in to continue

+
+ +
+ +
+
+ + {/* Bottom section */} +
+

+ Don't have an account?{" "} + + Sign Up + +

+
+
+
+ + {/* CSS for the shimmer animation */} + +
+ ) +} + +// Types +interface Shape { + x: number + y: number + radius: number + dx: number + dy: number + color: string + opacity: number +} From 0bfac772a096a0664b6cf46e2e34922e658d4e9d Mon Sep 17 00:00:00 2001 From: NitinTheGreat Date: Sat, 14 Jun 2025 21:46:17 +0530 Subject: [PATCH 03/29] feat: add auth --- devboard/app/auth/callback/page.tsx | 103 +++++++ devboard/app/layout.tsx | 56 ++-- devboard/app/login/page.tsx | 103 ++++++- devboard/app/page.tsx | 84 +++++- devboard/components/Navbar/index.tsx | 153 ++++++++++ devboard/lib/auth.tsx | 409 +++++++++++++++++++++++++++ 6 files changed, 864 insertions(+), 44 deletions(-) create mode 100644 devboard/app/auth/callback/page.tsx create mode 100644 devboard/components/Navbar/index.tsx create mode 100644 devboard/lib/auth.tsx diff --git a/devboard/app/auth/callback/page.tsx b/devboard/app/auth/callback/page.tsx new file mode 100644 index 0000000..c014c50 --- /dev/null +++ b/devboard/app/auth/callback/page.tsx @@ -0,0 +1,103 @@ +"use client" + +import { useEffect, useState } from "react" +import { useRouter, useSearchParams } from "next/navigation" +import { Loader2, CheckCircle, XCircle } from "lucide-react" +import { toast } from "sonner" +import { useAuth } from "@/lib/auth" + +export default function AuthCallback() { + const [status, setStatus] = useState<"loading" | "success" | "error">("loading") + const [message, setMessage] = useState("") + const router = useRouter() + const searchParams = useSearchParams() + const { login } = useAuth() + + useEffect(() => { + const handleCallback = async () => { + try { + console.log("🔄 AUTH CALLBACK STARTED") + console.log("🌐 Current URL:", window.location.href) + console.log("📋 All URL params:", Object.fromEntries(searchParams.entries())) + + // Get tokens from query parameters + const accessToken = searchParams.get("access-token") + const refreshToken = searchParams.get("refreshtoken") + const error = searchParams.get("error") + + console.log("🎫 Extracted tokens:", { + hasAccessToken: !!accessToken, + hasRefreshToken: !!refreshToken, + error, + }) + + if (error) { + throw new Error(decodeURIComponent(error)) + } + + if (!accessToken || !refreshToken) { + console.error("❌ Missing tokens in URL") + throw new Error("Missing authentication tokens in callback URL") + } + + console.log("🔐 Calling login function...") + await login(accessToken, refreshToken) + console.log("✅ Login function completed") + + setStatus("success") + setMessage("Successfully authenticated! Redirecting...") + + toast.success("Login successful!") + + // Redirect to home page + setTimeout(() => { + console.log("🏠 Redirecting to home...") + router.push("/") + }, 1500) + } catch (error) { + console.error("❌ Authentication callback error:", error) + setStatus("error") + setMessage(error instanceof Error ? error.message : "Authentication failed") + + toast.error("Authentication failed") + + // Redirect to login + setTimeout(() => { + console.log("🔙 Redirecting to login...") + router.push("/login") + }, 3000) + } + } + + handleCallback() + }, [searchParams, router, login]) + + return ( +
+
+
+ {status === "loading" && } + {status === "success" && } + {status === "error" && } +
+ +

+ {status === "loading" && "Authenticating..."} + {status === "success" && "Success!"} + {status === "error" && "Authentication Failed"} +

+ +

{message || "Processing your authentication..."}

+ + {status === "error" && ( + + )} +
+
+ ) +} diff --git a/devboard/app/layout.tsx b/devboard/app/layout.tsx index f7fa87e..756cd8d 100644 --- a/devboard/app/layout.tsx +++ b/devboard/app/layout.tsx @@ -1,34 +1,44 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; +import type React from "react" +import type { Metadata } from "next" +import { Inter } from "next/font/google" +import "./globals.css" +import { AuthProvider } from "@/lib/auth" +import { Toaster } from "sonner" +import Navbar from "@/components/Navbar" -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +const inter = Inter({ subsets: ["latin"] }) export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; + title: "DevBoard ", + description: "Transform your GitHub README into a stunning portfolio website and many more", +} export default function RootLayout({ children, -}: Readonly<{ - children: React.ReactNode; -}>) { +}: { + children: React.ReactNode +}) { return ( - - {children} + + +
+ + {children} +
+ +
- ); + ) } diff --git a/devboard/app/login/page.tsx b/devboard/app/login/page.tsx index 61d96bc..078e23d 100644 --- a/devboard/app/login/page.tsx +++ b/devboard/app/login/page.tsx @@ -1,8 +1,12 @@ "use client" -import { useEffect, useRef } from "react" +import { useEffect, useRef, useState } from "react" +import { useRouter, useSearchParams } from "next/navigation" import { Poppins } from "next/font/google" import Link from "next/link" +import { Loader2 } from "lucide-react" +import { toast } from "sonner" +import { useAuth } from "@/lib/auth" // Initialize the Poppins font const poppins = Poppins({ @@ -11,8 +15,52 @@ const poppins = Poppins({ variable: "--font-poppins", }) +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://140.245.225.60" + export default function LoginPage() { const canvasRef = useRef(null) + const [isLoading, setIsLoading] = useState(false) + const router = useRouter() + const searchParams = useSearchParams() + const { login } = useAuth() + + // Handle tokens when they come back from backend + useEffect(() => { + const handleTokens = async () => { + const accessToken = searchParams.get("access_token") + const refreshToken = searchParams.get("refresh_token") + const error = searchParams.get("error") + + console.log("🔐 LOGIN PAGE - Checking for tokens:", { + hasAccessToken: !!accessToken, + hasRefreshToken: !!refreshToken, + error, + }) + + if (error) { + toast.error("Authentication failed: " + decodeURIComponent(error)) + return + } + + if (accessToken && refreshToken) { + console.log("✅ Tokens found, logging in...") + setIsLoading(true) + + try { + await login(accessToken, refreshToken) + toast.success("Login successful!") + router.push("/") + } catch (error) { + console.error("❌ Login failed:", error) + toast.error("Login failed") + } finally { + setIsLoading(false) + } + } + } + + handleTokens() + }, [searchParams, login, router]) useEffect(() => { const canvas = canvasRef.current @@ -83,6 +131,19 @@ export default function LoginPage() { } }, []) + const handleGitHubLogin = () => { + if (isLoading) return + + setIsLoading(true) + + // Create the callback URL for the current environment + const callbackUrl = `${window.location.origin}/login` + + // Redirect directly to the backend auth endpoint + const authUrl = `${API_BASE_URL}/api/auth/login?callback_url=${encodeURIComponent(callbackUrl)}` + window.location.href = authUrl + } + return (
{/* Background canvas for floating shapes */} @@ -90,7 +151,11 @@ export default function LoginPage() { {/* Logo */}
-

DevBoard

+ +

+ DevBoard +

+
{/* Login Card */} @@ -105,23 +170,31 @@ export default function LoginPage() {
diff --git a/devboard/app/page.tsx b/devboard/app/page.tsx index f427e97..40a2314 100644 --- a/devboard/app/page.tsx +++ b/devboard/app/page.tsx @@ -1,10 +1,82 @@ -import Image from "next/image"; -import HeroSection from "@/components/Hero"; +"use client" +import { useEffect, useState } from "react" +import { useSearchParams } from "next/navigation" +import { Loader2 } from "lucide-react" +import { toast } from "sonner" +import { useAuth } from "@/lib/auth" +import HeroSection from "@/components/Hero" export default function Home() { + const [isProcessingAuth, setIsProcessingAuth] = useState(false) + const searchParams = useSearchParams() + const { login, isAuthenticated } = useAuth() + + // Handle authentication tokens from URL (since backend redirects to home page) + useEffect(() => { + const handleAuthTokens = async () => { + try { + const accessToken = searchParams.get("access_token") + const refreshToken = searchParams.get("refresh_token") + const error = searchParams.get("error") + + console.log("🏠 HOME PAGE - Checking for auth tokens...") + console.log("🎫 URL Tokens:", { + hasAccessToken: !!accessToken, + hasRefreshToken: !!refreshToken, + error, + fullURL: window.location.href, + }) + + if (error) { + console.error("❌ Auth error in URL:", error) + toast.error("Authentication failed: " + decodeURIComponent(error)) + return + } + + if (accessToken && refreshToken && !isAuthenticated) { + console.log("🔐 Found tokens in URL, processing login...") + setIsProcessingAuth(true) + + try { + await login(accessToken, refreshToken) + console.log("✅ Login successful from URL tokens") + toast.success("Successfully logged in!") + + // Clean up URL by removing the tokens + const cleanUrl = window.location.pathname + window.history.replaceState({}, document.title, cleanUrl) + } catch (error) { + console.error("❌ Login failed:", error) + toast.error("Login failed: " + (error instanceof Error ? error.message : "Unknown error")) + } finally { + setIsProcessingAuth(false) + } + } + } catch (error) { + console.error("❌ Error handling auth tokens:", error) + setIsProcessingAuth(false) + } + } + + handleAuthTokens() + }, [searchParams, login, isAuthenticated]) + + // Show loading state while processing authentication + if (isProcessingAuth) { + return ( +
+
+ +

Processing authentication...

+
+
+ ) + } + return ( - <> - - - ); + <> + + {/* Main content will be added here */} + + ) } diff --git a/devboard/components/Navbar/index.tsx b/devboard/components/Navbar/index.tsx new file mode 100644 index 0000000..e0e1d85 --- /dev/null +++ b/devboard/components/Navbar/index.tsx @@ -0,0 +1,153 @@ +"use client" + +import { useState, useRef, useEffect } from "react" +import Link from "next/link" +import { useRouter } from "next/navigation" +import { ChevronDown, LogOut, User, Settings, Github } from "lucide-react" +import { useAuth } from "@/lib/auth" +import { toast } from "sonner" + +export default function Navbar() { + const [isDropdownOpen, setIsDropdownOpen] = useState(false) + const dropdownRef = useRef(null) + const { user, isAuthenticated, logout, isLoading } = useAuth() + const router = useRouter() + + // Close dropdown when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsDropdownOpen(false) + } + } + + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + }, []) + + const handleLogout = async () => { + try { + await logout() + toast.success("Logged out successfully") + router.push("/login") + } catch (error) { + toast.error("Logout failed") + } + setIsDropdownOpen(false) + } + + return ( +
+
+ {/* DevBoard Logo */} + +

+ DevBoard +

+ + + {/* User Section */} +
+ {!isLoading && ( + <> + {isAuthenticated && user ? ( +
+ + + {/* Dropdown Menu */} + {isDropdownOpen && ( +
+ {/* User Info */} +
+

{user.name || user.username}

+

@{user.username}

+ {user.email &&

{user.email}

} +
+ + {/* Menu Items */} +
+ + + + +
+ + +
+
+ )} +
+ ) : ( + + Login + + )} + + )} +
+
+
+ ) +} diff --git a/devboard/lib/auth.tsx b/devboard/lib/auth.tsx new file mode 100644 index 0000000..754e7dd --- /dev/null +++ b/devboard/lib/auth.tsx @@ -0,0 +1,409 @@ +"use client" + +import type React from "react" +import { createContext, useContext, useEffect, useState, useCallback } from "react" + +// Types +export interface User { + username: string + avatar_url?: string + name?: string + email?: string + exp: number + iat: number +} + +export interface AuthState { + user: User | null + isAuthenticated: boolean + isLoading: boolean + error: string | null +} + +export interface AuthContextType extends AuthState { + login: (accessToken: string, refreshToken: string) => Promise + logout: () => Promise + refreshToken: () => Promise + clearError: () => void +} + +// Constants +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://140.245.225.60" +const ACCESS_TOKEN_KEY = "devboard_access_token" +const REFRESH_TOKEN_KEY = "devboard_refresh_token" + +// Context +const AuthContext = createContext(undefined) + +// Simple localStorage utilities +const setToken = (key: string, value: string): boolean => { + if (typeof window === "undefined") return false + + try { + localStorage.setItem(key, value) + console.log(`✅ Token stored: ${key}`) + return true + } catch (error) { + console.error(`❌ Failed to store token ${key}:`, error) + return false + } +} + +const getToken = (key: string): string | null => { + if (typeof window === "undefined") return null + + try { + const token = localStorage.getItem(key) + console.log(`🔍 Token retrieved: ${key} = ${token ? "EXISTS" : "NULL"}`) + return token + } catch (error) { + console.error(`❌ Failed to retrieve token ${key}:`, error) + return null + } +} + +const clearTokens = (): void => { + if (typeof window === "undefined") return + + try { + localStorage.removeItem(ACCESS_TOKEN_KEY) + localStorage.removeItem(REFRESH_TOKEN_KEY) + console.log("🗑️ All tokens cleared") + } catch (error) { + console.error("❌ Failed to clear tokens:", error) + } +} + +// JWT utilities +const parseJWT = (token: string): any => { + try { + const base64Url = token.split(".")[1] + const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/") + const jsonPayload = decodeURIComponent( + atob(base64) + .split("") + .map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)) + .join(""), + ) + return JSON.parse(jsonPayload) + } catch (error) { + console.error("❌ Error parsing JWT:", error) + return null + } +} + +const isTokenExpired = (token: string): boolean => { + const payload = parseJWT(token) + if (!payload || !payload.exp) return true + + const currentTime = Math.floor(Date.now() / 1000) + const bufferTime = 60 + return payload.exp <= currentTime + bufferTime +} + +// Auth Provider Component +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [state, setState] = useState({ + user: null, + isAuthenticated: false, + isLoading: true, + error: null, + }) + + // Initialize auth state + const initializeAuth = useCallback(async () => { + console.log("🚀 Initializing auth...") + + try { + const accessToken = getToken(ACCESS_TOKEN_KEY) + const refreshToken = getToken(REFRESH_TOKEN_KEY) + + console.log("🔍 Token check:", { + hasAccessToken: !!accessToken, + hasRefreshToken: !!refreshToken, + }) + + if (!accessToken || !refreshToken) { + console.log("❌ No tokens found, setting unauthenticated") + setState({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + }) + return + } + + // Check if access token is expired + if (isTokenExpired(accessToken)) { + console.log("⏰ Access token expired, attempting refresh...") + const refreshSuccess = await performTokenRefresh() + if (!refreshSuccess) { + console.log("❌ Token refresh failed") + setState({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + }) + return + } + } + + // Parse user data from current token + const currentToken = getToken(ACCESS_TOKEN_KEY) + if (currentToken) { + const userData = parseJWT(currentToken) + console.log("👤 Parsed user data:", userData) + + if (userData) { + const user: User = { + username: userData.username, + avatar_url: userData.avatar_url, + name: userData.name, + email: userData.email, + exp: userData.exp, + iat: userData.iat, + } + + console.log("✅ Setting authenticated user:", user.username) + setState({ + user, + isAuthenticated: true, + isLoading: false, + error: null, + }) + } else { + console.log("❌ Failed to parse user data") + setState({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + }) + } + } + } catch (error) { + console.error("❌ Auth initialization error:", error) + setState({ + user: null, + isAuthenticated: false, + isLoading: false, + error: "Failed to initialize authentication", + }) + } + }, []) + + // Login function + const login = useCallback(async (accessToken: string, refreshToken: string): Promise => { + console.log("🔐 LOGIN FUNCTION CALLED") + console.log("📝 Received tokens:", { + accessToken: accessToken ? `${accessToken.substring(0, 20)}...` : "NULL", + refreshToken: refreshToken ? `${refreshToken.substring(0, 20)}...` : "NULL", + }) + + try { + // Store tokens + const accessSuccess = setToken(ACCESS_TOKEN_KEY, accessToken) + const refreshSuccess = setToken(REFRESH_TOKEN_KEY, refreshToken) + + if (!accessSuccess || !refreshSuccess) { + throw new Error("Failed to store tokens") + } + + console.log("💾 Tokens stored successfully") + + // Parse user data + const userData = parseJWT(accessToken) + console.log("👤 Parsing user data:", userData) + + if (!userData) { + throw new Error("Invalid access token") + } + + const user: User = { + username: userData.username, + avatar_url: userData.avatar_url, + name: userData.name, + email: userData.email, + exp: userData.exp, + iat: userData.iat, + } + + console.log("✅ Setting user state:", user.username) + + // Update state immediately + setState({ + user, + isAuthenticated: true, + isLoading: false, + error: null, + }) + + console.log("🎉 Login completed successfully!") + } catch (error) { + console.error("❌ Login error:", error) + setState((prev) => ({ + ...prev, + error: error instanceof Error ? error.message : "Login failed", + isLoading: false, + })) + throw error + } + }, []) + + // Token refresh function + const performTokenRefresh = useCallback(async (): Promise => { + try { + const refreshToken = getToken(REFRESH_TOKEN_KEY) + if (!refreshToken) { + throw new Error("No refresh token available") + } + + console.log("🔄 Attempting token refresh...") + + const response = await fetch(`${API_BASE_URL}/api/auth/access-token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${refreshToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || "Token refresh failed") + } + + const data = await response.json() + + // Store new access token + setToken(ACCESS_TOKEN_KEY, data.access_token) + + // Update user data with new token + const userData = parseJWT(data.access_token) + if (userData) { + const user: User = { + username: userData.username, + avatar_url: userData.avatar_url, + name: userData.name, + email: userData.email, + exp: userData.exp, + iat: userData.iat, + } + + setState({ + user, + isAuthenticated: true, + isLoading: false, + error: null, + }) + } + + console.log("✅ Token refresh successful") + return true + } catch (error) { + console.error("❌ Token refresh error:", error) + clearTokens() + + setState({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + }) + + return false + } + }, []) + + // Public refresh function + const refreshToken = useCallback(async (): Promise => { + return await performTokenRefresh() + }, [performTokenRefresh]) + + // Logout function + const logout = useCallback(async (): Promise => { + try { + const accessToken = getToken(ACCESS_TOKEN_KEY) + + if (accessToken) { + console.log("📤 Calling logout API...") + try { + await fetch(`${API_BASE_URL}/api/auth/logout`, { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + console.log("✅ Logout API call successful") + } catch (apiError) { + console.error("❌ Logout API failed:", apiError) + // Continue with local logout even if API fails + } + } + } catch (error) { + console.error("❌ Logout error:", error) + } finally { + console.log("🚪 Clearing tokens and logging out") + clearTokens() + + setState({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + }) + } + }, []) + + // Clear error function + const clearError = useCallback(() => { + setState((prev) => ({ ...prev, error: null })) + }, []) + + // Initialize on mount + useEffect(() => { + initializeAuth() + }, [initializeAuth]) + + const contextValue: AuthContextType = { + ...state, + login, + logout, + refreshToken, + clearError, + } + + return {children} +} + +// Custom hook to use auth context +export function useAuth(): AuthContextType { + const context = useContext(AuthContext) + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider") + } + return context +} + +// Authenticated fetch utility +export async function authenticatedFetch(url: string, options: RequestInit = {}): Promise { + const accessToken = getToken(ACCESS_TOKEN_KEY) + + if (!accessToken) { + throw new Error("No access token available") + } + + if (isTokenExpired(accessToken)) { + throw new Error("Access token expired") + } + + return fetch(url, { + ...options, + headers: { + ...options.headers, + Authorization: `Bearer ${accessToken}`, + }, + }) +} From 73322c8e52ad3ba30b6e23e56913ec5039e25bef Mon Sep 17 00:00:00 2001 From: NitinTheGreat Date: Sat, 14 Jun 2025 21:58:55 +0530 Subject: [PATCH 04/29] feat: add profile page --- devboard/app/globals.css | 122 +++++++- devboard/app/profile/page.tsx | 275 ++++++++++++++++++ devboard/components.json | 21 ++ devboard/components/Navbar/index.tsx | 17 +- devboard/components/protected-route/index.tsx | 69 +++++ devboard/components/ui/button.tsx | 59 ++++ devboard/components/ui/card.tsx | 92 ++++++ devboard/components/ui/sonner.tsx | 25 ++ devboard/lib/auth.tsx | 118 ++++---- devboard/lib/utils.ts | 6 + devboard/package-lock.json | 107 ++++++- devboard/package.json | 9 +- 12 files changed, 845 insertions(+), 75 deletions(-) create mode 100644 devboard/app/profile/page.tsx create mode 100644 devboard/components.json create mode 100644 devboard/components/protected-route/index.tsx create mode 100644 devboard/components/ui/button.tsx create mode 100644 devboard/components/ui/card.tsx create mode 100644 devboard/components/ui/sonner.tsx create mode 100644 devboard/lib/utils.ts diff --git a/devboard/app/globals.css b/devboard/app/globals.css index a2dc41e..dc98be7 100644 --- a/devboard/app/globals.css +++ b/devboard/app/globals.css @@ -1,26 +1,122 @@ @import "tailwindcss"; +@import "tw-animate-css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } } diff --git a/devboard/app/profile/page.tsx b/devboard/app/profile/page.tsx new file mode 100644 index 0000000..a194d36 --- /dev/null +++ b/devboard/app/profile/page.tsx @@ -0,0 +1,275 @@ +"use client" + +import { useState, useEffect } from "react" +import { useRouter } from "next/navigation" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { User, Mail, MapPin, LinkIcon, Star, Building, Clock, ArrowLeft } from "lucide-react" +import { useAuth } from "@/lib/auth" +import { toast } from "sonner" +import ProtectedRoute from "@/components/protected-route" + +function ProfileContent() { + const { user, isAuthenticated } = useAuth() + const [githubData, setGithubData] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const router = useRouter() + + useEffect(() => { + const fetchGitHubProfile = async () => { + if (!user?.username) return + + try { + const response = await fetch(`https://api.github.com/users/${user.username}`) + if (response.ok) { + const data = await response.json() + setGithubData(data) + } + } catch (error) { + console.error("Failed to fetch GitHub profile:", error) + toast.error("Failed to load GitHub profile data") + } finally { + setIsLoading(false) + } + } + + fetchGitHubProfile() + }, [user]) + + if (!isAuthenticated || !user) { + return null + } + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }) + } + + const formatNumber = (num: number) => { + if (num >= 1000) { + return (num / 1000).toFixed(1) + "k" + } + return num.toString() + } + + return ( +
+
+ {/* Header */} +
+ +

Profile

+
+ +
+ {/* Profile Card */} +
+ + +
+ {/* Avatar */} +
+ {githubData?.avatar_url || user.avatar_url ? ( + {user.username} + ) : ( + + )} +
+ + {/* Basic Info */} +

+ {githubData?.name || user.name || user.username} +

+

@{user.username}

+ + {githubData?.bio &&

{githubData.bio}

} + + {/* GitHub Stats */} + {githubData && ( +
+
+
{formatNumber(githubData.followers)}
+
Followers
+
+
+
{formatNumber(githubData.following)}
+
Following
+
+
+
{formatNumber(githubData.public_repos)}
+
Repos
+
+
+ )} + + {/* GitHub Profile Link */} + {githubData?.html_url && ( + + )} +
+
+
+
+ + {/* Details Cards */} +
+ {/* Contact Information */} + + + + + Contact Information + + + + {user.email && ( +
+ + {user.email} +
+ )} + + {githubData?.location && ( +
+ + {githubData.location} +
+ )} + + {githubData?.company && ( +
+ + {githubData.company} +
+ )} + + {githubData?.blog && ( + + )} +
+
+ + {/* Account Details */} + + + + + Account Details + + + +
+
+

Username

+

{user.username}

+
+ + {githubData?.created_at && ( +
+

GitHub Member Since

+

{formatDate(githubData.created_at)}

+
+ )} + +
+

Login Session

+

{formatDate(new Date(user.iat * 1000).toISOString())}

+
+ +
+

Session Expires

+

{formatDate(new Date(user.exp * 1000).toISOString())}

+
+
+
+
+ + {/* GitHub Activity */} + {githubData && ( + + + + + GitHub Activity + + + +
+
+
+ {formatNumber(githubData.public_repos)} +
+
Public Repos
+
+ +
+
{formatNumber(githubData.followers)}
+
Followers
+
+ +
+
{formatNumber(githubData.following)}
+
Following
+
+ + {githubData.public_gists !== undefined && ( +
+
+ {formatNumber(githubData.public_gists)} +
+
Public Gists
+
+ )} +
+
+
+ )} +
+
+
+
+ ) +} + +export default function Profile() { + return ( + + + + ) +} diff --git a/devboard/components.json b/devboard/components.json new file mode 100644 index 0000000..335484f --- /dev/null +++ b/devboard/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/devboard/components/Navbar/index.tsx b/devboard/components/Navbar/index.tsx index e0e1d85..53de103 100644 --- a/devboard/components/Navbar/index.tsx +++ b/devboard/components/Navbar/index.tsx @@ -3,7 +3,7 @@ import { useState, useRef, useEffect } from "react" import Link from "next/link" import { useRouter } from "next/navigation" -import { ChevronDown, LogOut, User, Settings, Github } from "lucide-react" +import { ChevronDown, LogOut, User, Settings } from "lucide-react" import { useAuth } from "@/lib/auth" import { toast } from "sonner" @@ -73,9 +73,18 @@ export default function Navbar() { }} /> ) : ( - + + + )} - + + + {/* Username */} @@ -129,7 +138,7 @@ export default function Navbar() { onClick={handleLogout} className="flex items-center gap-3 w-full px-4 py-2 text-red-400 hover:bg-red-500/10 hover:text-red-300 transition-colors" > - + Logout diff --git a/devboard/components/protected-route/index.tsx b/devboard/components/protected-route/index.tsx new file mode 100644 index 0000000..64e1b48 --- /dev/null +++ b/devboard/components/protected-route/index.tsx @@ -0,0 +1,69 @@ +"use client" + +import type React from "react" + +import { useEffect, useState } from "react" +import { useRouter } from "next/navigation" +import { Loader2 } from "lucide-react" +import { useAuth } from "@/lib/auth" + +interface ProtectedRouteProps { + children: React.ReactNode + fallback?: React.ReactNode +} + +export default function ProtectedRoute({ children, fallback }: ProtectedRouteProps) { + const [isLoading, setIsLoading] = useState(true) + const [isAuthed, setIsAuthed] = useState(false) + const router = useRouter() + const { isAuthenticated, refreshToken } = useAuth() + useEffect(() => { + const checkAuth = async () => { + try { + // First check if we have valid tokens + if (isAuthenticated) { + setIsAuthed(true) + setIsLoading(false) + return + } + + // Try to refresh the token + const refreshResult = await refreshToken() + if (refreshResult) { + setIsAuthed(true) + } else { + // No valid authentication, redirect to login + router.push("/login") + } + } catch (error) { + console.error("Auth check error:", error) + router.push("/login") + } finally { + setIsLoading(false) + } + } + + checkAuth() + }, [router]) + + if (isLoading) { + return ( + fallback || ( +
+
+ +

Checking authentication...

+
+
+ ) + ) + } + + if (!isAuthed) { + return null // Will redirect to login + } + + return <>{children} +} + + diff --git a/devboard/components/ui/button.tsx b/devboard/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/devboard/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/devboard/components/ui/card.tsx b/devboard/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/devboard/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/devboard/components/ui/sonner.tsx b/devboard/components/ui/sonner.tsx new file mode 100644 index 0000000..957524e --- /dev/null +++ b/devboard/components/ui/sonner.tsx @@ -0,0 +1,25 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner, ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/devboard/lib/auth.tsx b/devboard/lib/auth.tsx index 754e7dd..3b9980a 100644 --- a/devboard/lib/auth.tsx +++ b/devboard/lib/auth.tsx @@ -29,48 +29,58 @@ export interface AuthContextType extends AuthState { // Constants const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "http://140.245.225.60" -const ACCESS_TOKEN_KEY = "devboard_access_token" -const REFRESH_TOKEN_KEY = "devboard_refresh_token" // Context const AuthContext = createContext(undefined) -// Simple localStorage utilities -const setToken = (key: string, value: string): boolean => { +// Cookie utilities +const setCookie = (name: string, value: string, days = 7): boolean => { if (typeof window === "undefined") return false try { - localStorage.setItem(key, value) - console.log(`✅ Token stored: ${key}`) + const expires = new Date() + expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000) + document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;secure;samesite=strict` + console.log(`Cookie stored: ${name}`) return true } catch (error) { - console.error(`❌ Failed to store token ${key}:`, error) + console.error(`Failed to store cookie ${name}:`, error) return false } } -const getToken = (key: string): string | null => { +const getCookie = (name: string): string | null => { if (typeof window === "undefined") return null try { - const token = localStorage.getItem(key) - console.log(`🔍 Token retrieved: ${key} = ${token ? "EXISTS" : "NULL"}`) - return token + const nameEQ = name + "=" + const ca = document.cookie.split(";") + for (let i = 0; i < ca.length; i++) { + let c = ca[i] + while (c.charAt(0) === " ") c = c.substring(1, c.length) + if (c.indexOf(nameEQ) === 0) { + const value = c.substring(nameEQ.length, c.length) + console.log(`Cookie retrieved: ${name} = ${value ? "EXISTS" : "NULL"}`) + return value + } + } + console.log(`Cookie retrieved: ${name} = NULL`) + return null } catch (error) { - console.error(`❌ Failed to retrieve token ${key}:`, error) + console.error(`Failed to retrieve cookie ${name}:`, error) return null } } -const clearTokens = (): void => { +const clearCookies = (): void => { if (typeof window === "undefined") return try { - localStorage.removeItem(ACCESS_TOKEN_KEY) - localStorage.removeItem(REFRESH_TOKEN_KEY) - console.log("🗑️ All tokens cleared") + document.cookie = "devboard_access_token=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;secure;samesite=strict" + document.cookie = "devboard_refresh_token=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;secure;samesite=strict" + console.log("All cookies cleared") } catch (error) { - console.error("❌ Failed to clear tokens:", error) + console.error("Failed to clear cookies:", error) } } @@ -87,7 +97,7 @@ const parseJWT = (token: string): any => { ) return JSON.parse(jsonPayload) } catch (error) { - console.error("❌ Error parsing JWT:", error) + console.error("Error parsing JWT:", error) return null } } @@ -112,19 +122,19 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // Initialize auth state const initializeAuth = useCallback(async () => { - console.log("🚀 Initializing auth...") + console.log("Initializing auth...") try { - const accessToken = getToken(ACCESS_TOKEN_KEY) - const refreshToken = getToken(REFRESH_TOKEN_KEY) + const accessToken = getCookie("devboard_access_token") + const refreshToken = getCookie("devboard_refresh_token") - console.log("🔍 Token check:", { + console.log("Token check:", { hasAccessToken: !!accessToken, hasRefreshToken: !!refreshToken, }) if (!accessToken || !refreshToken) { - console.log("❌ No tokens found, setting unauthenticated") + console.log("No tokens found, setting unauthenticated") setState({ user: null, isAuthenticated: false, @@ -136,10 +146,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // Check if access token is expired if (isTokenExpired(accessToken)) { - console.log("⏰ Access token expired, attempting refresh...") + console.log("Access token expired, attempting refresh...") const refreshSuccess = await performTokenRefresh() if (!refreshSuccess) { - console.log("❌ Token refresh failed") + console.log("Token refresh failed") setState({ user: null, isAuthenticated: false, @@ -151,10 +161,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } // Parse user data from current token - const currentToken = getToken(ACCESS_TOKEN_KEY) + const currentToken = getCookie("devboard_access_token") if (currentToken) { const userData = parseJWT(currentToken) - console.log("👤 Parsed user data:", userData) + console.log("Parsed user data:", userData) if (userData) { const user: User = { @@ -166,7 +176,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { iat: userData.iat, } - console.log("✅ Setting authenticated user:", user.username) + console.log("Setting authenticated user:", user.username) setState({ user, isAuthenticated: true, @@ -174,7 +184,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { error: null, }) } else { - console.log("❌ Failed to parse user data") + console.log("Failed to parse user data") setState({ user: null, isAuthenticated: false, @@ -184,7 +194,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } } } catch (error) { - console.error("❌ Auth initialization error:", error) + console.error("Auth initialization error:", error) setState({ user: null, isAuthenticated: false, @@ -196,26 +206,26 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // Login function const login = useCallback(async (accessToken: string, refreshToken: string): Promise => { - console.log("🔐 LOGIN FUNCTION CALLED") - console.log("📝 Received tokens:", { + console.log("LOGIN FUNCTION CALLED") + console.log("Received tokens:", { accessToken: accessToken ? `${accessToken.substring(0, 20)}...` : "NULL", refreshToken: refreshToken ? `${refreshToken.substring(0, 20)}...` : "NULL", }) try { - // Store tokens - const accessSuccess = setToken(ACCESS_TOKEN_KEY, accessToken) - const refreshSuccess = setToken(REFRESH_TOKEN_KEY, refreshToken) + // Store tokens in cookies + const accessSuccess = setCookie("devboard_access_token", accessToken, 1) // 1 day + const refreshSuccess = setCookie("devboard_refresh_token", refreshToken, 7) // 7 days if (!accessSuccess || !refreshSuccess) { throw new Error("Failed to store tokens") } - console.log("💾 Tokens stored successfully") + console.log("Tokens stored successfully") // Parse user data const userData = parseJWT(accessToken) - console.log("👤 Parsing user data:", userData) + console.log("Parsing user data:", userData) if (!userData) { throw new Error("Invalid access token") @@ -230,7 +240,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { iat: userData.iat, } - console.log("✅ Setting user state:", user.username) + console.log("Setting user state:", user.username) // Update state immediately setState({ @@ -240,9 +250,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { error: null, }) - console.log("🎉 Login completed successfully!") + console.log("Login completed successfully!") } catch (error) { - console.error("❌ Login error:", error) + console.error("Login error:", error) setState((prev) => ({ ...prev, error: error instanceof Error ? error.message : "Login failed", @@ -255,12 +265,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // Token refresh function const performTokenRefresh = useCallback(async (): Promise => { try { - const refreshToken = getToken(REFRESH_TOKEN_KEY) + const refreshToken = getCookie("devboard_refresh_token") if (!refreshToken) { throw new Error("No refresh token available") } - console.log("🔄 Attempting token refresh...") + console.log("Attempting token refresh...") const response = await fetch(`${API_BASE_URL}/api/auth/access-token`, { method: "POST", @@ -278,7 +288,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const data = await response.json() // Store new access token - setToken(ACCESS_TOKEN_KEY, data.access_token) + setCookie("devboard_access_token", data.access_token, 1) // Update user data with new token const userData = parseJWT(data.access_token) @@ -300,11 +310,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }) } - console.log("✅ Token refresh successful") + console.log("Token refresh successful") return true } catch (error) { - console.error("❌ Token refresh error:", error) - clearTokens() + console.error("Token refresh error:", error) + clearCookies() setState({ user: null, @@ -325,10 +335,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { // Logout function const logout = useCallback(async (): Promise => { try { - const accessToken = getToken(ACCESS_TOKEN_KEY) + const accessToken = getCookie("devboard_access_token") if (accessToken) { - console.log("📤 Calling logout API...") + console.log("Calling logout API...") try { await fetch(`${API_BASE_URL}/api/auth/logout`, { method: "POST", @@ -336,17 +346,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { Authorization: `Bearer ${accessToken}`, }, }) - console.log("✅ Logout API call successful") + console.log("Logout API call successful") } catch (apiError) { - console.error("❌ Logout API failed:", apiError) + console.error("Logout API failed:", apiError) // Continue with local logout even if API fails } } } catch (error) { - console.error("❌ Logout error:", error) + console.error("Logout error:", error) } finally { - console.log("🚪 Clearing tokens and logging out") - clearTokens() + console.log("Clearing cookies and logging out") + clearCookies() setState({ user: null, @@ -389,7 +399,7 @@ export function useAuth(): AuthContextType { // Authenticated fetch utility export async function authenticatedFetch(url: string, options: RequestInit = {}): Promise { - const accessToken = getToken(ACCESS_TOKEN_KEY) + const accessToken = getCookie("devboard_access_token") if (!accessToken) { throw new Error("No access token available") diff --git a/devboard/lib/utils.ts b/devboard/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/devboard/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/devboard/package-lock.json b/devboard/package-lock.json index 53a29fe..b5b3c13 100644 --- a/devboard/package-lock.json +++ b/devboard/package-lock.json @@ -8,10 +8,16 @@ "name": "devboard", "version": "0.1.0", "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "lucide-react": "^0.515.0", "next": "15.3.3", + "next-themes": "^0.4.6", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "sonner": "^2.0.5", + "tailwind-merge": "^3.3.1" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -19,6 +25,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "tailwindcss": "^4", + "tw-animate-css": "^1.3.4", "typescript": "^5" } }, @@ -655,6 +662,39 @@ "node": ">= 10" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -960,7 +1000,7 @@ "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1017,12 +1057,33 @@ "node": ">=18" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -1072,7 +1133,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/detect-libc": { @@ -1492,6 +1553,16 @@ } } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -1647,6 +1718,16 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sonner": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.5.tgz", + "integrity": "sha512-YwbHQO6cSso3HBXlbCkgrgzDNIhws14r4MO87Ofy+cV2X7ES4pOoAK3+veSmVTvqNx1BWUxlhPmZzP00Crk2aQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1687,6 +1768,16 @@ } } }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz", @@ -1728,6 +1819,16 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tw-animate-css": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.4.tgz", + "integrity": "sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", diff --git a/devboard/package.json b/devboard/package.json index ce7fe46..0a41ad9 100644 --- a/devboard/package.json +++ b/devboard/package.json @@ -9,10 +9,16 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "lucide-react": "^0.515.0", "next": "15.3.3", + "next-themes": "^0.4.6", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "sonner": "^2.0.5", + "tailwind-merge": "^3.3.1" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -20,6 +26,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "tailwindcss": "^4", + "tw-animate-css": "^1.3.4", "typescript": "^5" } } From 66d2cc88942d3e259a1e37b2908436769abad40b Mon Sep 17 00:00:00 2001 From: NitinTheGreat Date: Sat, 14 Jun 2025 22:08:45 +0530 Subject: [PATCH 05/29] fix: styles --- devboard/app/layout.tsx | 2 +- devboard/app/login/page.tsx | 9 ++++++- devboard/app/page.tsx | 15 +++++------ devboard/components/Hero/index.tsx | 41 +++++++++++++++++++++++++----- 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/devboard/app/layout.tsx b/devboard/app/layout.tsx index 756cd8d..0a5ab62 100644 --- a/devboard/app/layout.tsx +++ b/devboard/app/layout.tsx @@ -28,7 +28,7 @@ export default function RootLayout({
{ + if (isAuthenticated) { + router.push("/") + } + }, [isAuthenticated, router]) // Handle tokens when they come back from backend useEffect(() => { diff --git a/devboard/app/page.tsx b/devboard/app/page.tsx index 40a2314..52f7ebc 100644 --- a/devboard/app/page.tsx +++ b/devboard/app/page.tsx @@ -19,8 +19,8 @@ export default function Home() { const refreshToken = searchParams.get("refresh_token") const error = searchParams.get("error") - console.log("🏠 HOME PAGE - Checking for auth tokens...") - console.log("🎫 URL Tokens:", { + console.log("HOME PAGE - Checking for auth tokens...") + console.log("URL Tokens:", { hasAccessToken: !!accessToken, hasRefreshToken: !!refreshToken, error, @@ -28,32 +28,32 @@ export default function Home() { }) if (error) { - console.error("❌ Auth error in URL:", error) + console.error("Auth error in URL:", error) toast.error("Authentication failed: " + decodeURIComponent(error)) return } if (accessToken && refreshToken && !isAuthenticated) { - console.log("🔐 Found tokens in URL, processing login...") + console.log("Found tokens in URL, processing login...") setIsProcessingAuth(true) try { await login(accessToken, refreshToken) - console.log("✅ Login successful from URL tokens") + console.log("Login successful from URL tokens") toast.success("Successfully logged in!") // Clean up URL by removing the tokens const cleanUrl = window.location.pathname window.history.replaceState({}, document.title, cleanUrl) } catch (error) { - console.error("❌ Login failed:", error) + console.error("Login failed:", error) toast.error("Login failed: " + (error instanceof Error ? error.message : "Unknown error")) } finally { setIsProcessingAuth(false) } } } catch (error) { - console.error("❌ Error handling auth tokens:", error) + console.error("Error handling auth tokens:", error) setIsProcessingAuth(false) } } @@ -76,7 +76,6 @@ export default function Home() { return ( <> - {/* Main content will be added here */} ) } diff --git a/devboard/components/Hero/index.tsx b/devboard/components/Hero/index.tsx index 345d0c3..63a182e 100644 --- a/devboard/components/Hero/index.tsx +++ b/devboard/components/Hero/index.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from "react" import { Poppins } from "next/font/google" -import { ArrowRight } from "lucide-react" +import { ArrowRight, Loader2 } from "lucide-react" // Initialize the Poppins font const poppins = Poppins({ @@ -88,10 +88,7 @@ export default function HeroSection() { {/* Background canvas for floating shapes */} - {/* Logo */} -
-

DevBoard

-
+ {/* Hero content */}
@@ -107,12 +104,42 @@ export default function HeroSection() { ipsum.lorem ipsum. lorem ipsum.

- + */} +
+
) } From 8c3bad729236ca069b702b54930da5428644d146 Mon Sep 17 00:00:00 2001 From: NitinTheGreat Date: Sat, 14 Jun 2025 22:09:34 +0530 Subject: [PATCH 06/29] chore: remove unwanted files --- devboard/app/auth/callback/page.tsx | 103 ---------------------------- 1 file changed, 103 deletions(-) delete mode 100644 devboard/app/auth/callback/page.tsx diff --git a/devboard/app/auth/callback/page.tsx b/devboard/app/auth/callback/page.tsx deleted file mode 100644 index c014c50..0000000 --- a/devboard/app/auth/callback/page.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client" - -import { useEffect, useState } from "react" -import { useRouter, useSearchParams } from "next/navigation" -import { Loader2, CheckCircle, XCircle } from "lucide-react" -import { toast } from "sonner" -import { useAuth } from "@/lib/auth" - -export default function AuthCallback() { - const [status, setStatus] = useState<"loading" | "success" | "error">("loading") - const [message, setMessage] = useState("") - const router = useRouter() - const searchParams = useSearchParams() - const { login } = useAuth() - - useEffect(() => { - const handleCallback = async () => { - try { - console.log("🔄 AUTH CALLBACK STARTED") - console.log("🌐 Current URL:", window.location.href) - console.log("📋 All URL params:", Object.fromEntries(searchParams.entries())) - - // Get tokens from query parameters - const accessToken = searchParams.get("access-token") - const refreshToken = searchParams.get("refreshtoken") - const error = searchParams.get("error") - - console.log("🎫 Extracted tokens:", { - hasAccessToken: !!accessToken, - hasRefreshToken: !!refreshToken, - error, - }) - - if (error) { - throw new Error(decodeURIComponent(error)) - } - - if (!accessToken || !refreshToken) { - console.error("❌ Missing tokens in URL") - throw new Error("Missing authentication tokens in callback URL") - } - - console.log("🔐 Calling login function...") - await login(accessToken, refreshToken) - console.log("✅ Login function completed") - - setStatus("success") - setMessage("Successfully authenticated! Redirecting...") - - toast.success("Login successful!") - - // Redirect to home page - setTimeout(() => { - console.log("🏠 Redirecting to home...") - router.push("/") - }, 1500) - } catch (error) { - console.error("❌ Authentication callback error:", error) - setStatus("error") - setMessage(error instanceof Error ? error.message : "Authentication failed") - - toast.error("Authentication failed") - - // Redirect to login - setTimeout(() => { - console.log("🔙 Redirecting to login...") - router.push("/login") - }, 3000) - } - } - - handleCallback() - }, [searchParams, router, login]) - - return ( -
-
-
- {status === "loading" && } - {status === "success" && } - {status === "error" && } -
- -

- {status === "loading" && "Authenticating..."} - {status === "success" && "Success!"} - {status === "error" && "Authentication Failed"} -

- -

{message || "Processing your authentication..."}

- - {status === "error" && ( - - )} -
-
- ) -} From 5dd82d3518d4fabebe4bb4792ea157e7ef1a8b9f Mon Sep 17 00:00:00 2001 From: NitinTheGreat Date: Sun, 22 Jun 2025 12:25:58 +0530 Subject: [PATCH 07/29] chore: fix navbar --- devboard/app/api/llm/generate/route.ts | 68 + devboard/app/api/portfolio/generate/route.ts | 66 + devboard/app/portfolio/page.tsx | 398 ++++++ devboard/components/Navbar/index.tsx | 11 +- devboard/components/ui/label.tsx | 24 + devboard/components/ui/progress.tsx | 24 + devboard/components/ui/select.tsx | 185 +++ devboard/components/ui/textarea.tsx | 18 + devboard/lib/portfolio-workflow.ts | 180 +++ devboard/package-lock.json | 1170 +++++++++++++++++- devboard/package.json | 9 +- 11 files changed, 2131 insertions(+), 22 deletions(-) create mode 100644 devboard/app/api/llm/generate/route.ts create mode 100644 devboard/app/api/portfolio/generate/route.ts create mode 100644 devboard/app/portfolio/page.tsx create mode 100644 devboard/components/ui/label.tsx create mode 100644 devboard/components/ui/progress.tsx create mode 100644 devboard/components/ui/select.tsx create mode 100644 devboard/components/ui/textarea.tsx create mode 100644 devboard/lib/portfolio-workflow.ts diff --git a/devboard/app/api/llm/generate/route.ts b/devboard/app/api/llm/generate/route.ts new file mode 100644 index 0000000..0539db3 --- /dev/null +++ b/devboard/app/api/llm/generate/route.ts @@ -0,0 +1,68 @@ +import { type NextRequest, NextResponse } from "next/server" +import { ChatGoogleGenerativeAI } from "@langchain/google-genai" + +const model = new ChatGoogleGenerativeAI({ + modelName: "gemini-2.0-flash-exp", + apiKey: process.env.GEMINI_API_KEY, + temperature: 0.7, + streaming: true, +}) + +export async function POST(request: NextRequest) { + try { + const { prompt, systemPrompt } = await request.json() + + if (!prompt) { + return NextResponse.json({ error: "Prompt is required" }, { status: 400 }) + } + + const messages = [] + if (systemPrompt) { + messages.push({ role: "system", content: systemPrompt }) + } + messages.push({ role: "user", content: prompt }) + + // Create streaming response + const encoder = new TextEncoder() + const stream = new ReadableStream({ + async start(controller) { + try { + const streamResponse = await model.stream(messages) + + for await (const chunk of streamResponse) { + const content = chunk.content + if (content) { + const data = encoder.encode(`data: ${JSON.stringify({ content })}\n\n`) + controller.enqueue(data) + } + } + + const endChunk = encoder.encode(`data: ${JSON.stringify({ type: "end" })}\n\n`) + controller.enqueue(endChunk) + controller.close() + } catch (error) { + console.error("LLM streaming error:", error) + const errorChunk = encoder.encode( + `data: ${JSON.stringify({ + type: "error", + error: error instanceof Error ? error.message : "LLM generation failed", + })}\n\n`, + ) + controller.enqueue(errorChunk) + controller.close() + } + }, + }) + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }) + } catch (error) { + console.error("LLM API route error:", error) + return NextResponse.json({ error: "Failed to generate content" }, { status: 500 }) + } +} diff --git a/devboard/app/api/portfolio/generate/route.ts b/devboard/app/api/portfolio/generate/route.ts new file mode 100644 index 0000000..f1ccf1f --- /dev/null +++ b/devboard/app/api/portfolio/generate/route.ts @@ -0,0 +1,66 @@ +import { type NextRequest, NextResponse } from "next/server" +import { portfolioGraph } from "@/lib/portfolio-workflow" + +export async function POST(request: NextRequest) { + try { + const { content, customMessage, style } = await request.json() + + if (!content) { + return NextResponse.json({ error: "Content is required" }, { status: 400 }) + } + + // Create a readable stream for real-time updates + const encoder = new TextEncoder() + const stream = new ReadableStream({ + async start(controller) { + try { + // Initialize the workflow state + const initialState = { + content, + customMessage: customMessage || "", + style: style || "minimal", + parsedData: "", + portfolioCode: "", + currentStep: "parsing", + progress: 0, + error: null, + } + + // Stream the workflow execution + const config = { configurable: { thread_id: "portfolio-gen-" + Date.now() } } + + for await (const event of await portfolioGraph.stream(initialState, config)) { + const chunk = encoder.encode(`data: ${JSON.stringify(event)}\n\n`) + controller.enqueue(chunk) + } + + // Send completion signal + const completionChunk = encoder.encode(`data: ${JSON.stringify({ type: "complete" })}\n\n`) + controller.enqueue(completionChunk) + controller.close() + } catch (error) { + console.error("Portfolio generation error:", error) + const errorChunk = encoder.encode( + `data: ${JSON.stringify({ + type: "error", + error: error instanceof Error ? error.message : "Unknown error", + })}\n\n`, + ) + controller.enqueue(errorChunk) + controller.close() + } + }, + }) + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }) + } catch (error) { + console.error("API route error:", error) + return NextResponse.json({ error: "Failed to generate portfolio" }, { status: 500 }) + } +} diff --git a/devboard/app/portfolio/page.tsx b/devboard/app/portfolio/page.tsx new file mode 100644 index 0000000..28eac14 --- /dev/null +++ b/devboard/app/portfolio/page.tsx @@ -0,0 +1,398 @@ +"use client" + +import type React from "react" + +import { useState, useRef, useEffect } from "react" +import { useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Progress } from "@/components/ui/progress" +import { Loader2, Upload, Copy, Download, ArrowLeft } from "lucide-react" +import { toast } from "sonner" +import { useAuth } from "@/lib/auth" + +interface GenerationState { + currentStep: string + progress: number + parsedData: string + portfolioCode: string + error: string | null +} + +export default function PortfolioGenerator() { + const [isUploading, setIsUploading] = useState(false) + const [isGenerating, setIsGenerating] = useState(false) + const [content, setContent] = useState("") + const [customMessage, setCustomMessage] = useState("") + const [style, setStyle] = useState("minimal") + const [generationState, setGenerationState] = useState({ + currentStep: "", + progress: 0, + parsedData: "", + portfolioCode: "", + error: null, + }) + const [streamingCode, setStreamingCode] = useState("") + const fileInputRef = useRef(null) + const codeRef = useRef(null) + const { isAuthenticated } = useAuth() + const router = useRouter() + + // Auto-scroll to bottom of code as it streams + useEffect(() => { + if (codeRef.current) { + codeRef.current.scrollTop = codeRef.current.scrollHeight + } + }, [streamingCode]) + + const handleFileUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + + setIsUploading(true) + try { + const text = await file.text() + setContent(text) + toast.success(`${file.name} uploaded successfully`) + } catch (error) { + toast.error("Error uploading file") + } finally { + setIsUploading(false) + } + } + + const handleGenerate = async () => { + if (!isAuthenticated) { + toast.error("Please log in to generate your portfolio") + router.push("/login") + return + } + + if (!content) { + toast.error("Please upload a README or resume first") + return + } + + setIsGenerating(true) + setStreamingCode("") + setGenerationState({ + currentStep: "starting", + progress: 0, + parsedData: "", + portfolioCode: "", + error: null, + }) + + try { + const response = await fetch("/api/portfolio/generate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content, customMessage, style }), + }) + + if (!response.ok) { + throw new Error("Failed to start generation") + } + + const reader = response.body?.getReader() + const decoder = new TextDecoder() + + if (reader) { + while (true) { + const { done, value } = await reader.read() + if (done) break + + const chunk = decoder.decode(value) + const lines = chunk.split("\n") + + for (const line of lines) { + if (line.startsWith("data: ")) { + try { + const data = JSON.parse(line.slice(6)) + + if (data.type === "complete") { + setIsGenerating(false) + toast.success("Portfolio generated successfully!") + break + } + + if (data.type === "error") { + throw new Error(data.error) + } + + // Update generation state + if (data.currentStep || data.progress !== undefined) { + setGenerationState((prev) => ({ + ...prev, + currentStep: data.currentStep || prev.currentStep, + progress: data.progress !== undefined ? data.progress : prev.progress, + parsedData: data.parsedData || prev.parsedData, + portfolioCode: data.portfolioCode || prev.portfolioCode, + })) + } + + // Handle streaming code generation + if (data.currentStep === "generating" && data.portfolioCode) { + setStreamingCode(data.portfolioCode) + } + } catch (e) { + // Ignore parsing errors for incomplete chunks + } + } + } + } + } + } catch (error) { + console.error("Generation error:", error) + toast.error("Failed to generate portfolio") + setGenerationState((prev) => ({ + ...prev, + error: error instanceof Error ? error.message : "Unknown error", + currentStep: "error", + })) + } finally { + setIsGenerating(false) + } + } + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(generationState.portfolioCode || streamingCode) + toast.success("Code copied to clipboard!") + } catch (error) { + toast.error("Failed to copy code") + } + } + + const downloadCode = () => { + const code = generationState.portfolioCode || streamingCode + const blob = new Blob([code], { type: "text/javascript" }) + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = "Portfolio.jsx" + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + toast.success("Code downloaded!") + } + + const getStepDescription = (step: string) => { + switch (step) { + case "starting": + return "Initializing portfolio generation..." + case "parsing": + return "Analyzing your content and extracting key information..." + case "generating": + return "Generating your React portfolio code..." + case "complete": + return "Portfolio generation completed!" + case "error": + return "An error occurred during generation" + default: + return "Processing..." + } + } + + return ( +
+
+ {/* Header */} +
+ +

Portfolio Generator

+
+ +
+ {/* Configuration Panel */} + + + Configuration + + + {/* File Upload */} +
+ + + +
+ + {/* Content Textarea */} +
+ +