refactor: convert admin panel to Tailwind CSS

- Convert admin layout, sidebar, and navigation to Tailwind classes
- Convert dashboard components (StatusCard, DiagnosticRow, etc.) to Tailwind
- Convert all ~48 admin page files from admin CSS classes to Tailwind
- Remove unused admin CSS from globals.css (973 → 712 lines)
- Convert developers page badges to Tailwind
- Remove <style jsx> block from OnlineUsersWidget
This commit is contained in:
remco committed 2026-07-02 15:23:07 +02:00
1 parent 64f50b2dde
commit bd299be55d
52 files changed
+277 -644

No files matched your search

+6 -1
View File
@@ -14,7 +14,12 @@ export function AdminNavLink({ href, label }: { href: string; label: string }) {
href === "/admin" ? pathname === "/admin" : pathname === href || pathname.startsWith(`${href}/`);
return (
<Link href={href} className={`admin-nav-link${active ? " active" : ""}`}>
<Link
href={href}
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-[#c8cbe0] text-sm font-semibold no-underline transition-[background,color] duration-150 hover:bg-white/7 hover:text-white hover:no-underline ${
active ? "bg-[var(--color-primary)] text-[#1a1a2e] shadow-[0_2px_8px_rgba(238,180,37,0.35)]" : ""
}`}
>
{label}
</Link>
);
+47 -163
View File
@@ -27,18 +27,24 @@ export function StatusCard({
state?: State;
icon?: string;
}) {
const borderColor =
state === "ok" ? "border-l-green-600" :
state === "warn" ? "border-l-[var(--color-primary)]" :
state === "danger" ? "border-l-red-500" :
"border-l-[var(--color-text-muted)]";
return (
<div className={`admin-stat-card ${state}`}>
<div className="admin-stat-top">
<span className="admin-stat-label">{label}</span>
<div className={`bg-[var(--color-surface)] border border-[var(--border)] border-l-4 ${borderColor} rounded-xl p-3.5 shadow-card`}>
<div className="flex items-center justify-between gap-2">
<span className="text-[0.72rem] font-bold uppercase tracking-wider text-[var(--color-text-muted)]">{label}</span>
{icon ? (
<span className="admin-stat-icon" aria-hidden>
<span className="text-lg leading-none" aria-hidden>
{icon}
</span>
) : null}
</div>
<div className="admin-stat-value">{value}</div>
{hint ? <div className="admin-stat-hint">{hint}</div> : null}
<div className="text-[1.7rem] font-extrabold leading-tight mt-1.5">{value}</div>
{hint ? <div className="text-[0.78rem] text-[var(--color-text-muted)] mt-1">{hint}</div> : null}
</div>
);
}
@@ -54,12 +60,19 @@ export function DiagnosticRow({
state: Exclude<State, "neutral">;
}) {
const text = state === "ok" ? "OK" : state === "warn" ? "WARN" : "ERROR";
const dotColor = state === "ok" ? "bg-green-600 shadow-[0_0_0_3px_rgba(22,163,74,0.22)]" :
state === "warn" ? "bg-[var(--color-primary)] shadow-[0_0_0_3px_rgba(238,180,37,0.22)]" :
"bg-red-500 shadow-[0_0_0_3px_rgba(239,68,68,0.22)]";
const badgeColor = state === "ok" ? "bg-green-600/20" :
state === "danger" ? "bg-red-500/22" :
"bg-[var(--color-primary)]/18";
return (
<div className="admin-diag-row">
<span className={`admin-dot ${state}`} aria-hidden />
<span className="admin-diag-label">{label}</span>
<span className="admin-diag-detail muted">{detail}</span>
<span className={`admin-badge ${state === "ok" ? "ok" : state === "danger" ? "danger" : ""}`}>
<div className="flex items-center gap-2.5 py-2 px-0.5 border-t border-[var(--border)] first:border-t-0">
<span className={`flex-none w-[9px] h-[9px] rounded-full ${dotColor}`} aria-hidden />
<span className="font-bold text-sm">{label}</span>
<span className="flex-1 text-[0.82rem] text-[var(--color-text-muted)]">{detail}</span>
<span className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${badgeColor} text-[var(--color-text)]`}>
{text}
</span>
</div>
@@ -69,8 +82,8 @@ export function DiagnosticRow({
/** Small labelled key/value used in the server-info card. */
export function InfoItem({ label, value }: { label: string; value: ReactNode }) {
return (
<div className="admin-info-item">
<span className="muted">{label}</span>
<div className="flex flex-col gap-0.5 text-sm">
<span className="text-[var(--color-text-muted)]">{label}</span>
<strong>{value}</strong>
</div>
);
@@ -95,49 +108,50 @@ export function OnlineUsersWidget({
};
return (
<div className="online-users-widget">
<div className="widget-header">
<h3>Online Now</h3>
<span className="online-count">
<div className="bg-[var(--color-surface)] rounded-xl border border-[var(--border)] p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="m-0 text-lg font-semibold text-[var(--color-text)]">Online Now</h3>
<span className="text-sm text-[var(--color-text-muted)] bg-[var(--color-primary)]/10 px-3 py-1 rounded-full">
{isLoading ? "..." : users.length} users
</span>
</div>
<div className="online-users-list">
<div className="max-h-80 overflow-y-auto flex flex-col gap-3">
{users.length === 0 ? (
<div className="empty-state">
<span className="muted">No users online</span>
<div className="py-8 text-center bg-[var(--color-text-muted)]/5 rounded-lg">
<span className="text-[var(--color-text-muted)] text-sm">No users online</span>
</div>
) : (
users.map((user) => (
<div key={user.id} className="online-user-row">
<div className="user-avatar-container">
<div
key={user.id}
className="flex items-center gap-3 p-3 bg-[var(--color-text-muted)]/5 rounded-lg transition-all duration-150 hover:bg-[var(--color-text-muted)]/10 hover:translate-x-0.5"
>
<div className="relative flex-none w-[50px] h-[62px]">
<img
src={`https://www.habbo.com/habbo-imaging/avatarimage?width=50&height=62&direction=2&headonly=1&crop=face&action=wav&gender=m&look=${encodeURIComponent(user.look)}`}
alt={user.username}
className="user-avatar"
className="w-full h-full rounded object-cover border border-[var(--border)]"
loading="lazy"
/>
{user.roomId && user.roomName && (
<div className="room-indicator">
<span className="room-dot" />
<div className="absolute -bottom-0.5 -right-0.5">
<span className="block w-[10px] h-[10px] bg-green-500 rounded-full border-2 border-[var(--color-surface)]" />
</div>
)}
</div>
<div className="user-info">
<div className="user-name">
<a href={`/u/${user.username}`} className="user-link">
<div className="flex-1 min-w-0">
<div className="font-medium text-[var(--color-text)] mb-1 truncate">
<a href={`/u/${user.username}`} className="text-inherit no-underline hover:text-[var(--color-primary)] hover:underline">
{user.username}
</a>
</div>
<div className="user-status">
<div className="text-[0.75rem] text-[var(--color-text-muted)]">
{user.roomId && user.roomName ? (
<span className="in-room">
🏠 {user.roomName}
</span>
<span className="text-green-500 font-medium">🏠 {user.roomName}</span>
) : (
<span className="afk">⏱️ {formatLastSeen(user.lastActivity)}</span>
<span className="text-red-500">⏱️ {formatLastSeen(user.lastActivity)}</span>
)}
</div>
</div>
@@ -145,136 +159,6 @@ export function OnlineUsersWidget({
))
)}
</div>
<style jsx>{`
.online-users-widget {
background: var(--color-surface);
border-radius: 12px;
border: 1px solid color-mix(in srgb, var(--color-text-muted) 15%, transparent);
padding: 1.5rem;
}
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.widget-header h3 {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--color-text);
}
.online-count {
font-size: 0.875rem;
color: var(--color-text-muted);
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
padding: 0.25rem 0.75rem;
border-radius: 999px;
}
.online-users-list {
max-height: 320px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.empty-state {
padding: 2rem;
text-align: center;
background: color-mix(in srgb, var(--color-text-muted) 5%, transparent);
border-radius: 8px;
}
.online-user-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: color-mix(in srgb, var(--color-text-muted) 5%, transparent);
border-radius: 8px;
transition: all 0.15s ease;
}
.online-user-row:hover {
background: color-mix(in srgb, var(--color-text-muted) 10%, transparent);
transform: translateX(2px);
}
.user-avatar-container {
position: relative;
width: 50px;
height: 62px;
flex-shrink: 0;
}
.user-avatar {
width: 100%;
height: 100%;
border-radius: 4px;
object-fit: cover;
border: 1px solid color-mix(in srgb, var(--color-text-muted) 20%, transparent);
}
.room-indicator {
position: absolute;
bottom: -2px;
right: -2px;
}
.room-dot {
width: 10px;
height: 10px;
background: #22c55e;
border-radius: 999px;
border: 2px solid var(--color-surface);
display: block;
}
.user-info {
flex: 1;
min-width: 0;
}
.user-name {
font-weight: 500;
color: var(--color-text);
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-name .user-link {
color: inherit;
text-decoration: none;
transition: color 0.15s ease;
}
.user-name .user-link:hover {
color: var(--color-primary);
text-decoration: underline;
}
.user-status {
font-size: 0.75rem;
color: var(--color-text-muted);
}
.in-room {
color: #22c55e;
font-weight: 500;
}
.afk {
color: #ef4444;
}
`}</style>
</div>
);
}