diff --git a/client/package-lock.json b/client/package-lock.json index 120d4a9..ef26e2c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,7 @@ "name": "client", "version": "0.0.0", "dependencies": { + "@radix-ui/react-slot": "^1.2.4", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.13.0", @@ -1000,6 +1001,37 @@ "node": ">= 8" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", diff --git a/client/package.json b/client/package.json index 38f7437..5b06000 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@radix-ui/react-slot": "^1.2.4", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.13.0", diff --git a/client/src/App.tsx b/client/src/App.tsx index bf78790..0ae3c15 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,6 +6,12 @@ import { RegisterPage } from '@/pages/RegisterPage'; import { DashboardPage } from '@/pages/DashboardPage'; import { GuestListPage } from '@/pages/GuestListPage'; import { RsvpPage } from '@/pages/RsvpPage'; +import { CreateEventPage } from '@/pages/CreateEventPage'; +import { EventDetailPage } from '@/pages/EventDetailPage'; + +function Protected({ children }: { children: React.ReactNode }) { + return {children}; +} export default function App() { return ( @@ -18,22 +24,10 @@ export default function App() { } /> {/* Protected routes */} - - - - } - /> - - - - } - /> + } /> + } /> + } /> + } /> } /> } /> diff --git a/client/src/components/ComplianceChecklist.tsx b/client/src/components/ComplianceChecklist.tsx new file mode 100644 index 0000000..00712da --- /dev/null +++ b/client/src/components/ComplianceChecklist.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; + +interface ComplianceChecklistProps { + onDismiss?: () => void; + readOnly?: boolean; + checkedItems?: Record; + onItemChange?: (key: string, checked: boolean) => void; +} + +const CHECKLIST_ITEMS = [ + { key: 'municipal_permit', label: 'אישור עירייה התקבל (נדרש על פי חוק לאירועים עם 100+ אורחים)' }, + { key: 'fire_safety', label: 'תעודת בטיחות אש של המקום התקבלה' }, + { key: 'liability_insurance', label: 'ביטוח אחריות לאירוע בתוקף' }, + { key: 'noise_curfew', label: 'עוצר הרעש הובן — האירוע יסתיים עד 23:00 בהתאם לתקנות' }, +]; + +export function ComplianceChecklist({ onDismiss, readOnly = false, checkedItems = {}, onItemChange }: ComplianceChecklistProps) { + const [localChecked, setLocalChecked] = useState>(checkedItems); + + function handleCheck(key: string, checked: boolean) { + setLocalChecked(prev => ({ ...prev, [key]: checked })); + onItemChange?.(key, checked); + } + + const items = readOnly ? checkedItems : localChecked; + const allChecked = CHECKLIST_ITEMS.every(item => items[item.key]); + + return ( + + +
+ + 📋 רשימת ציות לאירועים עם 100+ אורחים + + {onDismiss && ( + + )} +
+

+ אלו דרישות חוקיות — המארגן מאשר בעצמו. המערכת אינה מאמתת. +

+
+ +
    + {CHECKLIST_ITEMS.map(item => ( +
  • + {readOnly ? ( + + {items[item.key] ? '✓' : '○'} + + ) : ( + handleCheck(item.key, e.target.checked)} + className="mt-1 flex-shrink-0 h-4 w-4 accent-blue-600" + /> + )} + +
  • + ))} +
+ {!readOnly && allChecked && ( +

✓ כל הפריטים סומנו

+ )} +
+
+ ); +} diff --git a/client/src/components/EventCard.tsx b/client/src/components/EventCard.tsx new file mode 100644 index 0000000..d29d870 --- /dev/null +++ b/client/src/components/EventCard.tsx @@ -0,0 +1,146 @@ +import { Link } from 'react-router-dom'; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; + +export interface EventSummary { + id: string; + title: string; + event_date?: string; + venue_name?: string; + venue_address?: string; + max_guests?: number; + venue_capacity?: number; + status: 'draft' | 'published' | 'cancelled' | 'completed'; + rsvp_confirmed: number; + rsvp_pending: number; + rsvp_total: number; + vendors_confirmed: number; + kashrut_level?: string; + budget?: number; +} + +const STATUS_LABELS: Record = { + draft: 'טיוטה', + published: 'פורסם', + cancelled: 'בוטל', + completed: 'הסתיים', +}; + +const STATUS_VARIANTS: Record = { + draft: 'secondary', + published: 'success', + cancelled: 'destructive', + completed: 'outline', +}; + +interface EventCardProps { + event: EventSummary; + onCancel: (id: string, title: string) => void; + onPublish: (id: string) => void; +} + +export function EventCard({ event, onCancel, onPublish }: EventCardProps) { + const formattedDate = event.event_date + ? new Date(event.event_date).toLocaleDateString('he-IL', { + weekday: 'short', day: 'numeric', month: 'long', year: 'numeric', + timeZone: 'Asia/Jerusalem', + }) + : null; + + const daysUntil = event.event_date + ? Math.ceil((new Date(event.event_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24)) + : null; + + const rsvpPercent = event.max_guests && event.rsvp_total > 0 + ? Math.round((event.rsvp_confirmed / event.max_guests) * 100) + : null; + + return ( + + +
+ {event.title} + + {STATUS_LABELS[event.status]} + +
+ {formattedDate && ( +

+ 📅 {formattedDate} + {daysUntil !== null && daysUntil > 0 && ( + ({daysUntil} ימים) + )} +

+ )} +
+ + + {event.venue_name && ( +

📍 {event.venue_name}{event.venue_address ? `, ${event.venue_address}` : ''}

+ )} + +
+
+

מאושרים

+

{event.rsvp_confirmed}

+
+
+

מוזמנים

+

{event.rsvp_total}{event.max_guests ? `/${event.max_guests}` : ''}

+
+
+

ספקים

+

{event.vendors_confirmed}

+
+
+ + {rsvpPercent !== null && ( +
+
+
= 90 ? 'bg-orange-500' : 'bg-green-500'}`} + style={{ width: `${Math.min(rsvpPercent, 100)}%` }} + /> +
+

{rsvpPercent}% אישרו הגעה

+
+ )} + + + + + + {event.status === 'draft' && ( + <> + + + + )} + {event.status === 'published' && ( + + )} + {!['cancelled', 'completed'].includes(event.status) && ( + + )} + + + ); +} diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx index 3d42017..a437412 100644 --- a/client/src/components/ui/button.tsx +++ b/client/src/components/ui/button.tsx @@ -1,4 +1,5 @@ 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'; @@ -29,12 +30,15 @@ const buttonVariants = cva( export interface ButtonProps extends React.ButtonHTMLAttributes, - VariantProps {} + VariantProps { + asChild?: boolean; +} const Button = React.forwardRef( - ({ className, variant, size, ...props }, ref) => { + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; return ( - +

יצירת אירוע חדש

+
+ + + + פרטי האירוע + + +
+
+ + handleChange('title', e.target.value)} + placeholder="חתונת יוסי ומיכל" + required + /> +
+ +
+
+ + handleChange('event_date', e.target.value)} + min={minDate} + required + dir="ltr" + /> +
+
+ + handleChange('event_time', e.target.value)} + dir="ltr" + /> +
+
+ +
+ + handleChange('venue_name', e.target.value)} + placeholder="אולם הנשיאים" + required + /> +
+ +
+ + handleChange('venue_address', e.target.value)} + placeholder="רחוב הרצל 1, תל אביב" + /> +
+ +
+ +