Reapply "Add missing admin action files and navigation links"

This reverts commit 4d515bc400.
This commit is contained in:
Simo committed 2026-07-11 20:52:56 +02:00
1 parent 96ed768f14
commit 5b4228261a
338 files changed
+28143 -5948

No files matched your search

+2 -7
View File
@@ -136,9 +136,7 @@ export const HABBO_FONT_GROUPS: HabboFontGroup[] = [
},
{
groupName: "Custom creations",
fonts: [
{ name: "Jurrasic", styleName: "jurrasic" },
],
fonts: [{ name: "Jurrasic", styleName: "jurrasic" }],
},
{
groupName: "Halloween fonts",
@@ -211,11 +209,8 @@ export const HABBO_FONT_GROUPS: HabboFontGroup[] = [
},
{
groupName: "Special fonts",
fonts: [
{ name: "Relax", styleName: "relax" },
],
fonts: [{ name: "Relax", styleName: "relax" }],
},
];
export function buildFontUrl(styleName: string, text: string): string {
+17 -11
View File
@@ -60,7 +60,9 @@ export default function LogoGenerator() {
if (!cancelled) setFontError(true);
});
return () => { cancelled = true; };
return () => {
cancelled = true;
};
}, [styleName, safeText]);
const handleDownload = useCallback(() => {
@@ -81,7 +83,10 @@ export default function LogoGenerator() {
if (!canvas) return;
setSaveStatus("saving");
canvas.toBlob((blob) => {
if (!blob) { setSaveStatus("error"); return; }
if (!blob) {
setSaveStatus("error");
return;
}
const file = new File([blob], "logo.png", { type: "image/png" });
const fd = new FormData();
fd.append("file", file);
@@ -110,11 +115,16 @@ export default function LogoGenerator() {
try {
const info = await getFontInfo(font.styleName);
const canvas = renderToCanvas(info.chars, info.h, info.top, info.sheet, safeText, info.avgW);
if (!canvas) { fail++; continue; }
if (!canvas) {
fail++;
continue;
}
const blob = await new Promise<Blob | null>((r) => canvas.toBlob((b) => r(b), "image/png"));
if (blob) folder.file(`${font.styleName}.png`, blob);
else fail++;
} catch { fail++; }
} catch {
fail++;
}
}
const content = await zip.generateAsync({ type: "blob" });
@@ -198,12 +208,7 @@ export default function LogoGenerator() {
? "❌ Error"
: "💾 Save as site logo"}
</button>
<button
type="button"
className="btn btn-secondary"
onClick={handleDownloadAll}
disabled={zipping}
>
<button type="button" className="btn btn-secondary" onClick={handleDownloadAll} disabled={zipping}>
{zipping ? "⏳ Creating ZIP..." : "📦 Download all fonts"}
</button>
</div>
@@ -219,7 +224,8 @@ export default function LogoGenerator() {
borderRadius: "12px",
border: "1px dashed color-mix(in srgb, var(--color-text-muted) 30%, transparent)",
overflow: "auto",
background: "repeating-conic-gradient(color-mix(in srgb, var(--color-text-muted) 14%, transparent) 0% 25%, transparent 0% 50%) 50% / 22px 22px",
background:
"repeating-conic-gradient(color-mix(in srgb, var(--color-text-muted) 14%, transparent) 0% 25%, transparent 0% 50%) 50% / 22px 22px",
}}
>
{fontError ? (
+1 -5
View File
@@ -66,11 +66,7 @@ export function PhotoLightbox({ photos }: { photos: LightboxPhoto[] }) {
aria-label={p.alt}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={p.url}
alt={p.alt}
style={{ width: "100%", height: "auto", display: "block" }}
/>
<img src={p.url} alt={p.alt} style={{ width: "100%", height: "auto", display: "block" }} />
<div style={{ padding: "0.7rem 0.85rem" }}>
<p className="muted" style={{ margin: 0, fontSize: "0.85rem" }}>
{p.date}
+3 -8
View File
@@ -80,8 +80,7 @@ function parseNowPlaying(data: unknown): string {
if (np) return np;
// { song, artist } / { title, artist } at the top level.
const top =
joinSongArtist(data.song, data.artist) ?? joinSongArtist(data.title, data.artist);
const top = joinSongArtist(data.song, data.artist) ?? joinSongArtist(data.title, data.artist);
if (top) return top;
// AzureCast: { now_playing: { song: { title, artist, text } } }.
@@ -278,9 +277,7 @@ export default function RadioPlayer() {
height: "8px",
borderRadius: "999px",
background: playing ? "#16a34a" : muted,
boxShadow: playing
? "0 0 0 3px color-mix(in srgb, #16a34a 22%, transparent)"
: "none",
boxShadow: playing ? "0 0 0 3px color-mix(in srgb, #16a34a 22%, transparent)" : "none",
}}
/>
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
@@ -304,9 +301,7 @@ export default function RadioPlayer() {
</div>
<span
aria-label={
listeners != null ? `${listeners.toLocaleString()} listeners` : "Listeners"
}
aria-label={listeners != null ? `${listeners.toLocaleString()} listeners` : "Listeners"}
style={{
flex: "none",
display: "inline-flex",
+9 -3
View File
@@ -6,7 +6,10 @@ interface CharInfo {
w: number;
}
const fontCache = new Map<string, { chars: CharInfo[]; h: number; top: number; sheet: HTMLImageElement; avgW: number }>();
const fontCache = new Map<
string,
{ chars: CharInfo[]; h: number; top: number; sheet: HTMLImageElement; avgW: number }
>();
function isTransparent(data: Uint8ClampedArray, x: number, y: number, w: number): boolean {
return data[(y * w + x) * 4 + 3] === 0;
@@ -45,7 +48,9 @@ function scanCharWidths(data: Uint8ClampedArray, sw: number, sh: number): CharIn
return chars;
}
export async function getFontInfo(font: string): Promise<{ chars: CharInfo[]; h: number; top: number; sheet: HTMLImageElement; avgW: number }> {
export async function getFontInfo(
font: string,
): Promise<{ chars: CharInfo[]; h: number; top: number; sheet: HTMLImageElement; avgW: number }> {
const cached = fontCache.get(font);
if (cached) return cached;
@@ -68,7 +73,8 @@ export async function getFontInfo(font: string): Promise<{ chars: CharInfo[]; h:
const chars = scanCharWidths(imageData.data, canvas.width, canvas.height);
// measure total height by finding first/last non-empty row
let top = canvas.height, bottom = 0;
let top = canvas.height,
bottom = 0;
for (let y = 0; y < canvas.height; y++) {
for (let x = 0; x < canvas.width; x++) {
if (!isTransparent(imageData.data, x, y, canvas.width)) {
+2 -14
View File
@@ -48,15 +48,7 @@ export function ContentCard({
}
/** A value + label tile (profile stats, rare-value summaries, …). */
export function StatBlock({
value,
label,
icon,
}: {
value: ReactNode;
label: string;
icon?: string;
}) {
export function StatBlock({ value, label, icon }: { value: ReactNode; label: string; icon?: string }) {
return (
<div className="stat-block">
<div className="stat-block-value">
@@ -96,9 +88,7 @@ export function OnlineBadge({ online }: { online: boolean }) {
export function RankBadge({ position }: { position: number }) {
const medal = position === 1 ? "🥇" : position === 2 ? "🥈" : position === 3 ? "🥉" : null;
return (
<span className={`rank-badge rank-${position <= 3 ? position : "n"}`}>
{medal ?? `#${position}`}
</span>
<span className={`rank-badge rank-${position <= 3 ? position : "n"}`}>{medal ?? `#${position}`}</span>
);
}
@@ -134,5 +124,3 @@ export function SkeletonCard({ lines = 3 }: { lines?: number }) {
</div>
);
}