diff --git a/src/app/(protected)/dashboard/page.tsx b/src/app/(protected)/dashboard/page.tsx index f57a03c..687b419 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 { useOrganizerAnalytics } from '@/hooks/useOrganizerAnalytics' import { exportAnalyticsCsv } from '@/lib/exportAnalyticsCsv' diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index 24b5a25..c766f37 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -4,6 +4,7 @@ import Image from "next/image"; import Link from "next/link"; import dynamic from "next/dynamic"; import { motion } from "framer-motion"; +import dynamic from "next/dynamic"; import { Calendar, Clock, @@ -25,6 +26,9 @@ const LandingFooter = dynamic( { ssr: false, loading: () =>
} ); +// 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 a002294..9a8f357 100644 --- a/src/components/auth/login-form.tsx +++ b/src/components/auth/login-form.tsx @@ -13,12 +13,14 @@ import { motion } from "framer-motion"; import { containerVariants, itemVariants, headerVariants } from "@/lib/animations/motionVariants"; 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; @@ -30,14 +32,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"); @@ -79,7 +83,7 @@ export default function LoginForm() { > Welcome Back @@ -112,12 +116,20 @@ export default function LoginForm() { /> + + + + + - + {errors.root && (

{errors.root.message} @@ -160,6 +172,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 75f1c02..d7e9e64 100644 --- a/src/components/auth/signup-form.tsx +++ b/src/components/auth/signup-form.tsx @@ -69,7 +69,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 ( + <> +