refactor: remove gh/GitHub references from tokens, classes, and component names

Rename all --color-gh-* CSS tokens to --color-*, update Tailwind class names
(bg-gh-*, text-gh-*, border-gh-*, etc.) across all TSX/TS files, rename
GhButton component and file to Button, and remove trivial descriptive comments.
This commit is contained in:
2026-03-21 22:11:13 +01:00
parent 3c643d5dcc
commit 71fa228013
31 changed files with 308 additions and 372 deletions

View File

@@ -96,7 +96,7 @@ export function AssistRulesEditor({ serverId, config }: AssistRulesEditorProps)
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{assistFields.map(({ key, label, type }) => (
<div key={key}>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">{label}</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
{type === 'number' ? (
<input
type="number"
@@ -122,13 +122,13 @@ export function AssistRulesEditor({ serverId, config }: AssistRulesEditorProps)
))}
</div>
<div className="border-t border-gh-border pt-6">
<label className="flex items-center gap-2 text-sm text-gh-muted">
<div className="border-t border-border pt-6">
<label className="flex items-center gap-2 text-sm text-muted">
<input
type="checkbox"
checked={restart}
onChange={(e) => setRestart(e.target.checked)}
className="h-4 w-4 rounded border-gh-border bg-gh-overlay accent-gh-green focus:ring-1 focus:ring-gh-blue"
className="h-4 w-4 rounded border-border bg-overlay accent-green focus:ring-1 focus:ring-blue"
/>
Restart server after saving
</label>
@@ -138,7 +138,7 @@ export function AssistRulesEditor({ serverId, config }: AssistRulesEditorProps)
<button
type="submit"
disabled={isSubmitting}
className="rounded-md bg-gh-btn-green border border-gh-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-gh-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
className="rounded-md bg-btn-green border border-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
>
{isSubmitting ? 'Saving…' : 'Save Changes'}
</button>

View File

@@ -47,7 +47,7 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
<form onSubmit={handleSubmit} className="max-w-3xl space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">UDP Port</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">UDP Port</label>
<input
type="number"
disabled={isSubmitting}
@@ -58,7 +58,7 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">TCP Port</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">TCP Port</label>
<input
type="number"
disabled={isSubmitting}
@@ -69,7 +69,7 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">Max Connections</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">Max Connections</label>
<input
type="number"
disabled={isSubmitting}
@@ -80,7 +80,7 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">LAN Discovery</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">LAN Discovery</label>
<select
disabled={isSubmitting}
value={formData.lanDiscovery}
@@ -93,7 +93,7 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">Register To Lobby</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">Register To Lobby</label>
<select
disabled={isSubmitting}
value={formData.registerToLobby}
@@ -106,13 +106,13 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
</div>
</div>
<div className="border-t border-gh-border pt-6">
<label className="flex items-center gap-2 text-sm text-gh-muted">
<div className="border-t border-border pt-6">
<label className="flex items-center gap-2 text-sm text-muted">
<input
type="checkbox"
checked={restart}
onChange={(e) => setRestart(e.target.checked)}
className="h-4 w-4 rounded border-gh-border bg-gh-overlay accent-gh-green focus:ring-1 focus:ring-gh-blue"
className="h-4 w-4 rounded border-border bg-overlay accent-green focus:ring-1 focus:ring-blue"
/>
Restart server after saving
</label>
@@ -122,7 +122,7 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
<button
type="submit"
disabled={isSubmitting}
className="rounded-md bg-gh-btn-green border border-gh-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-gh-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
className="rounded-md bg-btn-green border border-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
>
{isSubmitting ? 'Saving…' : 'Save Changes'}
</button>

View File

@@ -99,12 +99,12 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
return (
<form onSubmit={handleSubmit} className="max-w-4xl space-y-8">
<div className="space-y-6">
<h3 className="border-b border-gh-border-muted pb-2 text-sm font-semibold text-gh-primary">
<h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">
Basic Event Settings
</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">Track</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">Track</label>
<select
disabled={isSubmitting}
value={formData.track}
@@ -140,7 +140,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label className="mb-1.5 block text-sm font-medium text-secondary">
Pre-Race Waiting Time (seconds)
</label>
<input
@@ -154,7 +154,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label className="mb-1.5 block text-sm font-medium text-secondary">
Session Over Time (seconds)
</label>
<input
@@ -168,7 +168,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label className="mb-1.5 block text-sm font-medium text-secondary">
Post Qualify Seconds
</label>
<input
@@ -182,7 +182,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label className="mb-1.5 block text-sm font-medium text-secondary">
Post Race Seconds
</label>
<input
@@ -198,12 +198,12 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div className="space-y-6">
<h3 className="border-b border-gh-border-muted pb-2 text-sm font-semibold text-gh-primary">
<h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">
Weather Settings
</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label className="mb-1.5 block text-sm font-medium text-secondary">
Ambient Temperature (°C)
</label>
<input
@@ -218,7 +218,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label className="mb-1.5 block text-sm font-medium text-secondary">
Cloud Level (0.0-1.0)
</label>
<input
@@ -234,7 +234,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">Rain (0.0-1.0)</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">Rain (0.0-1.0)</label>
<input
type="number"
disabled={isSubmitting}
@@ -248,7 +248,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label className="mb-1.5 block text-sm font-medium text-secondary">
Weather Randomness
</label>
<input
@@ -263,7 +263,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label className="mb-1.5 block text-sm font-medium text-secondary">
Simracer Weather Conditions
</label>
<select
@@ -278,7 +278,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label className="mb-1.5 block text-sm font-medium text-secondary">
Fixed Condition Qualification
</label>
<select
@@ -295,13 +295,13 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div className="space-y-6">
<div className="flex items-center justify-between border-b border-gh-border-muted pb-2">
<h3 className="text-sm font-semibold text-gh-primary">Sessions</h3>
<div className="flex items-center justify-between border-b border-border-muted pb-2">
<h3 className="text-sm font-semibold text-primary">Sessions</h3>
<button
type="button"
onClick={addSession}
disabled={isSubmitting}
className="rounded-md border border-gh-border bg-gh-overlay px-3 py-1.5 text-xs font-medium text-gh-secondary hover:bg-gh-border disabled:opacity-40"
className="rounded-md border border-border bg-overlay px-3 py-1.5 text-xs font-medium text-secondary hover:bg-border disabled:opacity-40"
>
+ Add Session
</button>
@@ -309,14 +309,14 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
<div className="space-y-4">
{formData.sessions.map((session, index) => (
<div key={index} className="rounded-lg border border-gh-border bg-gh-canvas p-4">
<div key={index} className="rounded-lg border border-border bg-canvas p-4">
<div className="mb-4 flex items-center justify-between">
<h4 className="text-sm font-medium text-gh-primary">Session {index + 1}</h4>
<h4 className="text-sm font-medium text-primary">Session {index + 1}</h4>
<button
type="button"
onClick={() => removeSession(index)}
disabled={isSubmitting}
className="rounded px-2 py-1 text-xs text-gh-red hover:bg-gh-red-bg disabled:opacity-40"
className="rounded px-2 py-1 text-xs text-red hover:bg-red-bg disabled:opacity-40"
>
Remove
</button>
@@ -324,7 +324,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-5">
<div>
<label className="mb-1 block text-xs font-medium text-gh-muted">
<label className="mb-1 block text-xs font-medium text-muted">
Session Type
</label>
<select
@@ -342,7 +342,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gh-muted">
<label className="mb-1 block text-xs font-medium text-muted">
Hour of Day
</label>
<input
@@ -357,7 +357,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gh-muted">
<label className="mb-1 block text-xs font-medium text-muted">
Day of Weekend
</label>
<input
@@ -372,7 +372,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gh-muted">
<label className="mb-1 block text-xs font-medium text-muted">
Time Multiplier
</label>
<input
@@ -387,7 +387,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gh-muted">
<label className="mb-1 block text-xs font-medium text-muted">
Duration (minutes)
</label>
<input
@@ -407,13 +407,13 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
</div>
</div>
<div className="border-t border-gh-border pt-6">
<label className="flex items-center gap-2 text-sm text-gh-muted">
<div className="border-t border-border pt-6">
<label className="flex items-center gap-2 text-sm text-muted">
<input
type="checkbox"
checked={restart}
onChange={(e) => setRestart(e.target.checked)}
className="h-4 w-4 rounded border-gh-border bg-gh-overlay accent-gh-green focus:ring-1 focus:ring-gh-blue"
className="h-4 w-4 rounded border-border bg-overlay accent-green focus:ring-1 focus:ring-blue"
/>
Restart server after saving
</label>
@@ -423,7 +423,7 @@ export function EventConfigEditor({ serverId, config }: EventConfigEditorProps)
<button
type="submit"
disabled={isSubmitting}
className="rounded-md bg-gh-btn-green border border-gh-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-gh-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
className="rounded-md bg-btn-green border border-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
>
{isSubmitting ? 'Saving…' : 'Save Changes'}
</button>

View File

@@ -105,11 +105,11 @@ export function EventRulesEditor({ serverId, config }: EventRulesEditorProps) {
return (
<form onSubmit={handleSubmit} className="max-w-4xl space-y-8">
<div className="space-y-6">
<h3 className="border-b border-gh-border-muted pb-2 text-sm font-semibold text-gh-primary">Race Rules</h3>
<h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">Race Rules</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{numberFields.map(({ key, label, min, max }) => (
<div key={key}>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">{label}</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
<input
type="number"
disabled={isSubmitting}
@@ -125,13 +125,13 @@ export function EventRulesEditor({ serverId, config }: EventRulesEditorProps) {
</div>
<div className="space-y-6">
<h3 className="border-b border-gh-border-muted pb-2 text-sm font-semibold text-gh-primary">
<h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">
Pitstop & Refuelling Rules
</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{booleanFields.map(({ key, label }) => (
<div key={key}>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">{label}</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
<select
disabled={isSubmitting}
value={formData[key] ? 'true' : 'false'}
@@ -146,13 +146,13 @@ export function EventRulesEditor({ serverId, config }: EventRulesEditorProps) {
</div>
</div>
<div className="border-t border-gh-border pt-6">
<label className="flex items-center gap-2 text-sm text-gh-muted">
<div className="border-t border-border pt-6">
<label className="flex items-center gap-2 text-sm text-muted">
<input
type="checkbox"
checked={restart}
onChange={(e) => setRestart(e.target.checked)}
className="h-4 w-4 rounded border-gh-border bg-gh-overlay accent-gh-green focus:ring-1 focus:ring-gh-blue"
className="h-4 w-4 rounded border-border bg-overlay accent-green focus:ring-1 focus:ring-blue"
/>
Restart server after saving
</label>
@@ -162,7 +162,7 @@ export function EventRulesEditor({ serverId, config }: EventRulesEditorProps) {
<button
type="submit"
disabled={isSubmitting}
className="rounded-md bg-gh-btn-green border border-gh-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-gh-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
className="rounded-md bg-btn-green border border-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
>
{isSubmitting ? 'Saving…' : 'Save Changes'}
</button>

View File

@@ -124,13 +124,13 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
return (
<form onSubmit={handleSubmit} className="max-w-4xl space-y-8">
<div className="space-y-6">
<h3 className="border-b border-gh-border-muted pb-2 text-sm font-semibold text-gh-primary">
<h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">
Basic Settings
</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{textFields.map(({ key, label, type }) => (
<div key={key}>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">{label}</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
<input
type={type}
disabled={isSubmitting}
@@ -142,7 +142,7 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
))}
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">Car Group</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">Car Group</label>
<select
disabled={isSubmitting}
value={formData.carGroup}
@@ -160,13 +160,13 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
</div>
<div className="space-y-6">
<h3 className="border-b border-gh-border-muted pb-2 text-sm font-semibold text-gh-primary">
<h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">
Requirements & Limits
</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{numberFields.map(({ key, label, min, max }) => (
<div key={key}>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">{label}</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
<input
type="number"
disabled={isSubmitting}
@@ -182,13 +182,13 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
</div>
<div className="space-y-6">
<h3 className="border-b border-gh-border-muted pb-2 text-sm font-semibold text-gh-primary">
<h3 className="border-b border-border-muted pb-2 text-sm font-semibold text-primary">
Race Options
</h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{selectFields.map(({ key, label }) => (
<div key={key}>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">{label}</label>
<label className="mb-1.5 block text-sm font-medium text-secondary">{label}</label>
<select
disabled={isSubmitting}
value={formData[key] as number}
@@ -202,7 +202,7 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
))}
<div>
<label className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label className="mb-1.5 block text-sm font-medium text-secondary">
Formation Lap Type
</label>
<select
@@ -221,13 +221,13 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
</div>
</div>
<div className="border-t border-gh-border pt-6">
<label className="flex items-center gap-2 text-sm text-gh-muted">
<div className="border-t border-border pt-6">
<label className="flex items-center gap-2 text-sm text-muted">
<input
type="checkbox"
checked={restart}
onChange={(e) => setRestart(e.target.checked)}
className="h-4 w-4 rounded border-gh-border bg-gh-overlay accent-gh-green focus:ring-1 focus:ring-gh-blue"
className="h-4 w-4 rounded border-border bg-overlay accent-green focus:ring-1 focus:ring-blue"
/>
Restart server after saving
</label>
@@ -237,7 +237,7 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
<button
type="submit"
disabled={isSubmitting}
className="rounded-md bg-gh-btn-green border border-gh-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-gh-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
className="rounded-md bg-btn-green border border-btn-green px-5 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover disabled:cursor-not-allowed disabled:opacity-40"
>
{isSubmitting ? 'Saving…' : 'Save Changes'}
</button>

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import { GhButton } from '@/components/ui/GhButton';
import { Button } from '@/components/ui/Button';
import { Leaderboard, LeaderboardDriver, FlPoints } from '@/lib/schemas/leaderboard';
import { saveLeaderboardAction } from '@/lib/actions/leaderboard';
import { LeaderboardStandings } from './LeaderboardStandings';
@@ -136,19 +136,19 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager
return (
<div className="space-y-4">
{dirty && (
<div className="flex items-center justify-between rounded-lg border border-gh-yellow/30 bg-gh-yellow-bg px-4 py-2 text-sm text-gh-yellow">
<div className="flex items-center justify-between rounded-lg border border-yellow/30 bg-yellow-bg px-4 py-2 text-sm text-yellow">
<span>Unsaved changes</span>
<div className="flex gap-2">
<button
onClick={() => setDraft(saved)}
className="rounded px-2 py-1 text-xs text-gh-yellow hover:text-gh-primary"
className="rounded px-2 py-1 text-xs text-yellow hover:text-primary"
>
Discard
</button>
<button
onClick={handleSave}
disabled={isSubmitting}
className="rounded bg-gh-yellow px-3 py-1 text-xs font-semibold text-gh-base hover:opacity-90 disabled:opacity-40"
className="rounded bg-yellow px-3 py-1 text-xs font-semibold text-base hover:opacity-90 disabled:opacity-40"
>
{isSubmitting ? 'Saving…' : 'Save'}
</button>
@@ -157,7 +157,7 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager
)}
{error && (
<div className="rounded-lg border border-gh-red/30 bg-gh-red-bg px-4 py-2 text-sm text-gh-red">
<div className="rounded-lg border border-red/30 bg-red-bg px-4 py-2 text-sm text-red">
{error}
</div>
)}
@@ -169,8 +169,8 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager
onClick={() => setActiveSection(s.id)}
className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
activeSection === s.id
? 'bg-gh-blue text-white'
: 'bg-gh-overlay text-gh-secondary hover:bg-gh-border'
? 'bg-blue text-white'
: 'bg-overlay text-secondary hover:bg-border'
}`}
>
{s.label}
@@ -182,9 +182,9 @@ export function LeaderboardManager({ serverId, initialData }: LeaderboardManager
{!dirty && (
<div className="flex justify-end">
<GhButton variant="primary" size="md" onClick={handleSave} disabled={isSubmitting}>
<Button variant="primary" size="md" onClick={handleSave} disabled={isSubmitting}>
{isSubmitting ? 'Saving…' : 'Save Leaderboard'}
</GhButton>
</Button>
</div>
)}
</div>

View File

@@ -23,26 +23,26 @@ export default function LoginForm({
}, [expired]);
const [state, formAction] = useActionState(loginAction, initialState);
return (
<div className="flex min-h-screen items-center justify-center bg-gh-base px-4">
<div className="w-full max-w-md space-y-6 rounded-lg border border-gh-border bg-gh-canvas p-8 shadow-xl">
<div className="flex min-h-screen items-center justify-center bg-base px-4">
<div className="w-full max-w-md space-y-6 rounded-lg border border-border bg-canvas p-8 shadow-xl">
<div className="text-center">
<h1 className="text-xl font-bold text-gh-primary">ACC Server Manager</h1>
<p className="mt-1 text-sm text-gh-muted">Sign in to manage your servers</p>
<h1 className="text-xl font-bold text-primary">ACC Server Manager</h1>
<p className="mt-1 text-sm text-muted">Sign in to manage your servers</p>
</div>
{expired && (
<div className="rounded-md border border-gh-yellow/30 bg-gh-yellow-bg px-3 py-2 text-sm text-gh-yellow">
<div className="rounded-md border border-yellow/30 bg-yellow-bg px-3 py-2 text-sm text-yellow">
Your session has expired. Please sign in again.
</div>
)}
{state?.success ? null : (
<div className="rounded-md border border-gh-red/30 bg-gh-red-bg px-3 py-2 text-sm text-gh-red">
<div className="rounded-md border border-red/30 bg-red-bg px-3 py-2 text-sm text-red">
{state?.message}
</div>
)}
<form action={formAction} className="space-y-4">
<div>
<label htmlFor="username" className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label htmlFor="username" className="mb-1.5 block text-sm font-medium text-secondary">
Username
</label>
<input
@@ -56,7 +56,7 @@ export default function LoginForm({
</div>
<div>
<label htmlFor="password" className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label htmlFor="password" className="mb-1.5 block text-sm font-medium text-secondary">
Password
</label>
<input
@@ -71,7 +71,7 @@ export default function LoginForm({
<button
type="submit"
className="w-full rounded-md bg-gh-btn-green border border-gh-btn-green px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-gh-btn-green-hover focus:outline-none focus:ring-2 focus:ring-gh-blue focus:ring-offset-2 focus:ring-offset-gh-base"
className="w-full rounded-md bg-btn-green border border-btn-green px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-btn-green-hover focus:outline-none focus:ring-2 focus:ring-blue focus:ring-offset-2 focus:ring-offset-base"
>
Sign in
</button>

View File

@@ -3,7 +3,7 @@
import { useState } from 'react';
import type { Role } from '@/lib/schemas';
import { createUserAction } from '@/lib/actions/membership';
import { GhButton } from '@/components/ui/GhButton';
import { Button } from '@/components/ui/Button';
interface CreateUserModalProps {
roles: Role[];
@@ -46,11 +46,11 @@ export function CreateUserModal({ roles, onClose }: CreateUserModalProps) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
<div className="w-full max-w-md rounded-lg border border-gh-border bg-gh-canvas p-6 shadow-xl">
<h2 className="mb-4 text-base font-semibold text-gh-primary">Create New User</h2>
<div className="w-full max-w-md rounded-lg border border-border bg-canvas p-6 shadow-xl">
<h2 className="mb-4 text-base font-semibold text-primary">Create New User</h2>
{error && (
<div className="mb-4 rounded-md border border-gh-red/20 bg-gh-red-bg p-3 text-sm text-gh-red">
<div className="mb-4 rounded-md border border-red/20 bg-red-bg p-3 text-sm text-red">
{error}
</div>
)}
@@ -59,7 +59,7 @@ export function CreateUserModal({ roles, onClose }: CreateUserModalProps) {
<div className="mb-4">
<label
htmlFor="create-username"
className="mb-1.5 block text-sm font-medium text-gh-secondary"
className="mb-1.5 block text-sm font-medium text-secondary"
>
Username
</label>
@@ -77,7 +77,7 @@ export function CreateUserModal({ roles, onClose }: CreateUserModalProps) {
<div className="mb-4">
<label
htmlFor="create-password"
className="mb-1.5 block text-sm font-medium text-gh-secondary"
className="mb-1.5 block text-sm font-medium text-secondary"
>
Password
</label>
@@ -95,7 +95,7 @@ export function CreateUserModal({ roles, onClose }: CreateUserModalProps) {
<div className="mb-6">
<label
htmlFor="create-role"
className="mb-1.5 block text-sm font-medium text-gh-secondary"
className="mb-1.5 block text-sm font-medium text-secondary"
>
Role
</label>
@@ -117,12 +117,12 @@ export function CreateUserModal({ roles, onClose }: CreateUserModalProps) {
</div>
<div className="flex justify-end gap-2">
<GhButton type="button" variant="ghost" onClick={onClose} disabled={isSubmitting}>
<Button type="button" variant="ghost" onClick={onClose} disabled={isSubmitting}>
Cancel
</GhButton>
<GhButton type="submit" variant="primary" disabled={isSubmitting}>
</Button>
<Button type="submit" variant="primary" disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create User'}
</GhButton>
</Button>
</div>
</form>
</div>

View File

@@ -3,7 +3,7 @@
import { useState } from 'react';
import type { User } from '@/lib/schemas';
import { deleteUserAction } from '@/lib/actions/membership';
import { GhButton } from '@/components/ui/GhButton';
import { Button } from '@/components/ui/Button';
interface DeleteUserModalProps {
user: User;
@@ -39,28 +39,28 @@ export function DeleteUserModal({ user, onClose }: DeleteUserModalProps) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
<div className="w-full max-w-md rounded-lg border border-gh-border bg-gh-canvas p-6 shadow-xl">
<h2 className="mb-4 text-base font-semibold text-gh-primary">Delete User</h2>
<div className="w-full max-w-md rounded-lg border border-border bg-canvas p-6 shadow-xl">
<h2 className="mb-4 text-base font-semibold text-primary">Delete User</h2>
{error && (
<div className="mb-4 rounded-md border border-gh-red/20 bg-gh-red-bg p-3 text-sm text-gh-red">
<div className="mb-4 rounded-md border border-red/20 bg-red-bg p-3 text-sm text-red">
{error}
</div>
)}
<p className="mb-6 text-sm text-gh-secondary">
<p className="mb-6 text-sm text-secondary">
Are you sure you want to delete the user &quot;{user.username}&quot;? This action cannot
be undone.
</p>
<form onSubmit={handleSubmit}>
<div className="flex justify-end gap-2">
<GhButton type="button" variant="ghost" onClick={onClose} disabled={isSubmitting}>
<Button type="button" variant="ghost" onClick={onClose} disabled={isSubmitting}>
Cancel
</GhButton>
<GhButton type="submit" variant="danger-solid" disabled={isSubmitting}>
</Button>
<Button type="submit" variant="danger-solid" disabled={isSubmitting}>
{isSubmitting ? 'Deleting...' : 'Delete User'}
</GhButton>
</Button>
</div>
</form>
</div>

View File

@@ -6,7 +6,7 @@ import type { Role, User } from '@/lib/schemas';
import { hasPermission } from '@/lib/schemas/user';
import { CreateUserModal } from './CreateUserModal';
import { DeleteUserModal } from './DeleteUserModal';
import { GhButton } from '@/components/ui/GhButton';
import { Button } from '@/components/ui/Button';
interface UserManagementTableProps {
initialData: User[];
@@ -79,24 +79,22 @@ export function UserManagementTable({ initialData, roles, currentUser }: UserMan
return (
<>
{/* Internal topbar */}
<header className="flex h-12 items-center justify-between border-b border-gh-border-muted px-5">
<header className="flex h-12 items-center justify-between border-b border-border-muted px-5">
<div className="flex items-center gap-3">
<span className="text-sm font-semibold text-gh-primary">User Management</span>
<span className="text-sm font-semibold text-primary">User Management</span>
</div>
{hasPermission(currentUser, 'membership.create') && (
<GhButton variant="primary" size="sm" onClick={openCreateModal}>
<Button variant="primary" size="sm" onClick={openCreateModal}>
+ Create User
</GhButton>
</Button>
)}
</header>
{/* Filters */}
<div className="mx-5 mt-4 rounded-lg border border-gh-border bg-gh-canvas p-4">
<div className="mb-3 text-sm font-semibold text-gh-primary">Filters</div>
<div className="mx-5 mt-4 rounded-lg border border-border bg-canvas p-4">
<div className="mb-3 text-sm font-semibold text-primary">Filters</div>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div>
<label htmlFor="username" className="mb-1 block text-xs font-medium text-gh-muted">
<label htmlFor="username" className="mb-1 block text-xs font-medium text-muted">
Username
</label>
<input
@@ -109,7 +107,7 @@ export function UserManagementTable({ initialData, roles, currentUser }: UserMan
/>
</div>
<div>
<label htmlFor="role" className="mb-1 block text-xs font-medium text-gh-muted">
<label htmlFor="role" className="mb-1 block text-xs font-medium text-muted">
Role
</label>
<input
@@ -123,27 +121,25 @@ export function UserManagementTable({ initialData, roles, currentUser }: UserMan
</div>
</div>
<div className="mt-3 flex gap-2">
<GhButton variant="primary" size="sm" onClick={applyFilters}>
<Button variant="primary" size="sm" onClick={applyFilters}>
Apply
</GhButton>
<GhButton variant="ghost" size="sm" onClick={resetFilters}>
</Button>
<Button variant="ghost" size="sm" onClick={resetFilters}>
Reset
</GhButton>
</Button>
</div>
</div>
{/* Results count */}
<div className="mx-5 mt-3 text-xs text-gh-muted">Showing {initialData.length} users</div>
<div className="mx-5 mt-3 text-xs text-muted">Showing {initialData.length} users</div>
{/* Users table */}
<div className="mx-5 mt-2 mb-5 overflow-hidden rounded-lg border border-gh-border bg-gh-canvas">
<div className="mx-5 mt-2 mb-5 overflow-hidden rounded-lg border border-border bg-canvas">
<table className="min-w-full">
<thead>
<tr className="border-b border-gh-border-muted bg-gh-base">
<tr className="border-b border-border-muted bg-base">
<th className="px-4 py-2 text-left">
<button
onClick={() => handleSort('username')}
className="flex items-center gap-1 text-xs font-medium uppercase tracking-wider text-gh-subtle hover:text-gh-primary"
className="flex items-center gap-1 text-xs font-medium uppercase tracking-wider text-subtle hover:text-primary"
>
Username <span>{getSortIcon('username')}</span>
</button>
@@ -151,13 +147,13 @@ export function UserManagementTable({ initialData, roles, currentUser }: UserMan
<th className="px-4 py-2 text-left">
<button
onClick={() => handleSort('role')}
className="flex items-center gap-1 text-xs font-medium uppercase tracking-wider text-gh-subtle hover:text-gh-primary"
className="flex items-center gap-1 text-xs font-medium uppercase tracking-wider text-subtle hover:text-primary"
>
Role <span>{getSortIcon('role')}</span>
</button>
</th>
{hasPermission(currentUser, 'membership.edit') && (
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-gh-subtle">
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-subtle">
Actions
</th>
)}
@@ -168,26 +164,26 @@ export function UserManagementTable({ initialData, roles, currentUser }: UserMan
initialData.map((user) => (
<tr
key={user.id}
className="border-b border-gh-border-muted transition-colors hover:bg-gh-hover last:border-0"
className="border-b border-border-muted transition-colors hover:bg-hover last:border-0"
>
<td className="px-4 py-3 text-sm font-medium text-gh-primary">
<td className="px-4 py-3 text-sm font-medium text-primary">
{user.username}
</td>
<td className="px-4 py-3">
<span className="inline-flex rounded-full border border-gh-blue/20 bg-gh-blue-bg px-2 py-0.5 text-xs font-medium text-gh-blue">
<span className="inline-flex rounded-full border border-blue/20 bg-blue-bg px-2 py-0.5 text-xs font-medium text-blue">
{user.role.name}
</span>
</td>
{hasPermission(currentUser, 'membership.edit') && (
<td className="px-4 py-3">
<div className="flex gap-3">
<button className="text-xs text-gh-blue hover:underline">
<button className="text-xs text-blue hover:underline">
Edit
</button>
{hasPermission(currentUser, 'membership.delete') && (
<button
onClick={() => openDeleteModal(user)}
className="text-xs text-gh-red hover:underline"
className="text-xs text-red hover:underline"
>
Delete
</button>
@@ -201,7 +197,7 @@ export function UserManagementTable({ initialData, roles, currentUser }: UserMan
<tr>
<td
colSpan={hasPermission(currentUser, 'membership.edit') ? 3 : 2}
className="px-4 py-8 text-center text-sm text-gh-muted"
className="px-4 py-8 text-center text-sm text-muted"
>
No users found
</td>
@@ -211,7 +207,6 @@ export function UserManagementTable({ initialData, roles, currentUser }: UserMan
</table>
</div>
{/* Modals */}
{showCreateModal && <CreateUserModal roles={roles} onClose={closeCreateModal} />}
{showDeleteModal && selectedUser && (

View File

@@ -3,7 +3,7 @@
import { useState, useEffect, useActionState, useTransition } from 'react';
import { Modal } from '@/components/ui/Modal';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { GhButton } from '@/components/ui/GhButton';
import { Button } from '@/components/ui/Button';
import { createServerAction, type ServerActionResult } from '@/lib/actions/server-management';
import { useServerCreationPopup } from '@/lib/context/ServerCreationPopupContext';
@@ -59,12 +59,12 @@ export function CreateServerModal({ isOpen, onClose }: CreateServerModalProps) {
return (
<Modal isOpen={isOpen} onClose={handleClose} title="Create New Server">
{!state.success && state.message && (
<div className="mb-4 rounded-md border border-gh-red/20 bg-gh-red-bg p-3 text-sm text-gh-red">{state.message}</div>
<div className="mb-4 rounded-md border border-red/20 bg-red-bg p-3 text-sm text-red">{state.message}</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-6">
<label htmlFor="server-name" className="mb-1.5 block text-sm font-medium text-gh-secondary">
<label htmlFor="server-name" className="mb-1.5 block text-sm font-medium text-secondary">
Server Name
</label>
<input
@@ -80,7 +80,7 @@ export function CreateServerModal({ isOpen, onClose }: CreateServerModalProps) {
</div>
<div className="flex justify-end space-x-2">
<GhButton
<Button
type="button"
variant="ghost"
size="md"
@@ -88,8 +88,8 @@ export function CreateServerModal({ isOpen, onClose }: CreateServerModalProps) {
disabled={isSubmitting || isPending}
>
Cancel
</GhButton>
<GhButton
</Button>
<Button
type="submit"
variant="primary"
size="md"
@@ -103,7 +103,7 @@ export function CreateServerModal({ isOpen, onClose }: CreateServerModalProps) {
) : (
'Create Server'
)}
</GhButton>
</Button>
</div>
</form>
</Modal>

View File

@@ -3,7 +3,7 @@
import { useState, useTransition } from 'react';
import { Modal } from '@/components/ui/Modal';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { GhButton } from '@/components/ui/GhButton';
import { Button } from '@/components/ui/Button';
import { deleteServerAction } from '@/lib/actions/server-management';
import { Server } from '@/lib/schemas/server';
@@ -43,25 +43,25 @@ export function DeleteServerModal({ isOpen, onClose, server }: DeleteServerModal
return (
<Modal isOpen={isOpen} onClose={handleClose} title="Delete Server">
{error && <div className="mb-4 rounded-md border border-gh-red/20 bg-gh-red-bg p-3 text-sm text-gh-red">{error}</div>}
{error && <div className="mb-4 rounded-md border border-red/20 bg-red-bg p-3 text-sm text-red">{error}</div>}
<div className="mb-6">
<p className="text-gh-secondary">
<p className="text-secondary">
Are you sure you want to delete the server <strong>&quot;{server.name}&quot;</strong>?
</p>
<p className="mt-2 text-sm text-gh-muted">This action cannot be undone.</p>
<p className="mt-2 text-sm text-muted">This action cannot be undone.</p>
</div>
<div className="flex justify-end space-x-2">
<GhButton
<Button
variant="ghost"
size="md"
onClick={handleClose}
disabled={isPending}
>
Cancel
</GhButton>
<GhButton
</Button>
<Button
variant="danger-solid"
size="md"
onClick={handleDelete}
@@ -75,7 +75,7 @@ export function DeleteServerModal({ isOpen, onClose, server }: DeleteServerModal
) : (
'Delete Server'
)}
</GhButton>
</Button>
</div>
</Modal>
);

View File

@@ -11,7 +11,7 @@ import {
import { useRouter } from 'next/navigation';
import { StatusDot } from '@/components/ui/StatusDot';
import { Badge } from '@/components/ui/Badge';
import { GhButton } from '@/components/ui/GhButton';
import { Button } from '@/components/ui/Button';
interface ServerCardProps {
server: Server;
@@ -55,23 +55,23 @@ export function ServerCard({ server }: ServerCardProps) {
const isRunning = server.status === ServiceStatus.Running;
return (
<div className="overflow-hidden rounded-lg border border-gh-border bg-gh-canvas transition-colors hover:border-gh-blue/40">
<div className="overflow-hidden rounded-lg border border-border bg-canvas transition-colors hover:border-blue/40">
<Link href={`/dashboard/server/${server.id}`} className="block p-4">
<div className="flex items-start justify-between">
<span className="text-sm font-semibold text-gh-primary">{server.name}</span>
<span className="text-sm font-semibold text-primary">{server.name}</span>
<StatusDot status={server.status} />
</div>
<div className="mt-3 grid grid-cols-2 gap-3">
<div>
<div className="text-xs text-gh-subtle">Players</div>
<div className="text-sm font-semibold text-gh-primary">
<div className="text-xs text-subtle">Players</div>
<div className="text-sm font-semibold text-primary">
{server.state?.playerCount ?? 0}
</div>
</div>
<div>
<div className="text-xs text-gh-subtle">Track</div>
<div className="truncate text-sm font-semibold text-gh-primary">
<div className="text-xs text-subtle">Track</div>
<div className="truncate text-sm font-semibold text-primary">
{server.state?.track || 'N/A'}
</div>
</div>
@@ -84,31 +84,31 @@ export function ServerCard({ server }: ServerCardProps) {
</div>
</Link>
<div className="flex gap-1.5 border-t border-gh-border-muted bg-gh-base px-4 py-2">
<GhButton
<div className="flex gap-1.5 border-t border-border-muted bg-base px-4 py-2">
<Button
variant="primary"
size="sm"
onClick={startServer}
disabled={isRunning || disabled || isPending}
>
Start
</GhButton>
<GhButton
</Button>
<Button
variant="ghost"
size="sm"
onClick={restartServer}
disabled={!isRunning || disabled || isPending}
>
Restart
</GhButton>
<GhButton
</Button>
<Button
variant="danger-outline"
size="sm"
onClick={stopServer}
disabled={!isRunning || disabled || isPending}
>
Stop
</GhButton>
</Button>
</div>
</div>
);

View File

@@ -72,7 +72,7 @@ export function ServerConfigurationTabs({
return (
<div className="flex flex-1 flex-col overflow-hidden">
<nav className="flex overflow-x-auto border-b border-gh-border-muted px-5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" aria-label="Tabs">
<nav className="flex overflow-x-auto border-b border-border-muted px-5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" aria-label="Tabs">
{tabs.map((tab) => {
const isActive = currentTab === tab.id;
return (
@@ -81,8 +81,8 @@ export function ServerConfigurationTabs({
onClick={() => setCurrentTab(tab.id)}
className={`flex shrink-0 items-center gap-1.5 border-b-2 px-3 py-3 text-sm font-medium whitespace-nowrap transition-colors ${
isActive
? 'border-gh-orange text-gh-primary'
: 'border-transparent text-gh-muted hover:text-gh-primary'
? 'border-orange text-primary'
: 'border-transparent text-muted hover:text-primary'
}`}
>
<span className="text-sm">{tab.icon}</span>

View File

@@ -11,7 +11,7 @@ import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { DeleteServerModal } from './DeleteServerModal';
import { Badge } from '@/components/ui/Badge';
import { GhButton } from '@/components/ui/GhButton';
import { Button } from '@/components/ui/Button';
interface ServerHeaderProps {
server: Server;
@@ -48,12 +48,11 @@ export function ServerHeader({ server, user }: ServerHeaderProps) {
return (
<>
{/* Topbar */}
<header className="flex h-12 items-center justify-between border-b border-gh-border-muted px-5">
<header className="flex h-12 items-center justify-between border-b border-border-muted px-5">
<div className="flex items-center gap-2 text-sm">
<Link href="/dashboard" className="text-gh-blue hover:underline">Servers</Link>
<span className="text-gh-subtle">/</span>
<span className="font-semibold text-gh-primary">{server.name}</span>
<Link href="/dashboard" className="text-blue hover:underline">Servers</Link>
<span className="text-subtle">/</span>
<span className="font-semibold text-primary">{server.name}</span>
</div>
<div className="flex items-center gap-2">
<Badge variant={
@@ -62,25 +61,24 @@ export function ServerHeader({ server, user }: ServerHeaderProps) {
}>
● {serviceStatusToString(server.status)}
</Badge>
<GhButton variant="ghost" size="sm" onClick={startServer} disabled={server.status === ServiceStatus.Running || disabled || isPending}>Start</GhButton>
<GhButton variant="ghost" size="sm" onClick={restartServer} disabled={server.status === ServiceStatus.Stopped || disabled || isPending}>Restart</GhButton>
<GhButton variant="danger-outline" size="sm" onClick={stopServer} disabled={server.status === ServiceStatus.Stopped || disabled || isPending}>Stop</GhButton>
<Button variant="ghost" size="sm" onClick={startServer} disabled={server.status === ServiceStatus.Running || disabled || isPending}>Start</Button>
<Button variant="ghost" size="sm" onClick={restartServer} disabled={server.status === ServiceStatus.Stopped || disabled || isPending}>Restart</Button>
<Button variant="danger-outline" size="sm" onClick={stopServer} disabled={server.status === ServiceStatus.Stopped || disabled || isPending}>Stop</Button>
{canDeleteServer && (
<GhButton variant="danger-solid" size="sm" onClick={() => setIsDeleteModalOpen(true)} disabled={disabled || isPending}>Remove</GhButton>
<Button variant="danger-solid" size="sm" onClick={() => setIsDeleteModalOpen(true)} disabled={disabled || isPending}>Remove</Button>
)}
</div>
</header>
{/* Info strip */}
<div className="flex flex-wrap gap-6 border-b border-gh-border-muted px-5 py-3">
<div className="flex flex-wrap gap-6 border-b border-border-muted px-5 py-3">
{[
{ label: 'Track', value: server.state?.track ?? 'N/A' },
{ label: 'Players', value: `${server.state?.playerCount ?? 0} / ${server.state?.maxConnections ?? 0}` },
{ label: 'Session', value: server.state?.session ?? 'N/A' },
].map(({ label, value }) => (
<div key={label}>
<div className="text-xs text-gh-subtle">{label}</div>
<div className="text-sm font-semibold text-gh-primary">{value}</div>
<div className="text-xs text-subtle">{label}</div>
<div className="text-sm font-semibold text-primary">{value}</div>
</div>
))}
</div>

View File

@@ -8,7 +8,7 @@ import { ServerRow } from './ServerRow';
import { CreateServerModal } from './CreateServerModal';
import RefreshButton from '@/components/ui/RefreshButton';
import { useSteamCMD } from '@/lib/context/SteamCMDContext';
import { GhButton } from '@/components/ui/GhButton';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
interface ServerListWithActionsProps {
@@ -28,16 +28,15 @@ export function ServerListWithActions({ servers, user }: ServerListWithActionsPr
return (
<>
{/* Topbar */}
<header className="flex h-12 items-center justify-between border-b border-gh-border-muted px-5">
<header className="flex h-12 items-center justify-between border-b border-border-muted px-5">
<div className="flex items-center gap-3">
<span className="text-sm font-semibold text-gh-primary">Servers</span>
<span className="text-xs text-gh-muted">{servers.length} total</span>
<span className="text-sm font-semibold text-primary">Servers</span>
<span className="text-xs text-muted">{servers.length} total</span>
</div>
<div className="flex items-center gap-2">
<RefreshButton />
{canCreateServer && (
<GhButton
<Button
variant="primary"
size="sm"
onClick={() => setIsCreateModalOpen(true)}
@@ -45,12 +44,11 @@ export function ServerListWithActions({ servers, user }: ServerListWithActionsPr
title={isSteamCMDRunning ? 'Disabled while SteamCMD is running' : undefined}
>
+ New Server
</GhButton>
</Button>
)}
</div>
</header>
{/* Status summary bar */}
<div className="flex items-center px-5 py-3">
<div className="flex gap-2">
{runningCount > 0 && <Badge variant="green">● {runningCount} running</Badge>}
@@ -58,7 +56,6 @@ export function ServerListWithActions({ servers, user }: ServerListWithActionsPr
</div>
</div>
{/* Responsive server list */}
<div className="px-5 pb-5">
{/* Card grid — desktop only */}
<div

View File

@@ -10,7 +10,7 @@ import {
stopServerEventAction
} from '@/lib/actions/servers';
import { StatusDot } from '@/components/ui/StatusDot';
import { GhButton } from '@/components/ui/GhButton';
import { Button } from '@/components/ui/Button';
interface ServerRowProps {
server: Server;
@@ -45,39 +45,39 @@ export function ServerRow({ server }: ServerRowProps) {
const disabled = transitioning || isPending;
return (
<div className="flex items-center justify-between gap-3 rounded-lg border border-gh-border bg-gh-canvas px-3 py-2.5 transition-colors hover:border-gh-border/60">
<div className="flex items-center justify-between gap-3 rounded-lg border border-border bg-canvas px-3 py-2.5 transition-colors hover:border-border/60">
<Link href={`/dashboard/server/${server.id}`} className="flex min-w-0 flex-1 items-center gap-3">
<StatusDot status={server.status} />
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-gh-primary">{server.name}</div>
<div className="truncate text-xs text-gh-muted">
<div className="truncate text-sm font-semibold text-primary">{server.name}</div>
<div className="truncate text-xs text-muted">
{server.state?.track ?? 'No track'} · {server.state?.playerCount ?? 0} players
</div>
</div>
</Link>
<div className="flex shrink-0 gap-1.5">
{server.status === ServiceStatus.Stopped ? (
<GhButton variant="primary" size="sm" onClick={startServer} disabled={disabled}>
<Button variant="primary" size="sm" onClick={startServer} disabled={disabled}>
Start
</GhButton>
</Button>
) : (
<>
<GhButton
<Button
variant="ghost"
size="sm"
onClick={restartServer}
disabled={disabled}
>
Restart
</GhButton>
<GhButton
</Button>
<Button
variant="danger-outline"
size="sm"
onClick={stopServer}
disabled={disabled}
>
Stop
</GhButton>
</Button>
</>
)}
</div>

View File

@@ -29,7 +29,7 @@ export function RecentSessions({ sessions }: RecentSessionsProps) {
if (sessions.length === 0) {
return (
<div className="px-4 py-8 text-center text-sm text-gh-muted">No recent sessions found</div>
<div className="px-4 py-8 text-center text-sm text-muted">No recent sessions found</div>
);
}
@@ -37,44 +37,44 @@ export function RecentSessions({ sessions }: RecentSessionsProps) {
<div className="overflow-x-auto">
<table className="min-w-full">
<thead>
<tr className="border-b border-gh-border-muted bg-gh-base">
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-gh-subtle">
<tr className="border-b border-border-muted bg-base">
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-subtle">
Date
</th>
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-gh-subtle">
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-subtle">
Type
</th>
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-gh-subtle">
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-subtle">
Track
</th>
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-gh-subtle">
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-subtle">
Duration
</th>
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-gh-subtle">
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider text-subtle">
Players
</th>
</tr>
</thead>
<tbody>
{sessions.map((session) => (
<tr key={session.id} className="border-b border-gh-border-muted transition-colors hover:bg-gh-hover last:border-0">
<td className="px-4 py-3 text-sm text-gh-muted">
<tr key={session.id} className="border-b border-border-muted transition-colors hover:bg-hover last:border-0">
<td className="px-4 py-3 text-sm text-muted">
{formatDate(session.date)}
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
session.type === 'R' ? 'bg-gh-green-bg text-gh-green' :
session.type === 'Q' ? 'bg-gh-yellow-bg text-gh-yellow' :
'bg-gh-blue-bg text-gh-blue'
session.type === 'R' ? 'bg-green-bg text-green' :
session.type === 'Q' ? 'bg-yellow-bg text-yellow' :
'bg-blue-bg text-blue'
}`}>
{session.type === 'R' ? 'Race' : session.type === 'Q' ? 'Qualify' : 'Practice'}
</span>
</td>
<td className="px-4 py-3 text-sm text-gh-secondary">{session.track}</td>
<td className="px-4 py-3 text-sm text-gh-muted">
<td className="px-4 py-3 text-sm text-secondary">{session.track}</td>
<td className="px-4 py-3 text-sm text-muted">
{formatDuration(session.duration)}
</td>
<td className="px-4 py-3 text-sm text-gh-secondary">
<td className="px-4 py-3 text-sm text-secondary">
{session.players}
</td>
</tr>

View File

@@ -15,10 +15,10 @@ interface SessionTypesChartProps {
}
const colors = [
'#3fb950', // gh-green
'#58a6ff', // gh-blue
'#f78166', // gh-red
'#d29922' // gh-yellow
'#3fb950', // green
'#58a6ff', // blue
'#f78166', // red
'#d29922' // yellow
];
export function SessionTypesChart({ data }: SessionTypesChartProps) {

View File

@@ -6,12 +6,12 @@ interface StatCardProps {
export function StatCard({ title, value, icon }: StatCardProps) {
return (
<div className="rounded-lg border border-gh-border bg-gh-canvas p-4">
<div className="rounded-lg border border-border bg-canvas p-4">
<div className="mb-1 flex items-center gap-2">
<span className="text-base">{icon}</span>
<span className="text-xs text-gh-muted">{title}</span>
<span className="text-xs text-muted">{title}</span>
</div>
<div className="text-2xl font-bold text-gh-primary">{value}</div>
<div className="text-2xl font-bold text-primary">{value}</div>
</div>
);
}

View File

@@ -16,15 +16,14 @@ export function StatisticsDashboard({ stats }: StatisticsDashboardProps) {
return (
<div className="py-16 text-center">
<div className="mb-3 text-5xl">📊</div>
<h3 className="mb-1 text-base font-semibold text-gh-primary">No Statistics Available</h3>
<p className="text-sm text-gh-muted">No data found for the selected date range.</p>
<h3 className="mb-1 text-base font-semibold text-primary">No Statistics Available</h3>
<p className="text-sm text-muted">No data found for the selected date range.</p>
</div>
);
}
return (
<div className="flex flex-col gap-4">
{/* 4 stat cards */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<StatCard title="Total Sessions" value={stats.totalSessions ?? 0} icon="🏁" />
<StatCard
@@ -40,26 +39,24 @@ export function StatisticsDashboard({ stats }: StatisticsDashboardProps) {
<StatCard title="Peak Players" value={stats.peakPlayers ?? 0} icon="🔥" />
</div>
{/* Charts row */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<div className="lg:col-span-2 rounded-lg border border-gh-border bg-gh-canvas p-4">
<h3 className="mb-3 text-sm font-semibold text-gh-primary">Player Count Over Time</h3>
<div className="lg:col-span-2 rounded-lg border border-border bg-canvas p-4">
<h3 className="mb-3 text-sm font-semibold text-primary">Player Count Over Time</h3>
<PlayerCountChart data={stats.playerCountOverTime ?? []} />
</div>
<div className="rounded-lg border border-gh-border bg-gh-canvas p-4">
<h3 className="mb-3 text-sm font-semibold text-gh-primary">Session Types</h3>
<div className="rounded-lg border border-border bg-canvas p-4">
<h3 className="mb-3 text-sm font-semibold text-primary">Session Types</h3>
<SessionTypesChart data={stats.sessionTypes ?? []} />
</div>
</div>
<div className="rounded-lg border border-gh-border bg-gh-canvas p-4">
<h3 className="mb-3 text-sm font-semibold text-gh-primary">Daily Activity</h3>
<div className="rounded-lg border border-border bg-canvas p-4">
<h3 className="mb-3 text-sm font-semibold text-primary">Daily Activity</h3>
<DailyActivityChart data={stats.dailyActivity ?? []} />
</div>
{/* Recent sessions */}
<div className="overflow-hidden rounded-lg border border-gh-border bg-gh-canvas">
<div className="border-b border-gh-border-muted px-4 py-3 text-sm font-semibold text-gh-primary">Recent Sessions</div>
<div className="overflow-hidden rounded-lg border border-border bg-canvas">
<div className="border-b border-border-muted px-4 py-3 text-sm font-semibold text-primary">Recent Sessions</div>
<RecentSessions sessions={stats.recentSessions ?? []} />
</div>
</div>

View File

@@ -8,7 +8,7 @@ interface AppShellProps {
export function AppShell({ children, showUsers }: AppShellProps) {
return (
<div className="min-h-screen bg-gh-base">
<div className="min-h-screen bg-base">
<Sidebar showUsers={showUsers} />
{/* Offset for sidebar (desktop: left margin, mobile: bottom padding) */}
<div className="pb-14 md:pb-0 md:pl-14">

View File

@@ -9,11 +9,11 @@ interface BadgeProps {
}
const variantClasses: Record<BadgeVariant, string> = {
green: 'bg-gh-green-bg text-gh-green border border-gh-green/20',
yellow: 'bg-gh-yellow-bg text-gh-yellow border border-gh-yellow/20',
red: 'bg-gh-red-bg text-gh-red border border-gh-red/20',
blue: 'bg-gh-blue-bg text-gh-blue border border-gh-blue/20',
gray: 'bg-gh-overlay text-gh-muted border border-gh-border',
green: 'bg-green-bg text-green border border-green/20',
yellow: 'bg-yellow-bg text-yellow border border-yellow/20',
red: 'bg-red-bg text-red border border-red/20',
blue: 'bg-blue-bg text-blue border border-blue/20',
gray: 'bg-overlay text-muted border border-border',
};
export function Badge({ variant, children, className }: BadgeProps) {

73
src/components/ui/Button.tsx Executable file → Normal file
View File

@@ -1,46 +1,47 @@
import { forwardRef, ButtonHTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
import { LoadingSpinner } from './LoadingSpinner';
import { ButtonHTMLAttributes } from 'react';
type ButtonVariant = 'primary' | 'ghost' | 'danger-outline' | 'danger-solid';
type ButtonSize = 'sm' | 'md';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
variant?: ButtonVariant;
size?: ButtonSize;
}
const baseClasses =
'inline-flex items-center justify-center font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
ghost: 'text-gray-300 hover:text-white hover:bg-gray-700 focus:ring-gray-500'
const variantClasses: Record<ButtonVariant, string> = {
primary: 'bg-btn-green border border-btn-green-hover text-white hover:bg-btn-green-hover',
ghost: 'bg-transparent border border-border text-secondary hover:bg-overlay hover:text-primary',
'danger-outline': 'bg-transparent border border-border text-red hover:border-red/40 hover:bg-red-bg',
'danger-solid': 'bg-overlay border border-red/20 text-red hover:bg-red-bg',
};
const sizes = {
sm: 'px-3 py-1 text-sm',
const sizeClasses: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base'
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{ className, variant = 'primary', size = 'md', isLoading, children, disabled, ...props },
ref
) => {
return (
<button
className={cn(baseClasses, variants[variant], sizes[size], className)}
disabled={disabled || isLoading}
ref={ref}
{...props}
>
{isLoading && <LoadingSpinner size="sm" className="mr-2" />}
{children}
</button>
);
}
);
Button.displayName = 'Button';
export function Button({
variant = 'ghost',
size = 'md',
className,
children,
disabled,
...props
}: ButtonProps) {
return (
<button
{...props}
disabled={disabled}
className={cn(
'inline-flex items-center gap-1.5 justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue focus:ring-offset-1 focus:ring-offset-base',
'disabled:cursor-not-allowed disabled:opacity-40',
variantClasses[variant],
sizeClasses[size],
className
)}
>
{children}
</button>
);
}

View File

@@ -1,47 +0,0 @@
import { cn } from '@/lib/utils';
import { ButtonHTMLAttributes } from 'react';
type ButtonVariant = 'primary' | 'ghost' | 'danger-outline' | 'danger-solid';
type ButtonSize = 'sm' | 'md';
interface GhButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
}
const variantClasses: Record<ButtonVariant, string> = {
primary: 'bg-gh-btn-green border border-gh-btn-green-hover text-white hover:bg-gh-btn-green-hover',
ghost: 'bg-transparent border border-gh-border text-gh-secondary hover:bg-gh-overlay hover:text-gh-primary',
'danger-outline': 'bg-transparent border border-gh-border text-gh-red hover:border-gh-red/40 hover:bg-gh-red-bg',
'danger-solid': 'bg-gh-overlay border border-gh-red/20 text-gh-red hover:bg-gh-red-bg',
};
const sizeClasses: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
};
export function GhButton({
variant = 'ghost',
size = 'md',
className,
children,
disabled,
...props
}: GhButtonProps) {
return (
<button
{...props}
disabled={disabled}
className={cn(
'inline-flex items-center gap-1.5 justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-gh-blue focus:ring-offset-1 focus:ring-offset-gh-base',
'disabled:cursor-not-allowed disabled:opacity-40',
variantClasses[variant],
sizeClasses[size],
className
)}
>
{children}
</button>
);
}

View File

@@ -37,13 +37,13 @@ export function Modal({ isOpen, onClose, title, children, className }: ModalProp
<div className="absolute inset-0" onClick={onClose} />
<div
className={cn(
'relative w-full max-w-md rounded-lg border border-gh-border bg-gh-canvas p-6 shadow-xl',
'relative w-full max-w-md rounded-lg border border-border bg-canvas p-6 shadow-xl',
className
)}
>
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-gh-primary">{title}</h3>
<button onClick={onClose} className="text-gh-muted hover:text-gh-primary">
<h3 className="text-lg font-semibold text-primary">{title}</h3>
<button onClick={onClose} className="text-muted hover:text-primary">
×
</button>
</div>

View File

@@ -10,7 +10,7 @@ interface SidebarProps {
function ServerIcon({ active }: { active: boolean }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className={cn('h-5 w-5', active ? 'text-gh-blue' : 'text-gh-subtle')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className={cn('h-5 w-5', active ? 'text-blue' : 'text-subtle')}>
<rect x="2" y="3" width="20" height="5" rx="1" />
<rect x="2" y="10" width="20" height="5" rx="1" />
<rect x="2" y="17" width="20" height="5" rx="1" />
@@ -23,7 +23,7 @@ function ServerIcon({ active }: { active: boolean }) {
function UsersIcon({ active }: { active: boolean }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className={cn('h-5 w-5', active ? 'text-gh-blue' : 'text-gh-subtle')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className={cn('h-5 w-5', active ? 'text-blue' : 'text-subtle')}>
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" />
@@ -43,16 +43,13 @@ export function Sidebar({ showUsers }: SidebarProps) {
return (
<>
{/* Desktop icon sidebar */}
<aside className="fixed inset-y-0 left-0 z-40 hidden w-14 flex-col items-center border-r border-gh-border-muted bg-gh-sidebar py-3 md:flex">
{/* Logo */}
<div className="mb-4 flex h-8 w-8 items-center justify-center rounded-lg bg-gh-blue">
<aside className="fixed inset-y-0 left-0 z-40 hidden w-14 flex-col items-center border-r border-border-muted bg-sidebar py-3 md:flex">
<div className="mb-4 flex h-8 w-8 items-center justify-center rounded-lg bg-blue">
<svg viewBox="0 0 24 24" fill="none" stroke="#0d1117" strokeWidth={2.5} className="h-4 w-4">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
{/* Nav icons */}
<nav className="flex flex-1 flex-col items-center gap-1">
{navItems.map((item) => (
<Link
@@ -61,7 +58,7 @@ export function Sidebar({ showUsers }: SidebarProps) {
title={item.label}
className={cn(
'flex h-9 w-9 items-center justify-center rounded-lg transition-colors',
item.active ? 'bg-gh-blue-bg' : 'hover:bg-gh-overlay'
item.active ? 'bg-blue-bg' : 'hover:bg-overlay'
)}
>
{item.icon}
@@ -69,19 +66,17 @@ export function Sidebar({ showUsers }: SidebarProps) {
))}
</nav>
{/* Avatar placeholder */}
<div className="mt-auto h-7 w-7 rounded-full border-2 border-gh-border bg-gh-overlay" />
<div className="mt-auto h-7 w-7 rounded-full border-2 border-border bg-overlay" />
</aside>
{/* Mobile bottom tab bar */}
<nav className="fixed inset-x-0 bottom-0 z-40 flex h-14 items-center justify-around border-t border-gh-border-muted bg-gh-sidebar md:hidden">
<nav className="fixed inset-x-0 bottom-0 z-40 flex h-14 items-center justify-around border-t border-border-muted bg-sidebar md:hidden">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
'flex flex-col items-center justify-center gap-0.5 px-4 py-2 text-xs transition-colors',
item.active ? 'text-gh-blue' : 'text-gh-subtle'
item.active ? 'text-blue' : 'text-subtle'
)}
>
{item.icon}

View File

@@ -8,13 +8,13 @@ interface StatusDotProps {
export function StatusDot({ status, className }: StatusDotProps) {
const dotClass = {
[ServiceStatus.Running]: 'bg-gh-green shadow-[0_0_0_3px_#3fb95022]',
[ServiceStatus.Starting]: 'bg-gh-yellow animate-pulse',
[ServiceStatus.Stopping]: 'bg-gh-yellow animate-pulse',
[ServiceStatus.Restarting]: 'bg-gh-yellow animate-pulse',
[ServiceStatus.Stopped]: 'bg-gh-subtle',
[ServiceStatus.Unknown]: 'bg-gh-subtle',
}[status] ?? 'bg-gh-subtle';
[ServiceStatus.Running]: 'bg-green shadow-[0_0_0_3px_#3fb95022]',
[ServiceStatus.Starting]: 'bg-yellow animate-pulse',
[ServiceStatus.Stopping]: 'bg-yellow animate-pulse',
[ServiceStatus.Restarting]: 'bg-yellow animate-pulse',
[ServiceStatus.Stopped]: 'bg-subtle',
[ServiceStatus.Unknown]: 'bg-subtle',
}[status] ?? 'bg-subtle';
return (
<span

View File

@@ -12,11 +12,11 @@ export function SteamCMDNotification() {
const serverCount = runningSteamServers.size;
return (
<div className="border-l-4 border-gh-yellow bg-gh-yellow-bg px-4 py-3">
<div className="border-l-4 border-yellow bg-yellow-bg px-4 py-3">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-gh-yellow"
className="h-5 w-5 text-yellow"
viewBox="0 0 20 20"
fill="currentColor"
>
@@ -28,7 +28,7 @@ export function SteamCMDNotification() {
</svg>
</div>
<div className="ml-3">
<p className="text-sm text-gh-yellow">
<p className="text-sm text-yellow">
<strong>SteamCMD is currently running</strong> for {serverCount} server{serverCount !== 1 ? 's' : ''}.
Server actions are temporarily disabled to prevent conflicts.
This will automatically resolve when the download completes.
@@ -36,8 +36,8 @@ export function SteamCMDNotification() {
</div>
<div className="ml-auto flex-shrink-0">
<div className="flex items-center">
<div className="animate-spin h-4 w-4 border-2 border-gh-yellow border-t-transparent rounded-full mr-2"></div>
<span className="text-xs text-gh-yellow">Downloading...</span>
<div className="animate-spin h-4 w-4 border-2 border-yellow border-t-transparent rounded-full mr-2"></div>
<span className="text-xs text-yellow">Downloading...</span>
</div>
</div>
</div>