872 lines
33 KiB
TypeScript
872 lines
33 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from "react";
|
||
import Layout from "@/components/Layout";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import LoadingScreen from "@/components/LoadingScreen";
|
||
import { aethexToast } from "@/lib/aethex-toast";
|
||
import { cn } from "@/lib/utils";
|
||
import { Link } from "react-router-dom";
|
||
import type { FormEvent } from "react";
|
||
import {
|
||
Users,
|
||
MessageCircle,
|
||
Github,
|
||
MessageSquare,
|
||
Twitter,
|
||
ArrowRight,
|
||
Star,
|
||
Calendar,
|
||
MapPin,
|
||
Award,
|
||
TrendingUp,
|
||
Heart,
|
||
Coffee,
|
||
Code,
|
||
Gamepad2,
|
||
CheckCircle,
|
||
Loader2,
|
||
} from "lucide-react";
|
||
|
||
type EventStatus = "Registration Open" | "Recurring" | "Upcoming" | "Waitlist";
|
||
|
||
type CommunityEvent = {
|
||
id: string;
|
||
title: string;
|
||
date: string;
|
||
location: string;
|
||
type: string;
|
||
participants: number;
|
||
prize?: string | null;
|
||
status: EventStatus;
|
||
description: string;
|
||
agenda: string[];
|
||
registrationEnabled: boolean;
|
||
registrationUrl?: string;
|
||
};
|
||
|
||
type EventRegistrationPayload = {
|
||
name: string;
|
||
email: string;
|
||
teamName?: string;
|
||
message?: string;
|
||
};
|
||
|
||
interface EventCardProps {
|
||
event: CommunityEvent;
|
||
animationDelay: number;
|
||
isRegistered: boolean;
|
||
registrant?: EventRegistrationPayload;
|
||
onRegister: (payload: EventRegistrationPayload) => void;
|
||
}
|
||
|
||
function EventCard({
|
||
event,
|
||
animationDelay,
|
||
isRegistered,
|
||
registrant,
|
||
onRegister,
|
||
}: EventCardProps) {
|
||
const [open, setOpen] = useState(false);
|
||
const [form, setForm] = useState({
|
||
name: registrant?.name ?? "",
|
||
email: registrant?.email ?? "",
|
||
teamName: registrant?.teamName ?? "",
|
||
message: registrant?.message ?? "",
|
||
});
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
if (registrant) {
|
||
setForm({
|
||
name: registrant.name,
|
||
email: registrant.email,
|
||
teamName: registrant.teamName ?? "",
|
||
message: registrant.message ?? "",
|
||
});
|
||
}
|
||
}, [registrant]);
|
||
|
||
useEffect(() => {
|
||
if (!open) {
|
||
setError(null);
|
||
}
|
||
}, [open]);
|
||
|
||
const handleSubmit = (eventSubmit: FormEvent<HTMLFormElement>) => {
|
||
eventSubmit.preventDefault();
|
||
const trimmedName = form.name.trim();
|
||
const trimmedEmail = form.email.trim();
|
||
const trimmedTeam = form.teamName.trim();
|
||
const trimmedMessage = form.message.trim();
|
||
|
||
if (!trimmedName || !trimmedEmail) {
|
||
setError("Please provide both your name and email to register.");
|
||
return;
|
||
}
|
||
|
||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!emailPattern.test(trimmedEmail)) {
|
||
setError("Please enter a valid email address.");
|
||
return;
|
||
}
|
||
|
||
setError(null);
|
||
setSubmitting(true);
|
||
|
||
setTimeout(() => {
|
||
onRegister({
|
||
name: trimmedName,
|
||
email: trimmedEmail,
|
||
teamName: trimmedTeam || undefined,
|
||
message: trimmedMessage || undefined,
|
||
});
|
||
setSubmitting(false);
|
||
setOpen(false);
|
||
}, 600);
|
||
};
|
||
|
||
const statusStyles: Record<EventStatus, string> = {
|
||
"Registration Open": "bg-gradient-to-r from-emerald-500/20 to-aethex-500/30 text-emerald-200 border border-emerald-400/40",
|
||
Recurring: "bg-blue-500/10 text-blue-200 border border-blue-400/40",
|
||
Upcoming: "bg-orange-500/10 text-orange-200 border border-orange-400/40",
|
||
Waitlist: "bg-amber-500/10 text-amber-200 border border-amber-400/40",
|
||
};
|
||
|
||
const buttonLabel = isRegistered ? "Manage Registration" : "Register";
|
||
const submitLabel = isRegistered ? "Update Registration" : "Confirm Registration";
|
||
|
||
return (
|
||
<Card
|
||
className="border-border/50 hover:border-aethex-400/50 transition-all duration-300 hover-lift animate-slide-right"
|
||
style={{ animationDelay: `${animationDelay}s` }}
|
||
>
|
||
<CardContent className="p-6 space-y-6">
|
||
<div className="flex flex-col md:flex-row md:items-start justify-between gap-6">
|
||
<div className="space-y-3">
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<h3 className="text-xl font-semibold text-gradient">{event.title}</h3>
|
||
<Badge className={cn("uppercase tracking-wide", statusStyles[event.status])}>
|
||
{event.status}
|
||
</Badge>
|
||
{isRegistered && (
|
||
<Badge className="border border-emerald-400/40 bg-emerald-500/10 text-emerald-200">
|
||
<CheckCircle className="mr-1 h-3.5 w-3.5" /> Registered
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
<p className="text-sm text-muted-foreground max-w-2xl">
|
||
{event.description}
|
||
</p>
|
||
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
|
||
<div className="flex items-center space-x-1">
|
||
<Calendar className="h-4 w-4" />
|
||
<span>{event.date}</span>
|
||
</div>
|
||
<div className="flex items-center space-x-1">
|
||
<MapPin className="h-4 w-4" />
|
||
<span>{event.location}</span>
|
||
</div>
|
||
<div className="flex items-center space-x-1">
|
||
<Users className="h-4 w-4" />
|
||
<span>{event.participants} spots</span>
|
||
</div>
|
||
{event.prize && (
|
||
<div className="flex items-center space-x-1">
|
||
<Award className="h-4 w-4" />
|
||
<span>{event.prize}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-col items-start gap-3 min-w-[200px]">
|
||
<Badge variant="outline" className="w-fit">
|
||
{event.type}
|
||
</Badge>
|
||
<Dialog open={open} onOpenChange={setOpen}>
|
||
<DialogTrigger asChild>
|
||
<Button size="sm" disabled={!event.registrationEnabled && !event.registrationUrl}>
|
||
{buttonLabel}
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent className="sm:max-w-lg">
|
||
<DialogHeader>
|
||
<DialogTitle>{event.title}</DialogTitle>
|
||
<DialogDescription>
|
||
Reserve your spot by sharing your details. We |