From a502c34244201b652e7c259100a098dbc40fe3c0 Mon Sep 17 00:00:00 2001 From: BashMan11 Date: Thu, 28 May 2026 19:05:56 +0000 Subject: [PATCH] Feat: solution for Issue #267, 268, 269, 270 --- src/app/(protected)/dashboard/page.tsx | 7 +- src/app/(public)/page.tsx | 4 + src/components/auth/login-form.tsx | 70 ++++++++++-------- src/components/auth/signup-form.tsx | 3 +- src/components/shared/Breadcrumb.tsx | 38 ++++++++++ src/components/shared/MobileNavDrawer.tsx | 89 +++++++++++++++++++++++ src/components/shared/OrganizerNavbar.tsx | 75 +++++++++++++++++++ src/components/ui/ConfirmationModal.tsx | 76 +++++++++++++++++++ src/components/ui/Pagination.tsx | 40 ++++++++++ src/components/ui/Skeleton.tsx | 39 ++++++++++ src/features/verification/page.tsx | 12 ++- src/hooks/useSession.ts | 32 ++++---- src/lib/animations/motionVariants.ts | 35 ++++++++- src/lib/createEventValidation.ts | 12 +++ src/middleware.ts | 19 +++-- tailwind.config.js | 1 + 16 files changed, 495 insertions(+), 57 deletions(-) create mode 100644 src/components/shared/Breadcrumb.tsx create mode 100644 src/components/shared/MobileNavDrawer.tsx create mode 100644 src/components/shared/OrganizerNavbar.tsx create mode 100644 src/components/ui/ConfirmationModal.tsx create mode 100644 src/components/ui/Pagination.tsx create mode 100644 src/components/ui/Skeleton.tsx diff --git a/src/app/(protected)/dashboard/page.tsx b/src/app/(protected)/dashboard/page.tsx index 73d3f54..ecf103c 100644 --- a/src/app/(protected)/dashboard/page.tsx +++ b/src/app/(protected)/dashboard/page.tsx @@ -12,7 +12,12 @@ import { EventImage } from '@/components/dashboard/EventImage' import { RevenueChart } from '@/components/dashboard/charts/RevenueChart' import { PerformanceChart } from '@/components/dashboard/charts/PerformanceChart' import { EmptyState } from '@/components/EmptyState' -import { TicketTypeChart } from '@/components/dashboard/charts/TicketTypeChart' +import dynamic from 'next/dynamic' +// Code-split Recharts-based chart away from the initial dashboard bundle +const TicketTypeChart = dynamic( + () => import('@/components/dashboard/charts/TicketTypeChart').then((m) => m.TicketTypeChart), + { ssr: false, loading: () =>
} +) import { DemographicsSection } from '@/components/dashboard/DemographicsSection' import { eventImages } from '@/components/dashboard/constants' import { diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index 3ca644c..93979d1 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -3,6 +3,7 @@ import Image from "next/image"; import Link from "next/link"; import { motion } from "framer-motion"; +import dynamic from "next/dynamic"; import { Calendar, Clock, @@ -16,6 +17,9 @@ import { FaDiscord, FaFacebookF, FaXTwitter } from "react-icons/fa6"; import { howItWorksSteps, trendingEvents } from "@/mocks/landing"; import NewsletterForm from "@/components/NewsletterForm"; +// Lazy-load below-the-fold newsletter form to reduce initial bundle +const LazyNewsletterForm = dynamic(() => import("@/components/NewsletterForm"), { ssr: false }); + const MotionLink = motion(Link); const fadeUp = { diff --git a/src/components/auth/login-form.tsx b/src/components/auth/login-form.tsx index ec1abea..289cf74 100644 --- a/src/components/auth/login-form.tsx +++ b/src/components/auth/login-form.tsx @@ -11,12 +11,14 @@ import { toast } from "react-toastify"; import { motion } from "framer-motion"; import { loginUser } from "@/lib/auth"; import { useRouter, useSearchParams } from "next/navigation"; +import { FcGoogle } from "react-icons/fc"; const loginSchema = z.object({ email: z.email("Please enter a valid email address"), password: z .string("Please enter your password") .min(6, "Password must be at least 6 characters"), + rememberMe: z.boolean().optional(), }); type FormValues = z.infer; @@ -28,14 +30,16 @@ export default function LoginForm() { handleSubmit, formState: { isSubmitting, errors }, control, + register, setError, - } = useForm({ + } = useForm({ resolver: zodResolver(loginSchema), + defaultValues: { rememberMe: false }, }); const onSubmit = async (data: FormValues) => { try { - await loginUser({ email: data.email, password: data.password }); + await loginUser({ email: data.email, password: data.password, rememberMe: data.rememberMe }); toast.success("Login successful!"); const next = searchParams.get("next"); router.push(next && next.startsWith("/") ? next : "/dashboard"); @@ -47,31 +51,12 @@ export default function LoginForm() { const containerVariants = { hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - duration: 0.5, - staggerChildren: 0.12, - }, - }, + visible: { opacity: 1, transition: { duration: 0.5, staggerChildren: 0.12 } }, }; const itemVariants = { hidden: { opacity: 0, y: 20 }, - visible: { - opacity: 1, - y: 0, - transition: { duration: 0.4 }, - }, - }; - - const headerVariants = { - hidden: { opacity: 0, scale: 0.95 }, - visible: { - opacity: 1, - scale: 1, - transition: { duration: 0.5 }, - }, + visible: { opacity: 1, y: 0, transition: { duration: 0.4 } }, }; return ( @@ -83,7 +68,7 @@ export default function LoginForm() { > Welcome Back @@ -116,12 +101,20 @@ export default function LoginForm() { /> + + + + + - + {errors.root && (

{errors.root.message} @@ -141,6 +134,25 @@ export default function LoginForm() { Sign Up + + {process.env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED === "true" && ( + +

+
+ or +
+
+ + + )} ); } diff --git a/src/components/auth/signup-form.tsx b/src/components/auth/signup-form.tsx index 8fe3672..0ad21bc 100644 --- a/src/components/auth/signup-form.tsx +++ b/src/components/auth/signup-form.tsx @@ -68,7 +68,8 @@ export default function SignUpForm() { } toast.success("Account created successfully"); - + // Show email verification prompt + toast.info("Please check your email to verify your account before signing in.", { autoClose: 8000 }); // router.push("/login"); } catch (error: any) { diff --git a/src/components/shared/Breadcrumb.tsx b/src/components/shared/Breadcrumb.tsx new file mode 100644 index 0000000..1140ad6 --- /dev/null +++ b/src/components/shared/Breadcrumb.tsx @@ -0,0 +1,38 @@ +import Link from "next/link"; +import { ChevronRight } from "lucide-react"; + +export interface BreadcrumbItem { + label: string; + href?: string; +} + +interface BreadcrumbProps { + items: BreadcrumbItem[]; + className?: string; +} + +export function Breadcrumb({ items, className = "" }: BreadcrumbProps) { + return ( + + ); +} diff --git a/src/components/shared/MobileNavDrawer.tsx b/src/components/shared/MobileNavDrawer.tsx new file mode 100644 index 0000000..e1a3dfa --- /dev/null +++ b/src/components/shared/MobileNavDrawer.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import Link from "next/link"; +import { X } from "lucide-react"; +import { usePathname } from "next/navigation"; + +interface MobileNavDrawerProps { + isOpen: boolean; + onClose: () => void; +} + +const navLinks = [ + { href: "/dashboard", label: "Dashboard" }, + { href: "/events/manage", label: "My Events" }, + { href: "/events/create", label: "Create Event" }, + { href: "/verify", label: "Verify Tickets" }, + { href: "/tickets", label: "My Tickets" }, +]; + +export function MobileNavDrawer({ isOpen, onClose }: MobileNavDrawerProps) { + const pathname = usePathname(); + const closeRef = useRef(null); + + useEffect(() => { + if (isOpen) closeRef.current?.focus(); + }, [isOpen]); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; + if (isOpen) document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, [isOpen, onClose]); + + // Prevent body scroll when open + useEffect(() => { + document.body.style.overflow = isOpen ? "hidden" : ""; + return () => { document.body.style.overflow = ""; }; + }, [isOpen]); + + if (!isOpen) return null; + + return ( + <> +