feat: mobile nav, radio layout/requests, leaderboard tabs, client i18n, habbo background, navbar fixes
This commit is contained in:
1 parent
16096eff0f
commit
d6ccc250f6
20 files changed
+936
-140
No files matched your search
@@ -58,15 +58,15 @@ export function LanguageSwitcher() {
|
||||
|
||||
{open && (
|
||||
<div
|
||||
className="absolute right-0 mt-1 min-w-[160px] rounded-md shadow-lg z-50 py-1"
|
||||
style={{ backgroundColor: "var(--color-dropdown)" }}
|
||||
className="absolute right-0 mt-1 min-w-[160px] rounded-xl shadow-xl z-50 py-2 border backdrop-blur-md"
|
||||
style={{ backgroundColor: "color-mix(in srgb, var(--color-dropdown) 97%, transparent)", borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)" }}
|
||||
>
|
||||
{LOCALES.map((l) => (
|
||||
<button
|
||||
key={l.code}
|
||||
type="button"
|
||||
onClick={() => switchLocale(l.code)}
|
||||
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm font-semibold transition-colors ${
|
||||
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm font-medium transition-all duration-100 rounded-lg mx-1 ${
|
||||
l.code === locale ? "opacity-100" : "opacity-70 hover:opacity-100"
|
||||
}`}
|
||||
style={{
|
||||
@@ -74,6 +74,15 @@ export function LanguageSwitcher() {
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
width: "calc(100% - 8px)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "color-mix(in srgb, var(--color-primary) 10%, transparent)";
|
||||
e.currentTarget.style.color = "var(--color-primary)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
e.currentTarget.style.color = "var(--color-text)";
|
||||
}}
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export function MobileNav({ children }: { children: React.ReactNode }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex md:hidden items-center justify-center w-10 h-10 rounded-lg transition-colors duration-150"
|
||||
style={{ color: "var(--color-navbar-text)" }}
|
||||
aria-label={open ? "Sluit menu" : "Open menu"}
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
{open ? (
|
||||
<>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
className="fixed inset-0 z-30 md:hidden"
|
||||
onClick={() => setOpen(false)}
|
||||
style={{ backgroundColor: "rgba(0,0,0,0.4)" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`${open ? "max-h-[80vh] opacity-100" : "max-h-0 opacity-0 pointer-events-none md:pointer-events-auto"} overflow-hidden transition-all duration-300 ease-in-out md:!max-h-none md:!opacity-100 md:!overflow-visible md:flex md:items-center md:gap-x-1 w-full md:w-auto`}
|
||||
>
|
||||
<div
|
||||
className="flex flex-col md:flex-row md:items-center gap-y-0.5 p-3 md:p-0 rounded-xl md:rounded-none border md:border-0 shadow-lg md:shadow-none mt-2 md:mt-0"
|
||||
style={{
|
||||
backgroundColor: "var(--color-navbar)",
|
||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const COLORS = [
|
||||
{ name: "Wit", value: "#ffffff", text: "#1e293b" },
|
||||
{ name: "Zwart", value: "#0f172a", text: "#f8fafc" },
|
||||
{ name: "Donker", value: "#1e293b", text: "#f8fafc" },
|
||||
{ name: "Grijs", value: "#f1f5f9", text: "#1e293b" },
|
||||
{ name: "Amber", value: "#f59e0b", text: "#1e293b" },
|
||||
{ name: "Goud", value: "#eab308", text: "#1e293b" },
|
||||
{ name: "Oranje", value: "#ea580c", text: "#ffffff" },
|
||||
{ name: "Rood", value: "#ef4444", text: "#ffffff" },
|
||||
{ name: "Roze", value: "#ec4899", text: "#ffffff" },
|
||||
{ name: "Paars", value: "#8b5cf6", text: "#ffffff" },
|
||||
{ name: "Indigo", value: "#6366f1", text: "#ffffff" },
|
||||
{ name: "Blauw", value: "#3b82f6", text: "#ffffff" },
|
||||
{ name: "Cyaan", value: "#06b6d4", text: "#ffffff" },
|
||||
{ name: "Groen", value: "#22c55e", text: "#1e293b" },
|
||||
{ name: "Olijf", value: "#84cc16", text: "#1e293b" },
|
||||
];
|
||||
|
||||
export function NavbarColorPicker() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [active, setActive] = useState(() => {
|
||||
if (typeof document === "undefined") return 0;
|
||||
const current = getComputedStyle(document.documentElement).getPropertyValue("--color-navbar").trim();
|
||||
const idx = COLORS.findIndex((c) => c.value === current);
|
||||
return idx >= 0 ? idx : 0;
|
||||
});
|
||||
|
||||
function apply(index: number) {
|
||||
const c = COLORS[index];
|
||||
if (!c) return;
|
||||
document.documentElement.style.setProperty("--color-navbar", c.value);
|
||||
document.documentElement.style.setProperty("--color-navbar-text", c.text);
|
||||
try {
|
||||
localStorage.setItem("navbarColor", c.value);
|
||||
localStorage.setItem("navbarTextColor", c.text);
|
||||
} catch { }
|
||||
setActive(index);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem("navbarColor");
|
||||
const savedText = localStorage.getItem("navbarTextColor");
|
||||
if (saved && savedText) {
|
||||
document.documentElement.style.setProperty("--color-navbar", saved);
|
||||
document.documentElement.style.setProperty("--color-navbar-text", savedText);
|
||||
const idx = COLORS.findIndex((c) => c.value === saved);
|
||||
if (idx >= 0) setActive(idx);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
className="nav-item cursor-pointer px-2"
|
||||
aria-label="Navbar kleur"
|
||||
title="Navbar kleur"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="13.5" cy="6.5" r="0.5" fill="currentColor" />
|
||||
<circle cx="17.5" cy="10.5" r="0.5" fill="currentColor" />
|
||||
<circle cx="8.5" cy="7.5" r="0.5" fill="currentColor" />
|
||||
<circle cx="6.5" cy="12.5" r="0.5" fill="currentColor" />
|
||||
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.93 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.01-.23-.26-.38-.61-.38-1 0-.83.67-1.5 1.5-1.5H16c3.31 0 6-2.69 6-6 0-5.5-4.5-10-10-10z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
className="absolute right-0 mt-1 min-w-[200px] rounded-xl shadow-xl z-50 py-3 px-2 border backdrop-blur-md"
|
||||
style={{ backgroundColor: "color-mix(in srgb, var(--color-dropdown) 97%, transparent)", borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)" }}
|
||||
>
|
||||
<div className="text-xs font-semibold uppercase tracking-wider mb-2 px-2" style={{ color: "var(--color-text-muted)" }}>Navbar kleur</div>
|
||||
<div className="grid grid-cols-5 gap-1.5">
|
||||
{COLORS.map((c, i) => (
|
||||
<button
|
||||
key={c.name}
|
||||
type="button"
|
||||
onClick={() => apply(i)}
|
||||
className="flex flex-col items-center gap-1 p-1.5 rounded-lg transition-all duration-100"
|
||||
title={c.name}
|
||||
style={{
|
||||
background: i === active ? "color-mix(in srgb, var(--color-primary) 12%, transparent)" : "transparent",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (i !== active) e.currentTarget.style.backgroundColor = "color-mix(in srgb, var(--color-text-muted) 8%, transparent)";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (i !== active) e.currentTarget.style.backgroundColor = "transparent";
|
||||
}}
|
||||
>
|
||||
<span className="block w-6 h-6 rounded-full border-2 shrink-0" style={{ backgroundColor: c.value, borderColor: i === active ? "var(--color-primary)" : "color-mix(in srgb, var(--color-text-muted) 20%, transparent)" }} />
|
||||
<span className="text-[10px] font-medium leading-tight text-center" style={{ color: "var(--color-text-muted)" }}>{c.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+109
-75
@@ -2,6 +2,8 @@ import { getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||
import { MobileNav } from "@/components/mobile-nav";
|
||||
import { NavbarColorPicker } from "@/components/navbar-color-picker";
|
||||
import { ThemeSwitcher } from "@/components/theme-switcher";
|
||||
|
||||
export async function Navigation() {
|
||||
@@ -10,85 +12,117 @@ export async function Navigation() {
|
||||
const isStaff = (session?.user?.rank ?? 0) >= 7;
|
||||
|
||||
return (
|
||||
<nav className="relative shadow-sm" style={{ backgroundColor: "var(--color-navbar)" }}>
|
||||
<div className="mx-auto flex min-h-[60px] max-w-7xl flex-wrap items-center px-4 gap-x-1 md:gap-x-2">
|
||||
<Link href="/" className="nav-item">
|
||||
<img src="/assets/images/icons/navigation/me.png" alt="" className="mr-1 hidden w-5 h-5 lg:inline-block" />
|
||||
{t("home")}
|
||||
</Link>
|
||||
<nav className="sticky top-0 z-40 shadow-sm" style={{ backgroundColor: "var(--color-navbar)" }}>
|
||||
<div className="relative mx-auto flex min-h-[56px] max-w-7xl items-center px-4 md:px-6 gap-x-1">
|
||||
<div className="absolute bottom-0 left-0 right-0 h-[3px]" style={{ backgroundColor: "var(--color-primary, #eeb425)" }} />
|
||||
|
||||
<div className="flex items-center gap-x-1 md:gap-x-1 flex-1 md:flex-none">
|
||||
<Link href="/" className="nav-item shrink-0">
|
||||
<img src="/assets/images/icons/navigation/me.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
<span className="hidden sm:inline">{t("home")}</span>
|
||||
<span className="sm:hidden">{t("home")}</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<details className="relative">
|
||||
<summary className="nav-item list-none cursor-pointer [&::-webkit-details-marker]:hidden">
|
||||
<img src="/assets/images/icons/navigation/community.png" alt="" className="mr-1 hidden w-5 h-5 lg:inline-block" />
|
||||
{t("community")}
|
||||
</summary>
|
||||
<div
|
||||
className="absolute left-0 mt-1 min-w-[190px] rounded-md shadow-lg z-50 py-1"
|
||||
style={{ backgroundColor: "var(--color-dropdown)" }}
|
||||
>
|
||||
<Link href="/news" className="dropdown-item">{t("articles")}</Link>
|
||||
<Link href="/photos" className="dropdown-item">{t("photos")}</Link>
|
||||
<Link href="/staff" className="dropdown-item">{t("staff")}</Link>
|
||||
<Link href="/rankings" className="dropdown-item">{t("rankings")}</Link>
|
||||
<Link href="/guilds" className="dropdown-item">{t("guilds")}</Link>
|
||||
<MobileNav>
|
||||
<Link href="/" className="nav-item md:hidden">
|
||||
<img src="/assets/images/icons/navigation/me.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
{t("home")}
|
||||
</Link>
|
||||
|
||||
<details className="relative group md:flex">
|
||||
<summary className="nav-item list-none cursor-pointer [&::-webkit-details-marker]:hidden">
|
||||
<img src="/assets/images/icons/navigation/community.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
{t("community")}
|
||||
<svg className="ml-auto md:ml-1 w-3 h-3 transition-transform duration-200 group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
||||
</summary>
|
||||
<div
|
||||
className="md:absolute static left-0 mt-1.5 md:min-w-[200px] rounded-xl md:shadow-xl z-50 py-2 border backdrop-blur-md md:ml-0 ml-4"
|
||||
style={{ backgroundColor: "color-mix(in srgb, var(--color-dropdown) 97%, transparent)", borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)" }}
|
||||
>
|
||||
<Link href="/news" className="dropdown-item">{t("articles")}</Link>
|
||||
<Link href="/photos" className="dropdown-item">{t("photos")}</Link>
|
||||
<Link href="/staff" className="dropdown-item">{t("staff")}</Link>
|
||||
<Link href="/rankings" className="dropdown-item">{t("rankings")}</Link>
|
||||
<Link href="/guilds" className="dropdown-item">{t("guilds")}</Link>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details className="relative group md:flex">
|
||||
<summary className="nav-item list-none cursor-pointer [&::-webkit-details-marker]:hidden">
|
||||
<img src="/assets/images/icons/navigation/shop.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
{t("shop")}
|
||||
<svg className="ml-auto md:ml-1 w-3 h-3 transition-transform duration-200 group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
||||
</summary>
|
||||
<div
|
||||
className="md:absolute left-0 mt-1.5 min-w-[200px] rounded-xl shadow-xl z-50 py-2 border backdrop-blur-md"
|
||||
style={{ backgroundColor: "color-mix(in srgb, var(--color-dropdown) 97%, transparent)", borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)" }}
|
||||
>
|
||||
<Link href="/shop" className="dropdown-item">{t("shop")}</Link>
|
||||
<Link href="/marketplace" className="dropdown-item">{t("market")}</Link>
|
||||
<Link href="/rares" className="dropdown-item">{t("rareValues")}</Link>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<Link href="/leaderboard" className="nav-item">
|
||||
<img src="/assets/images/icons/navigation/leaderboards.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
{t("leaderboards")}
|
||||
</Link>
|
||||
|
||||
<Link href="/radio" className="nav-item">
|
||||
<img src="/assets/images/icons/navigation/bb.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
{t("radio")}
|
||||
</Link>
|
||||
|
||||
<details className="relative group md:flex">
|
||||
<summary className="nav-item list-none cursor-pointer [&::-webkit-details-marker]:hidden">
|
||||
<img src="/assets/images/icons/navigation/rules.gif" alt="" className="w-5 h-5 mr-1.5" />
|
||||
{t("assistance")}
|
||||
<svg className="ml-auto md:ml-1 w-3 h-3 transition-transform duration-200 group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
||||
</summary>
|
||||
<div
|
||||
className="md:absolute left-0 mt-1.5 min-w-[200px] rounded-xl shadow-xl z-50 py-2 border backdrop-blur-md"
|
||||
style={{ backgroundColor: "color-mix(in srgb, var(--color-dropdown) 97%, transparent)", borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)" }}
|
||||
>
|
||||
<Link href="/help" className="dropdown-item">{t("helpCenter")}</Link>
|
||||
{session?.user ? (
|
||||
<Link href="/help/tickets" className="dropdown-item">{t("myTickets")}</Link>
|
||||
) : null}
|
||||
<Link href="/badges" className="dropdown-item">{t("badges")}</Link>
|
||||
<Link href="/draw-badge" className="dropdown-item">{t("drawBadge")}</Link>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{session?.user ? (
|
||||
<>
|
||||
<Link href="/friends" className="nav-item">
|
||||
<img src="/assets/images/icons/navigation/goody.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
{t("friends")}
|
||||
</Link>
|
||||
<Link href="/messages" className="nav-item">
|
||||
<img src="/assets/images/icons/navigation/bb.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
{t("messages")}
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{isStaff ? (
|
||||
<Link href="/admin" className="nav-item">
|
||||
<img src="/assets/images/icons/navigation/home.png" alt="" className="w-5 h-5 mr-1.5" />
|
||||
{t("admin")}
|
||||
</Link>
|
||||
) : null}
|
||||
|
||||
<div className="flex items-center gap-x-1 pt-2 mt-2 border-t md:hidden" style={{ borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)" }}>
|
||||
<LanguageSwitcher />
|
||||
<NavbarColorPicker />
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</details>
|
||||
</MobileNav>
|
||||
|
||||
<details className="relative">
|
||||
<summary className="nav-item list-none cursor-pointer [&::-webkit-details-marker]:hidden">
|
||||
<img src="/assets/images/icons/navigation/shop.png" alt="" className="mr-1 hidden w-5 h-5 lg:inline-block" />
|
||||
{t("shop")}
|
||||
</summary>
|
||||
<div
|
||||
className="absolute left-0 mt-1 min-w-[190px] rounded-md shadow-lg z-50 py-1"
|
||||
style={{ backgroundColor: "var(--color-dropdown)" }}
|
||||
>
|
||||
<Link href="/shop" className="dropdown-item">{t("shop")}</Link>
|
||||
<Link href="/marketplace" className="dropdown-item">{t("market")}</Link>
|
||||
<Link href="/rares" className="dropdown-item">{t("rareValues")}</Link>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<Link href="/leaderboard" className="nav-item">
|
||||
<img src="/assets/images/icons/navigation/leaderboards.png" alt="" className="mr-1 hidden w-5 h-5 lg:inline-block" />
|
||||
{t("leaderboards")}
|
||||
</Link>
|
||||
|
||||
<Link href="/radio" className="nav-item">
|
||||
{t("radio")}
|
||||
</Link>
|
||||
|
||||
<details className="relative">
|
||||
<summary className="nav-item list-none cursor-pointer [&::-webkit-details-marker]:hidden">
|
||||
<img src="/assets/images/icons/navigation/rules.gif" alt="" className="mr-1 hidden w-5 h-5 lg:inline-block" />
|
||||
{t("assistance")}
|
||||
</summary>
|
||||
<div
|
||||
className="absolute left-0 mt-1 min-w-[190px] rounded-md shadow-lg z-50 py-1"
|
||||
style={{ backgroundColor: "var(--color-dropdown)" }}
|
||||
>
|
||||
<Link href="/help" className="dropdown-item">{t("helpCenter")}</Link>
|
||||
{session?.user ? (
|
||||
<Link href="/help/tickets" className="dropdown-item">{t("myTickets")}</Link>
|
||||
) : null}
|
||||
<Link href="/badges" className="dropdown-item">{t("badges")}</Link>
|
||||
<Link href="/draw-badge" className="dropdown-item">{t("drawBadge")}</Link>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{session?.user ? (
|
||||
<>
|
||||
<Link href="/friends" className="nav-item">{t("friends")}</Link>
|
||||
<Link href="/messages" className="nav-item">{t("messages")}</Link>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{isStaff ? (
|
||||
<Link href="/admin" className="nav-item">{t("admin")}</Link>
|
||||
) : null}
|
||||
|
||||
<div className="ml-auto flex items-center gap-x-2">
|
||||
<div className="ml-auto flex items-center gap-x-1 hidden md:flex">
|
||||
<LanguageSwitcher />
|
||||
<NavbarColorPicker />
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -101,3 +101,60 @@ export function RankBadge({ position }: { position: number }) {
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Skeleton loaders ────────────────────────────────────────── */
|
||||
|
||||
export function Skeleton({ className, style }: { className?: string; style?: React.CSSProperties }) {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={{
|
||||
...style,
|
||||
backgroundColor: "color-mix(in srgb, var(--color-text-muted) 12%, transparent)",
|
||||
borderRadius: "6px",
|
||||
animation: "shimmer 1.5s ease-in-out infinite",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SkeletonCard({ lines = 3 }: { lines?: number }) {
|
||||
return (
|
||||
<div className="content-card" style={{ padding: "1rem" }}>
|
||||
<div className="content-card-head" style={{ margin: "-1rem -1rem 1rem" }}>
|
||||
<Skeleton style={{ width: "44px", height: "44px", borderRadius: "999px" }} />
|
||||
<div style={{ flex: 1, display: "grid", gap: "0.35rem" }}>
|
||||
<Skeleton style={{ height: "16px", width: "60%" }} />
|
||||
<Skeleton style={{ height: "12px", width: "40%" }} />
|
||||
</div>
|
||||
</div>
|
||||
{Array.from({ length: lines }).map((_, i) => (
|
||||
<Skeleton key={i} style={{ height: "12px", width: `${85 - i * 10}%`, marginBottom: "0.6rem" }} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SkeletonTable({ rows = 5 }: { rows?: number }) {
|
||||
return (
|
||||
<div className="content-card" style={{ padding: "0.5rem 1rem 1rem" }}>
|
||||
{Array.from({ length: rows }).map((_, i) => (
|
||||
<div key={i} style={{ display: "flex", alignItems: "center", gap: "0.75rem", padding: "0.6rem 0", borderTop: i > 0 ? "1px solid color-mix(in srgb, var(--color-text-muted) 12%, transparent)" : undefined }}>
|
||||
{i === 0 ? (
|
||||
<>
|
||||
<Skeleton style={{ height: "12px", width: "30%" }} />
|
||||
<Skeleton style={{ height: "12px", width: "20%" }} />
|
||||
<Skeleton style={{ height: "12px", width: "25%", marginLeft: "auto" }} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Skeleton style={{ width: "28px", height: "28px", borderRadius: "8px" }} />
|
||||
<Skeleton style={{ height: "14px", width: "35%" }} />
|
||||
<Skeleton style={{ height: "14px", width: "20%", marginLeft: "auto" }} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export function ThemeSwitcher() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
className="nav-item cursor-pointer"
|
||||
className="nav-item cursor-pointer text-lg leading-none px-2"
|
||||
aria-label={dark ? "Switch to light mode" : "Switch to dark mode"}
|
||||
title={dark ? "Light mode" : "Dark mode"}
|
||||
>
|
||||
|
||||
@@ -67,8 +67,8 @@ export async function TopHeader() {
|
||||
Administration
|
||||
</summary>
|
||||
<div
|
||||
className="absolute right-0 mt-1 min-w-[180px] rounded-md shadow-lg z-50 py-1"
|
||||
style={{ backgroundColor: "var(--color-dropdown)" }}
|
||||
className="absolute right-0 mt-1 min-w-[180px] rounded-xl shadow-xl z-50 py-2 border backdrop-blur-md"
|
||||
style={{ backgroundColor: "color-mix(in srgb, var(--color-dropdown) 97%, transparent)", borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)" }}
|
||||
>
|
||||
<Link href="/admin" className="dropdown-item">
|
||||
Admin panel
|
||||
@@ -90,8 +90,8 @@ export async function TopHeader() {
|
||||
<span className="-ml-2 font-semibold">{session.user.name}</span>
|
||||
</summary>
|
||||
<div
|
||||
className="absolute right-0 mt-1 min-w-[180px] rounded-md shadow-lg z-50 py-1"
|
||||
style={{ backgroundColor: "var(--color-dropdown)" }}
|
||||
className="absolute right-0 mt-1 min-w-[200px] rounded-xl shadow-xl z-50 py-2 border backdrop-blur-md"
|
||||
style={{ backgroundColor: "color-mix(in srgb, var(--color-dropdown) 97%, transparent)", borderColor: "color-mix(in srgb, var(--color-text-muted) 10%, transparent)" }}
|
||||
>
|
||||
<Link href="/me" className="dropdown-item">
|
||||
My dashboard
|
||||
@@ -108,6 +108,7 @@ export async function TopHeader() {
|
||||
<Link href="/settings/sessions" className="dropdown-item">
|
||||
Login activity
|
||||
</Link>
|
||||
<div className="h-px my-1 mx-3" style={{ backgroundColor: "color-mix(in srgb, var(--color-text-muted) 12%, transparent)" }} />
|
||||
<form
|
||||
action={async () => {
|
||||
"use server";
|
||||
@@ -116,10 +117,13 @@ export async function TopHeader() {
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
className="dropdown-item w-full text-center"
|
||||
className="dropdown-item w-full text-center mx-1 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: "var(--button-danger-color)",
|
||||
color: "var(--button-danger-text-color)",
|
||||
backgroundColor: "color-mix(in srgb, var(--button-danger-color) 12%, transparent)",
|
||||
color: "var(--button-danger-color)",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
width: "calc(100% - 8px)",
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
|
||||
Reference in new issue
Block a user