Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 137 additions & 11 deletions app/events/[eventId]/EventDetailClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { PotionBackground } from "@/app/components/PotionBackground"
import { ErrorBoundary } from "@/app/components/ErrorBoundary"
import { Button } from "@/app/components/Button"
import { TextInput } from "@/app/components/TextInput"
import { supabaseClient } from "@/lib/supabaseClient"
import { isInterestedInEvent, removeEventInterest, toggleEventInterest } from "@/lib/eventInterests"

// Components //

Expand All @@ -21,14 +23,35 @@ export default function EventDetailClient() {
const [userInfo, setUserInfo] = useState<{ name: string; email: string }>({ name: "", email: "" })
const [registering, setRegistering] = useState(false)
const [hasStoredInfo, setHasStoredInfo] = useState(false)
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [isInterested, setIsInterested] = useState(false)
const [interestLoading, setInterestLoading] = useState(true)
const [interestSaving, setInterestSaving] = useState(false)
const nameInputRef = useRef<HTMLInputElement>(null)

useEffect(() => {
loadEvent()
loadSavedInfo()
loadInterestState()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventId])

useEffect(() => {
if (!event || !isLoggedIn) {
return
}

const isPast = new Date(event.start_at) < new Date()
if (!isPast || !isInterested) {
return
}

// Past events are no longer actionable — drop the DB row
removeEventInterest(eventId)
.then(() => setIsInterested(false))
.catch((error) => console.error("Failed to prune past event interest:", error))
}, [event, isLoggedIn, isInterested, eventId])

const loadEvent = async () => {
try {
const eventData = await lumaService.getEvent(eventId)
Expand All @@ -44,6 +67,53 @@ export default function EventDetailClient() {
}
}

const loadInterestState = async () => {
setInterestLoading(true)
try {
const {
data: { user }
} = await supabaseClient.auth.getUser()

if (!user) {
setIsLoggedIn(false)
setIsInterested(false)
return
}

setIsLoggedIn(true)
const interested = await isInterestedInEvent(eventId)
setIsInterested(interested)
} catch (error) {
console.error("Failed to load event interest:", error)
setIsInterested(false)
} finally {
setInterestLoading(false)
}
}

const handleToggleInterest = async () => {
if (!event || new Date(event.start_at) < new Date()) {
return
}

if (!isLoggedIn) {
const redirectUrl = encodeURIComponent(`/events/${eventId}`)
router.push(`/login?redirect=${redirectUrl}`)
return
}

setInterestSaving(true)
try {
const next = await toggleEventInterest(eventId)
setIsInterested(next)
} catch (error) {
console.error("Failed to update event interest:", error)
alert("Failed to update interest. Please try again.")
} finally {
setInterestSaving(false)
}
}

const loadSavedInfo = () => {
const savedUserInfo = localStorage.getItem("devx_user_info")
if (savedUserInfo) {
Expand Down Expand Up @@ -174,17 +244,19 @@ export default function EventDetailClient() {
<Header>
<Title>{event.name}</Title>
<DateTime>{formatEventDateTime(event.start_at, event.end_at)}</DateTime>
<Button
onClick={() => {
nameInputRef.current?.scrollIntoView({
behavior: "smooth",
block: "center"
})
setTimeout(() => nameInputRef.current?.focus(), 400)
}}
>
Attend This Event
</Button>
<MobileAttendButton>
<Button
onClick={() => {
nameInputRef.current?.scrollIntoView({
behavior: "smooth",
block: "center"
})
setTimeout(() => nameInputRef.current?.focus(), 400)
}}
>
Attend This Event
</Button>
</MobileAttendButton>
</Header>

{event.location && event.location.type === "online" && (
Expand All @@ -206,6 +278,32 @@ export default function EventDetailClient() {
<SidebarArea>
<span id="registration-form" />

{!isPastEvent && (
<InterestSection>
<SectionTitle>Save Event</SectionTitle>
<InterestCopy>
{isInterested
? "Saved to your account. Come back anytime while you decide."
: "On the fence? Mark this event so you can find it later."}
</InterestCopy>
<Button
variant={isInterested ? "secondary" : "primary"}
onClick={handleToggleInterest}
disabled={interestLoading || interestSaving}
>
{interestLoading
? "Loading..."
: interestSaving
? "Saving..."
: isInterested
? "Remove Interest"
: isLoggedIn
? "I'm Interested"
: "Log in to Save"}
</Button>
</InterestSection>
)}

{!isPastEvent && (
<RegistrationSection>
<SectionTitle>Registration</SectionTitle>
Expand Down Expand Up @@ -441,6 +539,12 @@ const Header = styled.header`
margin-bottom: 2rem;
`

const MobileAttendButton = styled.div`
@media (min-width: 768px) {
display: none;
}
`

const Title = styled.h1`
font-size: 2.25rem;
font-weight: bold;
Expand Down Expand Up @@ -600,6 +704,28 @@ const AttendeeLink = styled.a`
}
`

const InterestSection = styled.section`
background-color: rgba(255, 255, 255, 0.01);
backdrop-filter: blur(32px);
box-shadow: 4px 8px 8px 0 rgba(0, 0, 0, 0.05);
padding: 1.5rem;
border-radius: 0.5rem;
margin: 2rem 0 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
text-align: center;
`

const InterestCopy = styled.p`
font-size: 0.875rem;
color: #d1d5db;
line-height: 1.5;
margin: 0;
`

const RegistrationSection = styled.section`
background-color: rgba(255, 255, 255, 0.01);
backdrop-filter: blur(32px);
Expand Down
102 changes: 93 additions & 9 deletions app/events/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
"use client"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import styled from "styled-components"
import type { LumaEvent } from "@/app/services/luma"
import { lumaService } from "@/app/services/luma"
import { PotionBackground } from "../components/PotionBackground"
import { ErrorBoundary } from "../components/ErrorBoundary"
import { Card, CardContent, CardTitle, CardText } from "../components/Card"
import { Button } from "../components/Button"
import { supabaseClient } from "@/lib/supabaseClient"
import { pruneStaleEventInterests } from "@/lib/eventInterests"

// Types //

type EventFilter = "upcoming" | "past"
type EventFilter = "upcoming" | "past" | "saved"

// Components //

export default function Events() {
const router = useRouter()
const [events, setEvents] = useState<LumaEvent[]>([])
const [interestedIds, setInterestedIds] = useState<Set<string>>(new Set())
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [filter, setFilter] = useState<EventFilter>("upcoming")
const [loading, setLoading] = useState(true)

Expand All @@ -27,26 +33,79 @@ export default function Events() {
try {
const allEvents = await lumaService.listEvents()
setEvents(allEvents)

const {
data: { user }
} = await supabaseClient.auth.getUser()

if (!user) {
setIsLoggedIn(false)
setInterestedIds(new Set())
return
}

setIsLoggedIn(true)
const interests = await pruneStaleEventInterests(allEvents)
setInterestedIds(new Set(interests))
} catch (error) {
console.error("Failed to load events:", error)
} finally {
setLoading(false)
}
}

const handleFilterChange = async (nextFilter: EventFilter) => {
if (nextFilter === "saved" && !isLoggedIn) {
const {
data: { user }
} = await supabaseClient.auth.getUser()

if (!user) {
router.push(`/login?redirect=${encodeURIComponent("/events")}`)
return
}

setIsLoggedIn(true)
}

setFilter(nextFilter)

if (nextFilter === "saved") {
try {
const interests = await listInterestedEventIds()
setInterestedIds(new Set(interests))
} catch (error) {
console.error("Failed to load saved events:", error)
}
}
}

const now = new Date()
const isUpcoming = (event: LumaEvent) => new Date(event.start_at) >= now

const filteredEvents = events
.filter((event) => {
const eventDate = new Date(event.start_at)
const now = new Date()
return filter === "upcoming" ? eventDate >= now : eventDate < now
if (filter === "saved") {
// Saved is for deciding/signing up later — only upcoming events belong here
return interestedIds.has(event.api_id) && isUpcoming(event)
}

return filter === "upcoming" ? isUpcoming(event) : !isUpcoming(event)
})
.sort((a, b) => {
const dateA = new Date(a.start_at).getTime()
const dateB = new Date(b.start_at).getTime()
// Ascending for upcoming (oldest first), descending for past (newest first)
return filter === "upcoming" ? dateA - dateB : dateB - dateA

if (filter === "past") {
return dateB - dateA
}

// Upcoming and saved: soonest first
return dateA - dateB
})

const emptyMessage = getEmptyMessage(filter, isLoggedIn)

return (
<>
<BackgroundContainer>
Expand All @@ -67,22 +126,28 @@ export default function Events() {
<FilterToggle>
<Button
variant={filter === "upcoming" ? "primary" : "secondary"}
onClick={() => setFilter("upcoming")}
onClick={() => handleFilterChange("upcoming")}
>
Upcoming
</Button>
<Button
variant={filter === "past" ? "primary" : "secondary"}
onClick={() => setFilter("past")}
onClick={() => handleFilterChange("past")}
>
Past Events
</Button>
<Button
variant={filter === "saved" ? "primary" : "secondary"}
onClick={() => handleFilterChange("saved")}
>
Saved
</Button>
</FilterToggle>

{loading ? (
<LoadingMessage>Loading events...</LoadingMessage>
) : filteredEvents.length === 0 ? (
<NoEventsMessage>No {filter} events at this time. Check back soon!</NoEventsMessage>
<NoEventsMessage>{emptyMessage}</NoEventsMessage>
) : (
<EventsGrid>
{filteredEvents.map((event) => (
Expand All @@ -103,6 +168,13 @@ export default function Events() {
: "Online Event"}
</CardText>
)}
{filter !== "saved" &&
interestedIds.has(event.api_id) &&
isUpcoming(event) && (
<CardText $color="#8b5cf6" $weight="500">
Saved
</CardText>
)}
{event.guest_count !== undefined && event.guest_count !== -1 && (
<CardText $color="#8b5cf6" $weight="500">
{event.guest_count} attendees
Expand All @@ -127,6 +199,17 @@ export default function Events() {

// Functions //

function getEmptyMessage(filter: EventFilter, isLoggedIn: boolean): string {
if (filter === "saved") {
if (!isLoggedIn) {
return "Log in to view events you've saved."
}
return "No upcoming saved events. Open an event and tap I'm Interested."
}

return `No ${filter} events at this time. Check back soon!`
}

function formatEventDate(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleDateString("en-US", {
Expand Down Expand Up @@ -193,6 +276,7 @@ const EventDescription = styled.p`
const FilterToggle = styled.div`
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 2rem;
`
Expand Down
Loading
Loading