"use client" import { useState, useMemo, useEffect, useRef } from "react" import { Search, ExternalLink, Mail, FileText, X, ChevronDown } 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) 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 */}

Awesome Food Allergy Research Datasets

A curated collection of datasets for advancing food allergy research

{/* Search and Filters */}
setSearchQuery(e.target.value)} className="pl-10 h-12 text-base" />
{/* --- Actions bar (Share + Export) — 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 && ( )} {/* Results Count */}

Showing {filteredDatasets.length} of{" "} {datasets.length} datasets

{/* Dataset Grid */} {loading ? (

Loading datasets...

) : 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 */} !open && setSelectedDataset(null)}> {selectedDataset && ( <>
{selectedDataset.Category && ( {selectedDataset.Category} )} {selectedDataset.Data_Type && ( {selectedDataset.Data_Type} )} {selectedDataset.Availability && ( {selectedDataset.Availability} )}
{selectedDataset.Name} {selectedDataset.Task && ( Task: {selectedDataset.Task} )}

Description

{selectedDataset.Description}

{selectedDataset.Source && ( )}
{selectedDataset["Paper link"] && ( )} {selectedDataset.Contact && ( )}
)}
) }