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

@@ -1,35 +1,35 @@
@import "tailwindcss"; @import "tailwindcss";
@theme { @theme {
--color-gh-base: #0d1117; --color-base: #0d1117;
--color-gh-canvas: #161b22; --color-canvas: #161b22;
--color-gh-overlay: #21262d; --color-overlay: #21262d;
--color-gh-sidebar: #010409; --color-sidebar: #010409;
--color-gh-border: #30363d; --color-border: #30363d;
--color-gh-border-muted: #21262d; --color-border-muted: #21262d;
--color-gh-hover: #1c2128; --color-hover: #1c2128;
--color-gh-primary: #e6edf3; --color-primary: #e6edf3;
--color-gh-secondary: #c9d1d9; --color-secondary: #c9d1d9;
--color-gh-muted: #8b949e; --color-muted: #8b949e;
--color-gh-subtle: #6e7681; --color-subtle: #6e7681;
--color-gh-blue: #58a6ff; --color-blue: #58a6ff;
--color-gh-blue-bg: #1f6feb33; --color-blue-bg: #1f6feb33;
--color-gh-green: #3fb950; --color-green: #3fb950;
--color-gh-green-bg: #3fb95022; --color-green-bg: #3fb95022;
--color-gh-yellow: #d29922; --color-yellow: #d29922;
--color-gh-yellow-bg: #d2992220; --color-yellow-bg: #d2992220;
--color-gh-red: #f85149; --color-red: #f85149;
--color-gh-red-bg: #f8514920; --color-red-bg: #f8514920;
--color-gh-orange: #f78166; --color-orange: #f78166;
--color-gh-btn-green: #238636; --color-btn-green: #238636;
--color-gh-btn-green-hover: #2ea043; --color-btn-green-hover: #2ea043;
} }
@layer components { @layer components {
.form-input { .form-input {
@apply block w-full rounded-md border border-gh-border bg-gh-overlay px-3 py-2 text-sm text-gh-primary placeholder-gh-subtle focus:border-gh-blue focus:outline-none focus:ring-1 focus:ring-gh-blue; @apply block w-full rounded-md border border-border bg-overlay px-3 py-2 text-sm text-primary placeholder-subtle focus:border-blue focus:outline-none focus:ring-1 focus:ring-blue;
} }
.form-select { .form-select {
@apply block w-full rounded-md border border-gh-border bg-gh-overlay px-3 py-2 text-sm text-gh-primary focus:border-gh-blue focus:outline-none focus:ring-1 focus:ring-gh-blue; @apply block w-full rounded-md border border-border bg-overlay px-3 py-2 text-sm text-primary focus:border-blue focus:outline-none focus:ring-1 focus:ring-blue;
} }
} }

View File

@@ -13,7 +13,7 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<body className="bg-gh-base text-gh-primary antialiased">{children}</body> <body className="bg-base text-primary antialiased">{children}</body>
</html> </html>
); );
} }

View File

@@ -96,7 +96,7 @@ export function AssistRulesEditor({ serverId, config }: AssistRulesEditorProps)
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{assistFields.map(({ key, label, type }) => ( {assistFields.map(({ key, label, type }) => (
<div key={key}> <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' ? ( {type === 'number' ? (
<input <input
type="number" type="number"
@@ -122,13 +122,13 @@ export function AssistRulesEditor({ serverId, config }: AssistRulesEditorProps)
))} ))}
</div> </div>
<div className="border-t border-gh-border pt-6"> <div className="border-t border-border pt-6">
<label className="flex items-center gap-2 text-sm text-gh-muted"> <label className="flex items-center gap-2 text-sm text-muted">
<input <input
type="checkbox" type="checkbox"
checked={restart} checked={restart}
onChange={(e) => setRestart(e.target.checked)} 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 Restart server after saving
</label> </label>
@@ -138,7 +138,7 @@ export function AssistRulesEditor({ serverId, config }: AssistRulesEditorProps)
<button <button
type="submit" type="submit"
disabled={isSubmitting} 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'} {isSubmitting ? 'Saving…' : 'Save Changes'}
</button> </button>

View File

