From c48464ac9756c2fed450c078474d44e0cb2c9ce3 Mon Sep 17 00:00:00 2001 From: airewit-developer Date: Sat, 21 Feb 2026 18:41:42 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Event=20Creation=20&=20Management=20?= =?UTF-8?q?=E2=80=94=20Organizer=20Dashboard=20(42327b58)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - POST /api/events — create event, validates title/date/venue required, rejects past dates (Asia/Jerusalem aware), returns compliance_checklist flag when max_guests >= 100 - GET /api/events — list organizer events (scoped by JWT), paginated (limit 20), enriched with rsvp_confirmed/pending/total + vendors_confirmed counts, sorted by event_date ASC - GET /api/events/:id — single event with RSVP + vendor counts - PUT /api/events/:id — COALESCE update, validated status transitions (draft→published/cancelled, published→cancelled/completed), ownership enforced; returns compliance_checklist flag when crossing 100 guests - DELETE /api/events/:id — soft delete (deleted_at + status=cancelled), 204 Frontend: - DashboardPage — real event list, EventCard grid, pagination, prominent empty state with "צור אירוע ראשון" CTA - EventCard — title, date, venue, RSVP confirmed/total/vendors stats, progress bar (orange at 90%), days-until countdown, publish/edit/cancel actions - CreateEventPage — full form: title, date+time picker (min=today), venue, kashrut, budget, guest count; compliance checklist appears when max_guests >= 100, dismissible; Hebrew validation errors - EventDetailPage — full event detail with stat cards, days-until, read-only compliance checklist for 100+ events, quick-action links - ComplianceChecklist — reusable: interactive (create/edit) or read-only (detail page); 4 Israeli compliance items, dismissible banner - Button component: added asChild support via @radix-ui/react-slot - App.tsx: routes for /events/new, /events/:id, /events/:id/edit Build: 0 TS errors, 68 modules Co-Authored-By: Claude Sonnet 4.6 --- client/package-lock.json | 32 ++ client/package.json | 1 + client/src/App.tsx | 26 +- client/src/components/ComplianceChecklist.tsx | 76 +++++ client/src/components/EventCard.tsx | 146 +++++++++ client/src/components/ui/button.tsx | 10 +- client/src/pages/CreateEventPage.tsx | 255 +++++++++++++++ client/src/pages/DashboardPage.tsx | 129 ++++++-- client/src/pages/EventDetailPage.tsx | 153 +++++++++ routes/events.js | 295 ++++++++++++++++++ server.js | 4 +- 11 files changed, 1086 insertions(+), 41 deletions(-) create mode 100644 client/src/components/ComplianceChecklist.tsx create mode 100644 client/src/components/EventCard.tsx create mode 100644 client/src/pages/CreateEventPage.tsx create mode 100644 client/src/pages/EventDetailPage.tsx create mode 100644 routes/events.js 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, תל אביב" + /> +
+ +
+ +