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:
@@ -1,35 +1,35 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-gh-base: #0d1117;
|
||||
--color-gh-canvas: #161b22;
|
||||
--color-gh-overlay: #21262d;
|
||||
--color-gh-sidebar: #010409;
|
||||
--color-gh-border: #30363d;
|
||||
--color-gh-border-muted: #21262d;
|
||||
--color-gh-hover: #1c2128;
|
||||
--color-gh-primary: #e6edf3;
|
||||
--color-gh-secondary: #c9d1d9;
|
||||
--color-gh-muted: #8b949e;
|
||||
--color-gh-subtle: #6e7681;
|
||||
--color-gh-blue: #58a6ff;
|
||||
--color-gh-blue-bg: #1f6feb33;
|
||||
--color-gh-green: #3fb950;
|
||||
--color-gh-green-bg: #3fb95022;
|
||||
--color-gh-yellow: #d29922;
|
||||
--color-gh-yellow-bg: #d2992220;
|
||||
--color-gh-red: #f85149;
|
||||
--color-gh-red-bg: #f8514920;
|
||||
--color-gh-orange: #f78166;
|
||||
--color-gh-btn-green: #238636;
|
||||
--color-gh-btn-green-hover: #2ea043;
|
||||
--color-base: #0d1117;
|
||||
--color-canvas: #161b22;
|
||||
--color-overlay: #21262d;
|
||||
--color-sidebar: #010409;
|
||||
--color-border: #30363d;
|
||||
--color-border-muted: #21262d;
|
||||
--color-hover: #1c2128;
|
||||
--color-primary: #e6edf3;
|
||||
--color-secondary: #c9d1d9;
|
||||
--color-muted: #8b949e;
|
||||
--color-subtle: #6e7681;
|
||||
--color-blue: #58a6ff;
|
||||
--color-blue-bg: #1f6feb33;
|
||||
--color-green: #3fb950;
|
||||
--color-green-bg: #3fb95022;
|
||||
--color-yellow: #d29922;
|
||||
--color-yellow-bg: #d2992220;
|
||||
--color-red: #f85149;
|
||||
--color-red-bg: #f8514920;
|
||||
--color-orange: #f78166;
|
||||
--color-btn-green: #238636;
|
||||
--color-btn-green-hover: #2ea043;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.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 {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="bg-gh-base text-gh-primary antialiased">{children}</body>
|
||||
<body className="bg-base text-primary antialiased">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 "{user.username}"? 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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>"{server.name}"</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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
73
src/components/ui/Button.tsx
Executable file → Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user