"use client"; import Image from "next/image"; import { useEffect, useState } from "react"; import { GameProfileModal } from "@/components/game-profile-modal"; import type { Dictionary } from "@/lib/i18n/get-dictionary"; import { useLanyard } from "@/hooks/use-lanyard"; import { activityImageUrl, discordAvatarUrl, formatSpotifyList, spotifyTrackUrl, type DiscordStatus, type LanyardActivity, type LanyardData, } from "@/lib/game/lanyard"; import { discordGameUrl, type DiscordAppInfo } from "@/lib/game/discord-app"; import { fetchJson } from "@/lib/client-fetch"; import { formatElapsed } from "@/lib/game/game-format"; import type { Locale } from "@/lib/i18n/locales"; const STATUS_DOT: Record = { online: "bg-emerald-400", idle: "bg-amber-400", dnd: "bg-rose-500", offline: "bg-zinc-500", }; const MS_PER_SECOND = 1000; const SECONDS_PER_MINUTE = 60; function formatTrackTime(ms: number): string { const totalSeconds = Math.max(0, Math.floor(ms / MS_PER_SECOND)); const minutes = Math.floor(totalSeconds / SECONDS_PER_MINUTE); const seconds = totalSeconds % SECONDS_PER_MINUTE; return `${minutes}:${seconds.toString().padStart(2, "0")}`; } function SpotifyProgress({ start, end }: { start: number; end: number }) { const [now, setNow] = useState(start); useEffect(() => { const timer = window.setInterval(() => setNow(Date.now()), 1_000); return () => window.clearInterval(timer); }, []); const total = Math.max(1, end - start); const elapsed = Math.min(Math.max(0, now - start), total); const ratio = Math.min(1, Math.max(0, (now - start) / total)); const percent = Math.round(ratio * 100); return (
{formatTrackTime(elapsed)}
{formatTrackTime(total)}
); } const DISCORD_APP_CACHE_LIMIT = 50; const discordAppCache = new Map(); export function _resetDiscordAppCache(): void { discordAppCache.clear(); } function cacheDiscordApp(id: string, info: DiscordAppInfo): void { if (!discordAppCache.has(id) && discordAppCache.size >= DISCORD_APP_CACHE_LIMIT) { const oldest = discordAppCache.keys().next(); if (!oldest.done) { discordAppCache.delete(oldest.value); } } discordAppCache.set(id, info); } function useDiscordApp(applicationId: string | null | undefined): DiscordAppInfo | null { const [info, setInfo] = useState(() => applicationId ? (discordAppCache.get(applicationId) ?? null) : null, ); const [prevId, setPrevId] = useState(applicationId); if (prevId !== applicationId) { setPrevId(applicationId); setInfo(applicationId ? (discordAppCache.get(applicationId) ?? null) : null); } useEffect(() => { if (!applicationId || discordAppCache.has(applicationId)) { return; } const id = applicationId; let cancelled = false; async function load(): Promise { const resolved = await fetchJson(`/api/discord-app/${id}`); if (resolved) { cacheDiscordApp(id, resolved); } if (!cancelled) { setInfo(resolved); } } void load(); return () => { cancelled = true; }; }, [applicationId]); return info; } function ActivityRow({ activity, dict, locale, }: { activity: LanyardActivity; dict: Dictionary["discord"]; locale: Locale; }) { const appInfo = useDiscordApp(activity.application_id); const [profileOpen, setProfileOpen] = useState(false); const [heroSeed, setHeroSeed] = useState(0); const large = activityImageUrl(activity, "large") ?? appInfo?.iconUrl ?? null; const small = activityImageUrl(activity, "small"); const gameUrl = activity.application_id ? (appInfo?.gameUrl ?? discordGameUrl(activity.application_id)) : null; return (
{large ? (
{activity.assets?.large_text {small ? ( {activity.assets?.small_text ) : null}
) : (
{activity.name.charAt(0).toUpperCase()}
)}

{activity.name}

{activity.details ? (

{activity.details}

) : null} {activity.state ? (

{activity.state}

) : null}
{activity.timestamps?.start ? ( {formatElapsed(activity.timestamps.start, locale)} ) : null} {gameUrl ? ( ) : null}
{profileOpen && activity.application_id ? ( setProfileOpen(false)} /> ) : null}
); } export function DiscordCard({ initial, dict, locale, }: { initial: LanyardData | null; dict: Dictionary["discord"]; locale: Locale; }) { const data = useLanyard(initial); if (!data) { return (
{dict.offline}
); } const user = data.discord_user; const displayName = user.display_name ?? user.global_name ?? user.username; const customStatus = data.activities.find((a) => a.type === 4); const games = data.activities.filter((a) => a.type === 0); return (
{displayName}

{displayName}

@{user.username}

{dict[data.discord_status]} · {dict.live}

{customStatus?.state ? (

{dict.customStatus} {customStatus.emoji?.name ? ( {customStatus.emoji.name} ) : null} {customStatus.state}

) : null} {games.length > 0 ? (

{dict.playing}

{games.map((game) => ( ))}
) : null} {data.listening_to_spotify && data.spotify ? (
{data.spotify.album_art_url ? ( {formatSpotifyList(data.spotify.song)} ) : null}

{dict.listening} Spotify

{formatSpotifyList(data.spotify.song)}

{formatSpotifyList(data.spotify.artist)}

{data.spotify.timestamps ? ( ) : null} {dict.openOnSpotify} ↗
) : null} {games.length === 0 && !data.listening_to_spotify && !customStatus?.state ? (

{dict.noActivity}

) : null}
); }