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
30 changes: 30 additions & 0 deletions client/app/src/components/CreateCharacter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const CreateCharacter = ({
}) => {
const [error, setError] = useState<string | null>(null)
const [newCharacterName, setNewCharacterName] = useState<string>("")
const [eventJoinCode, setEventJoinCode] = useState<string>("")
const [showEventCode, setShowEventCode] = useState<boolean>(false)
const [isLoading, setIsLoading] = useState<boolean>(false)
const accessToken = useGameStore.use.access_token?.()
const inputRef = useRef<HTMLInputElement>(null)
Expand All @@ -36,6 +38,7 @@ export const CreateCharacter = ({
},
body: JSON.stringify({
name: newCharacterName,
...(eventJoinCode.trim() ? { event_join_code: eventJoinCode.trim() } : {}),
}),
}
)
Expand All @@ -45,6 +48,13 @@ export const CreateCharacter = ({
throw new Error(data.error)
}
const data = await response.json()
useGameStore
.getState()
.setPlayerEvent(
data.event_id && data.event_title ?
{ event_id: data.event_id, title: data.event_title }
: null
)
onCharacterCreate(data.character_id)
} catch (error) {
setError(error instanceof Error ? error.message : "Failed to create character")
Expand Down Expand Up @@ -93,6 +103,26 @@ export const CreateCharacter = ({
setNewCharacterName(e.target.value)
}}
/>
{!showEventCode ?
<button
type="button"
onClick={() => setShowEventCode(true)}
className="text-xs text-muted-foreground hover:text-foreground uppercase tracking-wider transition-colors"
>
Have an event code?
</button>
: <Input
type="text"
placeholder="Event Code (optional)"
className="text-center"
size="default"
value={eventJoinCode}
onChange={(e) => {
setEventJoinCode(e.target.value)
}}
autoFocus
/>
}
<Button
type="submit"
variant="default"
Expand Down
78 changes: 63 additions & 15 deletions client/app/src/components/dialogs/Leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect } from "react"
import { useCallback, useEffect } from "react"

import { MedalIcon } from "@phosphor-icons/react"

Expand All @@ -14,30 +14,78 @@ const LEADERBOARD_URL =
(import.meta.env.VITE_SERVER_URL || "http://localhost:54321/functions/v1") +
(import.meta.env.VITE_SERVER_LEADERBOARD_ENDPOINT ?? "/leaderboard_resources")

const WORLD_EVENTS_URL =
(import.meta.env.VITE_SERVER_URL || "http://localhost:54321/functions/v1") + "/world_events_list"

const GLOBAL_DIALOG_CACHE_TTL = 1000 * 60 * 5 // 5 minutes
const EVENT_DIALOG_CACHE_TTL = 1000 * 60 * 1 // 1 minute
const WORLD_EVENTS_CACHE_TTL = 1000 * 60 * 1 // 1 minute

async function fetchLeaderboardForScope(scope: "global" | "event") {
const { playerEvent } = useGameStore.getState()
const url =
scope === "event" && playerEvent ?
`${LEADERBOARD_URL}?event_id=${playerEvent.event_id}`
: LEADERBOARD_URL
const response = await fetch(url)
const data = await response.json()
useGameStore.getState().setLeaderboardDialogData(data, scope)
}

async function fetchWorldEvents() {
try {
const response = await fetch(WORLD_EVENTS_URL)
const data = await response.json()
if (data.success) {
useGameStore.getState().setWorldEvents(data.events)
}
} catch (e) {
console.debug("[LEADERBOARD] World events fetch failed", e)
}
}

function isDialogDataStale(): boolean {
const { leaderboardDialogLastUpdated, leaderboardDialogScope, leaderboardScope } =
useGameStore.getState()

// Scope changed — always refetch
if (leaderboardDialogScope !== leaderboardScope) return true

// No data yet
if (!leaderboardDialogLastUpdated) return true

const age = Date.now() - new Date(leaderboardDialogLastUpdated).getTime()
const ttl = leaderboardScope === "event" ? EVENT_DIALOG_CACHE_TTL : GLOBAL_DIALOG_CACHE_TTL
return age > ttl
}

export const Leaderboard = () => {
const setActiveModal = useGameStore.use.setActiveModal()
const activeModal = useGameStore.use.activeModal?.()
const leaderboardScope = useGameStore((state) => state.leaderboardScope)
const leaderboardDialogScope = useGameStore((state) => state.leaderboardDialogScope)

// Fetch dialog data on open and when scope changes or data is stale
useEffect(() => {
if (activeModal?.modal !== "leaderboard") return

const fetchLeaderboard = async () => {
console.debug("[LEADERBOARD] Fetching leaderboard data...")
const response = await fetch(`${LEADERBOARD_URL}`)
const data = await response.json()
useGameStore.getState().setLeaderboardData(data)
console.debug("[LEADERBOARD] Fetched leaderboard data:", data)
if (isDialogDataStale()) {
fetchLeaderboardForScope(leaderboardScope)
}
const leaderboardLastUpdated = useGameStore.getState().leaderboard_last_updated

// Fetch world events (with TTL check)
const worldEventsLastUpdated = useGameStore.getState().worldEventsLastUpdated
if (
leaderboardLastUpdated &&
new Date(leaderboardLastUpdated).getTime() + 1000 * 60 * 5 > Date.now()
!worldEventsLastUpdated ||
new Date(worldEventsLastUpdated).getTime() + WORLD_EVENTS_CACHE_TTL <= Date.now()
) {
console.debug("[LEADERBOARD] Leaderboard data is up to date, skipping fetch")
return
fetchWorldEvents()
}
fetchLeaderboard()
}, [activeModal])
}, [activeModal, leaderboardScope, leaderboardDialogScope])

const handleScopeChange = useCallback((scope: "global" | "event") => {
useGameStore.getState().setLeaderboardScope(scope)
}, [])

return (
<BaseDialog modalName="leaderboard" title="Leaderboard" size="3xl">
Expand All @@ -49,7 +97,7 @@ export const Leaderboard = () => {
</CardTitle>
</CardHeader>
<CardContent className="h-full min-h-0">
<LeaderboardPanel />
<LeaderboardPanel onScopeChange={handleScopeChange} />
</CardContent>
<CardFooter className="flex flex-col gap-6">
<Divider decoration="plus" color="accent" />
Expand Down
Loading
Loading