diff --git a/frontend/app/my-events/page.tsx b/frontend/app/my-events/page.tsx new file mode 100644 index 0000000..4f5ed07 --- /dev/null +++ b/frontend/app/my-events/page.tsx @@ -0,0 +1,5 @@ +import MyEvents from '@/components/myEvents/MyEvents'; + +export default function Page() { + return ; +} diff --git a/frontend/components/myEvents/MyEvents.tsx b/frontend/components/myEvents/MyEvents.tsx new file mode 100644 index 0000000..2046027 --- /dev/null +++ b/frontend/components/myEvents/MyEvents.tsx @@ -0,0 +1,840 @@ +'use client'; + +import React, { useState, useRef, useEffect } from 'react'; + +import { useTheme } from 'evergreen-ui'; + +// ── Types ───────────────────────────────────────────────────── + +type Tab = 'All' | 'Saved' | 'Created'; +type SortOption = 'Date (soonest)' | 'Date (latest)' | 'Recently added'; + +type Category = 'SPORTS' | 'ACADEMIC' | 'SOCIAL' | 'FOOD' | 'ARTS' | 'CAREER' | 'HOUSING' | 'OTHER'; + +const ALL_CATEGORIES: Category[] = [ + 'SPORTS', + 'ACADEMIC', + 'SOCIAL', + 'FOOD', + 'ARTS', + 'CAREER', + 'HOUSING', + 'OTHER', +]; + +const CATEGORY_COLORS: Record = { + SPORTS: { bg: '#E8F5E9', text: '#2E7D32' }, + ACADEMIC: { bg: '#E3F2FD', text: '#1565C0' }, + SOCIAL: { bg: '#F3E5F5', text: '#6A1B9A' }, + FOOD: { bg: '#FFF3E0', text: '#E65100' }, + ARTS: { bg: '#FCE4EC', text: '#AD1457' }, + CAREER: { bg: '#E8EAF6', text: '#283593' }, + HOUSING: { bg: '#E0F2F1', text: '#00695C' }, + OTHER: { bg: '#F5F5F5', text: '#424242' }, +}; + +interface Event { + id: string; + title: string; + dateISO: string; + displayDate: string; + timeRange: string; + location: string; + description: string; + interested: number; + category: Category; + tab: Tab[]; + isStarred?: boolean; + isOwned?: boolean; +} + +// ── Mock Data ───────────────────────────────────────────────── + +const EVENTS: Event[] = [ + { + id: '1', + title: 'Mock Interview Night', + dateISO: '2026-04-06', + displayDate: 'MON, APR 6', + timeRange: '6 PM – 8 PM', + location: 'Robertson Hall', + description: + 'Practice behavioral and technical interviews with alumni volunteers from top tech and finance firms. Sign up for ...', + interested: 29, + category: 'CAREER', + tab: ['All', 'Saved'], + isStarred: true, + }, + { + id: '2', + title: 'AI at Princeton: LLM Fine-Tuning Workshop', + dateISO: '2026-04-07', + displayDate: 'TUE, APR 7', + timeRange: '6 PM – 8 PM', + location: 'COS Building Room 302', + description: + 'Hands-on workshop on fine-tuning large language models using LoRA and QLoRA. Bring your laptop with ...', + interested: 118, + category: 'ACADEMIC', + tab: ['All', 'Saved'], + isStarred: true, + }, + { + id: '3', + title: 'Preview Day', + dateISO: '2026-04-08', + displayDate: 'WED, APR 8', + timeRange: '', + location: 'University-wide', + description: + 'Princeton Preview Day for admitted students. Campus tours, info sessions, and class visits across all...', + interested: 0, + category: 'SOCIAL', + tab: ['All', 'Saved'], + isStarred: true, + }, + { + id: '4', + title: 'Campus YMCA Yoga', + dateISO: '2026-04-10', + displayDate: 'FRI, APR 10', + timeRange: '4:30 PM – 5:30 PM', + location: 'Dillon Gymnasium', + description: 'All-level yoga class. No experience needed — just bring a mat!', + interested: 45, + category: 'SPORTS', + tab: ['All', 'Created'], + isOwned: true, + }, + { + id: '5', + title: 'Spring Fling Food Festival', + dateISO: '2026-04-12', + displayDate: 'SUN, APR 12', + timeRange: '12 PM – 4 PM', + location: 'Prospect Garden', + description: + 'Student-run food fair with dishes from 20+ cuisines. Live music and lawn games included.', + interested: 203, + category: 'FOOD', + tab: ['All', 'Created'], + isOwned: true, + }, + { + id: '6', + title: 'A Cappella Showcase', + dateISO: '2026-04-15', + displayDate: 'WED, APR 15', + timeRange: '7 PM – 9 PM', + location: 'Richardson Auditorium', + description: 'Six Princeton a cappella groups perform in this semester-end showcase.', + interested: 87, + category: 'ARTS', + tab: ['All', 'Created'], + isOwned: true, + }, +]; + +// ── Sort Options ────────────────────────────────────────────── + +const SORT_OPTIONS: SortOption[] = ['Date (soonest)', 'Date (latest)', 'Recently added']; + +// ── Dropdown Hook ───────────────────────────────────────────── + +function useClickOutside(ref: React.RefObject, cb: () => void) { + useEffect(() => { + function handler(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) cb(); + } + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, [ref, cb]); +} + +// ── Component ───────────────────────────────────────────────── + +export default function MyEvents() { + const { colors } = useTheme(); + const themeColors = colors as any; + + const TEAL = themeColors['hoagie-teal'] ?? '#00897B'; + + const [activeTab, setActiveTab] = useState('All'); + const [search, setSearch] = useState(''); + const [sort, setSort] = useState('Date (soonest)'); + const [sortOpen, setSortOpen] = useState(false); + const [categoryOpen, setCategoryOpen] = useState(false); + const [selectedCategories, setSelectedCategories] = useState([]); + + const sortRef = useRef(null!); + const categoryRef = useRef(null!); + + useClickOutside(sortRef, () => setSortOpen(false)); + useClickOutside(categoryRef, () => setCategoryOpen(false)); + + const tabCounts = { + All: EVENTS.length, + Saved: EVENTS.filter((e) => e.tab.includes('Saved')).length, + Created: EVENTS.filter((e) => e.tab.includes('Created')).length, + }; + + const query = search.trim().toLowerCase(); + + const filtered = EVENTS.filter((e) => { + if (!e.tab.includes(activeTab)) return false; + if (query && !e.title.toLowerCase().includes(query)) return false; + if (selectedCategories.length > 0 && !selectedCategories.includes(e.category)) return false; + return true; + }).sort((a, b) => { + if (sort === 'Date (soonest)') + return new Date(a.dateISO).getTime() - new Date(b.dateISO).getTime(); + if (sort === 'Date (latest)') + return new Date(b.dateISO).getTime() - new Date(a.dateISO).getTime(); + if (sort === 'Recently added') return Number(b.id) - Number(a.id); + return 0; + }); + + function toggleCategory(cat: Category) { + setSelectedCategories((prev) => + prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat] + ); + } + + const categoryLabel = + selectedCategories.length === 0 + ? 'Category' + : selectedCategories.length === 1 + ? selectedCategories[0] + : `${selectedCategories.length} categories`; + + return ( +
+ {/* Header */} +
+

