Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ DB_PORT=3306
DB_PASSWORD_TEST=app_pw_change_me
DB_NAME_TEST=red_tetris_test

CLIENT_URL=http://localhost:3000,http://127.0.0.1:3000
CLIENT_URL=http://localhost:3001,http://127.0.0.1:3001
CLIENT_PORT=3001

# Client Configuration
Expand Down
2 changes: 1 addition & 1 deletion client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ To build and run using Docker:
docker build -t my-app .

# Run the container
docker run -p 3000:3000 my-app
docker run -p 3001:3001 my-app
```

The containerized application can be deployed to any platform that supports Docker, including:
Expand Down
115 changes: 110 additions & 5 deletions client/app/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,119 @@
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";

/* ── Semantic surface colors ── */
--color-page: var(--page);
--color-surface: var(--surface);
--color-surface-alt: var(--surface-alt);
--color-surface-hover: var(--surface-hover);

/* ── Semantic foreground (text) colors ── */
--color-on-surface: var(--on-surface);
--color-on-surface-variant: var(--on-surface-variant);
--color-on-surface-muted: var(--on-surface-muted);
--color-on-surface-faint: var(--on-surface-faint);

/* ── Borders ── */
--color-outline: var(--outline);
--color-outline-variant: var(--outline-variant);

/* ── Overlay ── */
--color-scrim: var(--scrim);

/* ── Status colors ── */
--color-status-success-bg: var(--status-success-bg);
--color-status-success-border: var(--status-success-border);
--color-status-success-text: var(--status-success-text);
--color-status-error-bg: var(--status-error-bg);
--color-status-error-border: var(--status-error-border);
--color-status-error-text: var(--status-error-text);
}

/* ── Light theme (default) ── */
:root {
--page: #f1f5f9;
--surface: #ffffff;
--surface-alt: #f1f5f9;
--surface-hover: #e2e8f0;
--on-surface: #0f172a;
--on-surface-variant: #475569;
--on-surface-muted: #64748b;
--on-surface-faint: #94a3b8;
--outline: #cbd5e1;
--outline-variant: #e2e8f0;
--scrim: rgba(0, 0, 0, 0.15);

--status-success-bg: #f0fdf4;
--status-success-border: #bbf7d0;
--status-success-text: #15803d;
--status-error-bg: #fef2f2;
--status-error-border: #fecaca;
--status-error-text: #dc2626;

/* Game-board cell tokens */
--cell-grid: rgba(0, 0, 0, 0.08);
--cell-ghost-bg: rgba(0, 0, 0, 0.05);
--cell-ghost-border-light: rgba(0, 0, 0, 0.06);
--cell-ghost-border-dark: rgba(0, 0, 0, 0.1);
--cell-penalty-bg: #cbd5e1;
--cell-penalty-border-light: rgba(255, 255, 255, 0.4);
--cell-penalty-border-dark: rgba(0, 0, 0, 0.15);
--piece-border-light: rgba(255, 255, 255, 0.25);
--piece-border-dark: rgba(0, 0, 0, 0.2);
--mini-board-bg: #e2e8f0;
--opponent-ghost-alive: rgba(0, 0, 0, 0.04);
--opponent-ghost-dead: rgba(0, 0, 0, 0.02);
--opponent-penalty-alive: #cbd5e1;
--opponent-penalty-dead: #94a3b8;
--opponent-dead-piece: #9ca3af;

color-scheme: light;
}

/* ── Dark theme ── */
.dark {
--page: #0f172a;
--surface: #111827;
--surface-alt: #1f2937;
--surface-hover: #374151;
--on-surface: #ffffff;
--on-surface-variant: #d1d5db;
--on-surface-muted: #9ca3af;
--on-surface-faint: #6b7280;
--outline: #4b5563;
--outline-variant: rgba(255, 255, 255, 0.1);
--scrim: rgba(0, 0, 0, 0.4);

--status-success-bg: rgba(34, 197, 94, 0.15);
--status-success-border: #166534;
--status-success-text: #4ade80;
--status-error-bg: rgba(239, 68, 68, 0.15);
--status-error-border: #991b1b;
--status-error-text: #fca5a5;

--cell-grid: rgba(255, 255, 255, 0.12);
--cell-ghost-bg: rgba(255, 255, 255, 0.12);
--cell-ghost-border-light: rgba(255, 255, 255, 0.15);
--cell-ghost-border-dark: rgba(0, 0, 0, 0.3);
--cell-penalty-bg: #374151;
--cell-penalty-border-light: rgba(255, 255, 255, 0.08);
--cell-penalty-border-dark: rgba(0, 0, 0, 0.3);
--piece-border-light: rgba(255, 255, 255, 0.15);
--piece-border-dark: rgba(0, 0, 0, 0.3);
--mini-board-bg: #000000;
--opponent-ghost-alive: rgba(255, 255, 255, 0.06);
--opponent-ghost-dead: rgba(255, 255, 255, 0.03);
--opponent-penalty-alive: #111111;
--opponent-penalty-dead: #444444;
--opponent-dead-piece: #777777;

color-scheme: dark;
}

html,
body {
background: #0f172a;
background: var(--page);
color: var(--on-surface);
min-height: 100vh;

@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}
18 changes: 9 additions & 9 deletions client/app/components/GameModeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ export function GameModeSelector({ selected, onChange, disabled, compact }: Game
className={`relative rounded-lg p-2.5 text-left transition-all border ${
isActive
? `${info.accentBg} ${info.accentBorder} ring-1 ring-offset-0 ring-current ${info.accentColor}`
: 'bg-gray-800 border-gray-600 hover:border-gray-500'
: 'bg-surface-alt border-outline hover:border-on-surface-faint'
} ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
>
<div className='flex items-center gap-2 mb-1'>
<span className='text-base'>{info.icon}</span>
<span className={`text-sm font-semibold ${isActive ? info.accentColor : 'text-white'}`}>
<span className={`text-sm font-semibold ${isActive ? info.accentColor : 'text-on-surface'}`}>
{info.label}
</span>
</div>
<div className='text-[10px] text-gray-400'>{info.boardLabel}</div>
<div className='text-[10px] text-on-surface-muted'>{info.boardLabel}</div>
</button>
);
})}
Expand All @@ -59,22 +59,22 @@ export function GameModeSelector({ selected, onChange, disabled, compact }: Game
className={`relative rounded-lg p-3 text-left transition-all border ${
isActive
? `${info.accentBg} ${info.accentBorder} ring-1 ring-offset-0 ring-current ${info.accentColor}`
: 'bg-gray-800 border-gray-600 hover:border-gray-500 hover:bg-gray-800/80'
: 'bg-surface-alt border-outline hover:border-on-surface-faint hover:bg-surface-alt/80'
} ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
>
<div className='flex items-start gap-3'>
<span className='text-2xl mt-0.5'>{info.icon}</span>
<div className='flex-1 min-w-0'>
<div className='flex items-center justify-between mb-1'>
<span className={`text-sm font-bold ${isActive ? info.accentColor : 'text-white'}`}>
<span className={`text-sm font-bold ${isActive ? info.accentColor : 'text-on-surface'}`}>
{info.label}
</span>
<div className='flex items-center gap-2 text-[10px] text-gray-500'>
<span className='bg-gray-700/60 px-1.5 py-0.5 rounded'>{info.boardLabel}</span>
<span className='bg-gray-700/60 px-1.5 py-0.5 rounded'>{pieceCount} pcs</span>
<div className='flex items-center gap-2 text-[10px] text-on-surface-faint'>
<span className='bg-surface-hover/60 px-1.5 py-0.5 rounded'>{info.boardLabel}</span>
<span className='bg-surface-hover/60 px-1.5 py-0.5 rounded'>{pieceCount} pcs</span>
</div>
</div>
<p className='text-xs text-gray-400 leading-relaxed'>{info.description}</p>
<p className='text-xs text-on-surface-muted leading-relaxed'>{info.description}</p>
</div>
</div>
</button>
Expand Down
6 changes: 3 additions & 3 deletions client/app/components/LoadingOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
export function LoadingOverlay() {
return (
<div className='fixed inset-0 bg-black/40 flex items-center justify-center z-50'>
<div className='bg-gray-900 rounded-xl p-6 border border-gray-600'>
<div className='fixed inset-0 bg-scrim flex items-center justify-center z-50'>
<div className='bg-surface rounded-xl p-6 border border-outline shadow-lg'>
<div className='text-center'>
<div className='w-8 h-8 border-2 border-red-500 border-t-transparent rounded-full animate-spin mx-auto mb-4'></div>
<p className='text-gray-300 font-medium'>Loading...</p>
<p className='text-on-surface-variant font-medium'>Loading...</p>
</div>
</div>
</div>
Expand Down
31 changes: 31 additions & 0 deletions client/app/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
interface ThemeToggleProps {
theme: 'light' | 'dark';
toggle: () => void;
}

export function ThemeToggle({ theme, toggle }: ThemeToggleProps) {
return (
<button
onClick={toggle}
className='fixed top-4 right-4 z-50 p-2.5 rounded-xl bg-surface border border-outline hover:bg-surface-alt transition-colors shadow-sm'
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
>
{theme === 'dark' ? (
<span
className='w-5 h-5 text-yellow-400 flex items-center justify-center text-lg leading-none'
aria-hidden='true'
>
</span>
) : (
<span
className='w-5 h-5 text-slate-600 flex items-center justify-center text-2xl leading-none -mt-px'
aria-hidden='true'
>
</span>
)}
</button>
);
}
47 changes: 14 additions & 33 deletions client/app/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,34 @@ export function LoginForm() {
};

return (
<main className='min-h-screen bg-slate-900 flex items-center justify-center px-4 py-12'>
<main className='min-h-screen bg-page flex items-center justify-center px-4 py-12'>
<div className='w-full max-w-md'>
{/* Header */}
<div className='text-center mb-8'>
<h1 className='text-5xl font-bold text-red-500 mb-4'>RED TETRIS</h1>
<p className='text-xl text-gray-300'>Welcome Back!</p>
<p className='text-gray-400 mt-2'>Sign in to continue your game</p>
<p className='text-xl text-on-surface-variant'>Welcome Back!</p>
<p className='text-on-surface-muted mt-2'>Sign in to continue your game</p>
</div>

{/* Login Form */}
<div className='bg-gray-900 rounded-xl p-8 border border-gray-600'>
<div className='bg-surface rounded-xl p-8 border border-outline shadow-sm'>
<form
className='space-y-6'
onSubmit={(e) => {
void handleSubmit(e);
}}
>
{error && (
<div className='bg-red-500/20 backdrop-blur-sm border border-red-400/30 rounded-lg p-4'>
<div className='text-red-200 text-sm text-center'>{error}</div>
<div className='bg-status-error-bg border border-status-error-border rounded-lg p-4'>
<div className='text-status-error-text text-sm text-center'>{error}</div>
</div>
)}

<div className='space-y-4'>
<div>
<label
htmlFor='username'
className='block text-sm font-medium text-gray-300 mb-2'
className='block text-sm font-medium text-on-surface-muted mb-2'
>
Username
</label>
Expand All @@ -64,15 +64,15 @@ export function LoginForm() {
name='username'
type='text'
required
className='w-full bg-gray-800 border border-gray-600 rounded-lg px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500/50 transition-colors'
className='w-full bg-surface-alt border border-outline rounded-lg px-4 py-3 text-on-surface placeholder-on-surface-faint focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500/50 transition-colors'
placeholder='Enter your username'
disabled={isLoading}
/>
</div>
<div>
<label
htmlFor='password'
className='block text-sm font-medium text-gray-400 mb-2'
className='block text-sm font-medium text-on-surface-muted mb-2'
>
Password
</label>
Expand All @@ -81,7 +81,7 @@ export function LoginForm() {
name='password'
type='password'
required
className='w-full bg-gray-800 border border-gray-600 rounded-lg px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500/50 transition-colors'
className='w-full bg-surface-alt border border-outline rounded-lg px-4 py-3 text-on-surface placeholder-on-surface-faint focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500/50 transition-colors'
placeholder='Enter your password'
disabled={isLoading}
/>
Expand All @@ -91,39 +91,20 @@ export function LoginForm() {
<button
type='submit'
disabled={isLoading}
className='w-full bg-red-600 hover:bg-red-500 disabled:bg-gray-700 disabled:opacity-70 text-white font-bold py-4 px-6 rounded-lg transition-colors'
className='w-full bg-red-600 hover:bg-red-500 disabled:bg-surface-hover disabled:opacity-70 text-white font-bold py-4 px-6 rounded-lg transition-colors'
>
{isLoading ? (
<span className='flex items-center justify-center'>
<svg
className='animate-spin -ml-1 mr-3 h-5 w-5 text-white'
xmlns='http://www.w3.org/2000/svg'
fill='none'
viewBox='0 0 24 24'
>
<circle
className='opacity-25'
cx='12'
cy='12'
r='10'
stroke='currentColor'
strokeWidth='4'
></circle>
<path
className='opacity-75'
fill='currentColor'
d='m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'
></path>
</svg>
<span className='animate-spin -ml-1 mr-3 h-5 w-5 border-2 border-white border-t-transparent rounded-full inline-block'></span>
Signing in...
</span>
) : (
'Sign In'
)}
</button>

<div className='text-center pt-4 border-t border-white/10'>
<p className='text-gray-400 text-sm'>
<div className='text-center pt-4 border-t border-outline-variant'>
<p className='text-on-surface-muted text-sm'>
Don't have an account?{' '}
<Link
to='/register'
Expand Down
Loading
Loading