@@ -47,7 +47,7 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
<form onSubmit={handleSubmit} className="max-w-3xl space-y-6"> <form onSubmit={handleSubmit} className="max-w-3xl space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div> <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 <input
type="number" type="number"
disabled={isSubmitting} disabled={isSubmitting}
@@ -58,7 +58,7 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
</div> </div>
<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 <input
type="number" type="number"
disabled={isSubmitting} disabled={isSubmitting}
@@ -69,7 +69,7 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
</div> </div>
<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 <input
type="number" type="number"
disabled={isSubmitting} disabled={isSubmitting}
@@ -80,7 +80,7 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
</div> </div>
<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 <select
disabled={isSubmitting} disabled={isSubmitting}
value={formData.lanDiscovery} value={formData.lanDiscovery}
@@ -93,7 +93,7 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
</div> </div>
<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 <select
disabled={isSubmitting} disabled={isSubmitting}
value={formData.registerToLobby} value={formData.registerToLobby}
@@ -106,13 +106,13 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
</div> </div>
</div> </div>
<div className="border-t border-gh-border pt-6"> <div className="border-t border-border pt-6">
<label className="flex items-center gap-2 text-sm text-gh-muted"> <label className="flex items-center gap-2 text-sm text-muted">
<input <input
type="checkbox" type="checkbox"
checked={restart} checked={restart}
onChange={(e) => setRestart(e.target.checked)} 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 Restart server after saving
</label> </label>
@@ -122,7 +122,7 @@ export function ConfigurationEditor({ serverId, config }: ConfigurationEditorPro
<button <button
type="submit" type="submit"
disabled={isSubmitting} 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'} {isSubmitting ? 'Saving…' : 'Save Changes'}
</button> </button>

View File

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

View File

@@ -105,11 +105,11 @@ export function EventRulesEditor({ serverId, config }: EventRulesEditorProps) {
return ( return (
<form onSubmit={handleSubmit} className="max-w-4xl space-y-8"> <form onSubmit={handleSubmit} className="max-w-4xl space-y-8">
<div className="space-y-6"> <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"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{numberFields.map(({ key, label, min, max }) => ( {numberFields.map(({ key, label, min, max }) => (
<div key={key}> <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 <input
type="number" type="number"
disabled={isSubmitting} disabled={isSubmitting}
@@ -125,13 +125,13 @@ export function EventRulesEditor({ serverId, config }: EventRulesEditorProps) {
</div> </div>
<div className="space-y-6"> <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 Pitstop & Refuelling Rules
</h3> </h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{booleanFields.map(({ key, label }) => ( {booleanFields.map(({ key, label }) => (
<div key={key}> <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 <select
disabled={isSubmitting} disabled={isSubmitting}
value={formData[key] ? 'true' : 'false'} value={formData[key] ? 'true' : 'false'}
@@ -146,13 +146,13 @@ export function EventRulesEditor({ serverId, config }: EventRulesEditorProps) {
</div> </div>
</div> </div>
<div className="border-t border-gh-border pt-6"> <div className="border-t border-border pt-6">
<label className="flex items-center gap-2 text-sm text-gh-muted"> <label className="flex items-center gap-2 text-sm text-muted">
<input <input
type="checkbox" type="checkbox"
checked={restart} checked={restart}
onChange={(e) => setRestart(e.target.checked)} 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 Restart server after saving
</label> </label>
@@ -162,7 +162,7 @@ export function EventRulesEditor({ serverId, config }: EventRulesEditorProps) {
<button <button
type="submit" type="submit"
disabled={isSubmitting} 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'} {isSubmitting ? 'Saving…' : 'Save Changes'}
</button> </button>

View File

@@ -124,13 +124,13 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
return ( return (
<form onSubmit={handleSubmit} className="max-w-4xl space-y-8"> <form onSubmit={handleSubmit} className="max-w-4xl space-y-8">
<div className="space-y-6"> <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 Basic Settings
</h3> </h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{textFields.map(({ key, label, type }) => ( {textFields.map(({ key, label, type }) => (
<div key={key}> <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 <input
type={type} type={type}
disabled={isSubmitting} disabled={isSubmitting}
@@ -142,7 +142,7 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
))} ))}
<div> <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 <select
disabled={isSubmitting} disabled={isSubmitting}
value={formData.carGroup} value={formData.carGroup}
@@ -160,13 +160,13 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
</div> </div>
<div className="space-y-6"> <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 Requirements & Limits
</h3> </h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{numberFields.map(({ key, label, min, max }) => ( {numberFields.map(({ key, label, min, max }) => (
<div key={key}> <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 <input
type="number" type="number"
disabled={isSubmitting} disabled={isSubmitting}
@@ -182,13 +182,13 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
</div> </div>
<div className="space-y-6"> <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 Race Options
</h3> </h3>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{selectFields.map(({ key, label }) => ( {selectFields.map(({ key, label }) => (
<div key={key}> <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 <select
disabled={isSubmitting} disabled={isSubmitting}
value={formData[key] as number} value={formData[key] as number}
@@ -202,7 +202,7 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
))} ))}
<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">
Formation Lap Type Formation Lap Type
</label> </label>
<select <select
@@ -221,13 +221,13 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
</div> </div>
</div> </div>
<div className="border-t border-gh-border pt-6"> <div className="border-t border-border pt-6">
<label className="flex items-center gap-2 text-sm text-gh-muted"> <label className="flex items-center gap-2 text-sm text-muted">
<input <input
type="checkbox" type="checkbox"
checked={restart} checked={restart}
onChange={(e) => setRestart(e.target.checked)} 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 Restart server after saving
</label> </label>
@@ -237,7 +237,7 @@ export function ServerSettingsEditor({ serverId, config }: ServerSettingsEditorP
<button <button
type="submit" type="submit"
disabled={isSubmitting} 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'} {isSubmitting ? 'Saving…' : 'Save Changes'}
</button> </button>

View File

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

View File

@@ -23,26 +23,26 @@ export default function LoginForm({
}, [expired]); }, [expired]);
const [state, formAction] = useActionState(loginAction, initialState); const [state, formAction] = useActionState(loginAction, initialState);
return ( return (
<div className="flex min-h-screen items-center justify-center bg-gh-base px-4"> <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-gh-border bg-gh-canvas p-8 shadow-xl"> <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"> <div className="text-center">
<h1 className="text-xl font-bold text-gh-primary">ACC Server Manager</h1> <h1 className="text-xl font-bold text-primary">ACC Server Manager</h1>
<p className="mt-1 text-sm text-gh-muted">Sign in to manage your servers</p> <p className="mt-1 text-sm text-muted">Sign in to manage your servers</p>
</div> </div>
{expired && ( {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. Your session has expired. Please sign in again.
</div> </div>
)} )}
{state?.success ? null : ( {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} {state?.message}
</div> </div>
)} )}
<form action={formAction} className="space-y-4"> <form action={formAction} className="space-y-4">
<div> <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 Username
</label> </label>
<input <input
@@ -56,7 +56,7 @@ export default function LoginForm({
</div> </div>
<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 Password
</label> </label>
<input <input
@@ -71,7 +71,7 @@ export default function LoginForm({
<button <button
type="submit" 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 Sign in
</button> </button>

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
import { useState, useTransition } from 'react'; import { useState, useTransition } from 'react';
import { Modal } from '@/components/ui/Modal'; import { Modal } from '@/components/ui/Modal';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; 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 { deleteServerAction } from '@/lib/actions/server-management';
import { Server } from '@/lib/schemas/server'; import { Server } from '@/lib/schemas/server';
@@ -43,25 +43,25 @@ export function DeleteServerModal({ isOpen, onClose, server }: DeleteServerModal
return ( return (
<Modal isOpen={isOpen} onClose={handleClose} title="Delete Server"> <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"> <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>? Are you sure you want to delete the server <strong>&quot;{server.name}&quot;</strong>?
</p> </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>
<div className="flex justify-end space-x-2"> <div className="flex justify-end space-x-2">
<GhButton <Button
variant="ghost" variant="ghost"
size="md" size="md"
onClick={handleClose} onClick={handleClose}
disabled={isPending} disabled={isPending}
> >
Cancel Cancel
</GhButton> </Button>
<GhButton <Button
variant="danger-solid" variant="danger-solid"
size="md" size="md"
onClick={handleDelete} onClick={handleDelete}
@@ -75,7 +75,7 @@ export function DeleteServerModal({ isOpen, onClose, server }: DeleteServerModal
) : ( ) : (
'Delete Server' 'Delete Server'
)} )}
</GhButton> </Button>
</div> </div>
</Modal> </Modal>
); );

View File

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

View File

@@ -72,7 +72,7 @@ export function ServerConfigurationTabs({
return ( return (
<div className="flex flex-1 flex-col overflow-hidden"> <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) => { {tabs.map((tab) => {
const isActive = currentTab === tab.id; const isActive = currentTab === tab.id;
return ( return (
@@ -81,8 +81,8 @@ export function ServerConfigurationTabs({
onClick={() => setCurrentTab(tab.id)} 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 ${ 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 isActive
? 'border-gh-orange text-gh-primary' ? 'border-orange text-primary'
: 'border-transparent text-gh-muted hover:text-gh-primary' : 'border-transparent text-muted hover:text-primary'
}`} }`}
> >
<span className="text-sm">{tab.icon}</span> <span className="text-sm">{tab.icon}</span>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,12 +6,12 @@ interface StatCardProps {
export function StatCard({ title, value, icon }: StatCardProps) { export function StatCard({ title, value, icon }: StatCardProps) {
return ( 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"> <div className="mb-1 flex items-center gap-2">
<span className="text-base">{icon}</span> <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>
<div className="text-2xl font-bold text-gh-primary">{value}</div> <div className="text-2xl font-bold text-primary">{value}</div>
</div> </div>
); );
} }

View File

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

View File

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

View File

@@ -9,11 +9,11 @@ interface BadgeProps {
} }
const variantClasses: Record<BadgeVariant, string> = { const variantClasses: Record<BadgeVariant, string> = {
green: 'bg-gh-green-bg text-gh-green border border-gh-green/20', green: 'bg-green-bg text-green border border-green/20',
yellow: 'bg-gh-yellow-bg text-gh-yellow border border-gh-yellow/20', yellow: 'bg-yellow-bg text-yellow border border-yellow/20',
red: 'bg-gh-red-bg text-gh-red border border-gh-red/20', red: 'bg-red-bg text-red border border-red/20',
blue: 'bg-gh-blue-bg text-gh-blue border border-gh-blue/20', blue: 'bg-blue-bg text-blue border border-blue/20',
gray: 'bg-gh-overlay text-gh-muted border border-gh-border', gray: 'bg-overlay text-muted border border-border',
}; };
export function Badge({ variant, children, className }: BadgeProps) { 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 { 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> { interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; variant?: ButtonVariant;
size?: 'sm' | 'md' | 'lg'; size?: ButtonSize;
isLoading?: boolean;
} }
const baseClasses = const variantClasses: Record<ButtonVariant, string> = {
'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'; 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',
const variants = { 'danger-outline': 'bg-transparent border border-border text-red hover:border-red/40 hover:bg-red-bg',
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500', 'danger-solid': 'bg-overlay border border-red/20 text-red hover:bg-red-bg',
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 sizes = { const sizeClasses: Record<ButtonSize, string> = {
sm: 'px-3 py-1 text-sm', sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm', md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base'
}; };
export const Button = forwardRef<HTMLButtonElement, ButtonProps>( export function Button({
( variant = 'ghost',
{ className, variant = 'primary', size = 'md', isLoading, children, disabled, ...props }, size = 'md',
ref className,
) => { children,
return ( disabled,
<button ...props
className={cn(baseClasses, variants[variant], sizes[size], className)} }: ButtonProps) {
disabled={disabled || isLoading} return (
ref={ref} <button
{...props} {...props}
> disabled={disabled}
{isLoading && <LoadingSpinner size="sm" className="mr-2" />} className={cn(
{children} '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',
</button> 'disabled:cursor-not-allowed disabled:opacity-40',
); variantClasses[variant],
} sizeClasses[size],
); className
)}
Button.displayName = 'Button'; >
{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="absolute inset-0" onClick={onClose} />
<div <div
className={cn( 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 className
)} )}
> >
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-gh-primary">{title}</h3> <h3 className="text-lg font-semibold text-primary">{title}</h3>
<button onClick={onClose} className="text-gh-muted hover:text-gh-primary"> <button onClick={onClose} className="text-muted hover:text-primary">
× ×
</button> </button>
</div> </div>

View File

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

View File

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

View File

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