Phase 1 MVP: gateway, CRUD handlers, frontend components
Backend: - WebSocket gateway (hub, client, events) with fanout broadcast - Server CRUD handlers (create, list, get, update, delete) - Channel CRUD handlers (create, list, get, update, delete) - Message CRUD handlers (list with cursor pagination, create, update, delete) - cmd/migrate standalone migration CLI (up/down) - cmd/server wired to all handlers + WebSocket + static file serving Frontend: - Zustand stores: auth, server, channel, message, websocket - API client with fetch wrapper - Terminal-styled components: Layout, LoginForm, ChatArea, ChannelList, ServerBar, MemberList - React Router with login and main routes - Gruvbox dark palette throughout Ops: - Docker Compose with app service (multi-stage build) - Caddyfile with WebSocket upgrade support - Makefile for common tasks
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../stores/auth.ts';
|
||||
|
||||
export function LoginForm() {
|
||||
const [isRegister, setIsRegister] = useState(false);
|
||||
const [email, setEmail] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const { login, register, isLoading, error, clearError } = useAuthStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
clearError();
|
||||
|
||||
if (isRegister) {
|
||||
if (password !== confirmPassword) {
|
||||
return;
|
||||
}
|
||||
await register(email, username, password);
|
||||
} else {
|
||||
await login(email, password);
|
||||
}
|
||||
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gb-bg p-4">
|
||||
<div className="terminal-border bg-gb-bg-h p-6 w-full max-w-md">
|
||||
<pre className="text-gb-orange font-mono text-lg mb-6">{'┌─ DUMPSTER ─┐'}</pre>
|
||||
<h2 className="text-gb-fg-s mb-4">{isRegister ? '[REGISTER]' : '[LOGIN]'}</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gb-fg-t mb-1">EMAIL:</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="terminal-input"
|
||||
required
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
{isRegister && (
|
||||
<div>
|
||||
<label className="block text-gb-fg-t mb-1">USERNAME:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="terminal-input"
|
||||
required
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-gb-fg-t mb-1">PASSWORD:</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="terminal-input"
|
||||
required
|
||||
autoComplete={isRegister ? 'new-password' : 'current-password'}
|
||||
/>
|
||||
</div>
|
||||
{isRegister && (
|
||||
<div>
|
||||
<label className="block text-gb-fg-t mb-1">CONFIRM PASSWORD:</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="terminal-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{error && <p className="text-gb-red text-sm">ERR: {error}</p>}
|
||||
<button type="submit" className="terminal-button w-full" disabled={isLoading}>
|
||||
{isLoading ? '[PROCESSING...]' : isRegister ? '[REGISTER]' : '[LOGIN]'}
|
||||
</button>
|
||||
</form>
|
||||
<div className="mt-4 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsRegister(!isRegister);
|
||||
clearError();
|
||||
}}
|
||||
className="text-gb-blue hover:text-gb-aqua text-sm"
|
||||
>
|
||||
{isRegister ? '[BACK TO LOGIN]' : '[CREATE ACCOUNT]'}
|
||||
</button>
|
||||
</div>
|
||||
{!isRegister && (
|
||||
<p className="text-gb-fg-f text-xs mt-4 text-center">
|
||||
Use Link component: <Link to="/" className="text-gb-blue hover:text-gb-aqua">[HOME]</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user