diff --git a/abc.txt b/abc.txt
deleted file mode 100644
index f0ad0de..0000000
--- a/abc.txt
+++ /dev/null
@@ -1 +0,0 @@
-hello.txt
diff --git a/backend/.env b/backend/.env
index f9c9ed5..3d067fd 100644
--- a/backend/.env
+++ b/backend/.env
@@ -1,13 +1,14 @@
PORT=8080
NODE_ENV=development
-DATABASE_URL=libsql://db-dev-63adec08f5fd35eadac3-sumit22.aws-ap-south-1.turso.io
-DATABASE_AUTH_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3ODI3Mzg5MjcsImlkIjoiMDE5ZjEzODUtMzUwMS03ODA0LWExZTEtY2JlN2E4NDlhM2I2Iiwia2lkIjoiczltT1N1V3ZwX2pib21leUZZNzR3RVNoN1NOak12RmVDc3EtZk5YdHY1NCIsInJpZCI6IjU5OTg0M2RkLTc4NWItNDhhZS04MDI0LTgxODM2YmQzZDE0MSJ9.TsozKx4lkITuiGiQ7FQDR_xl1LiZtzExx6nDkrZgefNXI7QPWiKYrSTgapqNMlO9QiGzE_cPbcClHvCPnmgMDg
-S3_ENDPOINT=https://t3.storage.dev
-S3_BUCKET=plena-staging
-S3_PREFIX=backends/6892099c9a4be35629c49e8c/6a426fea762485cd9a634857/dev/
-S3_ACCESS_KEY_ID=tid_VozbolUlzrxAAhDfPdanwUzAMqsO_E_EroKpPZnCcwOuXlHUKd
-S3_SECRET_ACCESS_KEY=tsec_C6sLdJDir0Kz+4abePDcJIdah+zA7dni1Z5nNY3lH5f4-dd+Nr+GohZuXgZwetr1Hv-270
-S3_SESSION_TOKEN=
-PROJECT_ID=6a426fea762485cd9a634857
+API_PREFIX=
CORS_ORIGIN=*
LOG_LEVEL=debug
+PROJECT_ID=6a1d3e08d7c7b2a82853a3dd
+DATABASE_URL=libsql://db-17b1345995727386ada9-sumit22.aws-ap-south-1.turso.io
+DATABASE_AUTH_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3ODIxOTUxMDksImlkIjoiMDE5ZWVlZDctNTAwMS03MjkwLWE4YmEtY2FlMjMwMDkzOTcxIiwicmlkIjoiOGVlNjZiZmItNmI5NS00MjdiLTlhYjMtZDljODRkMTVlZDljIn0.8QLPZHplF5ofeifQz9vrAeVp3R0mN_2bELWH4WsSvRovsz4T6jr2c1J6fTStBXOvRTkatwQ7FOdpVWNfFGI4Ag
+S3_ENDPOINT=https://t3.storage.dev
+S3_BUCKET=plena-staging
+S3_PREFIX=backends/template-project/
+S3_ACCESS_KEY_ID=tid_bBVTJLLQFGYJFbjMnPYjKvPtYZRzVXGFMrATpuMvySWEkgCEME
+S3_SECRET_ACCESS_KEY=tsec_7R_ZSENyKt_7GiIyse2Y4ZgZDwsqzOWajWKrg6CJrfiHOwVPIExYMp0PtVUpEUV7hmCPip
+S3_SESSION_TOKEN=
diff --git a/backend/.gitignore b/backend/.gitignore
index fd4f2b0..d8f3372 100644
--- a/backend/.gitignore
+++ b/backend/.gitignore
@@ -1,2 +1,3 @@
node_modules
.DS_Store
+.env
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 34de1cb..4a5fa4b 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,11 +1,16 @@
-FROM oven/bun:1.2.18 AS base
+FROM oven/bun:1.2.18 AS website-builder
+WORKDIR /website
+COPY website/package.json ./
+RUN bun install
+COPY website/ .
+RUN bun run build
+FROM oven/bun:1.2.18
WORKDIR /app
-
-COPY package.json bun.lock tsconfig.json ./
-RUN bun install --frozen-lockfile
-
-COPY src ./src
+COPY backend/package.json backend/bun.lock backend/tsconfig.json ./
+RUN bun install --frozen-lockfile --production
+COPY backend/src ./src
+COPY --from=website-builder /website/dist ./public
ENV NODE_ENV=production
ENV PORT=8080
diff --git a/backend/bun.lock b/backend/bun.lock
index 8c2a12b..9ddfd1f 100644
--- a/backend/bun.lock
+++ b/backend/bun.lock
@@ -8,7 +8,6 @@
"cors": "^2.8.5",
"dotenv": "^17.2.0",
"express": "^5.1.0",
- "node-cron": "^4.5.0",
},
"devDependencies": {
"@types/cors": "^2.8.19",
@@ -61,7 +60,7 @@
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
- "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
@@ -127,8 +126,6 @@
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
- "node-cron": ["node-cron@4.5.0", "", {}, "sha512-4Trh+kjvbXokyJkwQumvD5YAgeJfgHLR/sKyu71uSmxfCR5QMO1hldpvmFZOICN5pLgNY+J5Y8+ar3XKo5/4tQ=="],
-
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
diff --git a/backend/package.json b/backend/package.json
index bd49e07..ff65b52 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -11,8 +11,7 @@
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^17.2.0",
- "express": "^5.1.0",
- "node-cron": "^4.5.0"
+ "express": "^5.1.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/backend/public/index.html b/backend/public/index.html
new file mode 100644
index 0000000..294e456
--- /dev/null
+++ b/backend/public/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Backend Running
+
+
+
+
+
Backend Running
+
API /api/health
+
Frontend build not found — run bun run build in website/
+
+
+
diff --git a/backend/src/index.ts b/backend/src/index.ts
index 405be4b..e0b0928 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -1,12 +1,15 @@
import 'dotenv/config';
+import path from 'path';
+import { fileURLToPath } from 'url';
import cors from 'cors';
import express, { type NextFunction, type Request, type Response } from 'express';
-import cron from 'node-cron';
import { buildApiPath, getConfig } from './config';
import { TursoClient } from './lib/db';
import { createHealthRouter } from './routes/health';
import { createTodosRouter } from './routes/todos';
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
const config = getConfig();
const app = express();
const db = new TursoClient(config);
@@ -19,24 +22,14 @@ const apiRoot = config.apiPrefix || '/';
app.use(apiRoot, createHealthRouter(config));
app.use(apiRoot, createTodosRouter(config, db));
-// 1. Cron Job A: Runs every 3 minutes
-cron.schedule('*/3 * * * *', () => {
- console.log('[Cron A] Heartbeat executing every 3 minutes: ' + new Date().toISOString());
-});
+const publicDir = path.join(__dirname, '../public');
+app.use(express.static(publicDir));
-// 2. Cron Job B: Runs every 10 minutes
-cron.schedule('*/10 * * * *', () => {
- console.log('[Cron B] Heartbeat executing every 10 minutes: ' + new Date().toISOString());
-});
-
-// 3. Cloud Run Cron Tick endpoint (No-op to wake up event loop)
-app.post('/_cron/tick', (req, res) => {
- const authHeader = req.headers.authorization;
- if (!authHeader || authHeader !== 'Bearer ' + process.env.CRON_TOKEN) {
- return res.status(401).json({ success: false, error: 'Unauthorized' });
- }
- console.log('[Tick] Heartbeat received. Waking up event loop.');
- res.status(200).json({ success: true });
+app.get('*', (req, res, next) => {
+ if (req.path.startsWith(apiRoot)) return next();
+ res.sendFile(path.join(publicDir, 'index.html'), (err) => {
+ if (err) next();
+ });
});
app.use((_req, res) => {
diff --git a/website/.gitignore b/website/.gitignore
new file mode 100644
index 0000000..8990dad
--- /dev/null
+++ b/website/.gitignore
@@ -0,0 +1,17 @@
+# Dependencies
+node_modules
+
+# Lockfiles
+bun.lock
+
+# Vite
+.vite
+
+# Build output
+dist
+
+# Cache
+cache
+
+# OS files
+.DS_Store
diff --git a/website/components.json b/website/components.json
new file mode 100644
index 0000000..e2c49ef
--- /dev/null
+++ b/website/components.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "src/index.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ }
+}
diff --git a/website/eslint.config.js b/website/eslint.config.js
new file mode 100644
index 0000000..82c2e20
--- /dev/null
+++ b/website/eslint.config.js
@@ -0,0 +1,28 @@
+import js from '@eslint/js';
+import globals from 'globals';
+import reactHooks from 'eslint-plugin-react-hooks';
+import reactRefresh from 'eslint-plugin-react-refresh';
+import tseslint from 'typescript-eslint';
+
+export default tseslint.config(
+ { ignores: ['dist'] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ['**/*.{ts,tsx}'],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ 'react-hooks': reactHooks,
+ 'react-refresh': reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+ }
+);
diff --git a/website/index.html b/website/index.html
new file mode 100644
index 0000000..1c8f4a6
--- /dev/null
+++ b/website/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Hello World Dapp
+
+
+
+
+
+
+
diff --git a/website/package.json b/website/package.json
new file mode 100644
index 0000000..99b11c6
--- /dev/null
+++ b/website/package.json
@@ -0,0 +1,88 @@
+{
+ "name": "node-solana-starter-app",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host",
+ "build": "vite build",
+ "lint": "eslint .",
+ "preview": "vite preview",
+ "watch": "vite build --watch"
+ },
+ "dependencies": {
+ "@coral-xyz/anchor": "0.30.0",
+ "@dhiwise/component-tagger": "1.0.15",
+ "@hookform/resolvers": "3.9.1",
+ "@radix-ui/react-accordion": "1.2.2",
+ "@radix-ui/react-alert-dialog": "1.1.4",
+ "@radix-ui/react-aspect-ratio": "1.1.1",
+ "@radix-ui/react-avatar": "1.1.2",
+ "@radix-ui/react-checkbox": "1.1.3",
+ "@radix-ui/react-collapsible": "1.1.2",
+ "@radix-ui/react-context-menu": "2.2.4",
+ "@radix-ui/react-dialog": "1.1.4",
+ "@radix-ui/react-dropdown-menu": "2.1.4",
+ "@radix-ui/react-hover-card": "1.1.4",
+ "@radix-ui/react-label": "2.1.1",
+ "@radix-ui/react-menubar": "1.1.4",
+ "@radix-ui/react-navigation-menu": "1.2.3",
+ "@radix-ui/react-popover": "1.1.4",
+ "@radix-ui/react-progress": "1.1.1",
+ "@radix-ui/react-radio-group": "1.2.2",
+ "@radix-ui/react-scroll-area": "1.2.2",
+ "@radix-ui/react-select": "2.1.4",
+ "@radix-ui/react-separator": "1.1.1",
+ "@radix-ui/react-slider": "1.2.2",
+ "@radix-ui/react-slot": "1.1.1",
+ "@radix-ui/react-switch": "1.1.2",
+ "@radix-ui/react-tabs": "1.1.2",
+ "@radix-ui/react-toast": "1.2.4",
+ "@radix-ui/react-toggle": "1.1.1",
+ "@radix-ui/react-toggle-group": "1.1.1",
+ "@radix-ui/react-tooltip": "1.1.6",
+ "@solana/spl-token": "0.4.13",
+ "@solana/wallet-adapter-base": "0.9.27",
+ "@solana/wallet-adapter-react": "0.15.39",
+ "@solana/wallet-adapter-react-ui": "0.9.38",
+ "@solana/wallet-adapter-wallets": "0.19.36",
+ "@solana/web3.js": "1.98.2",
+ "@supabase/supabase-js": "2.57.4",
+ "buffer": "6.0.3",
+ "class-variance-authority": "0.7.1",
+ "clsx": "2.1.1",
+ "cmdk": "1.1.1",
+ "embla-carousel-react": "8.5.1",
+ "framer-motion": "12.38.0",
+ "lucide-react": "0.454.0",
+ "react": "19.1.0",
+ "react-copy-to-clipboard": "5.1.0",
+ "react-day-picker": "8.10.1",
+ "react-dom": "19.1.0",
+ "react-hook-form": "7.54.1",
+ "react-resizable-panels": "2.1.7",
+ "react-router-dom": "6.22.1",
+ "recharts": "2.12.7",
+ "tailwind-merge": "2.5.5",
+ "tailwindcss-animate": "1.0.7",
+ "vaul": "0.9.6"
+ },
+ "devDependencies": {
+ "@eslint/js": "9.9.1",
+ "@types/react": "19.0.0",
+ "@types/react-dom": "19.0.0",
+ "@vitejs/plugin-react-swc": "4.3.0",
+ "autoprefixer": "10.4.20",
+ "eslint": "9.9.1",
+ "eslint-plugin-react-hooks": "5.1.0-rc.0",
+ "eslint-plugin-react-refresh": "0.4.11",
+ "estree-walker": "^3.0.3",
+ "globals": "15.9.0",
+ "magic-string": "^0.30.21",
+ "postcss": "8.4.35",
+ "tailwindcss": "3.4.11",
+ "typescript": "5.5.3",
+ "typescript-eslint": "8.3.0",
+ "vite": "8.0.8"
+ }
+}
diff --git a/website/plugins/component-tagger.ts b/website/plugins/component-tagger.ts
new file mode 100644
index 0000000..82867d2
--- /dev/null
+++ b/website/plugins/component-tagger.ts
@@ -0,0 +1,103 @@
+import { parse } from "@babel/parser";
+import { walk } from "estree-walker";
+import MagicString from "magic-string";
+import path from "node:path";
+import type { Plugin } from "vite";
+
+const VALID_EXTENSIONS = new Set([".jsx", ".tsx"]);
+
+/**
+ * Returns a Vite plugin that adds component data attributes for component selection.
+ */
+export default function componentTagger(): Plugin {
+ return {
+ name: "vite-plugin-component-tagger",
+ apply: () => true,
+ enforce: "pre",
+
+ async transform(code: string, id: string) {
+ try {
+ // Ignore non-jsx files and files inside node_modules
+ if (
+ !VALID_EXTENSIONS.has(path.extname(id)) ||
+ id.includes("node_modules")
+ )
+ return null;
+
+ const ast = parse(code, {
+ sourceType: "module",
+ plugins: ["jsx", "typescript"],
+ });
+
+ const ms = new MagicString(code);
+ const fileRelative = path.relative(process.cwd(), id);
+
+ walk(ast as any, {
+ enter(node: any) {
+ try {
+ if (node.type !== "JSXOpeningElement") return;
+
+ // ── 1. Extract the tag / component name ──────────────────────────────
+ if (node.name?.type !== "JSXIdentifier") return;
+ const tagName = node.name.name as string;
+ if (!tagName) return;
+
+ // Skip certain HTML elements that are too generic
+ const skipElements = new Set([
+ "html",
+ "head",
+ "body",
+ "script",
+ "style",
+ "meta",
+ "link",
+ "title",
+ ]);
+ if (skipElements.has(tagName.toLowerCase())) return;
+
+ // ── 2. Check whether the tag already has data-component-id ───────────────
+ const alreadyTagged = node.attributes?.some(
+ (attr: any) =>
+ attr.type === "JSXAttribute" &&
+ attr.name?.name === "data-component-id",
+ );
+ if (alreadyTagged) return;
+
+ // ── 3. Build the id "relative/file.jsx:line:column" ─────────────────
+ const loc = node.loc?.start;
+ if (!loc) return;
+ const componentId = `${fileRelative}:${loc.line}:${loc.column}`;
+
+ // ── 4. Inject the attributes just after the tag name ────────────────
+ if (node.name.end != null) {
+ ms.appendLeft(
+ node.name.end,
+ ` data-component-id="${componentId}" data-component-name="${tagName}"`,
+ );
+ }
+ } catch (error) {
+ console.warn(
+ `[component-tagger] Warning: Failed to process JSX node in ${id}:`,
+ error,
+ );
+ }
+ },
+ });
+
+ // If nothing changed bail out.
+ if (ms.toString() === code) return null;
+
+ return {
+ code: ms.toString(),
+ map: ms.generateMap({ hires: true }),
+ };
+ } catch (error) {
+ console.warn(
+ `[component-tagger] Warning: Failed to transform ${id}:`,
+ error,
+ );
+ return null;
+ }
+ },
+ };
+}
diff --git a/website/postcss.config.js b/website/postcss.config.js
new file mode 100644
index 0000000..a982c64
--- /dev/null
+++ b/website/postcss.config.js
@@ -0,0 +1,8 @@
+const config = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
+
+export default config;
diff --git a/website/public/placeholder.svg b/website/public/placeholder.svg
new file mode 100644
index 0000000..e763910
--- /dev/null
+++ b/website/public/placeholder.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/website/public/robots.txt b/website/public/robots.txt
new file mode 100644
index 0000000..c2a49f4
--- /dev/null
+++ b/website/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Allow: /
diff --git a/website/src/App.tsx b/website/src/App.tsx
new file mode 100644
index 0000000..b2fb155
--- /dev/null
+++ b/website/src/App.tsx
@@ -0,0 +1,35 @@
+import { useMemo } from 'react';
+import { Routes, Route } from 'react-router-dom';
+import { Toaster } from '@/components/ui/toaster';
+import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
+import { WalletAdapterNetwork } from '@solana/wallet-adapter-base';
+import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets';
+import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
+import { clusterApiUrl } from '@solana/web3.js';
+import NotFound from './pages/NotFound';
+import Index from './pages/Index';
+
+import '@solana/wallet-adapter-react-ui/styles.css';
+
+const App = () => {
+ const network = WalletAdapterNetwork.Devnet;
+ const endpoint = useMemo(() => clusterApiUrl(network), [network]);
+
+ const wallets = useMemo(() => [new PhantomWalletAdapter()], []);
+
+ return (
+
+
+
+
+ } />
+ } />
+
+
+
+
+
+ );
+};
+
+export default App;
diff --git a/website/src/components/ui/accordion.tsx b/website/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..24c788c
--- /dev/null
+++ b/website/src/components/ui/accordion.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/website/src/components/ui/alert-dialog.tsx b/website/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..25e7b47
--- /dev/null
+++ b/website/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/website/src/components/ui/alert.tsx b/website/src/components/ui/alert.tsx
new file mode 100644
index 0000000..41fa7e0
--- /dev/null
+++ b/website/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/website/src/components/ui/aspect-ratio.tsx b/website/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..d6a5226
--- /dev/null
+++ b/website/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/website/src/components/ui/avatar.tsx b/website/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..51e507b
--- /dev/null
+++ b/website/src/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/website/src/components/ui/badge.tsx b/website/src/components/ui/badge.tsx
new file mode 100644
index 0000000..f000e3e
--- /dev/null
+++ b/website/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/website/src/components/ui/breadcrumb.tsx b/website/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..60e6c96
--- /dev/null
+++ b/website/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/website/src/components/ui/button.tsx b/website/src/components/ui/button.tsx
new file mode 100644
index 0000000..36496a2
--- /dev/null
+++ b/website/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/website/src/components/ui/calendar.tsx b/website/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..61d2b45
--- /dev/null
+++ b/website/src/components/ui/calendar.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/website/src/components/ui/card.tsx b/website/src/components/ui/card.tsx
new file mode 100644
index 0000000..f62edea
--- /dev/null
+++ b/website/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/website/src/components/ui/carousel.tsx b/website/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..ec505d0
--- /dev/null
+++ b/website/src/components/ui/carousel.tsx
@@ -0,0 +1,262 @@
+"use client"
+
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/website/src/components/ui/chart.tsx b/website/src/components/ui/chart.tsx
new file mode 100644
index 0000000..8620baa
--- /dev/null
+++ b/website/src/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+