Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a2d73a6cb | |||
| ebe0842759 |
@@ -0,0 +1,76 @@
|
|||||||
|
.discord-status-compact {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discord-avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
box-shadow: 2px 2px 0px var(--pink-accent);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
right: -2px;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 0 2px #fffdfd;
|
||||||
|
}
|
||||||
|
.status-dot.online { background-color: #a7f3d0; border: 1px solid #34d399; }
|
||||||
|
.status-dot.idle { background-color: #fef08a; border: 1px solid #facc15; }
|
||||||
|
.status-dot.dnd { background-color: #fecdd3; border: 1px solid #fb7185; }
|
||||||
|
.status-dot.offline { background-color: var(--text-dim); border: 1px solid var(--text-main); }
|
||||||
|
|
||||||
|
.status-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text em {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--text-title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bubble-inline {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
border-bottom: 1px dashed var(--border);
|
||||||
|
max-width: 220px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 1px 1px 0px var(--pink-accent);
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import './discordstatus.css';
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface LanyardResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: LanyardData;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LanyardData {
|
||||||
|
discord_status: 'online' | 'idle' | 'dnd' | 'offline';
|
||||||
|
activities: DiscordActivity[];
|
||||||
|
discord_user: DiscordUser;
|
||||||
|
active_on_discord_web: boolean;
|
||||||
|
active_on_discord_desktop: boolean;
|
||||||
|
active_on_discord_mobile: boolean;
|
||||||
|
active_on_discord_embedded: boolean;
|
||||||
|
active_on_discord_vr: boolean;
|
||||||
|
listening_to_spotify: boolean;
|
||||||
|
spotify: null | Record<string, any>;
|
||||||
|
kv: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiscordUser {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
discriminator: string;
|
||||||
|
global_name: string;
|
||||||
|
display_name: string;
|
||||||
|
avatar: string;
|
||||||
|
bot: boolean;
|
||||||
|
public_flags: number;
|
||||||
|
avatar_decoration_data: null | any;
|
||||||
|
collectibles?: Record<string, any>;
|
||||||
|
display_name_styles?: Record<string, any>;
|
||||||
|
primary_guild?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiscordActivity {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: number;
|
||||||
|
session_id?: string;
|
||||||
|
created_at: number;
|
||||||
|
state?: string;
|
||||||
|
details?: string;
|
||||||
|
timestamps?: {
|
||||||
|
start?: number;
|
||||||
|
end?: number;
|
||||||
|
};
|
||||||
|
assets?: {
|
||||||
|
large_image?: string;
|
||||||
|
large_text?: string;
|
||||||
|
small_image?: string;
|
||||||
|
small_text?: string;
|
||||||
|
};
|
||||||
|
emoji?: {
|
||||||
|
name: string;
|
||||||
|
id?: string;
|
||||||
|
animated?: boolean;
|
||||||
|
};
|
||||||
|
application_id?: string;
|
||||||
|
flags?: number;
|
||||||
|
platform?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDiscordAsset(image: string | undefined): string {
|
||||||
|
if (!image) return "";
|
||||||
|
|
||||||
|
if (image.startsWith("mp:external/")) {
|
||||||
|
const httpsIndex = image.indexOf("/https/");
|
||||||
|
if (httpsIndex !== -1) {
|
||||||
|
const after = image.slice(httpsIndex + "/https/".length);
|
||||||
|
return `https://${after}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\d+$/.test(image) || image.startsWith("spotify:")) return "";
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiscordStatusParams {
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_LABELS: Record<LanyardData['discord_status'], string> = {
|
||||||
|
online: 'online',
|
||||||
|
idle: 'away',
|
||||||
|
dnd: 'busy',
|
||||||
|
offline: 'offline'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DiscordStatus({ userId }: DiscordStatusParams) {
|
||||||
|
const [presence, setPresence] = useState<LanyardData | null>(null);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchRichPresence() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://api.lanyard.rest/v1/users/${userId}`);
|
||||||
|
const json: LanyardResponse = await response.json();
|
||||||
|
if (json.success) {
|
||||||
|
setPresence(json.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch Lanyard presence:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRichPresence();
|
||||||
|
const interval = setInterval(fetchRichPresence, 30000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <p style={{ fontSize: '0.75rem', fontStyle: 'italic', color: 'var(--text-dim)' }}>loading status...</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!presence) {
|
||||||
|
return <p style={{ fontSize: '0.75rem', fontStyle: 'italic', color: 'var(--text-dim)' }}>offline</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customActivity = presence.activities.find(act => act.id === "custom");
|
||||||
|
const customStatusText = customActivity
|
||||||
|
? `${customActivity.emoji?.name || ''} ${customActivity.state || ''}`.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const gameActivity = presence.activities
|
||||||
|
.filter(act => act.type === 0)
|
||||||
|
.sort((a, b) => (b.assets ? 1 : 0) - (a.assets ? 1 : 0))[0] as DiscordActivity | undefined;
|
||||||
|
|
||||||
|
const gameImage = gameActivity?.assets
|
||||||
|
? resolveDiscordAsset(gameActivity.assets.small_image || gameActivity.assets.large_image)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="discord-status-compact">
|
||||||
|
<div className="avatar-container">
|
||||||
|
<img
|
||||||
|
src={`https://api.lanyard.rest/${userId}.png`}
|
||||||
|
alt="Discord avatar"
|
||||||
|
className="discord-avatar"
|
||||||
|
/>
|
||||||
|
<span className={`status-dot ${presence.discord_status}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="status-details">
|
||||||
|
<span className="status-text">
|
||||||
|
{gameActivity ? (
|
||||||
|
<>
|
||||||
|
playing: <em>{gameActivity.name.toLowerCase()}</em>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
currently: <em>{STATUS_LABELS[presence.discord_status]}</em>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{customStatusText && (
|
||||||
|
<span className="status-bubble-inline">
|
||||||
|
{customStatusText}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{gameImage && gameActivity && (
|
||||||
|
<img
|
||||||
|
src={gameImage}
|
||||||
|
alt={gameActivity.name}
|
||||||
|
className="game-icon"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import './page.css';
|
import './page.css';
|
||||||
|
import { DiscordStatus } from './components/discordstatus';
|
||||||
|
|
||||||
const TWITTER_LINK = "https://x.com/neruu444"
|
const TWITTER_LINK = "https://x.com/neruu444"
|
||||||
const DISCORD_USER = "neru444"
|
const DISCORD_USER = "neru444"
|
||||||
@@ -27,6 +28,11 @@ function Content() {
|
|||||||
<a href={STEAM_LINK} style={{ cursor: 'pointer' }}>steam</a>
|
<a href={STEAM_LINK} style={{ cursor: 'pointer' }}>steam</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<section className="content-box">
|
||||||
|
<h2 className="title">✧ discord ✧</h2>
|
||||||
|
<DiscordStatus userId={DISCORD_ID} />
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="content-box">
|
<section className="content-box">
|
||||||
<h2 className="title">✧ projects im currently working on ✧</h2>
|
<h2 className="title">✧ projects im currently working on ✧</h2>
|
||||||
<ul className="directory">
|
<ul className="directory">
|
||||||
|
|||||||
Reference in New Issue
Block a user