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} + + ))}
); } 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;