diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..6383a632418d52f1dd06da8c8f8f8f0b4773d9f9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+# Dependencies
+node_modules/
+/.pnp
+.pnp.js
+
+# 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*
+
+# Local env files
+.env*.local
+.env
+
+# Vercel
+.vercel
+
+# TypeScript
+*.tsbuildinfo
+next-env.d.ts
+
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
new file mode 100644
index 0000000000000000000000000000000000000000..165c08091aa59ca40078643d7a2b54bbee27f9b1
--- /dev/null
+++ b/DEPLOYMENT.md
@@ -0,0 +1,152 @@
+# Deployment Guide for Hugging Face Spaces
+
+This guide explains how to deploy the Datasets Navigator app to Hugging Face Spaces using Docker.
+
+## Prerequisites
+
+- Docker installed and running (for local testing)
+- Hugging Face account ([sign up here](https://huggingface.co/join))
+
+## Files Created for Docker Deployment
+
+- `Dockerfile` - Multi-stage Docker build configuration
+- `.dockerignore` - Excludes unnecessary files from Docker build
+- `docker-compose.yml` - For local testing
+- `README.md` - Updated with HF Spaces metadata
+- `next.config.mjs` - Updated with `output: 'standalone'` for Docker
+
+## Local Testing (Optional)
+
+Before deploying to Hugging Face Spaces, you can test the Docker build locally:
+
+### Option 1: Using Docker directly
+
+```bash
+# Build the image
+docker build -t datasets-navigator .
+
+# Run the container
+docker run -p 7860:7860 datasets-navigator
+
+# Access the app at http://localhost:7860
+```
+
+### Option 2: Using Docker Compose
+
+```bash
+# Build and run
+docker-compose up --build
+
+# Access the app at http://localhost:7860
+
+# Stop with Ctrl+C, or in detached mode:
+docker-compose down
+```
+
+## Deploying to Hugging Face Spaces
+
+### Method 1: Via Hugging Face Web Interface
+
+1. Go to [Hugging Face Spaces](https://huggingface.co/spaces)
+2. Click "Create new Space"
+3. Fill in the details:
+ - **Space name**: Choose a name (e.g., `datasets-navigator`)
+ - **License**: Select appropriate license
+ - **SDK**: Select "Docker"
+ - **Hardware**: Start with "CPU basic" (can upgrade later)
+4. Clone the Space repository:
+ ```bash
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
+ ```
+5. Copy all files from `datasets_navigator_app/` to the cloned repository
+6. Push to Hugging Face:
+ ```bash
+ cd YOUR_SPACE_NAME
+ git add .
+ git commit -m "Initial commit: Datasets Navigator app"
+ git push
+ ```
+
+### Method 2: Push Existing Repository
+
+If you already have a Space created:
+
+```bash
+# Add Hugging Face as a remote
+cd datasets_navigator_app
+git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
+
+# Push to Hugging Face
+git add .
+git commit -m "Deploy Datasets Navigator to HF Spaces"
+git push hf main
+```
+
+## Configuration
+
+The app is configured via `README.md` frontmatter:
+
+```yaml
+---
+title: Prova
+emoji: 🍽️
+colorFrom: blue
+colorTo: purple
+sdk: docker
+app_port: 7860
+---
+```
+
+You can customize:
+- `title`: The Space's display name
+- `emoji`: Icon shown in the Space
+- `colorFrom` / `colorTo`: Gradient colors for the Space card
+- `app_port`: Must match the port in Dockerfile (7860)
+
+## Environment Variables & Secrets
+
+If your app needs environment variables or secrets:
+
+1. Go to your Space settings on Hugging Face
+2. Navigate to "Variables and secrets"
+3. Add variables (public) or secrets (private)
+
+They will be available as environment variables in the Docker container.
+
+## Troubleshooting
+
+### Build Fails
+
+- Check the build logs in the Space's "Logs" tab
+- Verify all dependencies are in `package.json`
+- Ensure `package-lock.json` is committed
+
+### App Won't Start
+
+- Verify the app listens on `0.0.0.0:7860`
+- Check the Docker logs for errors
+- Ensure user permissions are correct (user ID 1000)
+
+### Port Issues
+
+- The app MUST listen on the port specified in `app_port` (7860)
+- Verify `ENV PORT=7860` in Dockerfile
+- Next.js reads the PORT environment variable automatically
+
+## Upgrading Space Hardware
+
+If you need more resources:
+
+1. Go to Space settings
+2. Select "Hardware" tab
+3. Choose upgrade (CPU/GPU options available)
+4. Confirm the upgrade
+
+Note: GPU and upgraded hardware have associated costs.
+
+## References
+
+- [HF Spaces Docker Documentation](https://huggingface.co/docs/hub/spaces-sdks-docker)
+- [Next.js Docker Documentation](https://nextjs.org/docs/app/building-your-application/deploying#docker-image)
+- [Hugging Face Spaces Examples](https://huggingface.co/spaces)
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..65509679c1ac776ee14026756c1c172d10d472b3
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,44 @@
+# Use Node.js 18 Alpine for a lightweight image
+FROM node:18-alpine AS base
+
+# Install dependencies only when needed
+FROM base AS deps
+RUN apk add --no-cache libc6-compat
+WORKDIR /app
+
+# Copy package files
+COPY package.json package-lock.json ./
+RUN npm ci
+
+# Rebuild the source code only when needed
+FROM base AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+
+# Build Next.js application
+RUN npm run build
+
+# Production image, copy all the files and run next
+FROM base AS runner
+WORKDIR /app
+
+ENV NODE_ENV=production
+ENV PORT=7860
+ENV HOSTNAME="0.0.0.0"
+
+# Use the existing node user (UID 1000) which matches HF Spaces requirement
+# Copy necessary files from builder
+COPY --from=builder --chown=node:node /app/public ./public
+COPY --from=builder --chown=node:node /app/.next/standalone ./
+COPY --from=builder --chown=node:node /app/.next/static ./.next/static
+
+# Switch to the node user (UID 1000)
+USER node
+
+# Expose the port the app runs on
+EXPOSE 7860
+
+# Start the application
+CMD ["node", "server.js"]
+
diff --git a/README copy.md b/README copy.md
new file mode 100644
index 0000000000000000000000000000000000000000..e03e741cbb515973de8ee13e0ef0f42eaf5e6fda
--- /dev/null
+++ b/README copy.md
@@ -0,0 +1,109 @@
+---
+title: Awesome Food Allergy Datasets
+emoji: 🍽️
+colorFrom: green
+colorTo: yellow
+sdk: docker
+app_port: 7860
+---
+
+# Datasets Navigator
+
+A modern web application for navigating and exploring food allergy datasets, built with Next.js 14, React, TypeScript, and Tailwind CSS.
+
+## Prerequisites
+
+- [Node.js](https://nodejs.org/) (v18 or higher)
+- [pnpm](https://pnpm.io/) (v8 or higher)
+
+If you don't have pnpm installed, you can install it with:
+```bash
+npm install -g pnpm
+```
+
+## Getting Started
+
+### Installation
+
+Install all dependencies:
+
+```bash
+pnpm install
+```
+
+### Development
+
+Run the development server:
+
+```bash
+pnpm dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+The application will automatically reload when you make changes to the source files.
+
+### Building for Production
+
+Create an optimized production build:
+
+```bash
+pnpm build
+```
+
+Start the production server:
+
+```bash
+pnpm start
+```
+
+### Linting
+
+Run the linter to check for code quality issues:
+
+```bash
+pnpm lint
+```
+
+## Tech Stack
+
+- **Framework:** [Next.js 14](https://nextjs.org/) with App Router
+- **Language:** [TypeScript](https://www.typescriptlang.org/)
+- **Styling:** [Tailwind CSS](https://tailwindcss.com/)
+- **UI Components:** [Radix UI](https://www.radix-ui.com/)
+- **Icons:** [Lucide React](https://lucide.dev/)
+- **Charts:** [Recharts](https://recharts.org/)
+- **Forms:** [React Hook Form](https://react-hook-form.com/) with [Zod](https://zod.dev/)
+
+## Project Structure
+
+```
+datasets_navigator/
+├── app/ # Next.js App Router pages
+├── components/ # React components
+│ └── ui/ # Reusable UI components
+├── hooks/ # Custom React hooks
+├── lib/ # Utility functions
+├── public/ # Static assets
+└── styles/ # Global styles
+```
+
+## Features
+
+- Interactive dataset exploration interface
+- Modern, responsive design
+- Dark/light theme support
+- Component-based architecture
+- Type-safe with TypeScript
+
+## Contributing
+
+1. Make your changes in a feature branch
+2. Run `pnpm lint` to ensure code quality
+3. Test your changes with `pnpm dev`
+4. Submit a pull request
+
+## License
+
+See the [LICENSE](../../LICENSE) file for details.
+
diff --git a/README.md b/README.md
index d745a21bcd1bcd91b29b247c23ef63a4dbe9e586..b0a3fb0b554db0ce45beead5dbe4f5db8ad0f187 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,10 @@
---
-title: Awesome Food Allergy Datasets Viewer
-emoji: 🐠
-colorFrom: yellow
-colorTo: blue
+title: Awesome Food Allergy Datasets
+emoji: 🍽️
+colorFrom: green
+colorTo: yellow
sdk: docker
pinned: false
-license: apache-2.0
-short_description: Navigate the Awesome Food Allergy Datasets collec
---
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000000000000000000000000000000000000..e716e9c39b91e615837414dc755b6a65bd14858a
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,125 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ --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);
+ --destructive-foreground: 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);
+ --radius: 0.625rem;
+ --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);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.145 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.145 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.985 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.396 0.141 25.723);
+ --destructive-foreground: oklch(0.637 0.237 25.331);
+ --border: oklch(0.269 0 0);
+ --input: oklch(0.269 0 0);
+ --ring: oklch(0.439 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(0.269 0 0);
+ --sidebar-ring: oklch(0.439 0 0);
+}
+
+@theme inline {
+ --font-sans: var(--font-inter);
+ --font-mono: var(--font-jetbrains-mono);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-destructive-foreground: var(--destructive-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..42d39db57d8dd4031ac10b69f094b825a820bfc4
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,36 @@
+import type React from "react"
+import type { Metadata } from "next"
+import { Inter } from "next/font/google"
+import { JetBrains_Mono as JetBrainsMono } from "next/font/google"
+import { Analytics } from "@vercel/analytics/next"
+import { Suspense } from "react"
+import "./globals.css"
+import { ThemeProvider } from "@/theme-provider"
+
+const inter = Inter({ subsets: ["latin"], variable: "--font-inter", display: "swap" })
+const jetBrainsMono = JetBrainsMono({ subsets: ["latin"], variable: "--font-jetbrains-mono", display: "swap" })
+
+export const metadata: Metadata = {
+ title: "Awesome Food Allergy Research Datasets",
+ description: "A curated collection of datasets for advancing food allergy research",
+ generator: "v0.app",
+}
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+
+ )
+}
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..25265d1fa41eb403300322f7276d774e2d5fc85b
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,5 @@
+import { DatasetAtlas } from "@/components/dataset-atlas"
+
+export default function Home() {
+ return
+}
diff --git a/components.json b/components.json
new file mode 100644
index 0000000000000000000000000000000000000000..4ee62ee10547066d7ee70f94d79c786226c3c12c
--- /dev/null
+++ b/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"
+}
diff --git a/components/dataset-atlas.tsx b/components/dataset-atlas.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..512b3c8a8ecd65426be5347ccecc3a35b2bb1df7
--- /dev/null
+++ b/components/dataset-atlas.tsx
@@ -0,0 +1,597 @@
+"use client"
+
+import { useState, useMemo, useEffect, useRef } from "react"
+import { Search, ExternalLink, Mail, FileText, X, ChevronDown, BarChart3 } from "lucide-react"
+import { Input } from "@/components/ui/input"
+import { Badge } from "@/components/ui/badge"
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuCheckboxItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
+import { DatasetStatistics } from "@/components/dataset-statistics"
+import { ScrollToTop } from "@/components/scroll-to-top"
+
+interface Dataset {
+ Name: string
+ Category: string
+ Description: string
+ Task: string
+ Data_Type: string
+ Source: string
+ "Paper link": string
+ Availability: string
+ Contact: string
+}
+
+function getCategoryColor(category: string): { bg: string; text: string; hover: string } {
+ const colors = [
+ { bg: "bg-blue-100 dark:bg-blue-950", text: "text-blue-700 dark:text-blue-300", hover: "hover:bg-blue-200 dark:hover:bg-blue-900" },
+ { bg: "bg-emerald-100 dark:bg-emerald-950", text: "text-emerald-700 dark:text-emerald-300", hover: "hover:bg-emerald-200 dark:hover:bg-emerald-900" },
+ { bg: "bg-amber-100 dark:bg-amber-950", text: "text-amber-700 dark:text-amber-300", hover: "hover:bg-amber-200 dark:hover:bg-amber-900" },
+ { bg: "bg-rose-100 dark:bg-rose-950", text: "text-rose-700 dark:text-rose-300", hover: "hover:bg-rose-200 dark:hover:bg-rose-900" },
+ { bg: "bg-cyan-100 dark:bg-cyan-950", text: "text-cyan-700 dark:text-cyan-300", hover: "hover:bg-cyan-200 dark:hover:bg-cyan-900" },
+ ]
+ const hash = category.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)
+ return colors[hash % colors.length]
+}
+
+function getDataTypeColor(dataType: string): { bg: string; text: string; hover: string } {
+ const colors = [
+ { bg: "bg-indigo-100 dark:bg-indigo-950", text: "text-indigo-700 dark:text-indigo-300", hover: "hover:bg-indigo-200 dark:hover:bg-indigo-900" },
+ { bg: "bg-teal-100 dark:bg-teal-950", text: "text-teal-700 dark:text-teal-300", hover: "hover:bg-teal-200 dark:hover:bg-teal-900" },
+ { bg: "bg-orange-100 dark:bg-orange-950", text: "text-orange-700 dark:text-orange-300", hover: "hover:bg-orange-200 dark:hover:bg-orange-900" },
+ { bg: "bg-pink-100 dark:bg-pink-950", text: "text-pink-700 dark:text-pink-300", hover: "hover:bg-pink-200 dark:hover:bg-pink-900" },
+ { bg: "bg-lime-100 dark:bg-lime-950", text: "text-lime-700 dark:text-lime-300", hover: "hover:bg-lime-200 dark:hover:bg-lime-900" },
+ { bg: "bg-sky-100 dark:bg-sky-950", text: "text-sky-700 dark:text-sky-300", hover: "hover:bg-sky-200 dark:hover:bg-sky-900" },
+ ]
+ const hash = dataType.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)
+ return colors[hash % colors.length]
+}
+
+function getCategoryHoverClasses(category: string): string {
+ const sets = [
+ { bg: "hover:!bg-blue-50 dark:hover:!bg-blue-950/40", text: "hover:!text-blue-700 dark:hover:!text-blue-300" },
+ { bg: "hover:!bg-indigo-50 dark:hover:!bg-indigo-950/40", text: "hover:!text-indigo-700 dark:hover:!text-indigo-300" },
+ { bg: "hover:!bg-violet-50 dark:hover:!bg-violet-950/40", text: "hover:!text-violet-700 dark:hover:!text-violet-300" },
+ { bg: "hover:!bg-emerald-50 dark:hover:!bg-emerald-950/40", text: "hover:!text-emerald-700 dark:hover:!text-emerald-300" },
+ { bg: "hover:!bg-amber-50 dark:hover:!bg-amber-950/40", text: "hover:!text-amber-700 dark:hover:!text-amber-300" },
+ { bg: "hover:!bg-rose-50 dark:hover:!bg-rose-950/40", text: "hover:!text-rose-700 dark:hover:!text-rose-300" },
+ ] as const
+ const hash = category.split("").reduce((acc, ch) => acc + ch.charCodeAt(0), 0)
+ const c = sets[hash % sets.length]
+ return `${c.bg} ${c.text}`
+}
+
+/* ---------- Share-link + CSV helpers ---------- */
+function buildShareUrl(opts: {
+ searchQuery: string
+ selectedCategory: string
+ selectedTasks: string[]
+ selectedDataTypes: string[]
+}) {
+ const u = new URL(window.location.href)
+ const p = u.searchParams
+ p.set("q", opts.searchQuery || "")
+ p.set("cat", opts.selectedCategory || "all")
+ p.set("tasks", opts.selectedTasks.join(","))
+ p.set("types", opts.selectedDataTypes.join(","))
+ u.search = p.toString()
+ return u.toString()
+}
+
+function downloadCSV(rows: any[], filename = "datasets_filtered.csv") {
+ if (!rows.length) {
+ alert("Nothing to export — adjust your filters first.")
+ return
+ }
+ const headers = Object.keys(rows[0])
+ const esc = (v: any) => `"${String(v ?? "").replace(/"/g, '""')}"`
+ const csv = [headers.join(","), ...rows.map(r => headers.map(h => esc(r[h])).join(","))].join("\n")
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8" })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement("a")
+ a.href = url
+ a.download = filename
+ document.body.appendChild(a)
+ a.click()
+ a.remove()
+ URL.revokeObjectURL(url)
+}
+
+export function DatasetAtlas() {
+ const [datasets, setDatasets] = useState([])
+ const [searchQuery, setSearchQuery] = useState("")
+ const [selectedCategory, setSelectedCategory] = useState("all")
+ const [selectedTasks, setSelectedTasks] = useState([])
+ const [selectedDataTypes, setSelectedDataTypes] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [selectedDataset, setSelectedDataset] = useState(null)
+ const taskTriggerRef = useRef(null)
+ const dataTypeTriggerRef = useRef(null)
+ const [taskMenuWidth, setTaskMenuWidth] = useState(null)
+ const [dataTypeMenuWidth, setDataTypeMenuWidth] = useState(null)
+
+ // Chart filters passed to
+ const [chartFilterCategory, setChartFilterCategory] = useState(null)
+ const [chartFilterDataType, setChartFilterDataType] = useState(null)
+ const [chartFilterAvailability, setChartFilterAvailability] = useState(null)
+
+ // Toggle for showing/hiding plots
+ const [showPlots, setShowPlots] = useState(false)
+
+ useEffect(() => {
+ async function fetchData() {
+ try {
+ const response = await fetch(
+ "https://blobs.vusercontent.net/blob/Awesome%20food%20allergy%20datasets%20-%20Copia%20di%20Full%20view%20%281%29-nbq61oXBgltNRiIJIq8djoMoqX7ItK.tsv",
+ )
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
+ const text = await response.text()
+ const parsed = parseTSV(text)
+ setDatasets(parsed)
+ } catch (error) {
+ console.error("[v0] Error fetching datasets:", error)
+ } finally {
+ setLoading(false)
+ }
+ }
+ fetchData()
+ }, [])
+
+ useEffect(() => {
+ function updateWidths() {
+ if (taskTriggerRef.current) setTaskMenuWidth(Math.round(taskTriggerRef.current.getBoundingClientRect().width))
+ if (dataTypeTriggerRef.current) setDataTypeMenuWidth(Math.round(dataTypeTriggerRef.current.getBoundingClientRect().width))
+ }
+ updateWidths()
+ window.addEventListener("resize", updateWidths)
+ return () => window.removeEventListener("resize", updateWidths)
+ }, [])
+
+ function parseTSV(text: string): Dataset[] {
+ const lines = text.split("\n")
+ const headers = lines[0].split("\t").map((h) => h.trim().replace(/^"|"$/g, ""))
+ return lines
+ .slice(1)
+ .filter((line) => line.trim())
+ .map((line) => {
+ const values = line.split("\t").map((v) => v.trim())
+ const dataset: any = {}
+ headers.forEach((header, index) => (dataset[header] = values[index] || ""))
+ return dataset as Dataset
+ })
+ }
+
+ const categories = useMemo(() => {
+ const cats = new Set(datasets.map((d) => d.Category).filter(Boolean))
+ return ["all", ...Array.from(cats).sort()]
+ }, [datasets])
+
+ const tasks = useMemo(() => {
+ const set = new Set()
+ datasets.forEach((d) => {
+ if (d.Task) d.Task.split(",").map((t) => t.trim()).forEach((t) => t && set.add(t))
+ })
+ return Array.from(set).sort()
+ }, [datasets])
+
+ const dataTypes = useMemo(() => {
+ const set = new Set(datasets.map((d) => d.Data_Type).filter(Boolean))
+ return Array.from(set).sort()
+ }, [datasets])
+
+ const filteredDatasets = useMemo(() => {
+ return datasets.filter((d) => {
+ const q = searchQuery.toLowerCase()
+ const matchesSearch =
+ d.Name?.toLowerCase().includes(q) ||
+ d.Description?.toLowerCase().includes(q) ||
+ d.Category?.toLowerCase().includes(q) ||
+ d.Task?.toLowerCase().includes(q) ||
+ d.Data_Type?.toLowerCase().includes(q)
+
+ const matchesCategory = selectedCategory === "all" || d.Category === selectedCategory
+ const matchesTask =
+ selectedTasks.length === 0 ||
+ d.Task.split(",").map((t) => t.trim()).some((t) => selectedTasks.includes(t))
+ const matchesDataType = selectedDataTypes.length === 0 || selectedDataTypes.includes(d.Data_Type)
+
+ const matchesChartCategory = !chartFilterCategory || d.Category === chartFilterCategory
+ const matchesChartDataType = !chartFilterDataType || d.Data_Type === chartFilterDataType
+ const matchesChartAvailability = !chartFilterAvailability || d.Availability === chartFilterAvailability
+
+ return matchesSearch && matchesCategory && matchesTask && matchesDataType &&
+ matchesChartCategory && matchesChartDataType && matchesChartAvailability
+ })
+ }, [datasets, searchQuery, selectedCategory, selectedTasks, selectedDataTypes, chartFilterCategory, chartFilterDataType, chartFilterAvailability])
+
+ const toggleTask = (task: string) =>
+ setSelectedTasks((prev) => (prev.includes(task) ? prev.filter((t) => t !== task) : [...prev, task]))
+ const removeTask = (task: string) => setSelectedTasks((prev) => prev.filter((t) => t !== task))
+ const clearAllTasks = () => setSelectedTasks([])
+
+ const toggleDataType = (dataType: string) =>
+ setSelectedDataTypes((prev) => (prev.includes(dataType) ? prev.filter((t) => t !== dataType) : [...prev, dataType]))
+ const removeDataType = (dataType: string) => setSelectedDataTypes((prev) => prev.filter((t) => t !== dataType))
+ const clearAllDataTypes = () => setSelectedDataTypes([])
+
+ const handleCategoryChartClick = (category: string) =>
+ setChartFilterCategory((prev) => (prev === category ? null : category))
+ const handleDataTypeChartClick = (dataType: string) =>
+ setChartFilterDataType((prev) => (prev === dataType ? null : dataType))
+ const handleAvailabilityChartClick = (availability: string) =>
+ setChartFilterAvailability((prev) => (prev === availability ? null : availability))
+
+ return (
+
+ {/* Header */}
+
+
+
+ {/* Search and Filters */}
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10 h-12 text-base"
+ />
+
+
+ {/* --- Actions bar (Share + Export + Toggle Plots) — right under search --- */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {categories.map((category) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ {tasks.map((task) => (
+ toggleTask(task)}>
+ {task}
+
+ ))}
+
+
+
+ {selectedTasks.length > 0 && (
+
+ {selectedTasks.map((task) => (
+
+ {task}
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {dataTypes.map((dataType) => (
+ toggleDataType(dataType)}>
+ {dataType}
+
+ ))}
+
+
+
+ {selectedDataTypes.length > 0 && (
+
+ {selectedDataTypes.map((dataType) => (
+
+ {dataType}
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+
+ {/* Statistics Charts */}
+ {!loading && datasets.length > 0 && showPlots && (
+
+ )}
+
+ {/* Results Count */}
+
+
+ Showing {filteredDatasets.length} of{" "}
+ {datasets.length} datasets
+
+
+
+ {/* Dataset Grid */}
+ {loading ? (
+
+ ) : filteredDatasets.length === 0 ? (
+
+
No datasets found matching your criteria.
+
+ ) : (
+
+ {filteredDatasets.map((dataset, index) => {
+ const categoryColors = getCategoryColor(dataset.Category)
+ const dataTypeColors = getDataTypeColor(dataset.Data_Type)
+ return (
+
setSelectedDataset(dataset)}>
+
+
+ {dataset.Category && (
+
+ {dataset.Category}
+
+ )}
+ {dataset.Data_Type && (
+
+ {dataset.Data_Type}
+
+ )}
+ {dataset.Availability && (
+
+ {dataset.Availability}
+
+ )}
+
+ {dataset.Name}
+ {dataset.Task && (
+
+ Task: {dataset.Task}
+
+ )}
+
+
+
+ {dataset.Description}
+
+
+
+ {dataset.Source && (
+
+ )}
+
+ {dataset["Paper link"] && (
+
+ )}
+ {dataset.Contact && (
+
+ )}
+
+
+
+ )
+ })}
+
+ )}
+
+
+ {/* Modal Dialog for Expanded Dataset View */}
+
+
+
+
+ )
+}
diff --git a/components/dataset-statistics.tsx b/components/dataset-statistics.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..918291f624012d20763d7ed5eb914169045d4d74
--- /dev/null
+++ b/components/dataset-statistics.tsx
@@ -0,0 +1,297 @@
+"use client"
+
+import { useMemo } from "react"
+import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from "recharts"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+
+interface Dataset {
+ Name: string
+ Category: string
+ Description: string
+ Task: string
+ Data_Type: string
+ Source: string
+ "Paper link": string
+ Availability: string
+ Contact: string
+}
+
+interface DatasetStatisticsProps {
+ datasets: Dataset[]
+ selectedCategory: string | null
+ selectedDataType: string | null
+ selectedAvailability: string | null
+ onCategoryClick: (category: string) => void
+ onDataTypeClick: (dataType: string) => void
+ onAvailabilityClick: (availability: string) => void
+}
+
+const CATEGORY_COLORS = [
+ "#6366f1", // indigo
+ "#10b981", // emerald
+ "#f59e0b", // amber
+ "#3b82f6", // blue
+ "#8b5cf6", // violet
+ "#06b6d4", // cyan
+]
+
+const DATA_TYPE_COLORS = [
+ "#3b82f6", // blue
+ "#10b981", // emerald
+ "#6366f1", // indigo
+ "#f59e0b", // amber
+ "#8b5cf6", // violet
+ "#06b6d4", // cyan
+]
+
+const AVAILABILITY_COLORS = {
+ "Open source": "#3b82f6", // blue
+ "Gated": "#10b981", // emerald
+ "Unknown": "#6b7280", // gray
+}
+
+export function DatasetStatistics({
+ datasets,
+ selectedCategory,
+ selectedDataType,
+ selectedAvailability,
+ onCategoryClick,
+ onDataTypeClick,
+ onAvailabilityClick
+}: DatasetStatisticsProps) {
+ const categoryData = useMemo(() => {
+ const counts: Record = {}
+ datasets.forEach((dataset) => {
+ if (dataset.Category) {
+ counts[dataset.Category] = (counts[dataset.Category] || 0) + 1
+ }
+ })
+ return Object.entries(counts)
+ .map(([name, value]) => ({ name, value }))
+ .sort((a, b) => b.value - a.value)
+ }, [datasets])
+
+ const dataTypeData = useMemo(() => {
+ const counts: Record = {}
+ datasets.forEach((dataset) => {
+ if (dataset.Data_Type) {
+ counts[dataset.Data_Type] = (counts[dataset.Data_Type] || 0) + 1
+ }
+ })
+ return Object.entries(counts)
+ .map(([name, value]) => ({ name, value }))
+ .sort((a, b) => b.value - a.value)
+ }, [datasets])
+
+ const availabilityData = useMemo(() => {
+ const counts: Record = {}
+ datasets.forEach((dataset) => {
+ if (dataset.Availability) {
+ counts[dataset.Availability] = (counts[dataset.Availability] || 0) + 1
+ }
+ })
+ return Object.entries(counts).map(([name, value]) => ({ name, value }))
+ }, [datasets])
+
+ const CustomTooltip = ({ active, payload }: any) => {
+ if (active && payload && payload.length) {
+ return (
+
+
{payload[0].payload.name}
+
Count: {payload[0].value}
+
+ )
+ }
+ return null
+ }
+
+ const renderCustomBarLabel = (props: any) => {
+ const { x, y, width, height, value } = props
+ return (
+
+ {value}
+
+ )
+ }
+
+ return (
+
+ {/* Top Row: By Data Type and Availability */}
+
+ {/* By Data Type */}
+
+
+
+ By Data Type
+ {selectedDataType && (
+
+ (Click to clear filter)
+
+ )}
+
+
+
+
+
+
+
+
+ } />
+ onDataTypeClick(data.name)}
+ cursor="pointer"
+ >
+ {dataTypeData.map((entry, index) => {
+ const isSelected = selectedDataType === entry.name
+ const shouldGreyscale = selectedDataType && !isSelected
+ const color = shouldGreyscale
+ ? "#94a3b8"
+ : DATA_TYPE_COLORS[index % DATA_TYPE_COLORS.length]
+ return (
+ |
+ )
+ })}
+
+
+
+
+
+
+ {/* Availability */}
+
+
+
+ Availability
+ {selectedAvailability && (
+
+ (Click to clear filter)
+
+ )}
+
+
+
+
+
+ `${name}: ${value}`}
+ outerRadius={80}
+ fill="#8884d8"
+ dataKey="value"
+ onClick={(data) => onAvailabilityClick(data.name)}
+ cursor="pointer"
+ >
+ {availabilityData.map((entry, index) => {
+ const isSelected = selectedAvailability === entry.name
+ const shouldGreyscale = selectedAvailability && !isSelected
+ const originalColor = AVAILABILITY_COLORS[entry.name as keyof typeof AVAILABILITY_COLORS] || "#6b7280"
+ const color = shouldGreyscale ? "#94a3b8" : originalColor
+ return (
+ |
+ )
+ })}
+
+ } />
+
+
+
+
+
+
+ {/* Bottom Row: By Category */}
+
+
+
+
+ By Category
+ {selectedCategory && (
+
+ (Click to clear filter)
+
+ )}
+
+
+
+
+
+
+
+
+ } />
+ onCategoryClick(data.name)}
+ cursor="pointer"
+ >
+ {categoryData.map((entry, index) => {
+ const isSelected = selectedCategory === entry.name
+ const shouldGreyscale = selectedCategory && !isSelected
+ const color = shouldGreyscale
+ ? "#94a3b8"
+ : CATEGORY_COLORS[index % CATEGORY_COLORS.length]
+ return (
+ |
+ )
+ })}
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/scroll-to-top.tsx b/components/scroll-to-top.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..113ddc8aec0dda0f9a31bfd81f33456322395dd5
--- /dev/null
+++ b/components/scroll-to-top.tsx
@@ -0,0 +1,49 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { ArrowUp } from "lucide-react"
+import { Button } from "@/components/ui/button"
+
+export function ScrollToTop() {
+ const [isVisible, setIsVisible] = useState(false)
+
+ useEffect(() => {
+ const toggleVisibility = () => {
+ // Show button when page is scrolled down 300px
+ if (window.scrollY > 300) {
+ setIsVisible(true)
+ } else {
+ setIsVisible(false)
+ }
+ }
+
+ window.addEventListener("scroll", toggleVisibility)
+
+ return () => {
+ window.removeEventListener("scroll", toggleVisibility)
+ }
+ }, [])
+
+ const scrollToTop = () => {
+ window.scrollTo({
+ top: 0,
+ behavior: "smooth",
+ })
+ }
+
+ return (
+ <>
+ {isVisible && (
+
+ )}
+ >
+ )
+}
+
diff --git a/components/site-footer.tsx b/components/site-footer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bc71c2b79f576cfea0b5ffdee930a535aae4050f
--- /dev/null
+++ b/components/site-footer.tsx
@@ -0,0 +1,11 @@
+export function SiteFooter() {
+ const year = new Date().getFullYear()
+ return (
+
+ )
+}
diff --git a/components/site-header.tsx b/components/site-header.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0ae6d8197be95405fad13793c3f706b508cfd031
--- /dev/null
+++ b/components/site-header.tsx
@@ -0,0 +1,15 @@
+"use client"
+
+import Link from "next/link"
+
+export function SiteHeader() {
+ return (
+
+ )
+}
diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..55c2f6eb60b22a313a4c27bd0b2d728063cb8ab9
--- /dev/null
+++ b/components/theme-provider.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import * as React from 'react'
+import {
+ ThemeProvider as NextThemesProvider,
+ type ThemeProviderProps,
+} from 'next-themes'
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children}
+}
diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d690cf7e4593893e4c8e3415ef32bd80c37ab371
--- /dev/null
+++ b/components/theme-toggle.tsx
@@ -0,0 +1,33 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { useTheme } from "next-themes"
+import { Sun, Moon } from "lucide-react"
+import { Button } from "@/components/ui/button"
+
+export function ThemeToggle() {
+ const { theme, setTheme } = useTheme()
+ const [mounted, setMounted] = useState(false)
+
+ useEffect(() => setMounted(true), [])
+
+ const cycleTheme = () => {
+ const next = theme === "light" ? "dark" : theme === "dark" ? "system" : "light"
+ setTheme(next)
+ }
+
+ if (!mounted) return null
+
+ return (
+
+ )
+}
diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e538a33b9946acb372e2f569a7cb4b22b17cff12
--- /dev/null
+++ b/components/ui/accordion.tsx
@@ -0,0 +1,66 @@
+'use client'
+
+import * as React from 'react'
+import * as AccordionPrimitive from '@radix-ui/react-accordion'
+import { ChevronDownIcon } from 'lucide-react'
+
+import { cn } from '@/lib/utils'
+
+function Accordion({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180',
+ className,
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ )
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9704452664dabbb8cef88fa154c12f81b243120d
--- /dev/null
+++ b/components/ui/alert-dialog.tsx
@@ -0,0 +1,157 @@
+'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'
+
+function AlertDialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogAction({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogCancel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e6751abe6a83bc38bbb422f7fc47a534ec65f459
--- /dev/null
+++ b/components/ui/alert.tsx
@@ -0,0 +1,66 @@
+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 px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
+ {
+ variants: {
+ variant: {
+ default: 'bg-card text-card-foreground',
+ destructive:
+ 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+)
+
+function Alert({
+ className,
+ variant,
+ ...props
+}: React.ComponentProps<'div'> & VariantProps) {
+ return (
+
+ )
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function AlertDescription({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..40bb1208dbbf471b59cac12b60b992cb3cfd30dd
--- /dev/null
+++ b/components/ui/aspect-ratio.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'
+
+function AspectRatio({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+export { AspectRatio }
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..aa98465a30f89336a5205d3298bc5bf836baa1fd
--- /dev/null
+++ b/components/ui/avatar.tsx
@@ -0,0 +1,53 @@
+'use client'
+
+import * as React from 'react'
+import * as AvatarPrimitive from '@radix-ui/react-avatar'
+
+import { cn } from '@/lib/utils'
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..098e43ce96ace98f308b13fffeac6985bd33cfce
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,29 @@
+import type * 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-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive: "border-transparent bg-destructive text-destructive-foreground shadow 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/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1750ff26a6f95d49cece850873e367f51c0be252
--- /dev/null
+++ b/components/ui/breadcrumb.tsx
@@ -0,0 +1,109 @@
+import * as React from 'react'
+import { Slot } from '@radix-ui/react-slot'
+import { ChevronRight, MoreHorizontal } from 'lucide-react'
+
+import { cn } from '@/lib/utils'
+
+function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
+ return
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
+ return (
+
+ )
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
+ return (
+
+ )
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<'a'> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot : 'a'
+
+ return (
+
+ )
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+
+ )
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<'li'>) {
+ return (
+ svg]:size-3.5', className)}
+ {...props}
+ >
+ {children ?? }
+
+ )
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) {
+ return (
+
+
+ More
+
+ )
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..73e52bcd774319fe1b1af38c44edc57cb602d518
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,60 @@
+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',
+ },
+ },
+)
+
+type ButtonProps = React.ComponentProps<'button'> &
+ 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/components/ui/calendar.tsx b/components/ui/calendar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..eaa373e25f99cd03cf06d7c11bec3fac969ad38d
--- /dev/null
+++ b/components/ui/calendar.tsx
@@ -0,0 +1,213 @@
+'use client'
+
+import * as React from 'react'
+import {
+ ChevronDownIcon,
+ ChevronLeftIcon,
+ ChevronRightIcon,
+} from 'lucide-react'
+import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'
+
+import { cn } from '@/lib/utils'
+import { Button, buttonVariants } from '@/components/ui/button'
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ captionLayout = 'label',
+ buttonVariant = 'ghost',
+ formatters,
+ components,
+ ...props
+}: React.ComponentProps & {
+ buttonVariant?: React.ComponentProps['variant']
+}) {
+ const defaultClassNames = getDefaultClassNames()
+
+ return (
+ svg]:rotate-180`,
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+ className,
+ )}
+ captionLayout={captionLayout}
+ formatters={{
+ formatMonthDropdown: (date) =>
+ date.toLocaleString('default', { month: 'short' }),
+ ...formatters,
+ }}
+ classNames={{
+ root: cn('w-fit', defaultClassNames.root),
+ months: cn(
+ 'flex gap-4 flex-col md:flex-row relative',
+ defaultClassNames.months,
+ ),
+ month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
+ nav: cn(
+ 'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
+ defaultClassNames.nav,
+ ),
+ button_previous: cn(
+ buttonVariants({ variant: buttonVariant }),
+ 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
+ defaultClassNames.button_previous,
+ ),
+ button_next: cn(
+ buttonVariants({ variant: buttonVariant }),
+ 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
+ defaultClassNames.button_next,
+ ),
+ month_caption: cn(
+ 'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)',
+ defaultClassNames.month_caption,
+ ),
+ dropdowns: cn(
+ 'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5',
+ defaultClassNames.dropdowns,
+ ),
+ dropdown_root: cn(
+ 'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md',
+ defaultClassNames.dropdown_root,
+ ),
+ dropdown: cn(
+ 'absolute bg-popover inset-0 opacity-0',
+ defaultClassNames.dropdown,
+ ),
+ caption_label: cn(
+ 'select-none font-medium',
+ captionLayout === 'label'
+ ? 'text-sm'
+ : 'rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5',
+ defaultClassNames.caption_label,
+ ),
+ table: 'w-full border-collapse',
+ weekdays: cn('flex', defaultClassNames.weekdays),
+ weekday: cn(
+ 'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none',
+ defaultClassNames.weekday,
+ ),
+ week: cn('flex w-full mt-2', defaultClassNames.week),
+ week_number_header: cn(
+ 'select-none w-(--cell-size)',
+ defaultClassNames.week_number_header,
+ ),
+ week_number: cn(
+ 'text-[0.8rem] select-none text-muted-foreground',
+ defaultClassNames.week_number,
+ ),
+ day: cn(
+ 'relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none',
+ defaultClassNames.day,
+ ),
+ range_start: cn(
+ 'rounded-l-md bg-accent',
+ defaultClassNames.range_start,
+ ),
+ range_middle: cn('rounded-none', defaultClassNames.range_middle),
+ range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
+ today: cn(
+ 'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none',
+ defaultClassNames.today,
+ ),
+ outside: cn(
+ 'text-muted-foreground aria-selected:text-muted-foreground',
+ defaultClassNames.outside,
+ ),
+ disabled: cn(
+ 'text-muted-foreground opacity-50',
+ defaultClassNames.disabled,
+ ),
+ hidden: cn('invisible', defaultClassNames.hidden),
+ ...classNames,
+ }}
+ components={{
+ Root: ({ className, rootRef, ...props }) => {
+ return (
+
+ )
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === 'left') {
+ return (
+
+ )
+ }
+
+ if (orientation === 'right') {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+ },
+ DayButton: CalendarDayButton,
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+
+ {children}
+
+ |
+ )
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ )
+}
+
+function CalendarDayButton({
+ className,
+ day,
+ modifiers,
+ ...props
+}: React.ComponentProps) {
+ const defaultClassNames = getDefaultClassNames()
+
+ const ref = React.useRef(null)
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus()
+ }, [modifiers.focused])
+
+ return (
+