diff --git a/apps/web/src/app/(app)/events/[id]/edit/edit-event-form.tsx b/apps/web/src/app/(app)/events/[id]/edit/edit-event-form.tsx index 601afbe..ab61ea1 100644 --- a/apps/web/src/app/(app)/events/[id]/edit/edit-event-form.tsx +++ b/apps/web/src/app/(app)/events/[id]/edit/edit-event-form.tsx @@ -21,6 +21,8 @@ import { useRouter } from "next/navigation"; import { useCallback, useRef, useState, useTransition } from "react"; import { type EventDetail, updateEvent } from "~/actions/events"; import { getPresignedUploadUrl } from "~/actions/upload"; +import { PageShell } from "~/components/layout/page-shell"; +import { Button } from "~/components/ui/button"; import { Calendar } from "~/components/ui/calendar"; import { Input } from "~/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover"; @@ -163,33 +165,21 @@ export function EditEventForm({ event, locations }: EditEventFormProps) { }; return ( -
+ {/* Top buttons */} -
-
- - +
+
+ +
- +
@@ -293,12 +283,12 @@ export function EditEventForm({ event, locations }: EditEventFormProps) { placeholder="Super Interesting Event Title" className={cn( "flex-1 text-[32px] font-serif font-bold text-black placeholder:text-forum-placeholder/40 outline-none", - errors.title && "placeholder:text-red-300", + errors.title && "placeholder:text-forum-coral/60", )} />
- {errors.title &&

{errors.title}

} + {errors.title &&

{errors.title}

}
{/* Event Description */} @@ -316,11 +306,11 @@ export function EditEventForm({ event, locations }: EditEventFormProps) { rows={8} className={cn( "border border-forum-medium-gray rounded-[8px] text-[14px] font-dm-sans placeholder:text-forum-placeholder resize-none focus:border-forum-cerulean", - errors.description && "border-red-400", + errors.description && "border-forum-coral", )} /> {errors.description && ( -

{errors.description}

+

{errors.description}

)}
@@ -338,7 +328,7 @@ export function EditEventForm({ event, locations }: EditEventFormProps) { className={cn( "flex items-center gap-2 h-[40px] px-[14px] border border-forum-medium-gray rounded-[8px] text-[13px] font-dm-sans", date ? "text-black" : "text-forum-placeholder", - errors.date && "border-red-400", + errors.date && "border-forum-coral", )} > @@ -356,7 +346,7 @@ export function EditEventForm({ event, locations }: EditEventFormProps) { /> - {errors.date &&

{errors.date}

} + {errors.date &&

{errors.date}

}
@@ -405,7 +395,7 @@ export function EditEventForm({ event, locations }: EditEventFormProps) { placeholder="Search by keyword or address" className={cn( "w-full h-[40px] pl-[34px] pr-[14px] border border-forum-medium-gray rounded-[8px] text-[13px] font-dm-sans outline-none focus:border-forum-cerulean", - errors.location && "border-red-400", + errors.location && "border-forum-coral", )} />
@@ -426,7 +416,7 @@ export function EditEventForm({ event, locations }: EditEventFormProps) { "w-full text-left px-3 py-2 rounded-md text-sm transition-colors", locationId === loc.id ? "bg-forum-turquoise/20 text-black" - : "text-forum-dark-gray hover:bg-gray-50", + : "text-forum-dark-gray hover:bg-forum-turquoise/10", )} > {loc.name} @@ -435,7 +425,9 @@ export function EditEventForm({ event, locations }: EditEventFormProps) { - {errors.location &&

{errors.location}

} + {errors.location && ( +

{errors.location}

+ )} {/* Visibility */} @@ -529,25 +521,20 @@ export function EditEventForm({ event, locations }: EditEventFormProps) { {/* Bottom actions */} -
- - + Back to top + +
- + ); } diff --git a/apps/web/src/app/(app)/events/[id]/event-detail-client.tsx b/apps/web/src/app/(app)/events/[id]/event-detail-client.tsx index 23550a4..9bdad2e 100644 --- a/apps/web/src/app/(app)/events/[id]/event-detail-client.tsx +++ b/apps/web/src/app/(app)/events/[id]/event-detail-client.tsx @@ -24,8 +24,10 @@ import { toggleRsvp, toggleSave, } from "~/actions/events"; +import { Panel } from "~/components/common/panel"; import { getCategoryColor } from "~/components/events/event-card"; import { EventCoverArt } from "~/components/events/event-cover-art"; +import { PageHeading, PageShell, SectionHeading } from "~/components/layout/page-shell"; import { AvatarStack } from "~/components/social/avatar-stack"; import { Button } from "~/components/ui/button"; import { @@ -126,53 +128,47 @@ export function EventDetailClient({ event, similarEvents }: EventDetailClientPro }; return ( -
+ {/* Back link */} - + {/* Main content */} -
+
{/* Left: Flyer */} -
-
+
+
{event.flyerUrl ? ( - {event.title} + ) : ( - + )}
{/* Right: Event info */} -
+
{/* Action buttons */} -
-
+
+
{event.isOwner && ( <> - - - + - + Delete + @@ -186,8 +182,8 @@ export function EventDetailClient({ event, similarEvents }: EventDetailClientPro - @@ -195,42 +191,34 @@ export function EventDetailClient({ event, similarEvents }: EventDetailClientPro )}
-
- - Calendar - - - + + +
{/* Title */} -

+ {event.title} -

+ {/* Org */} {event.orgName && ( @@ -296,19 +284,16 @@ export function EventDetailClient({ event, similarEvents }: EventDetailClientPro

{/* RSVP button */} - + {isRsvped ? "Cancel RSVP" : "RSVP now"} + {/* Attendees */}
@@ -339,22 +324,27 @@ export function EventDetailClient({ event, similarEvents }: EventDetailClientPro {/* Similar Events */} {similarEvents.length > 0 && ( -
-

Similar Events

-
+
+ Similar Events +
{similarEvents.map((se) => ( - -
-

+ + +

{se.title}

-

{se.datetime}

-
- +

{se.datetime}

+ + ))}
-
+ )} -
+ ); } diff --git a/apps/web/src/app/(app)/events/create/create-event-form.tsx b/apps/web/src/app/(app)/events/create/create-event-form.tsx index 26cf4bf..4f1ac3c 100644 --- a/apps/web/src/app/(app)/events/create/create-event-form.tsx +++ b/apps/web/src/app/(app)/events/create/create-event-form.tsx @@ -24,8 +24,11 @@ import { useRouter } from "next/navigation"; import { useCallback, useRef, useState, useTransition } from "react"; import { createEvent } from "~/actions/events"; import { getPresignedUploadUrl } from "~/actions/upload"; +import { FilterChip } from "~/components/common/filter-chip"; import { CoverPresetsPicker } from "~/components/events/cover-presets"; import { EventPreviewModal } from "~/components/events/event-preview-modal"; +import { PageShell } from "~/components/layout/page-shell"; +import { Button } from "~/components/ui/button"; import { Calendar } from "~/components/ui/calendar"; import { Input } from "~/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover"; @@ -219,35 +222,21 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { const handleSaveDraft = () => doCreate("draft"); return ( -
+ {/* Top buttons */} -
-
- - +
+
+ +
- +
@@ -359,12 +348,12 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { placeholder="Super Interesting Event Title" className={cn( "flex-1 text-[32px] font-serif font-bold text-black placeholder:text-forum-placeholder/40 outline-none", - errors.title && "placeholder:text-red-300", + errors.title && "placeholder:text-forum-coral/60", )} />
- {errors.title &&

{errors.title}

} + {errors.title &&

{errors.title}

}
{/* ── Affiliate Organizations ── */} @@ -398,7 +387,7 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { @@ -433,11 +422,11 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { rows={8} className={cn( "border border-forum-medium-gray rounded-[8px] text-[14px] font-dm-sans placeholder:text-forum-placeholder resize-none focus:border-forum-cerulean", - errors.description && "border-red-400", + errors.description && "border-forum-coral", )} /> {errors.description && ( -

{errors.description}

+

{errors.description}

)}
@@ -455,7 +444,7 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { className={cn( "flex items-center gap-2 h-[40px] px-[14px] border border-forum-medium-gray rounded-[8px] text-[13px] font-dm-sans", date ? "text-black" : "text-forum-placeholder", - errors.date && "border-red-400", + errors.date && "border-forum-coral", )} > @@ -474,7 +463,7 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { /> - {errors.date &&

{errors.date}

} + {errors.date &&

{errors.date}

}
@@ -525,7 +514,7 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { placeholder="Search by keyword or address" className={cn( "w-full h-[40px] pl-[34px] pr-[14px] border border-forum-medium-gray rounded-[8px] text-[13px] font-dm-sans outline-none focus:border-forum-cerulean", - errors.location && "border-red-400", + errors.location && "border-forum-coral", )} />
@@ -549,7 +538,7 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { "w-full text-left px-3 py-2 rounded-md text-sm transition-colors", locationId === loc.id ? "bg-forum-turquoise/20 text-black" - : "text-forum-dark-gray hover:bg-gray-50", + : "text-forum-dark-gray hover:bg-forum-turquoise/10", )} > {loc.name} @@ -559,7 +548,7 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { {errors.location && ( -

{errors.location}

+

{errors.location}

)}
{/* Map placeholder */} @@ -590,7 +579,7 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { className="w-full h-full object-cover" /> ) : ( -
+
)} @@ -646,43 +635,40 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { className="w-full h-[40px] pl-[34px] pr-[14px] border border-forum-medium-gray rounded-[8px] text-[13px] font-dm-sans outline-none focus:border-forum-cerulean" />
- - +
-
+
{tags.map((tag) => ( - + + ))}
{/* ── External Links ── */}
- + {externalLink ? (
@@ -692,9 +678,14 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { > {externalLink} - +
) : (
@@ -714,42 +705,28 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) {
{/* ── Bottom actions ── */} -
-
- - + + Back to top + +
-
- - +
+ +
@@ -773,6 +750,6 @@ export function CreateEventForm({ locations, userOrgs }: CreateEventFormProps) { flyerPreview={flyerPreview} coverPreset={coverPreset} /> -
+ ); } diff --git a/apps/web/src/app/(app)/events/my-events-client.tsx b/apps/web/src/app/(app)/events/my-events-client.tsx index 14560df..ac48303 100644 --- a/apps/web/src/app/(app)/events/my-events-client.tsx +++ b/apps/web/src/app/(app)/events/my-events-client.tsx @@ -1,17 +1,33 @@ "use client"; -import { Clock, MapPin, Plus } from "lucide-react"; +import { Clock, MapPin } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import type { FeedEvent } from "~/actions/events"; -import { getCategoryColor } from "~/components/events/event-card"; +import { Panel } from "~/components/common/panel"; +import { EmptyState } from "~/components/common/states"; import { EventCoverArt } from "~/components/events/event-cover-art"; -import { cn } from "~/lib/utils"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; const TABS = [ - { id: "created", label: "Events Created" }, - { id: "rsvped", label: "Events RSVP'd" }, - { id: "saved", label: "Events Saved" }, + { + id: "created", + label: "Events Created", + emptyTitle: "No events created yet", + emptyBody: "Share something with campus — create your first event.", + }, + { + id: "rsvped", + label: "Events RSVP'd", + emptyTitle: "No RSVP'd events", + emptyBody: "Events you've RSVP'd to will show up here.", + }, + { + id: "saved", + label: "Events Saved", + emptyTitle: "No saved events", + emptyBody: "Bookmark events you're interested in.", + }, ] as const; type TabId = (typeof TABS)[number]["id"]; @@ -23,61 +39,58 @@ interface MyEventsClientProps { } function EventListCard({ event }: { event: FeedEvent }) { - const color = getCategoryColor(event.tags); - return ( - - {/* Flyer thumbnail */} -
- {event.flyerUrl ? ( - {event.title} - ) : ( - - )} -
- - {/* Content */} -
-

- {event.title} -

- -
- - {event.datetime} + + {/* Flyer thumbnail */} +
+ {event.flyerUrl ? ( + + ) : ( + + )}
-
- - {event.location} -
+ {/* Content */} +
+

+ {event.title} +

- {event.tags.length > 0 && ( -
- {event.tags.slice(0, 3).map((tag) => ( - - {tag} - - ))} - {event.orgName && ( - - {event.orgName} - - )} +
+ + {event.datetime}
- )} -
- + +
+ + {event.location} +
+ + {event.tags.length > 0 && ( +
+ {event.tags.slice(0, 3).map((tag) => ( + + {tag} + + ))} + {event.orgName && ( + + {event.orgName} + + )} +
+ )} +
+ + ); } @@ -85,79 +98,34 @@ export function MyEventsClient({ created, rsvped, saved }: MyEventsClientProps) const [activeTab, setActiveTab] = useState("created"); const eventMap: Record = { created, rsvped, saved }; - const events = eventMap[activeTab]; return ( -
- {/* Tab bar */} -
-
-
- {TABS.map(({ id, label }) => ( - - ))} -
- {/* Active indicator line */} -
-
t.id === activeTab) * 100) / TABS.length}%`, - }} - /> -
-
-
- - {/* Event list */} - {events.length > 0 ? ( -
- {events.map((event) => ( - - ))} -
- ) : ( -
-

- {activeTab === "created" - ? "No events created yet" - : activeTab === "rsvped" - ? "No RSVP'd events" - : "No saved events"} -

-

- {activeTab === "created" - ? "Share something with campus — create your first event." - : activeTab === "rsvped" - ? "Events you've RSVP'd to will show up here." - : "Bookmark events you're interested in."} -

-
- )} + setActiveTab(value as TabId)}> + + {TABS.map(({ id, label }) => ( + + {label} + + ))} + - {/* Create Event button */} -
- - - CREATE AN EVENT - -
-
+ {TABS.map(({ id, emptyTitle, emptyBody }) => ( + + {eventMap[id].length > 0 ? ( +
+ {eventMap[id].map((event) => ( + + ))} +
+ ) : ( + + )} +
+ ))} + ); } diff --git a/apps/web/src/app/(app)/events/page.tsx b/apps/web/src/app/(app)/events/page.tsx index 393fa22..b393bd2 100644 --- a/apps/web/src/app/(app)/events/page.tsx +++ b/apps/web/src/app/(app)/events/page.tsx @@ -1,13 +1,28 @@ +import { Plus } from "lucide-react"; +import Link from "next/link"; import { getMyEvents } from "~/actions/events"; +import { PageHeading, PageShell } from "~/components/layout/page-shell"; +import { Button } from "~/components/ui/button"; import { MyEventsClient } from "./my-events-client"; export default async function MyEventsPage() { const { created, rsvped, saved } = await getMyEvents(); return ( -
-

Events

+ + + + + Create an event + + + } + > + Events + -
+ ); } diff --git a/apps/web/src/app/(app)/explore/explore-client.tsx b/apps/web/src/app/(app)/explore/explore-client.tsx index 597ac9e..81916ad 100644 --- a/apps/web/src/app/(app)/explore/explore-client.tsx +++ b/apps/web/src/app/(app)/explore/explore-client.tsx @@ -1,6 +1,6 @@ "use client"; -import { Expand, Plus, Search } from "lucide-react"; +import { Expand } from "lucide-react"; import Link from "next/link"; import { useCallback, useMemo, useState, useTransition } from "react"; import { @@ -10,8 +10,12 @@ import { toggleRsvp, toggleSave, } from "~/actions/events"; +import { Panel } from "~/components/common/panel"; +import { SearchInput } from "~/components/common/search-input"; +import { EmptyState, EventCardSkeletonList } from "~/components/common/states"; import { EventCard } from "~/components/events/event-card"; import { EventFilters } from "~/components/events/event-filters"; +import { PageHeading, PageShell, SectionHeading } from "~/components/layout/page-shell"; import { formatEventDateTime } from "~/lib/date-format"; interface ExploreClientProps { @@ -118,56 +122,51 @@ export function ExploreClient({ ); return ( -
+ {/* CENTER — Feed */} -
- {/* Greeting */} -
-

- Hi - {firstName}, -

-
-
-

Today is {getTodayString()}

-
-
+
+ + + + Today is {getTodayString()} + + + } + > + Hi + {firstName}, + - {/* Search */} -
-
- - setSearchQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") handleSearch(); - }} - className="bg-transparent text-[14px] font-dm-sans text-black placeholder:text-[#a6a8ae] outline-none flex-1 min-w-0" - /> -
-
+ setSearchQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleSearch(); + }} + /> - {/* Filters */} -
- -
+ {/* Feed */} -
- {events.length === 0 ? ( -
-

- {isPending ? "Loading events..." : "No events found"} -

-

- {activeFilters.length > 0 || searchQuery +

+ {isPending && events.length === 0 ? ( + + ) : events.length === 0 ? ( + 0 || searchQuery ? "Try adjusting your filters or search." - : "Events will appear here once they're created."} -

-
+ : "Events will appear here once they're created." + } + /> ) : ( events.map((event, index) => ( {/* RIGHT PANEL */} -
- {/* Create Event */} -
-
+
- - {/* Find My Friends */} -
-
-
-

Find My Friends

-
- -
+
@@ -271,46 +259,41 @@ export function ExploreClient({
-
+ {/* Upcoming Events */} -
-
-
-

Upcoming Events

-
-
+
+ Upcoming Events + {upcomingList.map((event) => ( -
+
{event.flyerUrl ? ( - {event.title} + ) : ( -
+
)}
-

+

{event.title}

- + DETAILS ))} {upcomingList.length === 0 && ( -

No upcoming events yet.

+

+ No upcoming events yet. +

)} -
-
-
-
+ + + + ); } diff --git a/apps/web/src/app/(app)/friends/friends-client.tsx b/apps/web/src/app/(app)/friends/friends-client.tsx index 44ffd43..d01b8da 100644 --- a/apps/web/src/app/(app)/friends/friends-client.tsx +++ b/apps/web/src/app/(app)/friends/friends-client.tsx @@ -1,6 +1,6 @@ "use client"; -import { Check, Clock, Search, UserMinus, UserPlus, Users, X } from "lucide-react"; +import { Check, Clock, Search, UserMinus, UserPlus, Users } from "lucide-react"; import { useCallback, useRef, useState, useTransition } from "react"; import { type FriendProfile, @@ -11,7 +11,11 @@ import { searchUsers, sendFriendRequest, } from "~/actions/friends"; -import { cn } from "~/lib/utils"; +import { Panel } from "~/components/common/panel"; +import { SearchInput } from "~/components/common/search-input"; +import { EmptyState, LoadingState } from "~/components/common/states"; +import { Button } from "~/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; function Avatar({ name, @@ -28,8 +32,8 @@ function Avatar({ return ( {name} ); @@ -37,7 +41,8 @@ function Avatar({ return (
{initial} @@ -132,210 +137,194 @@ export function FriendsClient({ initialFriends, initialPending }: FriendsClientP { id: "requests" as const, label: "Requests", count: pending.incoming.length }, ]; + const isSearchActive = searchQuery.trim().length > 0; + return ( -
- {/* Tab bar */} -
-
- {tabs.map(({ id, label, count }) => ( - - ))} -
-
-
t.id === activeTab) * 100) / tabs.length}%`, - }} - /> -
-
+ setActiveTab(value as typeof activeTab)} + className="gap-6" + > + + {tabs.map(({ id, label, count }) => ( + + {label} + {count > 0 && ( + + {count} + + )} + + ))} + - {/* Search bar */} -
- - handleSearch(e.target.value)} - placeholder="SEARCH USERS BY NAME OR NETID" - className="w-full h-[42px] bg-forum-bg border border-forum-bg rounded-[10px] shadow-[5px_5px_10px_5px_rgba(0,0,0,0.05)] pl-[42px] pr-[17px] text-[14px] font-dm-sans tracking-wider text-black placeholder:text-[#a6a8ae] outline-none" - /> -
+ handleSearch(e.target.value)} + /> - {/* Search results overlay */} - {searchQuery.trim() && ( -
+ {/* + Search results replace the active tab's content rather than sitting on + top of it, so only one list is ever on screen. + */} + {isSearchActive ? ( + {isSearching ? ( -
- Searching... -
+ ) : searchResults.length > 0 ? ( -
+
    {searchResults.map((user) => { const isFriend = friendIds.has(user.id); const isPendingSent = sentIds.has(user.id); return ( -
    -
    -

    +

    +

    {user.displayName}

    -

    +

    @{user.netId} {user.classYear && ` · '${user.classYear.slice(-2)}`}

    {isFriend ? ( - Friends + + Friends + ) : isPendingSent ? ( - - Sent + + Sent ) : ( - + + Add + )} -
    + ); })} -
    +
) : ( -
- No users found -
+ )} -
- )} - - {/* Tab content: Your Friends */} - {activeTab === "friends" && - !searchQuery.trim() && - (friends.length > 0 ? ( -
- {friends.map((friend) => ( -
- -
-

- {friend.displayName} -

-

- @{friend.netId} - {friend.classYear && ` · '${friend.classYear.slice(-2)}`} -

-
- + + ) : ( + <> + + {friends.length > 0 ? ( +
+ {friends.map((friend) => ( + + +
+

+ {friend.displayName} +

+

+ @{friend.netId} + {friend.classYear && ` · '${friend.classYear.slice(-2)}`} +

+
+ {/* + Kept reachable by keyboard: the control is always in the + tab order and reveals itself on focus, not just on hover. + */} + +
+ ))}
- ))} -
- ) : ( -
- -

No friends yet

-

- Search for classmates to start connecting. -

-
- ))} + ) : ( + + )} + - {/* Tab content: Find New Friends */} - {activeTab === "find" && !searchQuery.trim() && ( -
- -

Find your classmates

-

- Use the search bar above to find people by name or NetID. -

-
- )} + + + - {/* Tab content: Requests */} - {activeTab === "requests" && - !searchQuery.trim() && - (pending.incoming.length > 0 ? ( -
- {pending.incoming.map((req) => ( -
- -
-

- {req.displayName} ({req.netId}) sent you a - friend request. -

-
-
- - -
+ + {pending.incoming.length > 0 ? ( +
+ {pending.incoming.map((req) => ( + + +
+

+ {req.displayName} ({req.netId}) sent you + a friend request. +

+
+
+ + +
+
+ ))}
- ))} -
- ) : ( -
- -

All caught up

-

No pending friend requests.

-
- ))} -
+ ) : ( + + )} + + + )} + ); } diff --git a/apps/web/src/app/(app)/friends/page.tsx b/apps/web/src/app/(app)/friends/page.tsx index cb5f590..fe2db15 100644 --- a/apps/web/src/app/(app)/friends/page.tsx +++ b/apps/web/src/app/(app)/friends/page.tsx @@ -1,13 +1,14 @@ import { getFriends, getPendingRequests } from "~/actions/friends"; +import { PageHeading, PageShell } from "~/components/layout/page-shell"; import { FriendsClient } from "./friends-client"; export default async function FriendsPage() { const [friends, pending] = await Promise.all([getFriends(), getPendingRequests()]); return ( -
-

My Friends

+ + My Friends -
+ ); } diff --git a/apps/web/src/app/(app)/layout.tsx b/apps/web/src/app/(app)/layout.tsx index fa19921..a1889dc 100644 --- a/apps/web/src/app/(app)/layout.tsx +++ b/apps/web/src/app/(app)/layout.tsx @@ -1,33 +1,10 @@ import { SessionProvider } from "next-auth/react"; -import { headers } from "next/headers"; -import { GeometricBackground } from "~/components/layout/geometric-background"; -import { Sidebar } from "~/components/layout/sidebar"; -import { TopBar } from "~/components/layout/top-bar"; - -export default async function AppLayout({ children }: { children: React.ReactNode }) { - const headersList = await headers(); - const pathname = headersList.get("x-pathname") ?? ""; - const isMapRoute = pathname === "/map"; +import { AppChrome } from "~/components/layout/app-chrome"; +export default function AppLayout({ children }: { children: React.ReactNode }) { return ( - {isMapRoute ? ( - /* Map route: full-bleed — the map client renders its own floating nav */ -
{children}
- ) : ( -
- -
- -
-
- -
-
{children}
-
-
-
- )} + {children}
); } diff --git a/apps/web/src/app/(app)/map/_components/event-detail-modal.tsx b/apps/web/src/app/(app)/map/_components/event-detail-modal.tsx index 343c15f..5ce4563 100644 --- a/apps/web/src/app/(app)/map/_components/event-detail-modal.tsx +++ b/apps/web/src/app/(app)/map/_components/event-detail-modal.tsx @@ -3,6 +3,7 @@ import { Bookmark, MapPin, Share2, X } from "lucide-react"; import { useEffect, useState, useTransition } from "react"; import { getEvent } from "~/actions/events"; +import { Button } from "~/components/ui/button"; import { Dialog, DialogContent, DialogTitle } from "~/components/ui/dialog"; import { getEventColor } from "../_lib/map-helpers"; @@ -51,8 +52,8 @@ export function EventDetailModal({ eventId, onClose }: EventDetailModalProps) { {isPending || !event ? (
-
- Loading event +
+ Loading event
) : ( @@ -61,43 +62,33 @@ export function EventDetailModal({ eventId, onClose }: EventDetailModalProps) {
{event.orgName && ( -

{event.orgName}

+

{event.orgName}

)} -

{event.title}

+

{event.title}

- - - + + +
{/* Location + time */} -
+
{event.locationName && ( - + {event.locationName} )} - + {new Date(event.datetime).toLocaleDateString("en-US", { weekday: "short", month: "short", @@ -131,16 +122,15 @@ export function EventDetailModal({ eventId, onClose }: EventDetailModalProps) { {/* Description */} {event.description && ( -

{event.description}

+

+ {event.description} +

)} {/* RSVP button */} - +
)} diff --git a/apps/web/src/app/(app)/map/_components/event-list-panel.tsx b/apps/web/src/app/(app)/map/_components/event-list-panel.tsx index eb7935e..459dc15 100644 --- a/apps/web/src/app/(app)/map/_components/event-list-panel.tsx +++ b/apps/web/src/app/(app)/map/_components/event-list-panel.tsx @@ -1,10 +1,11 @@ "use client"; -import { Clock, MapPin, Maximize2, X } from "lucide-react"; -import Link from "next/link"; +import { MapPin, Maximize2, X } from "lucide-react"; import type { MapEvent } from "~/actions/map"; +import { EmptyState } from "~/components/common/states"; +import { Button } from "~/components/ui/button"; import { cn } from "~/lib/utils"; -import { URGENCY_STYLES, getEventColor, getRelativeLabel } from "../_lib/map-helpers"; +import { getEventColor } from "../_lib/map-helpers"; interface EventListPanelProps { events: MapEvent[]; @@ -24,27 +25,23 @@ export function EventListPanel({ return (
{/* Header */} -
- +
+ {events.length} event{events.length !== 1 ? "s" : ""} - +
{/* Event cards */}
{events.length === 0 && ( -
- -

No events found

-

Try adjusting your filters

-
+ )} {events.map((event) => ( @@ -72,31 +69,37 @@ function PanelEventCard({ onLocate: () => void; onExpand: () => void; }) { - const color = getEventColor(event.tags); - const rel = getRelativeLabel(event.rawDatetime); - const urgency = URGENCY_STYLES[rel.urgency]; const eventDate = new Date(event.rawDatetime); return ( -
+ - + +
{/* Tags */} {event.tags.length > 0 && ( -
+
{event.tags.slice(0, 3).map((tag) => { const tagColor = getEventColor([tag]); return ( {tag.replace(/-/g, " ")} @@ -140,7 +142,11 @@ function PanelEventCard({ )} {/* Org */} - {event.orgName &&

{event.orgName}

} - + {event.orgName && ( +

+ {event.orgName} +

+ )} +
); } diff --git a/apps/web/src/app/(app)/map/_components/map-filter-pills.tsx b/apps/web/src/app/(app)/map/_components/map-filter-pills.tsx index f247303..d052df1 100644 --- a/apps/web/src/app/(app)/map/_components/map-filter-pills.tsx +++ b/apps/web/src/app/(app)/map/_components/map-filter-pills.tsx @@ -1,7 +1,7 @@ "use client"; import { Heart, Users, Zap } from "lucide-react"; -import { cn } from "~/lib/utils"; +import { FilterChip } from "~/components/common/filter-chip"; import type { FilterKey } from "../map-client"; interface MapFilterPillsProps { @@ -17,26 +17,20 @@ const PILLS: { key: FilterKey; label: string; icon: typeof Users }[] = [ export function MapFilterPills({ activeFilters, onToggle }: MapFilterPillsProps) { return ( -
- {PILLS.map(({ key, label, icon: Icon }) => { - const isActive = activeFilters.has(key); - return ( - - ); - })} -
+
+ Filter map events + {PILLS.map(({ key, label, icon: Icon }) => ( + onToggle(key)} + // Sits over the map, so the inactive state needs a backdrop of its own. + className="shadow-sm aria-[pressed=false]:bg-white/95 aria-[pressed=false]:backdrop-blur-sm" + > + + {label} + + ))} +
); } diff --git a/apps/web/src/app/(app)/map/_components/map-legend.tsx b/apps/web/src/app/(app)/map/_components/map-legend.tsx index 49fad34..0902829 100644 --- a/apps/web/src/app/(app)/map/_components/map-legend.tsx +++ b/apps/web/src/app/(app)/map/_components/map-legend.tsx @@ -1,22 +1,30 @@ +import { Panel } from "~/components/common/panel"; import { FUTURE_COLOR, NOW_COLOR } from "../_lib/map-constants"; +const ENTRIES = [ + { label: "Now", color: NOW_COLOR }, + { label: "Future", color: FUTURE_COLOR }, +]; + export function MapLegend() { return ( -
-
- - NOW -
-
- - FUTURE -
-
+ + {ENTRIES.map(({ label, color }) => ( +
+ + + {label} + +
+ ))} +
); } diff --git a/apps/web/src/app/(app)/map/_components/map-popup-carousel.tsx b/apps/web/src/app/(app)/map/_components/map-popup-carousel.tsx index fb99970..8a9cea8 100644 --- a/apps/web/src/app/(app)/map/_components/map-popup-carousel.tsx +++ b/apps/web/src/app/(app)/map/_components/map-popup-carousel.tsx @@ -28,7 +28,7 @@ export function MapPopupCarousel({ events, onExpand }: MapPopupCarouselProps) { e.stopPropagation(); setActiveIndex((i) => (i - 1 + events.length) % events.length); }} - className="absolute left-1 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-white/90 shadow-sm border border-gray-200 flex items-center justify-center text-gray-500 hover:text-gray-800 hover:bg-white transition-colors" + className="absolute left-1 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-white/90 shadow-sm border border-forum-border flex items-center justify-center text-forum-dark-gray hover:text-black hover:bg-white transition-colors" > @@ -40,7 +40,7 @@ export function MapPopupCarousel({ events, onExpand }: MapPopupCarouselProps) { e.stopPropagation(); setActiveIndex((i) => (i + 1) % events.length); }} - className="absolute right-1 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-white/90 shadow-sm border border-gray-200 flex items-center justify-center text-gray-500 hover:text-gray-800 hover:bg-white transition-colors" + className="absolute right-1 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-white/90 shadow-sm border border-forum-border flex items-center justify-center text-forum-dark-gray hover:text-black hover:bg-white transition-colors" > @@ -56,7 +56,7 @@ export function MapPopupCarousel({ events, onExpand }: MapPopupCarouselProps) { setActiveIndex(i); }} className={`w-1.5 h-1.5 rounded-full transition-colors ${ - i === activeIndex ? "bg-gray-800" : "bg-gray-300" + i === activeIndex ? "bg-gray-800" : "bg-forum-medium-gray" }`} /> ))} diff --git a/apps/web/src/app/(app)/map/_components/map-popup.tsx b/apps/web/src/app/(app)/map/_components/map-popup.tsx index 3f787f4..56e3a65 100644 --- a/apps/web/src/app/(app)/map/_components/map-popup.tsx +++ b/apps/web/src/app/(app)/map/_components/map-popup.tsx @@ -1,5 +1,6 @@ import { Maximize2 } from "lucide-react"; import type { MapEvent } from "~/actions/map"; +import { Button } from "~/components/ui/button"; import { cn } from "~/lib/utils"; import { URGENCY_STYLES, getEventColor, getRelativeLabel } from "../_lib/map-helpers"; @@ -18,15 +19,15 @@ export function MapPopup({ event, onExpand }: MapPopupProps) {
-

{event.title}

-

+

{event.title}

+

{eventDate.toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric", })}

-

+

{eventDate.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", @@ -38,13 +39,15 @@ export function MapPopup({ event, onExpand }: MapPopupProps) { })}

- + +
{event.tags.length > 0 && ( @@ -65,9 +68,9 @@ export function MapPopup({ event, onExpand }: MapPopupProps) { )} {event.orgName && ( -
+
- {event.orgName} + {event.orgName}
)}
diff --git a/apps/web/src/app/(app)/map/_components/map-search-bar.tsx b/apps/web/src/app/(app)/map/_components/map-search-bar.tsx index fd9e45e..2dc0a0f 100644 --- a/apps/web/src/app/(app)/map/_components/map-search-bar.tsx +++ b/apps/web/src/app/(app)/map/_components/map-search-bar.tsx @@ -1,6 +1,6 @@ "use client"; -import { Search } from "lucide-react"; +import { SearchInput } from "~/components/common/search-input"; interface MapSearchBarProps { value: string; @@ -9,15 +9,13 @@ interface MapSearchBarProps { export function MapSearchBar({ value, onChange }: MapSearchBarProps) { return ( -
- - onChange(e.target.value)} - className="w-full pl-10 pr-4 py-2.5 rounded-xl bg-white/95 backdrop-blur-xl border border-gray-200/60 shadow-lg shadow-black/5 text-sm text-gray-800 placeholder:text-gray-400 placeholder:text-xs placeholder:tracking-wider placeholder:font-medium focus:outline-none focus:ring-2 focus:ring-sky-300/40" - /> -
+ onChange(e.target.value)} + // Floats over the map — needs the translucent backdrop the docked field doesn't. + className="bg-white/95 shadow-lg shadow-black/5 backdrop-blur-sm" + /> ); } diff --git a/apps/web/src/app/(app)/map/_components/timeline-scrubber.tsx b/apps/web/src/app/(app)/map/_components/timeline-scrubber.tsx index 83cc140..7146caa 100644 --- a/apps/web/src/app/(app)/map/_components/timeline-scrubber.tsx +++ b/apps/web/src/app/(app)/map/_components/timeline-scrubber.tsx @@ -21,16 +21,16 @@ export function TimelineScrubber({ const timelineDays = useMemo(() => getTimelineDays(days), [days]); return ( -
+
{/* Legend */}
- NOW + NOW
- FUTURE + FUTURE
@@ -38,7 +38,7 @@ export function TimelineScrubber({
{/* Background track line */} -
+
{/* Filled track segments between dates with events */} {timelineDays.map((day, i) => { @@ -54,7 +54,7 @@ export function TimelineScrubber({ return (
@@ -94,12 +94,12 @@ export function TimelineScrubber({ className={cn( "text-[9px] font-bold mt-1.5 uppercase tracking-wider leading-none", isSelected - ? "text-sky-600" + ? "text-forum-cerulean" : day.isToday - ? "text-sky-500" + ? "text-forum-cerulean" : hasEvents - ? "text-gray-600" - : "text-gray-400", + ? "text-forum-dark-gray" + : "text-forum-light-gray", )} > {day.isToday ? "TODAY" : day.dayName} @@ -108,10 +108,10 @@ export function TimelineScrubber({ className={cn( "text-[9px] leading-none mt-px", isSelected - ? "text-sky-600 font-bold" + ? "text-forum-cerulean font-bold" : hasEvents - ? "text-gray-500" - : "text-gray-400", + ? "text-forum-dark-gray" + : "text-forum-light-gray", )} > {day.monthShort} {String(day.dayNum).padStart(2, "0")} diff --git a/apps/web/src/app/(app)/map/_lib/map-helpers.ts b/apps/web/src/app/(app)/map/_lib/map-helpers.ts index b287aaa..d3bdcff 100644 --- a/apps/web/src/app/(app)/map/_lib/map-helpers.ts +++ b/apps/web/src/app/(app)/map/_lib/map-helpers.ts @@ -111,8 +111,8 @@ export const URGENCY_STYLES: Record now: { badge: "bg-[rgba(255,156,133,0.25)] text-[#FF7151]", text: "text-[#FF7151]" }, soon: { badge: "bg-[rgba(254,232,130,0.5)] text-[#854d0e]", text: "text-[#854d0e]" }, "later-today": { badge: "bg-[rgba(162,239,240,0.2)] text-[#0A9CD5]", text: "text-[#0A9CD5]" }, - tomorrow: { badge: "bg-gray-100 text-[#585858]", text: "text-[#585858]" }, - "this-week": { badge: "bg-gray-50 text-[#767676]", text: "text-[#767676]" }, + tomorrow: { badge: "bg-forum-medium-gray text-forum-dark-gray", text: "text-[#585858]" }, + "this-week": { badge: "bg-forum-medium-gray text-forum-light-gray", text: "text-[#767676]" }, }; /* ═══ Date helpers ═══ */ diff --git a/apps/web/src/app/(app)/map/map-client.tsx b/apps/web/src/app/(app)/map/map-client.tsx index 4770c9b..10cbd4f 100644 --- a/apps/web/src/app/(app)/map/map-client.tsx +++ b/apps/web/src/app/(app)/map/map-client.tsx @@ -1,13 +1,9 @@ "use client"; -import { Building2, CalendarDays, Home, LogOut, Map as MapIcon, Users } from "lucide-react"; -import { signOut } from "next-auth/react"; import dynamic from "next/dynamic"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; import { useCallback, useMemo, useRef, useState, useTransition } from "react"; import type { MapRef } from "react-map-gl/mapbox"; -import { type MapEvent, getMapEvents } from "~/actions/map"; +import type { MapEvent } from "~/actions/map"; import { cn } from "~/lib/utils"; import { getTimeGroup } from "./_lib/map-helpers"; @@ -17,10 +13,12 @@ const MapView = dynamic( { ssr: false, loading: () => ( -
+
-
- Loading map +
+ + Loading map +
), @@ -52,14 +50,6 @@ const EventDetailModal = dynamic( /* ═══ Filter types ═══ */ export type FilterKey = "friends" | "now" | "attending"; -/* ═══ Floating mini-nav items ═══ */ -const NAV_ITEMS = [ - { href: "/explore", icon: Home }, - { href: "/events", icon: CalendarDays }, - { href: "/map", icon: MapIcon }, - { href: "/friends", icon: Users }, -]; - /* ═══ Component ═══ */ interface MapClientProps { initialEvents: MapEvent[]; @@ -67,7 +57,6 @@ interface MapClientProps { export function MapClient({ initialEvents }: MapClientProps) { const mapRef = useRef(null); - const pathname = usePathname(); const [isPending, startTransition] = useTransition(); /* Data */ @@ -157,8 +146,13 @@ export function MapClient({ initialEvents }: MapClientProps) { return ( <> - {/* ═══ FULL-BLEED MAP — covers entire viewport ═══ */} -
+ {/* + Fills the app shell's content area rather than the viewport. It used to + be `fixed inset-0 z-40` with its own floating mini-nav, which meant the + map painted over the docked Sidebar and the route had to opt out of the + standard chrome. It now shares the same nav as every other page. + */} +
{/* Map fills everything */}
- {/* ═══ Floating mini-nav (left side) ═══ */} -
-
- {NAV_ITEMS.map(({ href, icon: Icon }) => { - const isActive = pathname.startsWith(href); - return ( - - - - ); - })} -
- {/* Log out button */} - -
- {/* ═══ Search bar + filter pills (top center) ═══ */} -
-
+ {/* The wider right inset on ≥sm clears the TopBar's bell + avatar. */} +
+
@@ -238,9 +204,9 @@ export function MapClient({ initialEvents }: MapClientProps) { {/* ═══ Loading overlay ═══ */} {isPending && (
-
-
- Loading events +
+
+ Loading events
)} diff --git a/apps/web/src/app/(app)/orgs/[id]/org-profile-client.tsx b/apps/web/src/app/(app)/orgs/[id]/org-profile-client.tsx index 8b18f98..cc8ccdc 100644 --- a/apps/web/src/app/(app)/orgs/[id]/org-profile-client.tsx +++ b/apps/web/src/app/(app)/orgs/[id]/org-profile-client.tsx @@ -1,14 +1,18 @@ "use client"; -import { CalendarDays, ChevronLeft, Heart, MapPin, Search, Shield, Users, X } from "lucide-react"; +import { CalendarDays, ChevronLeft, Heart, MapPin, Shield, Users, X } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useCallback, useState, useTransition } from "react"; import { toast } from "sonner"; import { type FriendProfile, searchUsers } from "~/actions/friends"; import { type OrgDetail, addOfficer, removeOfficer, toggleFollowOrg } from "~/actions/orgs"; +import { Panel } from "~/components/common/panel"; +import { SearchInput } from "~/components/common/search-input"; +import { EmptyState } from "~/components/common/states"; import { getCategoryColor } from "~/components/events/event-card"; -import { cn } from "~/lib/utils"; +import { PageHeading, PageShell, SectionHeading } from "~/components/layout/page-shell"; +import { Button } from "~/components/ui/button"; function colorFromString(str: string) { const colors = ["#6366f1", "#ec4899", "#14b8a6", "#f97316", "#8b5cf6", "#22c55e", "#3b82f6"]; @@ -85,31 +89,24 @@ export function OrgProfileClient({ org }: OrgProfileClientProps) { }; return ( -
+ {/* Back */} - + {/* Header */} -
+
{org.logoUrl ? ( - {org.name} + ) : ( {org.name[0]?.toUpperCase()} @@ -117,188 +114,192 @@ export function OrgProfileClient({ org }: OrgProfileClientProps) { )}
-
-
-

{org.name}

-

{org.category}

-
- -
+ {org.category}} + action={ + + } + > + {org.name} + -
- - +
+ + {followerCount} follower{followerCount !== 1 ? "s" : ""} - - + + {org.upcomingEvents.length} upcoming event{org.upcomingEvents.length !== 1 ? "s" : ""}
{org.description && ( -

+

{org.description}

)}
-
+
-
+
{/* Left: Events */} -
-

- Upcoming Events -

+
+ Upcoming Events {org.upcomingEvents.length > 0 ? ( -
+
{org.upcomingEvents.map((event) => { const color = getCategoryColor(event.tags); return ( - -
+ +
-
-

+

+

{event.title}

-

+

{event.datetime} · - {event.locationName} + {" "} + {event.locationName}

-
- + +
); })}
) : ( -
- -

No upcoming events

-
+ )} -
+
{/* Right: Members */} -
-

Team

-
+
+ Team + {[...owners, ...officers].map((member) => (
{member.avatarUrl ? ( ) : ( member.displayName[0]?.toUpperCase() )}
-
-

{member.displayName}

+
+

+ {member.displayName} +

- - {member.role === "owner" && } + + {member.role === "owner" && ( + + )} {member.role} {org.isOwner && member.role === "officer" && ( - + + )}
))} {owners.length === 0 && officers.length === 0 && ( -
-

No team members listed

-
+

+ No team members listed +

)} -
+
{/* Add officer search — owner only */} {org.isOwner && (
-
- - handleSearch(e.target.value)} - className="w-full pl-9 pr-3 py-2 text-sm rounded-lg border border-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-200 focus:border-indigo-300" - /> -
+ handleSearch(e.target.value)} + /> {searchResults.length > 0 && ( -
+ {searchResults.map((user) => (
{user.avatarUrl ? ( ) : ( user.displayName[0]?.toUpperCase() )}
- + {user.displayName} - +
))} -
+ + )} + {isSearching && ( +

Searching…

)} - {isSearching &&

Searching...

}
)} -
+
-
+ ); } diff --git a/apps/web/src/app/(app)/orgs/create/page.tsx b/apps/web/src/app/(app)/orgs/create/page.tsx index 22cccc5..b45f82c 100644 --- a/apps/web/src/app/(app)/orgs/create/page.tsx +++ b/apps/web/src/app/(app)/orgs/create/page.tsx @@ -5,6 +5,10 @@ import { useRouter } from "next/navigation"; import { useCallback, useRef, useState, useTransition } from "react"; import { createOrg } from "~/actions/orgs"; import { getPresignedUploadUrl } from "~/actions/upload"; +import { Field, fieldControlProps } from "~/components/common/field"; +import { FilterChip } from "~/components/common/filter-chip"; +import { Panel } from "~/components/common/panel"; +import { PageHeading, PageShell } from "~/components/layout/page-shell"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; @@ -101,49 +105,55 @@ export default function CreateOrgPage() { }; return ( -
-
-

Create Organization

-

Set up your student group on The Forum.

-
+ + + Create Organization + -
+ {/* Logo */}
{logoPreview ? (
- Logo - + + {isUploading && ( -
- -
+ + Uploading logo… + + )}
) : ( )}
-

Organization Logo

-

Optional. JPEG, PNG, or WebP.

+

Organization Logo

+

+ Optional. JPEG, PNG, or WebP. +

- {/* Name */} -
- + { setName(e.target.value); if (errors.name) setErrors((prev) => ({ ...prev, name: "" })); }} placeholder="e.g. Princeton Entrepreneurship Club" - className={cn( - "h-12 text-base border-gray-200 bg-gray-50/50 placeholder:text-gray-300", - errors.name && "border-red-300", - )} + className={cn("h-12 text-base", errors.name && "border-forum-coral")} /> - {errors.name &&

{errors.name}

} -
+ - {/* Description */} -
- +