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
1 change: 0 additions & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
npm test
76 changes: 76 additions & 0 deletions apps/frontend/src/components/profile/DeleteAccount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useState } from 'react';
import { Trash2, AlertTriangle } from 'lucide-react';

Check warning on line 2 in apps/frontend/src/components/profile/DeleteAccount.tsx

View workflow job for this annotation

GitHub Actions / quality

There should be at least one empty line between import groups
import { useAuthStore } from '../../stores/authStore';

interface DeleteAccountProps {
onDelete: () => Promise<void>;
}

export default function DeleteAccount({ onDelete }: DeleteAccountProps) {
const [showConfirm, setShowConfirm] = useState(false);
const [loading, setLoading] = useState(false);
const logout = useAuthStore((s) => s.logout);

const handleDelete = async () => {
setLoading(true);
try {
await onDelete();
await logout();
} catch (error) {
console.error('Delete failed:', error);
setLoading(false);
}
};

if (!showConfirm) {
return (
<div className="rounded-xl border border-[var(--border)] bg-[var(--card)] p-6">
<h3 className="mb-2 text-sm font-semibold text-[var(--foreground)]">Gefahrenzone</h3>
<p className="mb-4 text-sm text-[rgb(var(--muted-foreground-rgb))]">
Account unwiderruflich löschen. Alle Daten gehen verloren.
</p>
<button
type="button"
onClick={() => setShowConfirm(true)}
className="focus-ring inline-flex items-center gap-2 rounded-xl border border-[var(--destructive)] px-3 py-1.5 text-sm text-[var(--destructive)] hover:bg-[var(--destructive)] hover:text-white transition-colors"
>
<Trash2 className="h-4 w-4" />
<span>Account löschen</span>
</button>
</div>
);
}

return (
<div className="rounded-xl border border-[var(--destructive)] bg-[var(--card)] p-6">
<div className="mb-4 flex items-start gap-3">
<AlertTriangle className="h-5 w-5 flex-shrink-0 text-[var(--destructive)]" />
<div>
<h3 className="font-semibold text-[var(--destructive)]">Bist du sicher?</h3>
<p className="mt-1 text-sm text-[rgb(var(--muted-foreground-rgb))]">
Diese Aktion kann nicht rückgängig gemacht werden. Alle Daten gehen unwiderruflich
verloren.
</p>
</div>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={handleDelete}
disabled={loading}
className="focus-ring rounded-xl border border-[var(--destructive)] bg-transparent px-4 py-2 text-sm font-medium text-[var(--destructive)] hover:bg-[var(--destructive)] hover:text-white transition-colors disabled:opacity-50"
>
{loading ? 'Löscht...' : 'Ja, Account löschen'}
</button>
<button
type="button"
onClick={() => setShowConfirm(false)}
disabled={loading}
className="focus-ring rounded-xl border border-[var(--border)] bg-[var(--card)] px-4 py-2 text-sm hover:bg-[var(--surface-2)] disabled:opacity-50"
>
Abbrechen
</button>
</div>
</div>
);
}
107 changes: 107 additions & 0 deletions apps/frontend/src/components/profile/ProfileForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useState } from 'react';
import { Save, X } from 'lucide-react';

Check warning on line 2 in apps/frontend/src/components/profile/ProfileForm.tsx

View workflow job for this annotation

GitHub Actions / quality

There should be at least one empty line between import groups
import TextField from '../ui/TextField';
import FormError from '../ui/FormError';
import FormSuccess from '../ui/FormSuccess';
import { UpdateUserData } from '../../services/user';

interface ProfileFormProps {
initialData: UpdateUserData;
onSubmit: (data: UpdateUserData) => Promise<void>;
onCancel?: () => void;
}

export default function ProfileForm({ initialData, onSubmit, onCancel }: ProfileFormProps) {
const [formData, setFormData] = useState(initialData);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState(false);

const hasChanges =
formData.first_name !== initialData.first_name ||
formData.last_name !== initialData.last_name ||
formData.email !== initialData.email;

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setSuccess(false);
setLoading(true);

try {
await onSubmit(formData);
setSuccess(true);
setTimeout(() => setSuccess(false), 3000);
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Speichern');
} finally {
setLoading(false);
}
};

const handleReset = () => {
setFormData(initialData);
setError('');
setSuccess(false);
onCancel?.();
};

