Discord presence overhaul: game profiles, Spotify bar, Lanyard WS
Build & Push / Lint & Build (push) Successful in 1m19s
Build & Push / Build & Push Image (push) Successful in 31s

- Spotify: drop album line, normalize ";" separators to ",", add open-in-Spotify link and live progress bar
- Games: resolve missing artwork via public Discord RPC app icons with placeholder fallback; add in-site game profile modal (IGDB + Steam + ITAD: artwork, details, reviews, prices)
- Proxy game data through same-origin /api/game and /api/discord-app routes, CORS-locked to https://fristy.net
- Lanyard: SSR REST first paint, then live WebSocket with heartbeat, reconnect backoff and polling fallback
- Localize presence UI (tr/en/de): elapsed times, dates, numbers, currency, review labels, game descriptions
- Sort projects by last update, descending
- Slim dark scrollbars, compact Spotify progress bar
This commit is contained in:
fristy committed 2026-09-10 14:36:14 +03:00
1 parent ad943fff8c
commit bcc5662a44
16 files changed
+1888 -43

No files matched your search

+90
View File
@@ -2,6 +2,8 @@ export const DISCORD_ID = "1014549805608992901";
const LANYARD_API = `https://api.lanyard.rest/v1/users/${DISCORD_ID}`;
export const LANYARD_WS = "wss://api.lanyard.rest/socket";
export type DiscordStatus = "online" | "idle" | "dnd" | "offline";
export interface LanyardEmoji {
@@ -48,12 +50,100 @@ export function activityImageUrl(activity: LanyardActivity, small: boolean): str
export interface LanyardSpotify {
track_id: string;
timestamps: {
start: number;
end: number;
};
song: string;
artist: string;
album: string;
album_art_url: string | null;
}
export function formatSpotifyList(value: string | null | undefined): string {
if (!value) {
return "";
}
return value
.replace(/;/g, ",")
.replace(/,[ \t]+,/g, ",")
.replace(/\s{2,}/g, " ")
.trim();
}
export function spotifyTrackUrl(trackId: string): string {
return `https://open.spotify.com/track/${trackId}`;
}
export function discordAppIconUrl(
applicationId: string,
iconHash: string | null,
size = 128,
): string | null {
if (!iconHash) {
return null;
}
const extension = iconHash.startsWith("a_") ? "gif" : "png";
return `https://cdn.discordapp.com/app-icons/${applicationId}/${iconHash}.${extension}?size=${size}`;
}
export function discordGameUrl(gameId: string): string {
return `https://discord.com/games/${gameId}`;
}
export interface DiscordAppInfo {
id: string;
name: string;
icon: string | null;
iconUrl: string | null;
coverImage: string | null;
coverUrl: string | null;
gameId: string;
gameUrl: string;
}
interface DiscordRpcResponse {
id: string;
name: string;
icon?: string | null;
cover_image?: string | null;
linked_games?: Array<{ id: string }> | null;
}
export async function getDiscordAppInfo(applicationId: string): Promise<DiscordAppInfo | null> {
if (!/^\d+$/.test(applicationId)) {
return null;
}
try {
const response = await fetch(
`https://discord.com/api/v10/applications/${applicationId}/rpc`,
{
headers: { "User-Agent": "fristy.net (personal site)" },
next: { revalidate: 86_400 },
},
);
if (!response.ok) {
return null;
}
const json = (await response.json()) as DiscordRpcResponse;
const gameId = json.linked_games?.[0]?.id ?? json.id ?? applicationId;
return {
id: json.id ?? applicationId,
name: json.name ?? "",
icon: json.icon ?? null,
iconUrl: discordAppIconUrl(applicationId, json.icon ?? null),
coverImage: json.cover_image ?? null,
coverUrl: json.cover_image
? `https://cdn.discordapp.com/app-icons/${applicationId}/${json.cover_image}.png?size=512`
: null,
gameId,
gameUrl: discordGameUrl(gameId),
};
} catch {
return null;
}
}
export interface LanyardData {
discord_user: {
id: string;