199 lines
7.1 KiB
TypeScript
199 lines
7.1 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { useWhiteboardStrokes } from '../hooks/useWhiteboardStrokes';
|
|
import { XIcon } from './icons';
|
|
import {
|
|
WhiteboardCanvas,
|
|
type WhiteboardColor,
|
|
type WhiteboardTool,
|
|
type WhiteboardWidth,
|
|
} from './WhiteboardCanvas';
|
|
|
|
interface Props {
|
|
whiteboardId: string;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const COLORS: WhiteboardColor[] = ['#000000', '#ef4444', '#3b82f6', '#22c55e', '#facc15', '#a855f7'];
|
|
const WIDTHS: WhiteboardWidth[] = [2, 4, 8];
|
|
|
|
export function WhiteboardModal({ whiteboardId, onClose }: Props) {
|
|
const { t } = useTranslation();
|
|
const { strokes, loading, error, insertStroke, clearAll } = useWhiteboardStrokes(whiteboardId);
|
|
const [tool, setTool] = useState<WhiteboardTool>('pen');
|
|
const [color, setColor] = useState<WhiteboardColor>('#000000');
|
|
const [width, setWidth] = useState<WhiteboardWidth>(4);
|
|
const [confirmClear, setConfirmClear] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose();
|
|
};
|
|
window.addEventListener('keydown', onKey);
|
|
return () => window.removeEventListener('keydown', onKey);
|
|
}, [onClose]);
|
|
|
|
const handleConfirmClear = async () => {
|
|
setConfirmClear(false);
|
|
try {
|
|
await clearAll();
|
|
} catch (err) {
|
|
console.error('clearAll failed', err);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={t('app:whiteboard.title', { defaultValue: 'Whiteboard' })}
|
|
className="fixed inset-0 z-[80] flex flex-col bg-ink-950/95"
|
|
>
|
|
<header className="flex shrink-0 items-center justify-between border-b border-line/40 bg-surface-2 px-4 py-2">
|
|
<h2 className="font-display text-sm font-semibold text-fg">
|
|
{t('app:whiteboard.title', { defaultValue: 'Whiteboard' })}
|
|
</h2>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label={t('app:whiteboard.close', { defaultValue: 'Schließen' })}
|
|
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md text-fg-muted hover:bg-surface-3 hover:text-fg"
|
|
>
|
|
<XIcon className="h-3.5 w-3.5" />
|
|
</button>
|
|
</header>
|
|
|
|
<div className="flex min-h-0 flex-1 items-center justify-center overflow-hidden p-6">
|
|
{loading ? (
|
|
<p className="text-sm text-fg-muted">
|
|
{t('app:whiteboard.loading', { defaultValue: 'Lädt…' })}
|
|
</p>
|
|
) : error ? (
|
|
<p className="text-sm text-rose-400">
|
|
{t('app:whiteboard.error', { defaultValue: 'Whiteboard konnte nicht geladen werden.' })}
|
|
</p>
|
|
) : (
|
|
<WhiteboardCanvas
|
|
strokes={strokes}
|
|
tool={tool}
|
|
color={color}
|
|
width={width}
|
|
onStroke={(payload) => void insertStroke(payload)}
|
|
whiteboardId={whiteboardId}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<footer className="flex shrink-0 flex-wrap items-center gap-4 border-t border-line/40 bg-surface-2 px-4 py-3">
|
|
<div className="flex items-center gap-1">
|
|
{(['pen', 'eraser'] as WhiteboardTool[]).map((id) => {
|
|
const label = id === 'pen' ? 'Stift' : 'Radierer';
|
|
const active = tool === id;
|
|
return (
|
|
<button
|
|
key={id}
|
|
type="button"
|
|
onClick={() => setTool(id)}
|
|
aria-pressed={active}
|
|
title={label}
|
|
className={
|
|
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-xs font-semibold transition ' +
|
|
(active
|
|
? 'bg-accent/20 text-accent ring-2 ring-accent/40'
|
|
: 'bg-surface-3 text-fg-muted hover:bg-surface hover:text-fg')
|
|
}
|
|
>
|
|
{id === 'pen' ? '✎' : '⌫'}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="h-6 w-px bg-line/40" aria-hidden />
|
|
|
|
<div className="flex items-center gap-1">
|
|
{COLORS.map((c) => {
|
|
const active = color === c;
|
|
return (
|
|
<button
|
|
key={c}
|
|
type="button"
|
|
onClick={() => setColor(c)}
|
|
aria-pressed={active}
|
|
aria-label={c}
|
|
className={
|
|
'h-6 w-6 cursor-pointer rounded-full border-2 transition ' +
|
|
(active ? 'border-fg scale-110' : 'border-line/40 hover:scale-105')
|
|
}
|
|
style={{ backgroundColor: c }}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="h-6 w-px bg-line/40" aria-hidden />
|
|
|
|
<div className="flex items-center gap-1">
|
|
{WIDTHS.map((w) => {
|
|
const active = width === w;
|
|
return (
|
|
<button
|
|
key={w}
|
|
type="button"
|
|
onClick={() => setWidth(w)}
|
|
aria-pressed={active}
|
|
title={w + 'px'}
|
|
className={
|
|
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-md transition ' +
|
|
(active
|
|
? 'bg-accent/20 ring-2 ring-accent/40'
|
|
: 'bg-surface-3 hover:bg-surface')
|
|
}
|
|
>
|
|
<div
|
|
className="rounded-full bg-fg"
|
|
style={{ width: w + 2 + 'px', height: w + 2 + 'px' }}
|
|
/>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="ml-auto flex items-center gap-2">
|
|
{confirmClear ? (
|
|
<>
|
|
<span className="text-xs text-fg-muted">
|
|
{t('app:whiteboard.confirm_clear', { defaultValue: 'Alles löschen?' })}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setConfirmClear(false)}
|
|
className="cursor-pointer rounded-md border border-line bg-surface-3 px-3 py-1.5 text-xs font-medium text-fg-muted transition hover:bg-surface"
|
|
>
|
|
{t('app:whiteboard.cancel', { defaultValue: 'Abbrechen' })}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => void handleConfirmClear()}
|
|
className="cursor-pointer rounded-md bg-rose-500 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-rose-500/90"
|
|
>
|
|
{t('app:whiteboard.confirm', { defaultValue: 'Ja, löschen' })}
|
|
</button>
|
|
</>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={() => setConfirmClear(true)}
|
|
disabled={strokes.length === 0}
|
|
className="cursor-pointer rounded-md border border-rose-500/40 bg-rose-500/10 px-3 py-1.5 text-xs font-semibold text-rose-600 transition hover:bg-rose-500/20 disabled:cursor-not-allowed disabled:opacity-50 dark:text-rose-300"
|
|
>
|
|
{t('app:whiteboard.clear_all', { defaultValue: 'Alles löschen' })}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|