Wer ueber Discord hereinkommt, sieht sein Bild in der Kopfzeile, auf der
Kontoseite und neben seinen Eintraegen im Verlauf.
DIE ADRESSE BAUT DER DIENST, nicht die Oberflaeche. Gespeichert wird nur der
Bildstempel; das URL-Schema von Discord steht an EINER Stelle (avatarUrl in
db.ts) und nicht in jeder Ansicht, die ein Bild zeigt. Es hat drei Faelle, und
alle drei stehen dort:
- Stempel faengt mit "a_" an -> bewegtes Bild, muss als .gif angefragt werden
- Stempel vorhanden -> .png mit gewuenschter Groesse
- kein Stempel -> Discords Ersatzbild, dessen Nummer sich aus
der ID ergibt: (id >> 22) % 6. Das ist die
NEUE Rechnung; die alte ueber den
Diskriminator gilt seit den Pseudonymen
nicht mehr. Gegengerechnet an drei IDs.
Bild und Anzeigename werden BEI JEDER ANMELDUNG nachgezogen: wer sein Bild
wechselt, saehe sonst monatelang das alte und fragte sich, ob er im richtigen
Konto sitzt. Der Benutzername im Dashboard bleibt dagegen, wie er ist -- an ihm
haengen die Eintraege im Verlauf.
Der Avatar-Baustein kennt drei Faelle und keinen kaputten: Bild, sonst die
Initialen auf einer Farbe, die sich aus dem Namen ergibt (damit sie beim
Neuladen nicht springt), und fuer einen Token einen Schluessel -- ein Token hat
kein Gesicht, ein Platzhalterbild daneben waere eine Behauptung. Laedt das Bild
nicht (CDN nicht erreichbar, Stempel veraltet), faellt es auf die Initialen
zurueck statt ein kaputtes Bildsymbol zu zeigen.
Nachgemessen: cdn.discordapp.com liefert das Ersatzbild (200, image/png), die
Form der Bildadresse stimmt, und ein Passwort-Konto bekommt sauber avatar:null.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
150 lines
5.9 KiB
TypeScript
150 lines
5.9 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { HardDrive, History, Images, KeyRound, LogOut, PieChart, UserCog } from 'lucide-react'
|
|
import { run, useStore } from './store'
|
|
import { Anmelden } from './pages/Anmelden'
|
|
import { Galerie } from './pages/Galerie'
|
|
import { Speicher } from './pages/Speicher'
|
|
import { Tokens } from './pages/Tokens'
|
|
import { Konto } from './pages/Konto'
|
|
import { Verlauf } from './pages/Verlauf'
|
|
import { Meldungen } from './components/Meldungen'
|
|
import { Avatar } from './components/Avatar'
|
|
|
|
// Ein winziger Router über die History-API statt einer Bibliothek.
|
|
//
|
|
// Echte Pfade und keine Reiter im Zustand, weil der Dienst dafür schon gebaut
|
|
// ist: index.ts liefert auf alles Unbekannte die index.html aus, damit ein
|
|
// Neuladen auf /tokens im Router landet und nicht im 404.
|
|
|
|
const seiten = [
|
|
{ pfad: '/', name: 'Galerie', kind: Images, Inhalt: Galerie },
|
|
{ pfad: '/verlauf', name: 'Verlauf', kind: History, Inhalt: Verlauf },
|
|
{ pfad: '/speicher', name: 'Speicher', kind: PieChart, Inhalt: Speicher },
|
|
{ pfad: '/tokens', name: 'Token', kind: KeyRound, Inhalt: Tokens },
|
|
{ pfad: '/konto', name: 'Konto', kind: UserCog, Inhalt: Konto },
|
|
] as const
|
|
|
|
function usePfad(): [string, (p: string) => void] {
|
|
const [pfad, setPfad] = useState(window.location.pathname)
|
|
|
|
useEffect(() => {
|
|
const zurueck = () => setPfad(window.location.pathname)
|
|
window.addEventListener('popstate', zurueck)
|
|
return () => window.removeEventListener('popstate', zurueck)
|
|
}, [])
|
|
|
|
return [
|
|
pfad,
|
|
(p: string) => {
|
|
window.history.pushState(null, '', p)
|
|
setPfad(p)
|
|
},
|
|
]
|
|
}
|
|
|
|
export function App() {
|
|
// Einzeln ausgewählt und nicht als ganzer Laden: `useStore()` ohne
|
|
// Auswahlfunktion abonniert ALLES, und dann zeichnet jede Meldung das
|
|
// ganze Dashboard neu — bei 60 Kacheln mit Bildern ist das spürbar.
|
|
const user = useStore((s) => s.user)
|
|
const checking = useStore((s) => s.checking)
|
|
const checkSession = useStore((s) => s.checkSession)
|
|
const logout = useStore((s) => s.logout)
|
|
const [pfad, gehZu] = usePfad()
|
|
|
|
useEffect(() => {
|
|
void checkSession()
|
|
}, [checkSession])
|
|
|
|
// Solange nicht feststeht, ob eine Sitzung besteht, wird NICHTS gezeigt.
|
|
// Sonst blitzt bei jedem Neuladen kurz das Anmeldeformular auf, obwohl man
|
|
// angemeldet ist — und das sieht aus wie ein Fehler.
|
|
if (checking) {
|
|
return (
|
|
<div className="flex min-h-full items-center justify-center">
|
|
<HardDrive className="animate-pulse text-schrift-leise" />
|
|
<Meldungen />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!user) {
|
|
return (
|
|
<>
|
|
<Anmelden />
|
|
<Meldungen />
|
|
</>
|
|
)
|
|
}
|
|
|
|
const aktiv = seiten.find((s) => s.pfad === pfad) ?? seiten[0]
|
|
const Inhalt = aktiv.Inhalt
|
|
|
|
return (
|
|
<div className="flex min-h-full flex-col">
|
|
<header className="sticky top-0 z-40 border-b border-rand bg-flaeche/80 backdrop-blur">
|
|
<div className="mx-auto flex max-w-7xl flex-wrap items-center gap-x-6 gap-y-2 px-4 py-2.5">
|
|
<a
|
|
href="/"
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
gehZu('/')
|
|
}}
|
|
className="flex items-center gap-2 font-semibold"
|
|
>
|
|
<HardDrive size={18} className="text-akzent" />
|
|
d4rk_media
|
|
</a>
|
|
|
|
<nav className="flex flex-1 gap-1">
|
|
{seiten.map((s) => {
|
|
const an = s.pfad === aktiv.pfad
|
|
return (
|
|
<a
|
|
key={s.pfad}
|
|
href={s.pfad}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
gehZu(s.pfad)
|
|
}}
|
|
className={`flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm
|
|
transition-colors ${
|
|
an
|
|
? 'bg-flaeche-hoch text-schrift'
|
|
: 'text-schrift-leise hover:text-schrift'
|
|
}`}
|
|
>
|
|
<s.kind size={15} />
|
|
{s.name}
|
|
</a>
|
|
)
|
|
})}
|
|
</nav>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Avatar name={user.username} url={user.avatar} groesse={26} />
|
|
<span className="hidden text-sm text-schrift-leise sm:inline">
|
|
{user.username}
|
|
</span>
|
|
<button
|
|
onClick={() => void run(logout, 'Abgemeldet.')}
|
|
className="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm
|
|
text-schrift-leise hover:text-schrift"
|
|
aria-label="Abmelden"
|
|
>
|
|
<LogOut size={15} />
|
|
<span className="hidden md:inline">Abmelden</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="mx-auto w-full max-w-7xl flex-1 px-4 py-5">
|
|
<Inhalt />
|
|
</main>
|
|
|
|
<Meldungen />
|
|
</div>
|
|
)
|
|
}
|