return (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<TextField
label="Vorname"
value={formData.first_name}
onChange={(e) => setFormData({ ...formData, first_name: e.target.value })}
required
placeholder="Max"
autoComplete="given-name"
/>
<TextField
label="Nachname"
value={formData.last_name}
onChange={(e) => setFormData({ ...formData, last_name: e.target.value })}
required
placeholder="Mustermann"
autoComplete="family-name"
/>
</div>

<TextField
label="E-Mail"
type="email"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
placeholder="max@example.com"
autoComplete="email"
/>

<FormSuccess message={success ? 'Profil erfolgreich aktualisiert!' : ''} />
<FormError message={error} />

<div className="flex gap-3">
<button
type="submit"
disabled={loading || !hasChanges}
className="focus-ring inline-flex items-center gap-2 rounded-xl bg-[var(--primary)] px-4 py-2 font-medium text-white hover:bg-[var(--primary-hover)] disabled:opacity-50 disabled:cursor-not-allowed"
>
<Save className="h-4 w-4" />
<span>{loading ? 'Speichert...' : 'Speichern'}</span>
</button>

{hasChanges && (
<button
type="button"
onClick={handleReset}
disabled={loading}
className="focus-ring inline-flex items-center gap-2 rounded-xl border border-[var(--border)] bg-[var(--card)] px-4 py-2 hover:bg-[var(--surface-2)] disabled:opacity-50"
>
<X className="h-4 w-4" />
<span>Abbrechen</span>
</button>
)}
</div>
</form>
);
}
24 changes: 24 additions & 0 deletions apps/frontend/src/components/profile/ProfileHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { User } from 'lucide-react';

interface ProfileHeaderProps {
firstName: string;
lastName: string;
}

