feat: initial commit web-cad-neu with docker-compose, frontend and backend

This commit is contained in:
2026-06-26 10:50:24 +02:00
commit 4ec76fe406
102 changed files with 25722 additions and 0 deletions
+150
View File
@@ -0,0 +1,150 @@
/**
* Dashboard Page Project overview after login
*/
import { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../contexts/AuthContext';
const API_BASE = 'http://localhost:3001';
interface Project {
id: string;
name: string;
description: string | null;
created_at: string;
updated_at: string;
}
interface DashboardProps {
onOpenProject: (projectId: string) => void;
}
export function Dashboard({ onOpenProject }: DashboardProps) {
const { user, logout, token } = useAuth();
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
const [newName, setNewName] = useState('');
const [newDesc, setNewDesc] = useState('');
const fetchProjects = useCallback(async () => {
setLoading(true);
try {
const res = await fetch(`${API_BASE}/api/projects`, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok) {
setProjects(await res.json());
}
} catch {
// ignore
} finally {
setLoading(false);
}
}, [token]);
useEffect(() => {
fetchProjects();
}, [fetchProjects]);
const handleCreate = async () => {
if (!newName.trim()) return;
try {
const res = await fetch(`${API_BASE}/api/projects`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ name: newName, description: newDesc || null }),
});
if (res.ok) {
setNewName('');
setNewDesc('');
setShowCreate(false);
fetchProjects();
}
} catch {
// ignore
}
};
const handleDelete = async (id: string, e: React.MouseEvent) => {
e.stopPropagation();
if (!confirm('Projekt wirklich löschen?')) return;
try {
await fetch(`${API_BASE}/api/projects/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
});
fetchProjects();
} catch {
// ignore
}
};
return (
<div className="dashboard-page">
<header className="dashboard-header">
<div className="dashboard-brand">
<h1>Web CAD</h1>
<span className="dashboard-user">{user?.name} ({user?.role})</span>
</div>
<button className="dashboard-logout" onClick={logout}>Abmelden</button>
</header>
<div className="dashboard-content">
<div className="dashboard-section-header">
<h2>Projekte</h2>
<button className="dashboard-create-btn" onClick={() => setShowCreate(!showCreate)}>
+ Neues Projekt
</button>
</div>
{showCreate && (
<div className="dashboard-create-form">
<input
type="text"
placeholder="Projektname"
value={newName}
onChange={e => setNewName(e.target.value)}
autoFocus
/>
<input
type="text"
placeholder="Beschreibung (optional)"
value={newDesc}
onChange={e => setNewDesc(e.target.value)}
/>
<button onClick={handleCreate}>Erstellen</button>
<button onClick={() => setShowCreate(false)}>Abbrechen</button>
</div>
)}
{loading ? (
<p className="dashboard-loading">Lade Projekte</p>
) : projects.length === 0 ? (
<p className="dashboard-empty">Noch keine Projekte. Erstellen Sie ein neues Projekt.</p>
) : (
<div className="dashboard-project-grid">
{projects.map(project => (
<div
key={project.id}
className="dashboard-project-card"
onClick={() => onOpenProject(project.id)}
>
<h3>{project.name}</h3>
{project.description && <p>{project.description}</p>}
<div className="dashboard-project-meta">
<span>Erstellt: {new Date(project.created_at).toLocaleDateString('de-DE')}</span>
<button
className="dashboard-delete-btn"
onClick={(e) => handleDelete(project.id, e)}
>
Löschen
</button>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
+77
View File
@@ -0,0 +1,77 @@
/**
* Login Page Email/Password login
*/
import { useState, type FormEvent } from 'react';
import { useAuth } from '../contexts/AuthContext';
interface LoginProps {
onSwitchToRegister: () => void;
}
export function Login({ onSwitchToRegister }: LoginProps) {
const { login, loading, error, clearError } = useAuth();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
try {
await login(email, password);
} catch {
// error is set in context
}
};
return (
<div className="auth-page">
<div className="auth-card">
<h1 className="auth-title">Web CAD</h1>
<p className="auth-subtitle">Anmelden</p>
{error && (
<div className="auth-error" onClick={clearError}>
{error}
</div>
)}
<form onSubmit={handleSubmit} className="auth-form">
<div className="auth-field">
<label htmlFor="email">E-Mail</label>
<input
id="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
autoFocus
placeholder="name@example.com"
/>
</div>
<div className="auth-field">
<label htmlFor="password">Passwort</label>
<input
id="password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
required
placeholder="••••••••"
/>
</div>
<button type="submit" className="auth-button" disabled={loading}>
{loading ? 'Wird angemeldet…' : 'Anmelden'}
</button>
</form>
<p className="auth-switch">
Noch kein Konto?{' '}
<button type="button" className="auth-link" onClick={onSwitchToRegister}>
Registrieren
</button>
</p>
</div>
</div>
);
}
+117
View File
@@ -0,0 +1,117 @@
/**
* Register Page New user registration
*/
import { useState, type FormEvent } from 'react';
import { useAuth } from '../contexts/AuthContext';
interface RegisterProps {
onSwitchToLogin: () => void;
}
export function Register({ onSwitchToLogin }: RegisterProps) {
const { register, loading, error, clearError } = useAuth();
const [email, setEmail] = useState('');
const [name, setName] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [localError, setLocalError] = useState<string | null>(null);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setLocalError(null);
if (password !== confirmPassword) {
setLocalError('Passwörter stimmen nicht überein');
return;
}
if (password.length < 6) {
setLocalError('Passwort muss mindestens 6 Zeichen lang sein');
return;
}
try {
await register(email, password, name);
} catch {
// error is set in context
}
};
const displayError = localError || error;
return (
<div className="auth-page">
<div className="auth-card">
<h1 className="auth-title">Web CAD</h1>
<p className="auth-subtitle">Registrieren</p>
{displayError && (
<div className="auth-error" onClick={() => { clearError(); setLocalError(null); }}>
{displayError}
</div>
)}
<form onSubmit={handleSubmit} className="auth-form">
<div className="auth-field">
<label htmlFor="reg-name">Name</label>
<input
id="reg-name"
type="text"
value={name}
onChange={e => setName(e.target.value)}
required
autoFocus
placeholder="Ihr Name"
/>
</div>
<div className="auth-field">
<label htmlFor="reg-email">E-Mail</label>
<input
id="reg-email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
placeholder="name@example.com"
/>
</div>
<div className="auth-field">
<label htmlFor="reg-password">Passwort</label>
<input
id="reg-password"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
required
placeholder="min. 6 Zeichen"
/>
</div>
<div className="auth-field">
<label htmlFor="reg-confirm">Passwort bestätigen</label>
<input
id="reg-confirm"
type="password"
value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)}
required
placeholder="••••••••"
/>
</div>
<button type="submit" className="auth-button" disabled={loading}>
{loading ? 'Wird registriert…' : 'Registrieren'}
</button>
</form>
<p className="auth-switch">
Bereits ein Konto?{' '}
<button type="button" className="auth-link" onClick={onSwitchToLogin}>
Anmelden
</button>
</p>
</div>
</div>
);
}