From 39cc3cadedc47b0adce7267059f1b3b72dc87939 Mon Sep 17 00:00:00 2001 From: Niv Levy <140189588+NivLevy-gh@users.noreply.github.com> Date: Sun, 5 Apr 2026 03:19:54 -0400 Subject: [PATCH 1/2] Update page.tsx --- frontend/app/feature3/page.tsx | 447 +++++++++++++++++++++++---------- 1 file changed, 319 insertions(+), 128 deletions(-) diff --git a/frontend/app/feature3/page.tsx b/frontend/app/feature3/page.tsx index 7e7b756..3c861f4 100644 --- a/frontend/app/feature3/page.tsx +++ b/frontend/app/feature3/page.tsx @@ -1,132 +1,323 @@ -/** - * @overview Sample page 3 for the template app. - * - * Copyright © 2021-2026 Hoagie Club and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree or at https://github.com/hoagieclub/template/LICENSE. - * - * Permission is granted under the MIT License to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the software. This software is provided "as-is", without warranty of any kind. - */ - -'use client'; - -import { useState } from 'react'; - -import { useUser } from '@auth0/nextjs-auth0'; -import { Text, Heading, Pane, majorScale, Spinner, Button, Alert } from 'evergreen-ui'; -import { toast } from 'sonner'; - -import View from '@/components/View'; - -/** - * Simulates an API request to update the count. - * The function waits for 1 second before resolving and has a 50% chance of failure. - * - * @param {number} value - The new counter value to be updated. - * @returns {Promise} The updated counter value. - * @throws {Error} Throws an error if the update fails. - */ -const updateCountAPI = async (value: number): Promise => { - await new Promise((resolve) => setTimeout(resolve, 1000)); - if (Math.random() < 0.5) { - throw new Error('Update failed'); - } - return value; +import { FormEvent, useState } from "react"; + +const today = new Date().toISOString().split("T")[0]; + +type EventForm = { + name: string; + startDate: string; + startTime: string; + endDate: string; + endTime: string; + location: string; + host: string; + description: string; + selectedCategories: number[]; +}; + +type EventModalProps = { + categories: { id: number; name: string }[]; + onSubmit?: (event: { + name: string; + start: string; + end: string; + location: string; + host: string; + description: string; + category: number[]; + }) => void; }; -/** - * Main page component for the counter demo. - * - * This component shows the user's name, a counter, and two buttons to increment or decrement - * the counter. It also handles errors and displays a toast notification on success or failure. - * - * @returns {JSX.Element} The rendered page component. - */ -export function Feature3() { - const { user, error, isLoading } = useUser(); - const [count, setCount] = useState(0); - const [loading, setLoading] = useState(false); - - // If user data is loading, display a spinner. - if (isLoading) { - return ; - } - - if (error) { - return
{error.message}
; - } - - /** - * Handles the counter update logic. - * Calls `updateCountAPI` to simulate the API request, manages loading state, and shows toast notifications. - * - * @param {number} delta - The amount to increment or decrement the counter. - * @returns {Promise} Updates the counter and manages UI state. - */ - const updateCount = async (delta: number) => { - setLoading(true); - try { - const newCount = count + delta; - const result = await updateCountAPI(newCount); - setCount(result); - toast.success('Counter updated', { - description: `New count is ${result}`, - }); - } catch (_error) { - toast.error('Update failed', { - description: 'Could not update the counter. Please try again.', - }); - } finally { - setLoading(false); - } - }; - - return ( - - {/* Section displaying the user's name and the counter */} - - - Hi, {user?.name} - - Count: - - {count} - {loading && } - - - - {/* Buttons for incrementing and decrementing the counter */} - - - - - - {/* Informational alert about the counter's 50% failure rate */} - - - The counter has a 50% chance of failure to demonstrate error - handling. Please try again if the update fails. - - - - ); +const emptyForm: EventForm = { + name: "", + startDate: today, + startTime: "12:00", + endDate: today, + endTime: "13:00", + location: "", + host: "", + description: "", + selectedCategories: [], +}; + +function toDatetime(date: string, time: string): Date { + return new Date(`${date}T${time}`); +} + +function ErrorMessage({ text }: { text?: string }) { + if (!text) return null; + return

{text}

; +} + +function inputClass(hasError?: string): string { + return `w-full border rounded-lg px-3.5 py-2.5 text-sm text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 transition ${ + hasError + ? "border-red-400 focus:border-red-400 focus:ring-red-100" + : "border-slate-200 focus:border-cyan-400 focus:ring-cyan-100" + }`; } -export default Feature3; +export default function EventModal({ categories, onSubmit }: EventModalProps){ + const [open, setOpen] = useState(false); + const [form, setForm] = useState(emptyForm); + const [errors, setErrors] = useState>({}); + const [success, setSuccess] = useState(false); + + function updateField(field: keyof EventForm, value: string | number[]) { + setForm((prev) => ({ ...prev, [field]: value })); + if (errors[field]) setErrors((prev) => ({ ...prev, [field]: undefined })); + } + + function toggleCategory(id: number) { + setForm((prev) => ({ + ...prev, + selectedCategories: prev.selectedCategories.includes(id) + ? prev.selectedCategories.filter((c) => c !== id) + : [...prev.selectedCategories, id], + })); + } + + function validate() { + const errs: Record = {}; + if (!form.name.trim()) errs.name = "Event name is required."; + else if (form.name.length > 100) errs.name = "Maximum 100 characters."; + if (!form.startDate) errs.startDate = "Required."; + if (!form.startTime) errs.startTime = "Required."; + if (!form.endDate) errs.endDate = "Required."; + if (!form.endTime) errs.endTime = "Required."; + if (form.startDate && form.startTime && form.endDate && form.endTime) { + if (toDatetime(form.endDate, form.endTime) <= toDatetime(form.startDate, form.startTime)) { + errs.endDate = "End must be after start."; + } + } + if (form.location.length > 100) errs.location = "Maximum 100 characters."; + if (!form.host.trim()) errs.host = "Organizer is required."; + else if (form.host.length > 100) errs.host = "Maximum 100 characters."; + if (form.selectedCategories.length === 0) errs.selectedCategories = "Select at least one category."; + return errs; + } + + + function handleSubmit(e: FormEvent) { + e.preventDefault(); + const errs = validate(); + if (Object.keys(errs).length > 0) { + setErrors(errs); + return; + } + onSubmit?.({ + name: form.name.trim(), + start: toDatetime(form.startDate, form.startTime).toISOString(), + end: toDatetime(form.endDate, form.endTime).toISOString(), + location: form.location.trim(), + host: form.host.trim(), + description: form.description.trim(), + category: form.selectedCategories, + }); + setSuccess(true); + setTimeout(() => { + setSuccess(false); + setOpen(false); + setForm(emptyForm); + setErrors({}); + }, 1600); + } + + function handleClose() { + setOpen(false); + setForm(emptyForm); + setErrors({}); + } + + return ( + <> + + + {open && ( +
+
+ +
+
+

Add Event

+ +
+ +
+ +
+
+ + updateField("name", e.target.value)} + autoFocus + /> +
+ + {form.name.length}/100 +
+
+ +
+ +
+
+ updateField("startDate", e.target.value)} + /> + +
+
+ updateField("startTime", e.target.value)} + /> + +
+
+
+ +
+ +
+
+ updateField("endDate", e.target.value)} + /> + +
+
+ updateField("endTime", e.target.value)} + /> + +
+
+
+ +
+ + updateField("location", e.target.value)} + /> +
+ + {form.location.length}/100 +
+
+ +
+ + updateField("host", e.target.value)} + /> +
+ + {form.host.length}/100 +
+
+ +
+ +