export default function ProfileHeader({ firstName, lastName }: ProfileHeaderProps) {
const initials = `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();

return (
<div className="flex items-center gap-4">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-[var(--primary)] text-2xl font-semibold text-white">
{initials || <User className="h-10 w-10" />}
</div>
<div>
<h1 className="text-2xl font-semibold">
{firstName} {lastName}
</h1>
<p className="text-sm text-[rgb(var(--muted-foreground-rgb))]">Profil bearbeiten</p>
</div>
</div>
);
}
5 changes: 1 addition & 4 deletions apps/frontend/src/components/sidebar/ISidebar.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ElementType } from 'react';

Check warning on line 1 in apps/frontend/src/components/sidebar/ISidebar.ts

View workflow job for this annotation

GitHub Actions / quality

There should be at least one empty line between import groups
import { RouteKey } from './SidebarEnum';

export type NavItem = {
Expand All @@ -10,8 +10,5 @@
};

export type SidebarProps = {
active?: RouteKey;
onNavigate?: (key: RouteKey) => void;
logoUrl?: string; // default: /images/logo.png
user?: { name: string; role?: string };
logoUrl?: string;
};
79 changes: 52 additions & 27 deletions apps/frontend/src/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { FC } from 'react';
import { FC, useEffect } from 'react';
import { Link, useRouterState } from '@tanstack/react-router';

Check warning on line 2 in apps/frontend/src/components/sidebar/sidebar.tsx

View workflow job for this annotation

GitHub Actions / quality

There should be at least one empty line between import groups
import { RouteKey } from './SidebarEnum';
import type { NavItem, SidebarProps } from './ISidebar';

Check warning on line 4 in apps/frontend/src/components/sidebar/sidebar.tsx

View workflow job for this annotation

GitHub Actions / quality

There should be at least one empty line between import groups

import { useUserStore } from '../../stores/userStore';

Check warning on line 5 in apps/frontend/src/components/sidebar/sidebar.tsx

View workflow job for this annotation

GitHub Actions / quality

`../../stores/userStore` import should occur before import of `./SidebarEnum`

Check warning on line 5 in apps/frontend/src/components/sidebar/sidebar.tsx

View workflow job for this annotation

GitHub Actions / quality

There should be at least one empty line between import groups
import {

Check warning on line 6 in apps/frontend/src/components/sidebar/sidebar.tsx

View workflow job for this annotation

GitHub Actions / quality

`lucide-react` import should occur before import of `./SidebarEnum`
LayoutDashboard,
Info,
Users,
Shield,
ScanLine,
Wrench,
SlidersHorizontal,
Expand All @@ -15,13 +14,11 @@
} from 'lucide-react';

const navItems: NavItem[] = [
{ key: RouteKey.Dashboard, label: 'Dashboard', icon: LayoutDashboard },
{ key: RouteKey.Daily, label: 'Daily', icon: Info },
{ key: RouteKey.Users, label: 'User', icon: Users },
{ key: RouteKey.Info, label: 'Information', icon: Shield },
{ key: RouteKey.Scan, label: 'Scan', icon: ScanLine },
{ key: RouteKey.Test, label: 'Test', icon: Wrench },
{ key: RouteKey.Sections, label: 'Edit Sections', icon: SlidersHorizontal },
{ key: RouteKey.Dashboard, label: 'Dashboard', icon: LayoutDashboard, href: '/' },
{ key: RouteKey.Daily, label: 'Daily', icon: Info, href: '/daily' },
{ key: RouteKey.Scan, label: 'Scan', icon: ScanLine, href: '/scan' },
{ key: RouteKey.Test, label: 'Test', icon: Wrench, href: '/test' },
{ key: RouteKey.Sections, label: 'Edit Sections', icon: SlidersHorizontal, href: '/sections' },
];

const baseItem =
Expand All @@ -31,16 +28,36 @@
const labelShow =
'pointer-events-none origin-left scale-0 opacity-0 transition-all duration-200 group-hover:scale-100 group-hover:opacity-100';

const Sidebar: FC<SidebarProps> = ({ active, onNavigate, logoUrl = '/images/logo.png', user }) => {
const userName = user?.name ?? 'Gianluca Barbieri';
const userRole = user?.role ?? 'Lernender';
const Sidebar: FC<SidebarProps> = ({ logoUrl = '/images/logo.png' }) => {
const { user, fetchUser } = useUserStore();
const router = useRouterState();
const currentPath = router.location.pathname;

useEffect(() => {
if (!user) {
fetchUser();
}
}, [user, fetchUser]);

const getActiveKey = (): RouteKey | null => {
if (currentPath === '/') return RouteKey.Dashboard;
if (currentPath === '/profile') return RouteKey.Profile;
if (currentPath === '/settings') return RouteKey.Settings;
if (currentPath.startsWith('/daily')) return RouteKey.Daily;
if (currentPath.startsWith('/scan')) return RouteKey.Scan;
if (currentPath.startsWith('/test')) return RouteKey.Test;
if (currentPath.startsWith('/sections')) return RouteKey.Sections;
return null;
};

const renderItem = ({ key, label, icon: Icon, sublabel }: NavItem) => {
const active = getActiveKey();

const renderItem = ({ key, label, icon: Icon, sublabel, href }: NavItem & { href: string }) => {
const isActive = active === key;
return (
<button
<Link
key={key}
onClick={() => onNavigate?.(key)}
to={href}
title={label}
aria-current={isActive ? 'page' : undefined}
className={
Expand All @@ -56,7 +73,7 @@
<div>{label}</div>
{sublabel && <div className="text-xs text-white/80">{sublabel}</div>}
</div>
</button>
</Link>
);
};

Expand All @@ -70,33 +87,41 @@
>
<div className="flex h-full flex-col text-white">
{/* Brand */}
<button className={baseItem + ' px-3 py-4'}>
<Link to="/" className={baseItem + ' px-3 py-4'}>
<div className="grid h-10 w-10 place-items-center rounded-lg overflow-hidden shrink-0">
<img
src={logoUrl} // /images/logo.png aus /public
src={logoUrl}
alt="Sesh Logo"
className="block h-10 w-10 object-contain select-none pointer-events-none"
/>
</div>
<div className={`${labelShow} text-left text-xl font-bold`}>
<div>Sesh</div>
</div>
</button>
</Link>

{/* Main nav */}
<nav className="flex-1 space-y-1 px-2">{navItems.map((it) => renderItem(it))}</nav>
<nav className="flex-1 space-y-1 px-2">
{navItems.map((it) => renderItem({ ...it, href: it.href! }))}
</nav>

{/* Bottom actions */}
<div className="px-2 pb-3 pt-2 space-y-1">
{/* Settings as selectable item */}
{renderItem({ key: RouteKey.Settings, label: 'Settings', icon: Settings })}
{/* Settings */}
{renderItem({
key: RouteKey.Settings,
label: 'Einstellungen',
icon: Settings,
href: '/settings',
})}

{/* Profile as normal item (kein Card) */}
{/* Profile */}
{renderItem({
key: RouteKey.Profile,
label: userName,
sublabel: userRole,
label: user ? `${user.first_name} ${user.last_name}` : 'Profil',
sublabel: user?.email,
icon: UserCircle2,
href: '/profile',
})}
</div>
</div>
Expand Down
12 changes: 12 additions & 0 deletions apps/frontend/src/components/ui/FormSuccess.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CheckCircle } from 'lucide-react';

export default function FormSuccess({ message }: { message?: string }) {
if (!message) return null;

return (
<div className="flex items-start gap-2 rounded-lg bg-[rgb(var(--secondary-rgb)/0.08)] p-3 text-sm text-[var(--secondary)]">
<CheckCircle className="mt-0.5 h-4 w-4" />
<p>{message}</p>
</div>
);
}
Loading
Loading