diff --git a/app/datasets/[dataset]/page.tsx b/app/datasets/[dataset]/page.tsx
index d015fbc..11d8d39 100644
--- a/app/datasets/[dataset]/page.tsx
+++ b/app/datasets/[dataset]/page.tsx
@@ -1,8 +1,42 @@
'use client';
+import { DateRangePicker } from '@/components/ui/dateRangePicker';
+import { Input } from '@/components/ui/input';
+import { Slider } from '@/components/ui/slider';
+import { DatasetAPI, type DatasetSchema } from '@/lib/api/dataset';
+import { useQuery } from '@tanstack/react-query';
import { useParams } from 'next/navigation';
+import { useMemo } from 'react';
+
+// keyword -> Text input
+// text -> Text input
+// long | double -> Range input
+// date -> Date range input
export default function DatasetPage() {
const { dataset } = useParams<{ dataset: string }>();
- return
{dataset}
;
+
+ const { data: schema } = useQuery({
+ queryKey: ['schema', dataset],
+ queryFn: () => DatasetAPI.getSchema(dataset)
+ });
+
+ const inputDefs = useMemo(() => Object.entries(schema || {}), [schema]);
+
+ return (
+
+ {inputDefs.map(([inputName, inputSchema]) => {
+ if (inputSchema.type === 'keyword' || inputSchema.type === 'text') {
+ return ;
+ }
+ if (inputSchema.type === 'long' || inputSchema.type === 'double') {
+ return ;
+ }
+ if (inputSchema.type === 'date') {
+ return ;
+ }
+ return null;
+ })}
+
+ );
}
diff --git a/app/datasets/page.tsx b/app/datasets/page.tsx
index 63fbdae..4a969f4 100644
--- a/app/datasets/page.tsx
+++ b/app/datasets/page.tsx
@@ -12,13 +12,11 @@ export default function DatasetPage() {
return (
-
- {datasets?.map((dataset) => (
- -
- {dataset.name}
-
- ))}
-
+ {datasets.map((dataset) => (
+
+ {dataset.name}
+
+ ))}
);
}
diff --git a/app/globals.css b/app/globals.css
index b5c61c9..6baea5d 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,3 +1,66 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+ }
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ }
+}
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/bun.lockb b/bun.lockb
index 18bf32e..61828cf 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..aa6b0dc
--- /dev/null
+++ b/components.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "app/globals.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ }
+}
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..5c8de11
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,47 @@
+import { Slot } from '@radix-ui/react-slot';
+import { type VariantProps, cva } from 'class-variance-authority';
+import * as React from 'react';
+
+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-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring 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 shadow hover:bg-primary/90',
+ destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
+ outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
+ secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline'
+ },
+ size: {
+ default: 'h-9 px-4 py-2',
+ sm: 'h-8 rounded-md px-3 text-xs',
+ lg: 'h-10 rounded-md px-8',
+ icon: 'h-9 w-9'
+ }
+ },
+ 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/components/ui/calendar.tsx b/components/ui/calendar.tsx
new file mode 100644
index 0000000..d33c43b
--- /dev/null
+++ b/components/ui/calendar.tsx
@@ -0,0 +1,63 @@
+'use client';
+
+import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons';
+import type React from 'react';
+import { DayPicker } from 'react-day-picker';
+
+import { buttonVariants } from '@/components/ui/button';
+import { cn } from '@/lib/utils';
+
+export type CalendarProps = React.ComponentProps;
+
+function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
+ return (
+ .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md'
+ : '[&:has([aria-selected])]:rounded-md'
+ ),
+ day: cn(buttonVariants({ variant: 'ghost' }), 'h-8 w-8 p-0 font-normal aria-selected:opacity-100'),
+ day_range_start: 'day-range-start',
+ day_range_end: 'day-range-end',
+ day_selected:
+ 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
+ day_today: 'bg-accent text-accent-foreground',
+ day_outside:
+ 'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
+ day_disabled: 'text-muted-foreground opacity-50',
+ day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground',
+ day_hidden: 'invisible',
+ ...classNames
+ }}
+ components={{
+ // @ts-ignore
+ IconLeft: ({ ...props }) => ,
+ IconRight: ({ ...props }) =>
+ }}
+ {...props}
+ />
+ );
+}
+Calendar.displayName = 'Calendar';
+
+export { Calendar };
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..315f2a6
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,49 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+const Card = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+Card.displayName = 'Card';
+
+const CardHeader = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardTitle.displayName = 'CardTitle';
+
+const CardDescription = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardDescription.displayName = 'CardDescription';
+
+const CardContent = React.forwardRef>(
+ ({ className, ...props }, ref) =>
+);
+CardContent.displayName = 'CardContent';
+
+const CardFooter = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ )
+);
+CardFooter.displayName = 'CardFooter';
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
diff --git a/components/ui/dateRangePicker.tsx b/components/ui/dateRangePicker.tsx
new file mode 100644
index 0000000..490db99
--- /dev/null
+++ b/components/ui/dateRangePicker.tsx
@@ -0,0 +1,54 @@
+'use client';
+
+import { addDays, format } from 'date-fns';
+import { Calendar as CalendarIcon } from 'lucide-react';
+import * as React from 'react';
+import type { DateRange } from 'react-day-picker';
+
+import { Button } from '@/components/ui/button';
+import { Calendar } from '@/components/ui/calendar';
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
+import { cn } from '@/lib/utils';
+
+export function DateRangePicker({ className }: React.HTMLAttributes) {
+ const [date, setDate] = React.useState({
+ from: new Date(2022, 0, 20),
+ to: addDays(new Date(2022, 0, 20), 20)
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
new file mode 100644
index 0000000..ec82d3a
--- /dev/null
+++ b/components/ui/input.tsx
@@ -0,0 +1,22 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+export interface InputProps extends React.InputHTMLAttributes {}
+
+const Input = React.forwardRef(({ className, type, ...props }, ref) => {
+ return (
+
+ );
+});
+Input.displayName = 'Input';
+
+export { Input };
diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx
new file mode 100644
index 0000000..026866a
--- /dev/null
+++ b/components/ui/popover.tsx
@@ -0,0 +1,33 @@
+'use client';
+
+import * as PopoverPrimitive from '@radix-ui/react-popover';
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+const Popover = PopoverPrimitive.Root;
+
+const PopoverTrigger = PopoverPrimitive.Trigger;
+
+const PopoverAnchor = PopoverPrimitive.Anchor;
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+PopoverContent.displayName = PopoverPrimitive.Content.displayName;
+
+export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx
new file mode 100644
index 0000000..1ebf362
--- /dev/null
+++ b/components/ui/slider.tsx
@@ -0,0 +1,25 @@
+'use client';
+
+import * as SliderPrimitive from '@radix-ui/react-slider';
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+const Slider = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+
+));
+Slider.displayName = SliderPrimitive.Root.displayName;
+
+export { Slider };
diff --git a/lib/api/dataset.ts b/lib/api/dataset.ts
index d3aa611..1eb3146 100644
--- a/lib/api/dataset.ts
+++ b/lib/api/dataset.ts
@@ -6,7 +6,7 @@ export type Dataset = {
};
export type DatasetFieldSchema = {
- type: string;
+ type: 'keyword' | 'text' | 'long' | 'double' | 'date' | string;
format?: string;
};
export type DatasetSchema = Record;
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 0000000..9ad0df4
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/next.config.js b/next.config.js
index b6c3485..7f37d26 100644
--- a/next.config.js
+++ b/next.config.js
@@ -4,8 +4,6 @@
*/
await import('./lib/env.js');
-const isProd = process.env.NODE_ENV === 'production';
-
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
diff --git a/package.json b/package.json
index 26e708a..81e5440 100644
--- a/package.json
+++ b/package.json
@@ -12,13 +12,24 @@
"prepare": "lefthook install"
},
"dependencies": {
+ "@radix-ui/react-icons": "^1.3.0",
+ "@radix-ui/react-popover": "^1.1.2",
+ "@radix-ui/react-slider": "^1.2.1",
+ "@radix-ui/react-slot": "^1.1.0",
"@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^5.59.15",
"@tanstack/react-table": "^8.20.5",
"axios": "^1.7.7",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.1.1",
+ "date-fns": "^4.1.0",
+ "lucide-react": "^0.453.0",
"next": "^14.2.15",
"react": "^18.3.1",
+ "react-day-picker": "^9.2.0",
"react-dom": "^18.3.1",
+ "tailwind-merge": "^2.5.4",
+ "tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -28,9 +39,9 @@
"@types/node": "^22.7.6",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
- "daisyui": "^4.12.13",
"lefthook": "^1.7.22",
"postcss": "8.4.41",
+ "shadcn": "^2.1.2",
"tailwindcss": "3.4.10",
"typescript": "5.5.4"
},
diff --git a/tailwind.config.ts b/tailwind.config.ts
index f3f333e..01d393f 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -1,16 +1,63 @@
-import daisyui from 'daisyui';
import type { Config } from 'tailwindcss';
-import { fontFamily } from 'tailwindcss/defaultTheme';
const config = {
+ darkMode: ['class'],
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}'
],
- plugins: [daisyui],
- daisyui: {
- themes: ['light']
+ plugins: [require('tailwindcss-animate')],
+ theme: {
+ extend: {
+ borderRadius: {
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
+ sm: 'calc(var(--radius) - 4px)'
+ },
+ colors: {
+ background: 'hsl(var(--background))',
+ foreground: 'hsl(var(--foreground))',
+ card: {
+ DEFAULT: 'hsl(var(--card))',
+ foreground: 'hsl(var(--card-foreground))'
+ },
+ popover: {
+ DEFAULT: 'hsl(var(--popover))',
+ foreground: 'hsl(var(--popover-foreground))'
+ },
+ primary: {
+ DEFAULT: 'hsl(var(--primary))',
+ foreground: 'hsl(var(--primary-foreground))'
+ },
+ secondary: {
+ DEFAULT: 'hsl(var(--secondary))',
+ foreground: 'hsl(var(--secondary-foreground))'
+ },
+ muted: {
+ DEFAULT: 'hsl(var(--muted))',
+ foreground: 'hsl(var(--muted-foreground))'
+ },
+ accent: {
+ DEFAULT: 'hsl(var(--accent))',
+ foreground: 'hsl(var(--accent-foreground))'
+ },
+ destructive: {
+ DEFAULT: 'hsl(var(--destructive))',
+ foreground: 'hsl(var(--destructive-foreground))'
+ },
+ border: 'hsl(var(--border))',
+ input: 'hsl(var(--input))',
+ ring: 'hsl(var(--ring))',
+ chart: {
+ '1': 'hsl(var(--chart-1))',
+ '2': 'hsl(var(--chart-2))',
+ '3': 'hsl(var(--chart-3))',
+ '4': 'hsl(var(--chart-4))',
+ '5': 'hsl(var(--chart-5))'
+ }
+ }
+ }
}
} satisfies Config;