+ My Events +

+
+ + {/* Tabs */} +
+ {(['All', 'Saved', 'Created'] as Tab[]).map((tab) => ( + + ))} +
+ + {/* Search + Filters */} +
+ {/* Search */} +
+ + + + + setSearch(e.target.value)} + style={{ + width: '100%', + padding: '8px 10px 8px 32px', + border: `1px solid ${themeColors.gray300}`, + borderRadius: 6, + fontSize: 13, + background: colors.white, + color: themeColors.gray800, + outline: 'none', + boxSizing: 'border-box', + }} + /> +
+ + {/* Sort Dropdown */} +
+ + {sortOpen && ( + + {SORT_OPTIONS.map((opt) => ( + { + setSort(opt); + setSortOpen(false); + }} + /> + ))} + + )} +
+ + {/* Category Dropdown */} +
+ + {categoryOpen && ( + + {selectedCategories.length > 0 && ( +
setSelectedCategories([])} + style={{ + padding: '8px 12px', + fontSize: 12, + cursor: 'pointer', + color: TEAL, + fontWeight: 600, + borderBottom: `1px solid #f0f0f0`, + }} + > + Clear all +
+ )} + {ALL_CATEGORIES.map((cat) => { + const isSelected = selectedCategories.includes(cat); + const style = CATEGORY_COLORS[cat]; + return ( +
toggleCategory(cat)} + style={{ + padding: '8px 12px', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + gap: 8, + background: isSelected ? '#f9f9f9' : 'transparent', + }} + > + + + {cat} + + {isSelected && ( + + + + )} +
+ ); + })} +
+ )} +
+
+ + {/* Active category chips */} + {selectedCategories.length > 0 && ( +
+ {selectedCategories.map((cat) => { + const s = CATEGORY_COLORS[cat]; + return ( + toggleCategory(cat)} + style={{ + display: 'inline-flex', + alignItems: 'center', + gap: 4, + fontSize: 11, + fontWeight: 600, + background: s.bg, + color: s.text, + padding: '3px 8px', + borderRadius: 20, + cursor: 'pointer', + letterSpacing: '0.03em', + }} + > + {cat} + + + + + + ); + })} +
+ )} + + {/* Grid */} + {filtered.length === 0 ? ( +
+ No events match your filters. +
+ ) : ( +
+ {filtered.map((event) => ( + + ))} +
+ )} +
+ ); +} + +// ── Dropdown helpers ────────────────────────────────────────── + +function dropdownButtonStyle( + themeColors: any, + colors: any, + isOpen: boolean, + teal: string, + hasActive = false +) { + return { + padding: '7px 12px', + border: `1px solid ${hasActive || isOpen ? teal : themeColors.gray300}`, + borderRadius: 6, + background: hasActive ? `${teal}10` : colors.white, + cursor: 'pointer', + color: hasActive ? teal : themeColors.gray700, + fontSize: 13, + fontWeight: hasActive ? 600 : 400, + display: 'flex', + alignItems: 'center', + whiteSpace: 'nowrap' as const, + transition: 'border-color 0.15s, background 0.15s', + }; +} + +function DropdownMenu({ + children, + minWidth = 160, +}: { + children: React.ReactNode; + minWidth?: number; +}) { + return ( +
+ {children} +
+ ); +} + +function DropdownItem({ + label, + selected, + themeColors, + teal, + onClick, +}: { + label: string; + selected: boolean; + themeColors: any; + teal: string; + onClick: () => void; +}) { + return ( +
+ {label} + {selected && ( + + + + )} +
+ ); +} + +// ── Event Card ──────────────────────────────────────────────── + +function EventCard({ + event, + themeColors, + colors, +}: { + event: Event; + themeColors: any; + colors: any; + teal: string; +}) { + const catStyle = CATEGORY_COLORS[event.category]; + + return ( +
{ + (e.currentTarget as HTMLDivElement).style.boxShadow = '0 4px 16px rgba(0,0,0,0.09)'; + (e.currentTarget as HTMLDivElement).style.borderColor = themeColors.gray300; + }} + onMouseLeave={(e) => { + (e.currentTarget as HTMLDivElement).style.boxShadow = 'none'; + (e.currentTarget as HTMLDivElement).style.borderColor = themeColors.gray200; + }} + > +
+ {/* Date + Category + Star */} +
+
+ + {event.displayDate} + + + {event.category} + +
+ {event.isStarred && ( + + + + )} +
+ + {/* Title */} +
+ {event.title} +
+ + {/* Time + Location */} + {(event.timeRange || event.location) && ( +
+ {event.timeRange && ( + <> + + + + + {event.timeRange} + + )} + {event.location && ( + <> + + + + + + {event.location} + + )} +
+ )} + + {/* Description */} + {event.description && ( +
+ {event.description} +
+ )} +
+ + {/* Footer */} +
+ {event.interested > 0 ? ( + + {event.interested} interested + + ) : ( + + )} + {event.isOwned && ( + + )} +
+
+ ); +} diff --git a/frontend/lib/hoagie-ui/ProfileCard/index.tsx b/frontend/lib/hoagie-ui/ProfileCard/index.tsx index e2db00b..28087cf 100644 --- a/frontend/lib/hoagie-ui/ProfileCard/index.tsx +++ b/frontend/lib/hoagie-ui/ProfileCard/index.tsx @@ -40,8 +40,15 @@ export function ProfileCard({ user }: { user: User }) { {email} + + + - + );