Files
ChatApp/apps/desktop/src/components/WhiteboardModal.tsx
T

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>
);
}