feat: implement tailwind v4 and shadcn
This commit is contained in:
22
components.json
Normal file
22
components.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
16
package.json
16
package.json
@@ -21,15 +21,28 @@
|
||||
"@payloadcms/plugin-multi-tenant": "3.65.0",
|
||||
"@payloadcms/richtext-lexical": "3.65.0",
|
||||
"@payloadcms/ui": "3.65.0",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"graphql": "^16.9.0",
|
||||
"lucide-react": "^0.555.0",
|
||||
"next": "^15.2.3",
|
||||
"payload": "3.65.0",
|
||||
"postcss": "^8.5.6",
|
||||
"qs-esm": "7.0.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"sharp": "0.32.6"
|
||||
"sharp": "0.32.6",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "^3.28.0",
|
||||
@@ -40,6 +53,7 @@
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^15.0.0",
|
||||
"tsx": "^4.16.2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "5.5.2"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
1542
pnpm-lock.yaml
generated
1542
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
7
postcss.config.mjs
Normal file
7
postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -3,6 +3,19 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Loader2,
|
||||
LogOut,
|
||||
Sun,
|
||||
Moon,
|
||||
Sunrise,
|
||||
ClipboardList,
|
||||
Users,
|
||||
Settings,
|
||||
} from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
interface User {
|
||||
id: number
|
||||
@@ -30,7 +43,6 @@ export default function CaregiverDashboardPage() {
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Check auth
|
||||
const userRes = await fetch('/api/users/me', { credentials: 'include' })
|
||||
if (!userRes.ok) {
|
||||
router.push('/caregiver/login')
|
||||
@@ -43,7 +55,6 @@ export default function CaregiverDashboardPage() {
|
||||
}
|
||||
setUser(userData.user)
|
||||
|
||||
// Fetch today's orders stats
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const ordersRes = await fetch(`/api/meal-orders?where[date][equals]=${today}&limit=1000`, {
|
||||
credentials: 'include',
|
||||
@@ -77,8 +88,8 @@ export default function CaregiverDashboardPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="spinner" />
|
||||
<div className="min-h-screen flex items-center justify-center bg-muted/50">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -89,78 +100,127 @@ export default function CaregiverDashboardPage() {
|
||||
: 'Care Home'
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="header">
|
||||
<div className="header__content">
|
||||
<h1 className="header__title">{tenantName}</h1>
|
||||
<div className="header__user">
|
||||
<span className="header__user-name">{user?.name || user?.email}</span>
|
||||
<button onClick={handleLogout} className="btn btn--secondary">
|
||||
<div className="min-h-screen bg-muted/50">
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-16 items-center justify-between">
|
||||
<h1 className="text-xl font-semibold">{tenantName}</h1>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-muted-foreground">{user?.name || user?.email}</span>
|
||||
<Button variant="outline" size="sm" onClick={handleLogout}>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Logout
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container">
|
||||
<div className="page-title">
|
||||
<h1>Dashboard</h1>
|
||||
<p>Today's overview</p>
|
||||
<main className="container py-6">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-2xl font-bold tracking-tight">Dashboard</h2>
|
||||
<p className="text-muted-foreground">Today's overview</p>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-card__value">{stats.total}</div>
|
||||
<div className="stat-card__label">Total Orders Today</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-card__value">{stats.pending}</div>
|
||||
<div className="stat-card__label">Pending</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-card__value">{stats.preparing}</div>
|
||||
<div className="stat-card__label">Preparing</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-card__value">{stats.prepared}</div>
|
||||
<div className="stat-card__label">Prepared</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-8">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Orders</CardTitle>
|
||||
<ClipboardList className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.total}</div>
|
||||
<p className="text-xs text-muted-foreground">Today</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Pending</CardTitle>
|
||||
<div className="h-2 w-2 rounded-full bg-yellow-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.pending}</div>
|
||||
<p className="text-xs text-muted-foreground">Awaiting preparation</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Preparing</CardTitle>
|
||||
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.preparing}</div>
|
||||
<p className="text-xs text-muted-foreground">In progress</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Prepared</CardTitle>
|
||||
<div className="h-2 w-2 rounded-full bg-green-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.prepared}</div>
|
||||
<p className="text-xs text-muted-foreground">Ready to serve</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<h2>Quick Actions</h2>
|
||||
</div>
|
||||
<div className="card__body">
|
||||
<div className="quick-actions">
|
||||
<Link href="/caregiver/orders/new?mealType=breakfast" className="quick-action">
|
||||
<div className="quick-action__icon">🌅</div>
|
||||
<div className="quick-action__label">New Breakfast</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Quick Actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Link href="/caregiver/orders/new?mealType=breakfast">
|
||||
<Card className="cursor-pointer transition-colors hover:bg-accent">
|
||||
<CardContent className="flex flex-col items-center justify-center p-6">
|
||||
<Sunrise className="h-8 w-8 mb-2 text-orange-500" />
|
||||
<span className="font-medium">New Breakfast</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<Link href="/caregiver/orders/new?mealType=lunch" className="quick-action">
|
||||
<div className="quick-action__icon">☀️</div>
|
||||
<div className="quick-action__label">New Lunch</div>
|
||||
<Link href="/caregiver/orders/new?mealType=lunch">
|
||||
<Card className="cursor-pointer transition-colors hover:bg-accent">
|
||||
<CardContent className="flex flex-col items-center justify-center p-6">
|
||||
<Sun className="h-8 w-8 mb-2 text-yellow-500" />
|
||||
<span className="font-medium">New Lunch</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<Link href="/caregiver/orders/new?mealType=dinner" className="quick-action">
|
||||
<div className="quick-action__icon">🌙</div>
|
||||
<div className="quick-action__label">New Dinner</div>
|
||||
<Link href="/caregiver/orders/new?mealType=dinner">
|
||||
<Card className="cursor-pointer transition-colors hover:bg-accent">
|
||||
<CardContent className="flex flex-col items-center justify-center p-6">
|
||||
<Moon className="h-8 w-8 mb-2 text-indigo-500" />
|
||||
<span className="font-medium">New Dinner</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<Link href="/caregiver/orders" className="quick-action">
|
||||
<div className="quick-action__icon">📋</div>
|
||||
<div className="quick-action__label">View Orders</div>
|
||||
<Link href="/caregiver/orders">
|
||||
<Card className="cursor-pointer transition-colors hover:bg-accent">
|
||||
<CardContent className="flex flex-col items-center justify-center p-6">
|
||||
<ClipboardList className="h-8 w-8 mb-2 text-muted-foreground" />
|
||||
<span className="font-medium">View Orders</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<Link href="/caregiver/residents" className="quick-action">
|
||||
<div className="quick-action__icon">👥</div>
|
||||
<div className="quick-action__label">Residents</div>
|
||||
<Link href="/caregiver/residents">
|
||||
<Card className="cursor-pointer transition-colors hover:bg-accent">
|
||||
<CardContent className="flex flex-col items-center justify-center p-6">
|
||||
<Users className="h-8 w-8 mb-2 text-muted-foreground" />
|
||||
<span className="font-medium">Residents</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
<Link href="/admin" className="quick-action">
|
||||
<div className="quick-action__icon">⚙️</div>
|
||||
<div className="quick-action__label">Admin Panel</div>
|
||||
<Link href="/admin">
|
||||
<Card className="cursor-pointer transition-colors hover:bg-accent">
|
||||
<CardContent className="flex flex-col items-center justify-center p-6">
|
||||
<Settings className="h-8 w-8 mb-2 text-muted-foreground" />
|
||||
<span className="font-medium">Admin Panel</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
|
||||
export default function CaregiverLoginPage() {
|
||||
const router = useRouter()
|
||||
@@ -11,7 +18,6 @@ export default function CaregiverLoginPage() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [checking, setChecking] = useState(true)
|
||||
|
||||
// Check if already logged in
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
@@ -50,7 +56,6 @@ export default function CaregiverLoginPage() {
|
||||
throw new Error(data.errors?.[0]?.message || 'Login failed')
|
||||
}
|
||||
|
||||
// Check if user has caregiver or admin role
|
||||
const user = data.user
|
||||
const hasCaregiverRole =
|
||||
user?.roles?.includes('super-admin') ||
|
||||
@@ -60,7 +65,6 @@ export default function CaregiverLoginPage() {
|
||||
)
|
||||
|
||||
if (!hasCaregiverRole) {
|
||||
// Logout if not a caregiver
|
||||
await fetch('/api/users/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
@@ -78,31 +82,38 @@ export default function CaregiverLoginPage() {
|
||||
|
||||
if (checking) {
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="spinner" />
|
||||
<div className="min-h-screen flex items-center justify-center bg-muted/50">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-page__card">
|
||||
<div className="login-page__logo">
|
||||
<h1>Meal Planner</h1>
|
||||
<p>Caregiver Portal</p>
|
||||
<div className="min-h-screen flex items-center justify-center bg-muted/50 p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Meal Planner</h1>
|
||||
<p className="text-muted-foreground">Caregiver Portal</p>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="card__body">
|
||||
{error && <div className="message message--error">{error}</div>}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Login</CardTitle>
|
||||
<CardDescription>Enter your credentials to access the caregiver portal</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{error && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">Email</label>
|
||||
<input
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
type="email"
|
||||
id="email"
|
||||
className="input"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Enter your email"
|
||||
@@ -111,12 +122,11 @@ export default function CaregiverLoginPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
type="password"
|
||||
id="password"
|
||||
className="input"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Enter your password"
|
||||
@@ -125,16 +135,19 @@ export default function CaregiverLoginPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn--primary btn--block btn--large"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Logging in...
|
||||
</>
|
||||
) : (
|
||||
'Login'
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,6 +3,27 @@
|
||||
import React, { useState, useEffect, Suspense } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Loader2,
|
||||
Search,
|
||||
Sunrise,
|
||||
Sun,
|
||||
Moon,
|
||||
AlertTriangle,
|
||||
Check,
|
||||
} from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Resident {
|
||||
id: number
|
||||
@@ -119,6 +140,33 @@ const defaultDinner: DinnerOptions = {
|
||||
additions: { sugar: false, sweetener: false },
|
||||
}
|
||||
|
||||
function CheckboxOption({
|
||||
id,
|
||||
label,
|
||||
checked,
|
||||
onCheckedChange,
|
||||
}: {
|
||||
id: string
|
||||
label: string
|
||||
checked: boolean
|
||||
onCheckedChange: (checked: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center space-x-3 rounded-lg border p-3 cursor-pointer transition-colors',
|
||||
checked ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted/50'
|
||||
)}
|
||||
onClick={() => onCheckedChange(!checked)}
|
||||
>
|
||||
<Checkbox id={id} checked={checked} onCheckedChange={onCheckedChange} />
|
||||
<Label htmlFor={id} className="cursor-pointer flex-1">
|
||||
{label}
|
||||
</Label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NewOrderContent() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
@@ -217,289 +265,254 @@ function NewOrderContent() {
|
||||
}
|
||||
}
|
||||
|
||||
const renderCheckbox = (
|
||||
label: string,
|
||||
checked: boolean,
|
||||
onChange: (checked: boolean) => void,
|
||||
) => (
|
||||
<label className={`checkbox-item ${checked ? 'checkbox-item--checked' : ''}`}>
|
||||
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="spinner" />
|
||||
<div className="min-h-screen flex items-center justify-center bg-muted/50">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="header">
|
||||
<div className="header__content">
|
||||
<Link href="/caregiver/dashboard" className="btn btn--secondary">
|
||||
← Back
|
||||
</Link>
|
||||
<h1 className="header__title">New Meal Order</h1>
|
||||
<div className="min-h-screen bg-muted/50">
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-16 items-center">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/caregiver/dashboard">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Link>
|
||||
</Button>
|
||||
<h1 className="ml-4 text-xl font-semibold">New Meal Order</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container">
|
||||
<main className="container py-6">
|
||||
{/* Progress Steps */}
|
||||
<div className="steps">
|
||||
<div className={`steps__step ${step >= 1 ? 'steps__step--active' : ''} ${step > 1 ? 'steps__step--completed' : ''}`} />
|
||||
<div className={`steps__step ${step >= 2 ? 'steps__step--active' : ''} ${step > 2 ? 'steps__step--completed' : ''}`} />
|
||||
<div className={`steps__step ${step >= 3 ? 'steps__step--active' : ''} ${step > 3 ? 'steps__step--completed' : ''}`} />
|
||||
<div className={`steps__step ${step >= 4 ? 'steps__step--active' : ''}`} />
|
||||
<div className="flex gap-2 mb-6">
|
||||
{[1, 2, 3, 4].map((s) => (
|
||||
<div
|
||||
key={s}
|
||||
className={cn(
|
||||
'h-2 flex-1 rounded-full transition-colors',
|
||||
step >= s ? (step > s ? 'bg-green-500' : 'bg-primary') : 'bg-muted'
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && <div className="message message--error">{error}</div>}
|
||||
{error && (
|
||||
<Alert variant="destructive" className="mb-6">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Step 1: Select Meal Type */}
|
||||
{step === 1 && (
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<h2>Step 1: Select Meal Type</h2>
|
||||
</div>
|
||||
<div className="card__body">
|
||||
<div className="form-group">
|
||||
<label htmlFor="date">Date</label>
|
||||
<input
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Step 1: Select Meal Type</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="date">Date</Label>
|
||||
<Input
|
||||
type="date"
|
||||
id="date"
|
||||
className="input"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="meal-type-grid">
|
||||
<button
|
||||
type="button"
|
||||
className={`meal-type-btn ${mealType === 'breakfast' ? 'meal-type-btn--selected' : ''}`}
|
||||
onClick={() => setMealType('breakfast')}
|
||||
>
|
||||
<div className="meal-type-btn__icon">🌅</div>
|
||||
<div className="meal-type-btn__label">Breakfast</div>
|
||||
<div className="meal-type-btn__sublabel">Frühstück</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`meal-type-btn ${mealType === 'lunch' ? 'meal-type-btn--selected' : ''}`}
|
||||
onClick={() => setMealType('lunch')}
|
||||
>
|
||||
<div className="meal-type-btn__icon">☀️</div>
|
||||
<div className="meal-type-btn__label">Lunch</div>
|
||||
<div className="meal-type-btn__sublabel">Mittagessen</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`meal-type-btn ${mealType === 'dinner' ? 'meal-type-btn--selected' : ''}`}
|
||||
onClick={() => setMealType('dinner')}
|
||||
>
|
||||
<div className="meal-type-btn__icon">🌙</div>
|
||||
<div className="meal-type-btn__label">Dinner</div>
|
||||
<div className="meal-type-btn__sublabel">Abendessen</div>
|
||||
</button>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
{[
|
||||
{ type: 'breakfast' as MealType, icon: Sunrise, label: 'Breakfast', sublabel: 'Frühstück', color: 'text-orange-500' },
|
||||
{ type: 'lunch' as MealType, icon: Sun, label: 'Lunch', sublabel: 'Mittagessen', color: 'text-yellow-500' },
|
||||
{ type: 'dinner' as MealType, icon: Moon, label: 'Dinner', sublabel: 'Abendessen', color: 'text-indigo-500' },
|
||||
].map(({ type, icon: Icon, label, sublabel, color }) => (
|
||||
<Card
|
||||
key={type}
|
||||
className={cn(
|
||||
'cursor-pointer transition-colors',
|
||||
mealType === type ? 'border-primary bg-primary/5' : 'hover:bg-muted/50'
|
||||
)}
|
||||
onClick={() => setMealType(type)}
|
||||
>
|
||||
<CardContent className="flex flex-col items-center justify-center p-6">
|
||||
<Icon className={cn('h-10 w-10 mb-2', color)} />
|
||||
<span className="font-semibold">{label}</span>
|
||||
<span className="text-sm text-muted-foreground">{sublabel}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '1.5rem' }}>
|
||||
<button
|
||||
className="btn btn--primary btn--block btn--large"
|
||||
disabled={!mealType}
|
||||
onClick={() => setStep(2)}
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={!mealType}
|
||||
onClick={() => setStep(2)}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Step 2: Select Resident */}
|
||||
{step === 2 && (
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<h2>Step 2: Select Resident</h2>
|
||||
</div>
|
||||
<div className="card__body">
|
||||
<div className="form-group">
|
||||
<div className="search-box">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name or room..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Step 2: Select Resident</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search by name or room..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="resident-list">
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 max-h-96 overflow-y-auto">
|
||||
{filteredResidents.map((resident) => (
|
||||
<div
|
||||
<Card
|
||||
key={resident.id}
|
||||
className={`resident-card ${selectedResident?.id === resident.id ? 'resident-card--selected' : ''}`}
|
||||
className={cn(
|
||||
'cursor-pointer transition-colors',
|
||||
selectedResident?.id === resident.id ? 'border-primary bg-primary/5' : 'hover:bg-muted/50'
|
||||
)}
|
||||
onClick={() => setSelectedResident(resident)}
|
||||
>
|
||||
<div className="resident-card__name">{resident.name}</div>
|
||||
<div className="resident-card__details">
|
||||
<span>Room {resident.room}</span>
|
||||
{resident.table && <span>Table {resident.table}</span>}
|
||||
{resident.station && <span>{resident.station}</span>}
|
||||
</div>
|
||||
{resident.highCaloric && (
|
||||
<div className="resident-card__badge">High Caloric</div>
|
||||
)}
|
||||
</div>
|
||||
<CardContent className="p-4">
|
||||
<div className="font-semibold">{resident.name}</div>
|
||||
<div className="flex gap-2 text-sm text-muted-foreground mt-1">
|
||||
<span>Room {resident.room}</span>
|
||||
{resident.table && <span>Table {resident.table}</span>}
|
||||
</div>
|
||||
{resident.highCaloric && (
|
||||
<Badge variant="secondary" className="mt-2 bg-yellow-100 text-yellow-800">
|
||||
High Caloric
|
||||
</Badge>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '1.5rem', display: 'flex', gap: '1rem' }}>
|
||||
<button className="btn btn--secondary" onClick={() => setStep(1)}>
|
||||
<div className="flex gap-4 pt-4">
|
||||
<Button variant="outline" onClick={() => setStep(1)}>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--primary btn--block btn--large"
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
size="lg"
|
||||
disabled={!selectedResident}
|
||||
onClick={() => setStep(3)}
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Step 3: Meal Options */}
|
||||
{step === 3 && (
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<h2>Step 3: {mealType && getMealTypeLabel(mealType)} Options</h2>
|
||||
</div>
|
||||
<div className="card__body">
|
||||
{/* Show resident notes if any */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Step 3: {mealType && getMealTypeLabel(mealType)} Options</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{(selectedResident?.aversions || selectedResident?.notes) && (
|
||||
<div className="message message--warning">
|
||||
<strong>Notes for {selectedResident?.name}:</strong>
|
||||
{selectedResident?.aversions && <div>Aversions: {selectedResident.aversions}</div>}
|
||||
{selectedResident?.notes && <div>{selectedResident.notes}</div>}
|
||||
</div>
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>Notes for {selectedResident?.name}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{selectedResident?.aversions && <div>Aversions: {selectedResident.aversions}</div>}
|
||||
{selectedResident?.notes && <div>{selectedResident.notes}</div>}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* BREAKFAST OPTIONS */}
|
||||
{mealType === 'breakfast' && (
|
||||
<>
|
||||
<div className="section">
|
||||
<h3 className="section__title">General</h3>
|
||||
<div className="checkbox-group">
|
||||
{renderCheckbox('According to Plan (lt. Plan)', breakfast.accordingToPlan, (v) =>
|
||||
setBreakfast({ ...breakfast, accordingToPlan: v }),
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">General</h3>
|
||||
<CheckboxOption
|
||||
id="accordingToPlan"
|
||||
label="According to Plan (lt. Plan)"
|
||||
checked={breakfast.accordingToPlan}
|
||||
onCheckedChange={(v) => setBreakfast({ ...breakfast, accordingToPlan: v })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Bread (Brot)</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<CheckboxOption id="breadRoll" label="Bread Roll (Brötchen)" checked={breakfast.bread.breadRoll} onCheckedChange={(v) => setBreakfast({ ...breakfast, bread: { ...breakfast.bread, breadRoll: v } })} />
|
||||
<CheckboxOption id="wholeGrainRoll" label="Whole Grain Roll (Vollkornbrötchen)" checked={breakfast.bread.wholeGrainRoll} onCheckedChange={(v) => setBreakfast({ ...breakfast, bread: { ...breakfast.bread, wholeGrainRoll: v } })} />
|
||||
<CheckboxOption id="greyBread" label="Grey Bread (Graubrot)" checked={breakfast.bread.greyBread} onCheckedChange={(v) => setBreakfast({ ...breakfast, bread: { ...breakfast.bread, greyBread: v } })} />
|
||||
<CheckboxOption id="wholeGrainBread" label="Whole Grain Bread (Vollkornbrot)" checked={breakfast.bread.wholeGrainBread} onCheckedChange={(v) => setBreakfast({ ...breakfast, bread: { ...breakfast.bread, wholeGrainBread: v } })} />
|
||||
<CheckboxOption id="whiteBread" label="White Bread (Weißbrot)" checked={breakfast.bread.whiteBread} onCheckedChange={(v) => setBreakfast({ ...breakfast, bread: { ...breakfast.bread, whiteBread: v } })} />
|
||||
<CheckboxOption id="crispbread" label="Crispbread (Knäckebrot)" checked={breakfast.bread.crispbread} onCheckedChange={(v) => setBreakfast({ ...breakfast, bread: { ...breakfast.bread, crispbread: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Bread (Brot)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Bread Roll (Brötchen)', breakfast.bread.breadRoll, (v) =>
|
||||
setBreakfast({ ...breakfast, bread: { ...breakfast.bread, breadRoll: v } }),
|
||||
)}
|
||||
{renderCheckbox('Whole Grain Roll (Vollkornbrötchen)', breakfast.bread.wholeGrainRoll, (v) =>
|
||||
setBreakfast({ ...breakfast, bread: { ...breakfast.bread, wholeGrainRoll: v } }),
|
||||
)}
|
||||
{renderCheckbox('Grey Bread (Graubrot)', breakfast.bread.greyBread, (v) =>
|
||||
setBreakfast({ ...breakfast, bread: { ...breakfast.bread, greyBread: v } }),
|
||||
)}
|
||||
{renderCheckbox('Whole Grain Bread (Vollkornbrot)', breakfast.bread.wholeGrainBread, (v) =>
|
||||
setBreakfast({ ...breakfast, bread: { ...breakfast.bread, wholeGrainBread: v } }),
|
||||
)}
|
||||
{renderCheckbox('White Bread (Weißbrot)', breakfast.bread.whiteBread, (v) =>
|
||||
setBreakfast({ ...breakfast, bread: { ...breakfast.bread, whiteBread: v } }),
|
||||
)}
|
||||
{renderCheckbox('Crispbread (Knäckebrot)', breakfast.bread.crispbread, (v) =>
|
||||
setBreakfast({ ...breakfast, bread: { ...breakfast.bread, crispbread: v } }),
|
||||
)}
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Preparation</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
<CheckboxOption id="porridge" label="Porridge (Brei)" checked={breakfast.porridge} onCheckedChange={(v) => setBreakfast({ ...breakfast, porridge: v })} />
|
||||
<CheckboxOption id="sliced" label="Sliced (geschnitten)" checked={breakfast.preparation.sliced} onCheckedChange={(v) => setBreakfast({ ...breakfast, preparation: { ...breakfast.preparation, sliced: v } })} />
|
||||
<CheckboxOption id="spread" label="Spread (geschmiert)" checked={breakfast.preparation.spread} onCheckedChange={(v) => setBreakfast({ ...breakfast, preparation: { ...breakfast.preparation, spread: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Preparation</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Porridge (Brei)', breakfast.porridge, (v) =>
|
||||
setBreakfast({ ...breakfast, porridge: v }),
|
||||
)}
|
||||
{renderCheckbox('Sliced (geschnitten)', breakfast.preparation.sliced, (v) =>
|
||||
setBreakfast({ ...breakfast, preparation: { ...breakfast.preparation, sliced: v } }),
|
||||
)}
|
||||
{renderCheckbox('Spread (geschmiert)', breakfast.preparation.spread, (v) =>
|
||||
setBreakfast({ ...breakfast, preparation: { ...breakfast.preparation, spread: v } }),
|
||||
)}
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Spreads (Aufstrich)</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<CheckboxOption id="butter" label="Butter" checked={breakfast.spreads.butter} onCheckedChange={(v) => setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, butter: v } })} />
|
||||
<CheckboxOption id="margarine" label="Margarine" checked={breakfast.spreads.margarine} onCheckedChange={(v) => setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, margarine: v } })} />
|
||||
<CheckboxOption id="jam" label="Jam (Konfitüre)" checked={breakfast.spreads.jam} onCheckedChange={(v) => setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, jam: v } })} />
|
||||
<CheckboxOption id="diabeticJam" label="Diabetic Jam (Diab. Konfitüre)" checked={breakfast.spreads.diabeticJam} onCheckedChange={(v) => setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, diabeticJam: v } })} />
|
||||
<CheckboxOption id="honey" label="Honey (Honig)" checked={breakfast.spreads.honey} onCheckedChange={(v) => setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, honey: v } })} />
|
||||
<CheckboxOption id="cheese" label="Cheese (Käse)" checked={breakfast.spreads.cheese} onCheckedChange={(v) => setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, cheese: v } })} />
|
||||
<CheckboxOption id="quark" label="Quark" checked={breakfast.spreads.quark} onCheckedChange={(v) => setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, quark: v } })} />
|
||||
<CheckboxOption id="sausage" label="Sausage (Wurst)" checked={breakfast.spreads.sausage} onCheckedChange={(v) => setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, sausage: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Spreads (Aufstrich)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Butter', breakfast.spreads.butter, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, butter: v } }),
|
||||
)}
|
||||
{renderCheckbox('Margarine', breakfast.spreads.margarine, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, margarine: v } }),
|
||||
)}
|
||||
{renderCheckbox('Jam (Konfitüre)', breakfast.spreads.jam, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, jam: v } }),
|
||||
)}
|
||||
{renderCheckbox('Diabetic Jam (Diab. Konfitüre)', breakfast.spreads.diabeticJam, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, diabeticJam: v } }),
|
||||
)}
|
||||
{renderCheckbox('Honey (Honig)', breakfast.spreads.honey, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, honey: v } }),
|
||||
)}
|
||||
{renderCheckbox('Cheese (Käse)', breakfast.spreads.cheese, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, cheese: v } }),
|
||||
)}
|
||||
{renderCheckbox('Quark', breakfast.spreads.quark, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, quark: v } }),
|
||||
)}
|
||||
{renderCheckbox('Sausage (Wurst)', breakfast.spreads.sausage, (v) =>
|
||||
setBreakfast({ ...breakfast, spreads: { ...breakfast.spreads, sausage: v } }),
|
||||
)}
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Beverages (Getränke)</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<CheckboxOption id="coffee" label="Coffee (Kaffee)" checked={breakfast.beverages.coffee} onCheckedChange={(v) => setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, coffee: v } })} />
|
||||
<CheckboxOption id="tea" label="Tea (Tee)" checked={breakfast.beverages.tea} onCheckedChange={(v) => setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, tea: v } })} />
|
||||
<CheckboxOption id="hotMilk" label="Hot Milk (Milch heiß)" checked={breakfast.beverages.hotMilk} onCheckedChange={(v) => setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, hotMilk: v } })} />
|
||||
<CheckboxOption id="coldMilk" label="Cold Milk (Milch kalt)" checked={breakfast.beverages.coldMilk} onCheckedChange={(v) => setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, coldMilk: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Beverages (Getränke)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Coffee (Kaffee)', breakfast.beverages.coffee, (v) =>
|
||||
setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, coffee: v } }),
|
||||
)}
|
||||
{renderCheckbox('Tea (Tee)', breakfast.beverages.tea, (v) =>
|
||||
setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, tea: v } }),
|
||||
)}
|
||||
{renderCheckbox('Hot Milk (Milch heiß)', breakfast.beverages.hotMilk, (v) =>
|
||||
setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, hotMilk: v } }),
|
||||
)}
|
||||
{renderCheckbox('Cold Milk (Milch kalt)', breakfast.beverages.coldMilk, (v) =>
|
||||
setBreakfast({ ...breakfast, beverages: { ...breakfast.beverages, coldMilk: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Additions (Zusätze)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-3">
|
||||
{renderCheckbox('Sugar (Zucker)', breakfast.additions.sugar, (v) =>
|
||||
setBreakfast({ ...breakfast, additions: { ...breakfast.additions, sugar: v } }),
|
||||
)}
|
||||
{renderCheckbox('Sweetener (Süßstoff)', breakfast.additions.sweetener, (v) =>
|
||||
setBreakfast({ ...breakfast, additions: { ...breakfast.additions, sweetener: v } }),
|
||||
)}
|
||||
{renderCheckbox('Coffee Creamer (Kaffeesahne)', breakfast.additions.coffeeCreamer, (v) =>
|
||||
setBreakfast({ ...breakfast, additions: { ...breakfast.additions, coffeeCreamer: v } }),
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Additions (Zusätze)</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
<CheckboxOption id="sugar" label="Sugar (Zucker)" checked={breakfast.additions.sugar} onCheckedChange={(v) => setBreakfast({ ...breakfast, additions: { ...breakfast.additions, sugar: v } })} />
|
||||
<CheckboxOption id="sweetener" label="Sweetener (Süßstoff)" checked={breakfast.additions.sweetener} onCheckedChange={(v) => setBreakfast({ ...breakfast, additions: { ...breakfast.additions, sweetener: v } })} />
|
||||
<CheckboxOption id="coffeeCreamer" label="Coffee Creamer (Kaffeesahne)" checked={breakfast.additions.coffeeCreamer} onCheckedChange={(v) => setBreakfast({ ...breakfast, additions: { ...breakfast.additions, coffeeCreamer: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -508,83 +521,63 @@ function NewOrderContent() {
|
||||
{/* LUNCH OPTIONS */}
|
||||
{mealType === 'lunch' && (
|
||||
<>
|
||||
<div className="section">
|
||||
<h3 className="section__title">Portion Size</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-3">
|
||||
<label
|
||||
className={`checkbox-item ${lunch.portionSize === 'small' ? 'checkbox-item--checked' : ''}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="portionSize"
|
||||
checked={lunch.portionSize === 'small'}
|
||||
onChange={() => setLunch({ ...lunch, portionSize: 'small' })}
|
||||
/>
|
||||
<span>Small (Kleine)</span>
|
||||
</label>
|
||||
<label
|
||||
className={`checkbox-item ${lunch.portionSize === 'large' ? 'checkbox-item--checked' : ''}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="portionSize"
|
||||
checked={lunch.portionSize === 'large'}
|
||||
onChange={() => setLunch({ ...lunch, portionSize: 'large' })}
|
||||
/>
|
||||
<span>Large (Große)</span>
|
||||
</label>
|
||||
<label
|
||||
className={`checkbox-item ${lunch.portionSize === 'vegetarian' ? 'checkbox-item--checked' : ''}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="portionSize"
|
||||
checked={lunch.portionSize === 'vegetarian'}
|
||||
onChange={() => setLunch({ ...lunch, portionSize: 'vegetarian' })}
|
||||
/>
|
||||
<span>Vegetarian</span>
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Portion Size</h3>
|
||||
<RadioGroup
|
||||
value={lunch.portionSize}
|
||||
onValueChange={(v) => setLunch({ ...lunch, portionSize: v as 'small' | 'large' | 'vegetarian' })}
|
||||
className="grid gap-2 sm:grid-cols-3"
|
||||
>
|
||||
{[
|
||||
{ value: 'small', label: 'Small (Kleine)' },
|
||||
{ value: 'large', label: 'Large (Große)' },
|
||||
{ value: 'vegetarian', label: 'Vegetarian' },
|
||||
].map(({ value, label }) => (
|
||||
<div
|
||||
key={value}
|
||||
className={cn(
|
||||
'flex items-center space-x-3 rounded-lg border p-3 cursor-pointer transition-colors',
|
||||
lunch.portionSize === value ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted/50'
|
||||
)}
|
||||
onClick={() => setLunch({ ...lunch, portionSize: value as 'small' | 'large' | 'vegetarian' })}
|
||||
>
|
||||
<RadioGroupItem value={value} id={value} />
|
||||
<Label htmlFor={value} className="cursor-pointer">{label}</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Meal Options</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<CheckboxOption id="soup" label="Soup (Suppe)" checked={lunch.soup} onCheckedChange={(v) => setLunch({ ...lunch, soup: v })} />
|
||||
<CheckboxOption id="dessert" label="Dessert" checked={lunch.dessert} onCheckedChange={(v) => setLunch({ ...lunch, dessert: v })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Meal Options</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Soup (Suppe)', lunch.soup, (v) => setLunch({ ...lunch, soup: v }))}
|
||||
{renderCheckbox('Dessert', lunch.dessert, (v) => setLunch({ ...lunch, dessert: v }))}
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Special Preparations</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<CheckboxOption id="pureedFood" label="Pureed Food (passierte Kost)" checked={lunch.specialPreparations.pureedFood} onCheckedChange={(v) => setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, pureedFood: v } })} />
|
||||
<CheckboxOption id="pureedMeat" label="Pureed Meat (passiertes Fleisch)" checked={lunch.specialPreparations.pureedMeat} onCheckedChange={(v) => setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, pureedMeat: v } })} />
|
||||
<CheckboxOption id="slicedMeat" label="Sliced Meat (geschnittenes Fleisch)" checked={lunch.specialPreparations.slicedMeat} onCheckedChange={(v) => setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, slicedMeat: v } })} />
|
||||
<CheckboxOption id="mashedPotatoes" label="Mashed Potatoes (Kartoffelbrei)" checked={lunch.specialPreparations.mashedPotatoes} onCheckedChange={(v) => setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, mashedPotatoes: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Special Preparations</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Pureed Food (passierte Kost)', lunch.specialPreparations.pureedFood, (v) =>
|
||||
setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, pureedFood: v } }),
|
||||
)}
|
||||
{renderCheckbox('Pureed Meat (passiertes Fleisch)', lunch.specialPreparations.pureedMeat, (v) =>
|
||||
setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, pureedMeat: v } }),
|
||||
)}
|
||||
{renderCheckbox('Sliced Meat (geschnittenes Fleisch)', lunch.specialPreparations.slicedMeat, (v) =>
|
||||
setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, slicedMeat: v } }),
|
||||
)}
|
||||
{renderCheckbox('Mashed Potatoes (Kartoffelbrei)', lunch.specialPreparations.mashedPotatoes, (v) =>
|
||||
setLunch({ ...lunch, specialPreparations: { ...lunch.specialPreparations, mashedPotatoes: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Restrictions</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-3">
|
||||
{renderCheckbox('No Fish (ohne Fisch)', lunch.restrictions.noFish, (v) =>
|
||||
setLunch({ ...lunch, restrictions: { ...lunch.restrictions, noFish: v } }),
|
||||
)}
|
||||
{renderCheckbox('Finger Food', lunch.restrictions.fingerFood, (v) =>
|
||||
setLunch({ ...lunch, restrictions: { ...lunch.restrictions, fingerFood: v } }),
|
||||
)}
|
||||
{renderCheckbox('Only Sweet (nur süß)', lunch.restrictions.onlySweet, (v) =>
|
||||
setLunch({ ...lunch, restrictions: { ...lunch.restrictions, onlySweet: v } }),
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Restrictions</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
<CheckboxOption id="noFish" label="No Fish (ohne Fisch)" checked={lunch.restrictions.noFish} onCheckedChange={(v) => setLunch({ ...lunch, restrictions: { ...lunch.restrictions, noFish: v } })} />
|
||||
<CheckboxOption id="fingerFood" label="Finger Food" checked={lunch.restrictions.fingerFood} onCheckedChange={(v) => setLunch({ ...lunch, restrictions: { ...lunch.restrictions, fingerFood: v } })} />
|
||||
<CheckboxOption id="onlySweet" label="Only Sweet (nur süß)" checked={lunch.restrictions.onlySweet} onCheckedChange={(v) => setLunch({ ...lunch, restrictions: { ...lunch.restrictions, onlySweet: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -593,163 +586,161 @@ function NewOrderContent() {
|
||||
{/* DINNER OPTIONS */}
|
||||
{mealType === 'dinner' && (
|
||||
<>
|
||||
<div className="section">
|
||||
<h3 className="section__title">General</h3>
|
||||
<div className="checkbox-group">
|
||||
{renderCheckbox('According to Plan (lt. Plan)', dinner.accordingToPlan, (v) =>
|
||||
setDinner({ ...dinner, accordingToPlan: v }),
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">General</h3>
|
||||
<CheckboxOption
|
||||
id="dinnerAccordingToPlan"
|
||||
label="According to Plan (lt. Plan)"
|
||||
checked={dinner.accordingToPlan}
|
||||
onCheckedChange={(v) => setDinner({ ...dinner, accordingToPlan: v })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Bread (Brot)</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<CheckboxOption id="dinnerGreyBread" label="Grey Bread (Graubrot)" checked={dinner.bread.greyBread} onCheckedChange={(v) => setDinner({ ...dinner, bread: { ...dinner.bread, greyBread: v } })} />
|
||||
<CheckboxOption id="dinnerWholeGrainBread" label="Whole Grain Bread (Vollkornbrot)" checked={dinner.bread.wholeGrainBread} onCheckedChange={(v) => setDinner({ ...dinner, bread: { ...dinner.bread, wholeGrainBread: v } })} />
|
||||
<CheckboxOption id="dinnerWhiteBread" label="White Bread (Weißbrot)" checked={dinner.bread.whiteBread} onCheckedChange={(v) => setDinner({ ...dinner, bread: { ...dinner.bread, whiteBread: v } })} />
|
||||
<CheckboxOption id="dinnerCrispbread" label="Crispbread (Knäckebrot)" checked={dinner.bread.crispbread} onCheckedChange={(v) => setDinner({ ...dinner, bread: { ...dinner.bread, crispbread: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Bread (Brot)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Grey Bread (Graubrot)', dinner.bread.greyBread, (v) =>
|
||||
setDinner({ ...dinner, bread: { ...dinner.bread, greyBread: v } }),
|
||||
)}
|
||||
{renderCheckbox('Whole Grain Bread (Vollkornbrot)', dinner.bread.wholeGrainBread, (v) =>
|
||||
setDinner({ ...dinner, bread: { ...dinner.bread, wholeGrainBread: v } }),
|
||||
)}
|
||||
{renderCheckbox('White Bread (Weißbrot)', dinner.bread.whiteBread, (v) =>
|
||||
setDinner({ ...dinner, bread: { ...dinner.bread, whiteBread: v } }),
|
||||
)}
|
||||
{renderCheckbox('Crispbread (Knäckebrot)', dinner.bread.crispbread, (v) =>
|
||||
setDinner({ ...dinner, bread: { ...dinner.bread, crispbread: v } }),
|
||||
)}
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Preparation</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<CheckboxOption id="dinnerSpread" label="Spread (geschmiert)" checked={dinner.preparation.spread} onCheckedChange={(v) => setDinner({ ...dinner, preparation: { ...dinner.preparation, spread: v } })} />
|
||||
<CheckboxOption id="dinnerSliced" label="Sliced (geschnitten)" checked={dinner.preparation.sliced} onCheckedChange={(v) => setDinner({ ...dinner, preparation: { ...dinner.preparation, sliced: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Preparation</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Spread (geschmiert)', dinner.preparation.spread, (v) =>
|
||||
setDinner({ ...dinner, preparation: { ...dinner.preparation, spread: v } }),
|
||||
)}
|
||||
{renderCheckbox('Sliced (geschnitten)', dinner.preparation.sliced, (v) =>
|
||||
setDinner({ ...dinner, preparation: { ...dinner.preparation, sliced: v } }),
|
||||
)}
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Spreads (Aufstrich)</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<CheckboxOption id="dinnerButter" label="Butter" checked={dinner.spreads.butter} onCheckedChange={(v) => setDinner({ ...dinner, spreads: { ...dinner.spreads, butter: v } })} />
|
||||
<CheckboxOption id="dinnerMargarine" label="Margarine" checked={dinner.spreads.margarine} onCheckedChange={(v) => setDinner({ ...dinner, spreads: { ...dinner.spreads, margarine: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Spreads (Aufstrich)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Butter', dinner.spreads.butter, (v) =>
|
||||
setDinner({ ...dinner, spreads: { ...dinner.spreads, butter: v } }),
|
||||
)}
|
||||
{renderCheckbox('Margarine', dinner.spreads.margarine, (v) =>
|
||||
setDinner({ ...dinner, spreads: { ...dinner.spreads, margarine: v } }),
|
||||
)}
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Additional Items</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
<CheckboxOption id="dinnerSoup" label="Soup (Suppe)" checked={dinner.soup} onCheckedChange={(v) => setDinner({ ...dinner, soup: v })} />
|
||||
<CheckboxOption id="dinnerPorridge" label="Porridge (Brei)" checked={dinner.porridge} onCheckedChange={(v) => setDinner({ ...dinner, porridge: v })} />
|
||||
<CheckboxOption id="dinnerNoFish" label="No Fish (ohne Fisch)" checked={dinner.noFish} onCheckedChange={(v) => setDinner({ ...dinner, noFish: v })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Additional Items</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-3">
|
||||
{renderCheckbox('Soup (Suppe)', dinner.soup, (v) => setDinner({ ...dinner, soup: v }))}
|
||||
{renderCheckbox('Porridge (Brei)', dinner.porridge, (v) =>
|
||||
setDinner({ ...dinner, porridge: v }),
|
||||
)}
|
||||
{renderCheckbox('No Fish (ohne Fisch)', dinner.noFish, (v) =>
|
||||
setDinner({ ...dinner, noFish: v }),
|
||||
)}
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Beverages (Getränke)</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<CheckboxOption id="dinnerTea" label="Tea (Tee)" checked={dinner.beverages.tea} onCheckedChange={(v) => setDinner({ ...dinner, beverages: { ...dinner.beverages, tea: v } })} />
|
||||
<CheckboxOption id="dinnerCocoa" label="Cocoa (Kakao)" checked={dinner.beverages.cocoa} onCheckedChange={(v) => setDinner({ ...dinner, beverages: { ...dinner.beverages, cocoa: v } })} />
|
||||
<CheckboxOption id="dinnerHotMilk" label="Hot Milk (Milch heiß)" checked={dinner.beverages.hotMilk} onCheckedChange={(v) => setDinner({ ...dinner, beverages: { ...dinner.beverages, hotMilk: v } })} />
|
||||
<CheckboxOption id="dinnerColdMilk" label="Cold Milk (Milch kalt)" checked={dinner.beverages.coldMilk} onCheckedChange={(v) => setDinner({ ...dinner, beverages: { ...dinner.beverages, coldMilk: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Beverages (Getränke)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Tea (Tee)', dinner.beverages.tea, (v) =>
|
||||
setDinner({ ...dinner, beverages: { ...dinner.beverages, tea: v } }),
|
||||
)}
|
||||
{renderCheckbox('Cocoa (Kakao)', dinner.beverages.cocoa, (v) =>
|
||||
setDinner({ ...dinner, beverages: { ...dinner.beverages, cocoa: v } }),
|
||||
)}
|
||||
{renderCheckbox('Hot Milk (Milch heiß)', dinner.beverages.hotMilk, (v) =>
|
||||
setDinner({ ...dinner, beverages: { ...dinner.beverages, hotMilk: v } }),
|
||||
)}
|
||||
{renderCheckbox('Cold Milk (Milch kalt)', dinner.beverages.coldMilk, (v) =>
|
||||
setDinner({ ...dinner, beverages: { ...dinner.beverages, coldMilk: v } }),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<div className="section">
|
||||
<h3 className="section__title">Additions (Zusätze)</h3>
|
||||
<div className="checkbox-group checkbox-group--cols-2">
|
||||
{renderCheckbox('Sugar (Zucker)', dinner.additions.sugar, (v) =>
|
||||
setDinner({ ...dinner, additions: { ...dinner.additions, sugar: v } }),
|
||||
)}
|
||||
{renderCheckbox('Sweetener (Süßstoff)', dinner.additions.sweetener, (v) =>
|
||||
setDinner({ ...dinner, additions: { ...dinner.additions, sweetener: v } }),
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-semibold">Additions (Zusätze)</h3>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
<CheckboxOption id="dinnerSugar" label="Sugar (Zucker)" checked={dinner.additions.sugar} onCheckedChange={(v) => setDinner({ ...dinner, additions: { ...dinner.additions, sugar: v } })} />
|
||||
<CheckboxOption id="dinnerSweetener" label="Sweetener (Süßstoff)" checked={dinner.additions.sweetener} onCheckedChange={(v) => setDinner({ ...dinner, additions: { ...dinner.additions, sweetener: v } })} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '1.5rem', display: 'flex', gap: '1rem' }}>
|
||||
<button className="btn btn--secondary" onClick={() => setStep(2)}>
|
||||
<div className="flex gap-4 pt-4">
|
||||
<Button variant="outline" onClick={() => setStep(2)}>
|
||||
Back
|
||||
</button>
|
||||
<button className="btn btn--primary btn--block btn--large" onClick={() => setStep(4)}>
|
||||
</Button>
|
||||
<Button className="flex-1" size="lg" onClick={() => setStep(4)}>
|
||||
Review Order
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Step 4: Review and Submit */}
|
||||
{step === 4 && (
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<h2>Step 4: Review & Submit</h2>
|
||||
</div>
|
||||
<div className="card__body">
|
||||
<div className="order-summary">
|
||||
<div className="order-summary__row">
|
||||
<span className="order-summary__label">Resident</span>
|
||||
<span className="order-summary__value">{selectedResident?.name}</span>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Step 4: Review & Submit</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="rounded-lg bg-muted p-4 space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Resident</span>
|
||||
<span className="font-medium">{selectedResident?.name}</span>
|
||||
</div>
|
||||
<div className="order-summary__row">
|
||||
<span className="order-summary__label">Room</span>
|
||||
<span className="order-summary__value">{selectedResident?.room}</span>
|
||||
<Separator />
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Room</span>
|
||||
<span className="font-medium">{selectedResident?.room}</span>
|
||||
</div>
|
||||
<div className="order-summary__row">
|
||||
<span className="order-summary__label">Date</span>
|
||||
<span className="order-summary__value">{date}</span>
|
||||
<Separator />
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Date</span>
|
||||
<span className="font-medium">{date}</span>
|
||||
</div>
|
||||
<div className="order-summary__row">
|
||||
<span className="order-summary__label">Meal Type</span>
|
||||
<span className="order-summary__value">{mealType && getMealTypeLabel(mealType)}</span>
|
||||
<Separator />
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Meal Type</span>
|
||||
<span className="font-medium">{mealType && getMealTypeLabel(mealType)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedResident?.highCaloric && (
|
||||
<div className="message message--warning" style={{ marginTop: '1rem' }}>
|
||||
<strong>Note:</strong> This resident requires high caloric meals.
|
||||
</div>
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>Note:</strong> This resident requires high caloric meals.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '1.5rem', display: 'flex', gap: '1rem' }}>
|
||||
<button className="btn btn--secondary" onClick={() => setStep(3)}>
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline" onClick={() => setStep(3)}>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--success btn--block btn--large"
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1 bg-green-600 hover:bg-green-700"
|
||||
size="lg"
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? 'Creating...' : 'Create Order'}
|
||||
</button>
|
||||
{submitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
Create Order
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -757,8 +748,8 @@ export default function NewOrderPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="login-page">
|
||||
<div className="spinner" />
|
||||
<div className="min-h-screen flex items-center justify-center bg-muted/50">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, Plus, Loader2 } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
|
||||
interface Resident {
|
||||
id: number
|
||||
@@ -69,7 +91,16 @@ export default function OrdersListPage() {
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
return <span className={`badge badge--${status}`}>{status.charAt(0).toUpperCase() + status.slice(1)}</span>
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200">Pending</Badge>
|
||||
case 'preparing':
|
||||
return <Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">Preparing</Badge>
|
||||
case 'prepared':
|
||||
return <Badge variant="outline" className="bg-green-50 text-green-700 border-green-200">Prepared</Badge>
|
||||
default:
|
||||
return <Badge variant="outline">{status}</Badge>
|
||||
}
|
||||
}
|
||||
|
||||
const getResidentName = (resident: Resident | number) => {
|
||||
@@ -80,89 +111,99 @@ export default function OrdersListPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="header">
|
||||
<div className="header__content">
|
||||
<Link href="/caregiver/dashboard" className="btn btn--secondary">
|
||||
← Back
|
||||
</Link>
|
||||
<h1 className="header__title">Meal Orders</h1>
|
||||
<Link href="/caregiver/orders/new" className="btn btn--primary">
|
||||
+ New Order
|
||||
</Link>
|
||||
<div className="min-h-screen bg-muted/50">
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-16 items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/caregiver/dashboard">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Link>
|
||||
</Button>
|
||||
<h1 className="text-xl font-semibold">Meal Orders</h1>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link href="/caregiver/orders/new">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Order
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container">
|
||||
<div className="card">
|
||||
<div className="card__header">
|
||||
<h2>Filter Orders</h2>
|
||||
</div>
|
||||
<div className="card__body">
|
||||
<div className="grid grid--2">
|
||||
<div className="form-group">
|
||||
<label htmlFor="date">Date</label>
|
||||
<input
|
||||
<main className="container py-6">
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Filter Orders</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="date">Date</Label>
|
||||
<Input
|
||||
type="date"
|
||||
id="date"
|
||||
className="input"
|
||||
value={dateFilter}
|
||||
onChange={(e) => setDateFilter(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="mealType">Meal Type</label>
|
||||
<select
|
||||
id="mealType"
|
||||
className="select"
|
||||
value={mealTypeFilter}
|
||||
onChange={(e) => setMealTypeFilter(e.target.value)}
|
||||
>
|
||||
<option value="all">All Types</option>
|
||||
<option value="breakfast">Breakfast</option>
|
||||
<option value="lunch">Lunch</option>
|
||||
<option value="dinner">Dinner</option>
|
||||
</select>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mealType">Meal Type</Label>
|
||||
<Select value={mealTypeFilter} onValueChange={setMealTypeFilter}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select meal type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Types</SelectItem>
|
||||
<SelectItem value="breakfast">Breakfast</SelectItem>
|
||||
<SelectItem value="lunch">Lunch</SelectItem>
|
||||
<SelectItem value="dinner">Dinner</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="card" style={{ marginTop: '1rem' }}>
|
||||
<div className="card__body" style={{ padding: 0, overflowX: 'auto' }}>
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
{loading ? (
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
<div className="spinner" style={{ margin: '0 auto' }} />
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : orders.length === 0 ? (
|
||||
<div style={{ padding: '2rem', textAlign: 'center', color: 'var(--gray-500)' }}>
|
||||
No orders found for the selected criteria.
|
||||
<div className="flex flex-col items-center justify-center p-8 text-center">
|
||||
<p className="text-muted-foreground">No orders found for the selected criteria.</p>
|
||||
<Button asChild className="mt-4">
|
||||
<Link href="/caregiver/orders/new">Create New Order</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Resident</th>
|
||||
<th>Date</th>
|
||||
<th>Meal</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Resident</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Meal</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{orders.map((order) => (
|
||||
<tr key={order.id}>
|
||||
<td>{getResidentName(order.resident)}</td>
|
||||
<td>{order.date}</td>
|
||||
<td>{getMealTypeLabel(order.mealType)}</td>
|
||||
<td>{getStatusBadge(order.status)}</td>
|
||||
</tr>
|
||||
<TableRow key={order.id}>
|
||||
<TableCell className="font-medium">{getResidentName(order.resident)}</TableCell>
|
||||
<TableCell>{order.date}</TableCell>
|
||||
<TableCell>{getMealTypeLabel(order.mealType)}</TableCell>
|
||||
<TableCell>{getStatusBadge(order.status)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft, Search, Loader2, Plus } from 'lucide-react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
interface Resident {
|
||||
id: number
|
||||
@@ -51,83 +57,93 @@ export default function ResidentsListPage() {
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="header">
|
||||
<div className="header__content">
|
||||
<Link href="/caregiver/dashboard" className="btn btn--secondary">
|
||||
← Back
|
||||
</Link>
|
||||
<h1 className="header__title">Residents</h1>
|
||||
<div className="min-h-screen bg-muted/50">
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-16 items-center">
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
<Link href="/caregiver/dashboard">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Link>
|
||||
</Button>
|
||||
<h1 className="ml-4 text-xl font-semibold">Residents</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container">
|
||||
<div className="page-title">
|
||||
<h1>Residents</h1>
|
||||
<p>View resident information and dietary requirements</p>
|
||||
<main className="container py-6">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-2xl font-bold tracking-tight">Residents</h2>
|
||||
<p className="text-muted-foreground">View resident information and dietary requirements</p>
|
||||
</div>
|
||||
|
||||
<div className="actions-bar">
|
||||
<div className="search-box">
|
||||
<input
|
||||
<div className="mb-6">
|
||||
<div className="relative max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search by name, room, or station..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
<div className="spinner" style={{ margin: '0 auto' }} />
|
||||
<div className="flex items-center justify-center p-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : filteredResidents.length === 0 ? (
|
||||
<div className="card">
|
||||
<div className="card__body" style={{ textAlign: 'center', color: 'var(--gray-500)' }}>
|
||||
No residents found.
|
||||
</div>
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center p-8 text-center">
|
||||
<p className="text-muted-foreground">No residents found.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="resident-list">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredResidents.map((resident) => (
|
||||
<div key={resident.id} className="resident-card">
|
||||
<div className="resident-card__name">{resident.name}</div>
|
||||
<div className="resident-card__details">
|
||||
<span>Room {resident.room}</span>
|
||||
{resident.table && <span>Table {resident.table}</span>}
|
||||
{resident.station && <span>{resident.station}</span>}
|
||||
</div>
|
||||
{resident.highCaloric && (
|
||||
<div className="resident-card__badge">High Caloric</div>
|
||||
)}
|
||||
{(resident.aversions || resident.notes) && (
|
||||
<div style={{ marginTop: '0.75rem', fontSize: '0.875rem', color: 'var(--gray-600)' }}>
|
||||
{resident.aversions && (
|
||||
<div>
|
||||
<strong>Aversions:</strong> {resident.aversions}
|
||||
</div>
|
||||
)}
|
||||
{resident.notes && (
|
||||
<div>
|
||||
<strong>Notes:</strong> {resident.notes}
|
||||
</div>
|
||||
)}
|
||||
<Card key={resident.id}>
|
||||
<CardContent className="p-4">
|
||||
<div className="font-semibold text-lg">{resident.name}</div>
|
||||
<div className="flex flex-wrap gap-2 text-sm text-muted-foreground mt-1">
|
||||
<span>Room {resident.room}</span>
|
||||
{resident.table && <span>Table {resident.table}</span>}
|
||||
{resident.station && <span>{resident.station}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<Link
|
||||
href={`/caregiver/orders/new?resident=${resident.id}`}
|
||||
className="btn btn--primary btn--block"
|
||||
>
|
||||
Create Order
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{resident.highCaloric && (
|
||||
<Badge variant="secondary" className="mt-3 bg-yellow-100 text-yellow-800">
|
||||
High Caloric
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{(resident.aversions || resident.notes) && (
|
||||
<div className="mt-3 text-sm text-muted-foreground space-y-1">
|
||||
{resident.aversions && (
|
||||
<div>
|
||||
<span className="font-medium">Aversions:</span> {resident.aversions}
|
||||
</div>
|
||||
)}
|
||||
{resident.notes && (
|
||||
<div>
|
||||
<span className="font-medium">Notes:</span> {resident.notes}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button asChild className="w-full mt-4">
|
||||
<Link href={`/caregiver/orders/new?resident=${resident.id}`}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Order
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,704 +0,0 @@
|
||||
/* Caregiver Tablet App Styles */
|
||||
:root {
|
||||
--primary: #2563eb;
|
||||
--primary-hover: #1d4ed8;
|
||||
--success: #16a34a;
|
||||
--success-hover: #15803d;
|
||||
--warning: #ca8a04;
|
||||
--error: #dc2626;
|
||||
--gray-50: #f9fafb;
|
||||
--gray-100: #f3f4f6;
|
||||
--gray-200: #e5e7eb;
|
||||
--gray-300: #d1d5db;
|
||||
--gray-400: #9ca3af;
|
||||
--gray-500: #6b7280;
|
||||
--gray-600: #4b5563;
|
||||
--gray-700: #374151;
|
||||
--gray-800: #1f2937;
|
||||
--gray-900: #111827;
|
||||
--radius: 12px;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.caregiver-app {
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--gray-100);
|
||||
color: var(--gray-900);
|
||||
min-height: 100vh;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
padding: 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
&__content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
&__user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
&__user-name {
|
||||
color: var(--gray-600);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Page Title */
|
||||
.page-title {
|
||||
margin: 1.5rem 0;
|
||||
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--gray-500);
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-height: 52px;
|
||||
text-decoration: none;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: var(--success-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: white;
|
||||
color: var(--gray-700);
|
||||
border: 1px solid var(--gray-300);
|
||||
|
||||
&:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: var(--error);
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
}
|
||||
|
||||
&--large {
|
||||
padding: 1.25rem 2rem;
|
||||
font-size: 1.125rem;
|
||||
min-height: 64px;
|
||||
}
|
||||
|
||||
&--block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
|
||||
&__header {
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
background: var(--gray-50);
|
||||
|
||||
h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-800);
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius);
|
||||
background: white;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius);
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Checkbox Group */
|
||||
.checkbox-group {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
|
||||
&--cols-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&--cols-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.875rem;
|
||||
background: var(--gray-50);
|
||||
border: 2px solid var(--gray-200);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-height: 52px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--gray-300);
|
||||
}
|
||||
|
||||
&--checked {
|
||||
background: #eff6ff;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
accent-color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
}
|
||||
|
||||
/* Resident Card */
|
||||
.resident-card {
|
||||
background: white;
|
||||
border: 2px solid var(--gray-200);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--gray-300);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background: #eff6ff;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__details {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
&__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: var(--warning);
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Resident List */
|
||||
.resident-list {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
}
|
||||
|
||||
/* Meal Type Buttons */
|
||||
.meal-type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.meal-type-btn {
|
||||
background: white;
|
||||
border: 2px solid var(--gray-200);
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--gray-300);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background: #eff6ff;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-800);
|
||||
}
|
||||
|
||||
&__sublabel {
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Section */
|
||||
.section {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
&__title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
}
|
||||
|
||||
/* Steps indicator */
|
||||
.steps {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
&__step {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: var(--gray-200);
|
||||
border-radius: 2px;
|
||||
transition: background 0.3s;
|
||||
|
||||
&--active {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
&--completed {
|
||||
background: var(--success);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Order Summary */
|
||||
.order-summary {
|
||||
background: var(--gray-50);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-weight: 500;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
}
|
||||
|
||||
/* Message boxes */
|
||||
.message {
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&--success {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
border: 1px solid #86efac;
|
||||
}
|
||||
|
||||
&--error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fca5a5;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border: 1px solid #fcd34d;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 3px solid var(--gray-200);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
||||
&--2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
&--3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Login page */
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
|
||||
&__card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
&__logo {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--gray-900);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--gray-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Dashboard stats */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem;
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
&__value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: var(--gray-500);
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Quick actions */
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.quick-action {
|
||||
background: white;
|
||||
border: 2px solid var(--gray-200);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-weight: 500;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.875rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--gray-50);
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
}
|
||||
|
||||
/* Badge */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
|
||||
&--pending {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
&--preparing {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
&--prepared {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
}
|
||||
|
||||
/* Actions bar */
|
||||
.actions-bar {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.search-box {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 400px;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem 0.75rem 2.5rem;
|
||||
border: 1px solid var(--gray-300);
|
||||
border-radius: var(--radius);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '🔍';
|
||||
position: absolute;
|
||||
left: 0.75rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.meal-type-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.checkbox-group--cols-2,
|
||||
.checkbox-group--cols-3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid--2,
|
||||
.grid--3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.resident-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
import '@/app/globals.css'
|
||||
|
||||
export const metadata = {
|
||||
description: 'Meal ordering for caregivers',
|
||||
@@ -10,8 +10,8 @@ export const metadata = {
|
||||
// eslint-disable-next-line no-restricted-exports
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html className="caregiver-app" lang="en">
|
||||
<body>{children}</body>
|
||||
<html lang="en">
|
||||
<body className="min-h-screen bg-background font-sans antialiased m-auto">{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { WatchTenantCollection as WatchTenantCollection_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
import { TenantField as TenantField_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
import { AssignTenantFieldTrigger as AssignTenantFieldTrigger_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
import { WatchTenantCollection as WatchTenantCollection_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
import { TenantSelector as TenantSelector_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
|
||||
import { TenantSelectionProvider as TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
|
||||
import { KitchenDashboard as KitchenDashboard_466f0c465119ff8e562eb80399daabc0 } from './views/KitchenDashboard'
|
||||
|
||||
export const importMap = {
|
||||
"@payloadcms/plugin-multi-tenant/client#WatchTenantCollection": WatchTenantCollection_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/plugin-multi-tenant/client#TenantField": TenantField_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/plugin-multi-tenant/client#AssignTenantFieldTrigger": AssignTenantFieldTrigger_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/plugin-multi-tenant/client#WatchTenantCollection": WatchTenantCollection_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/plugin-multi-tenant/rsc#TenantSelector": TenantSelector_d6d5f193a167989e2ee7d14202901e62,
|
||||
"@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62
|
||||
"@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62,
|
||||
"/app/(payload)/admin/views/KitchenDashboard#KitchenDashboard": KitchenDashboard_466f0c465119ff8e562eb80399daabc0
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
'use client'
|
||||
import type { FormEvent } from 'react'
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import React from 'react'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'loginPage'
|
||||
|
||||
// go to /tenant1/home
|
||||
// redirects to /tenant1/login?redirect=%2Ftenant1%2Fhome
|
||||
// login, uses slug to set payload-tenant cookie
|
||||
|
||||
type Props = {
|
||||
tenantSlug?: string
|
||||
tenantDomain?: string
|
||||
}
|
||||
export const Login = ({ tenantSlug, tenantDomain }: Props) => {
|
||||
const usernameRef = React.useRef<HTMLInputElement>(null)
|
||||
const passwordRef = React.useRef<HTMLInputElement>(null)
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!usernameRef?.current?.value || !passwordRef?.current?.value) {
|
||||
return
|
||||
}
|
||||
const actionRes = await fetch('/api/users/external-users/login', {
|
||||
body: JSON.stringify({
|
||||
password: passwordRef.current.value,
|
||||
tenantSlug,
|
||||
tenantDomain,
|
||||
username: usernameRef.current.value,
|
||||
}),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'post',
|
||||
})
|
||||
const json = await actionRes.json()
|
||||
|
||||
if (actionRes.status === 200 && json.user) {
|
||||
const redirectTo = searchParams.get('redirect')
|
||||
if (redirectTo) {
|
||||
router.push(redirectTo)
|
||||
return
|
||||
} else {
|
||||
if (tenantDomain) {
|
||||
router.push('/tenant-domains')
|
||||
} else {
|
||||
router.push(`/tenant-slugs/${tenantSlug}`)
|
||||
}
|
||||
}
|
||||
} else if (actionRes.status === 400 && json?.errors?.[0]?.message) {
|
||||
window.alert(json.errors[0].message)
|
||||
} else {
|
||||
window.alert('Something went wrong, please try again.')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label>
|
||||
Username
|
||||
<input name="username" ref={usernameRef} type="text" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
Password
|
||||
<input name="password" ref={passwordRef} type="password" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
.loginPage {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 8px 16px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 4px;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
}
|
||||
169
src/app/globals.css
Normal file
169
src/app/globals.css
Normal file
@@ -0,0 +1,169 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
--color-background: hsl(0 0% 100%);
|
||||
--color-foreground: hsl(240 10% 3.9%);
|
||||
--color-card: hsl(0 0% 100%);
|
||||
--color-card-foreground: hsl(240 10% 3.9%);
|
||||
--color-popover: hsl(0 0% 100%);
|
||||
--color-popover-foreground: hsl(240 10% 3.9%);
|
||||
--color-primary: hsl(240 5.9% 10%);
|
||||
--color-primary-foreground: hsl(0 0% 98%);
|
||||
--color-secondary: hsl(240 4.8% 95.9%);
|
||||
--color-secondary-foreground: hsl(240 5.9% 10%);
|
||||
--color-muted: hsl(240 4.8% 95.9%);
|
||||
--color-muted-foreground: hsl(240 3.8% 46.1%);
|
||||
--color-accent: hsl(240 4.8% 95.9%);
|
||||
--color-accent-foreground: hsl(240 5.9% 10%);
|
||||
--color-destructive: hsl(0 84.2% 60.2%);
|
||||
--color-destructive-foreground: hsl(0 0% 98%);
|
||||
--color-border: hsl(240 5.9% 90%);
|
||||
--color-input: hsl(240 5.9% 90%);
|
||||
--color-ring: hsl(240 5.9% 10%);
|
||||
--color-chart-1: hsl(12 76% 61%);
|
||||
--color-chart-2: hsl(173 58% 39%);
|
||||
--color-chart-3: hsl(197 37% 24%);
|
||||
--color-chart-4: hsl(43 74% 66%);
|
||||
--color-chart-5: hsl(27 87% 67%);
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--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-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);
|
||||
--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);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--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);
|
||||
--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);
|
||||
--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.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 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.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 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(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.container {
|
||||
@apply mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
}
|
||||
66
src/components/ui/alert.tsx
Normal file
66
src/components/ui/alert.tsx
Normal file
@@ -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<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
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 badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
60
src/components/ui/button.tsx
Normal file
60
src/components/ui/button.tsx
Normal file
@@ -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 hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white 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 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",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
92
src/components/ui/card.tsx
Normal file
92
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
32
src/components/ui/checkbox.tsx
Normal file
32
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
21
src/components/ui/input.tsx
Normal file
21
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
45
src/components/ui/radio-group.tsx
Normal file
45
src/components/ui/radio-group.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn("grid gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
187
src/components/ui/select.tsx
Normal file
187
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
align = "center",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
28
src/components/ui/separator.tsx
Normal file
28
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
116
src/components/ui/table.tsx
Normal file
116
src/components/ui/table.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user