fix: SQLite boolean binding, CSS brace, race condition, 0 npm vulns, code-splitting
This commit is contained in:
@@ -131,6 +131,7 @@ const LayerPanel: React.FC<LayerPanelProps> = ({
|
||||
selectedId={activeLayerId}
|
||||
onSelect={onSelectLayer}
|
||||
onReorder={onReorder}
|
||||
draggable={true}
|
||||
renderIcon={(node) => node.icon}
|
||||
renderActions={(node) => {
|
||||
const layer = layers.find((l) => l.id === node.id);
|
||||
@@ -140,16 +141,16 @@ const LayerPanel: React.FC<LayerPanelProps> = ({
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onToggleLayer(layer.id); }}
|
||||
title={layer.visible ? 'Sichtbar' : 'Versteckt'}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: layer.visible ? 'var(--text-color, #ccc)' : 'var(--text-muted, #666)' }}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: layer.visible ? 'var(--text-color, #ccc)' : 'var(--text-muted, #666)', display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
{layer.visible ? '👁' : '🚫'}
|
||||
{layer.visible ? <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> : <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onToggleLock?.(layer.id); }}
|
||||
title={layer.locked ? 'Gesperrt' : 'Entsperrt'}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: layer.locked ? 'var(--accent, #f59e0b)' : 'var(--text-muted, #666)' }}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: layer.locked ? 'var(--accent, #f59e0b)' : 'var(--text-muted, #666)', display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
{layer.locked ? '🔒' : '🔓'}
|
||||
{layer.locked ? <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> : <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleAddSubLayer(layer.id); }}
|
||||
@@ -161,16 +162,16 @@ const LayerPanel: React.FC<LayerPanelProps> = ({
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onDuplicateLayer?.(layer.id); }}
|
||||
title="Duplizieren"
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: 'var(--text-color, #ccc)' }}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: 'var(--text-color, #ccc)', display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
⧉
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onDeleteLayer?.(layer.id); }}
|
||||
title="Loschen"
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: '#f44336' }}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: '#f44336', display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
✕
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -183,16 +184,16 @@ const LayerPanel: React.FC<LayerPanelProps> = ({
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onToggleElementVisible?.(element.id); }}
|
||||
title={isVisible ? 'Sichtbar' : 'Versteckt'}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: isVisible ? 'var(--text-color, #ccc)' : 'var(--text-muted, #666)' }}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: isVisible ? 'var(--text-color, #ccc)' : 'var(--text-muted, #666)', display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
{isVisible ? '👁' : '🚫'}
|
||||
{isVisible ? <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg> : <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onElementsDeleted?.([element.id]); }}
|
||||
title="Loschen"
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: '#f44336' }}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '2px', color: '#f44336', display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
✕
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -56,9 +56,9 @@ const sections: Array<{ label: string; tools: ToolDef[] }> = [
|
||||
{ label: 'Bestuhlung', tools: sectionBestuhlung },
|
||||
];
|
||||
|
||||
const LeftSidebar: React.FC<LeftSidebarProps> = ({ activeTool, onToolChange, selectedTemplate, onTemplateSelect, onCollapse }) => {
|
||||
const LeftSidebar: React.FC<LeftSidebarProps> = ({ activeTool, onToolChange, selectedTemplate, onTemplateSelect, onCollapse, className, collapsed, onToggleCollapse }) => {
|
||||
return (
|
||||
<aside className="leftbar" aria-label="Werkzeugpalette">
|
||||
<aside className={`leftbar${className ? ' ' + className : ''}${collapsed ? ' leftbar-collapsed' : ''}`} aria-label="Werkzeugpalette">
|
||||
<div className="leftbar-header">
|
||||
<span className="leftbar-title">Werkzeuge</span>
|
||||
<button className="leftbar-toggle" aria-label="Linke Leiste ausblenden" onClick={onCollapse}>
|
||||
@@ -120,7 +120,13 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({ activeTool, onToolChange, sel
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="leftbar-footer">
|
||||
{onToggleCollapse && (
|
||||
<button className="leftbar-footer-btn" title="Linke Leiste ein-/ausklappen" aria-label="Linke Leiste ein-/ausklappen" onClick={onToggleCollapse}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
</button>
|
||||
)}
|
||||
<button className="leftbar-footer-btn" title="Plugin hinzufügen" aria-label="Plugin hinzufügen">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* NotificationPanel – Shows user notifications with mark-read and delete
|
||||
*/
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { getNotifications, markNotificationRead, deleteNotification, type NotificationItem } from '../services/api';
|
||||
|
||||
interface NotificationPanelProps {
|
||||
token: string;
|
||||
}
|
||||
|
||||
export function NotificationPanel({ token }: NotificationPanelProps) {
|
||||
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchNotifications = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await getNotifications(token);
|
||||
setNotifications(data);
|
||||
} catch {
|
||||
setError('Fehler beim Laden');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
// Initial load on mount for badge count
|
||||
useEffect(() => {
|
||||
fetchNotifications();
|
||||
}, [fetchNotifications]);
|
||||
|
||||
// Refresh when dropdown opens
|
||||
useEffect(() => {
|
||||
if (open) fetchNotifications();
|
||||
}, [open, fetchNotifications]);
|
||||
|
||||
// Click-outside handler
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, [open]);
|
||||
|
||||
const unreadCount = notifications.filter(n => !n.read).length;
|
||||
|
||||
const handleMarkRead = async (id: string) => {
|
||||
try {
|
||||
await markNotificationRead(token, id);
|
||||
setNotifications(prev => prev.map(n => n.id === id ? { ...n, read: 1 } : n));
|
||||
} catch {
|
||||
setError('Fehler beim Markieren');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteNotification(token, id);
|
||||
setNotifications(prev => prev.filter(n => n.id !== id));
|
||||
} catch {
|
||||
setError('Fehler beim Löschen');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="notification-panel-wrapper" ref={wrapperRef}>
|
||||
<button
|
||||
className="notification-bell-btn"
|
||||
onClick={() => setOpen(!open)}
|
||||
title="Benachrichtigungen"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||
</svg>
|
||||
{unreadCount > 0 && <span className="notification-badge">{unreadCount}</span>}
|
||||
</button>
|
||||
{open && (
|
||||
<div className="notification-dropdown">
|
||||
<div className="notification-dropdown-header">
|
||||
<h3>Benachrichtigungen</h3>
|
||||
<button className="notification-close" onClick={() => setOpen(false)}>×</button>
|
||||
</div>
|
||||
{error && <p className="notification-empty">{error}</p>}
|
||||
{loading ? (
|
||||
<p className="notification-loading">Lädt…</p>
|
||||
) : notifications.length === 0 ? (
|
||||
<p className="notification-empty">Keine Benachrichtigungen</p>
|
||||
) : (
|
||||
<div className="notification-list">
|
||||
{notifications.map(n => (
|
||||
<div key={n.id} className={`notification-item ${n.read ? 'read' : 'unread'}`}>
|
||||
<div className="notification-item-content">
|
||||
<span className={`notification-type-badge type-${n.type}`}>{n.type}</span>
|
||||
<span className="notification-title">{n.title}</span>
|
||||
<span className="notification-message">{n.message}</span>
|
||||
<span className="notification-time">{new Date(n.created_at).toLocaleString('de-DE')}</span>
|
||||
</div>
|
||||
<div className="notification-item-actions">
|
||||
{!n.read && (
|
||||
<button className="notification-action-btn" onClick={() => handleMarkRead(n.id)} title="Als gelesen markieren">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="20 6 9 17 4 12" /></svg>
|
||||
</button>
|
||||
)}
|
||||
<button className="notification-action-btn delete" onClick={() => handleDelete(n.id)} title="Löschen">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import type { PropertiesPanelProps } from '../types/ui.types';
|
||||
|
||||
const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ selectedElement, layers, onUpdateProperty }) => {
|
||||
const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ selectedElement, layers, onUpdateProperty, onDelete }) => {
|
||||
const el = selectedElement;
|
||||
const x = el ? String(el.x) : '0';
|
||||
const y = el ? String(el.y) : '0';
|
||||
@@ -17,9 +17,21 @@ const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ selectedElement, laye
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">
|
||||
<span>Auswahl · 1 Stuhl</span>
|
||||
<button style={{ color: 'var(--color-text-muted)', fontSize: '11px' }} title="Mehr Optionen">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
{onDelete && el && (
|
||||
<button
|
||||
onClick={onDelete}
|
||||
title="Auswahl löschen (Entf)"
|
||||
aria-label="Auswahl löschen"
|
||||
style={{ color: '#f44336', background: 'none', border: 'none', cursor: 'pointer', padding: '2px', display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
</button>
|
||||
)}
|
||||
<button style={{ color: 'var(--color-text-muted)', fontSize: '11px', background: 'none', border: 'none', cursor: 'pointer', padding: '2px' }} title="Mehr Optionen">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const tabs: Array<{ id: RightPanel; label: string; svg: React.ReactNode; badge?:
|
||||
|
||||
const RightSidebar: React.FC<RightSidebarProps> = ({
|
||||
activePanel, onPanelChange, selectedElement, layers, blocks,
|
||||
className,
|
||||
activeLayerId, onSelectLayer, onAddLayer, onToggleLayer,
|
||||
onDeleteLayer, onRenameLayer, onDuplicateLayer, onToggleLock,
|
||||
onReorder, onAddSubLayer,
|
||||
@@ -22,9 +23,20 @@ const RightSidebar: React.FC<RightSidebarProps> = ({
|
||||
onRenameBlock, onDuplicateBlock, onDeleteBlock, onSvgImport, onSaveGroupAsBlock,
|
||||
onBlockCategoryChange, onBlockSearch, onDragBlock,
|
||||
kiMessages, kiSuggestions, onKISend, onKISuggestionClick, kiLoading, onUpdateElement,
|
||||
onCollapse,
|
||||
collapsed,
|
||||
onToggleCollapse,
|
||||
}) => {
|
||||
return (
|
||||
<aside className="rightbar" aria-label="Eigenschaften-Panels">
|
||||
<aside className={`rightbar${className ? ' ' + className : ''}${collapsed ? ' rightbar-collapsed' : ''}`} aria-label="Eigenschaften-Panels">
|
||||
<button className="rightbar-close" aria-label="Rechte Leiste schließen" onClick={onCollapse}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</button>
|
||||
{onToggleCollapse && (
|
||||
<button className="rightbar-collapse-toggle" aria-label="Rechte Leiste ein-/ausklappen" onClick={onToggleCollapse} title="Rechte Leiste ein-/ausklappen">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</button>
|
||||
)}
|
||||
<div className="rightbar-tabs" role="tablist">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
@@ -44,7 +56,7 @@ const RightSidebar: React.FC<RightSidebarProps> = ({
|
||||
|
||||
<div className="rightbar-content">
|
||||
<div className={`rightbar-panel${activePanel === 'tool' ? ' active' : ''}`} data-panel-content="tool">
|
||||
{activePanel === 'tool' && <PropertiesPanel selectedElement={selectedElement} layers={layers} onUpdateProperty={(key, value) => {
|
||||
{activePanel === 'tool' && <PropertiesPanel selectedElement={selectedElement} layers={layers} onDelete={selectedElement && onElementsDeleted ? () => onElementsDeleted([selectedElement.id]) : undefined} onUpdateProperty={(key, value) => {
|
||||
if (!selectedElement || !onUpdateElement) return;
|
||||
const updated = { ...selectedElement };
|
||||
if (key === 'x' || key === 'y' || key === 'width' || key === 'height') {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* ShareDialog – Share a project with other users by email
|
||||
*/
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { getProjectShares, createProjectShare, deleteProjectShare, type ProjectShare } from '../services/api';
|
||||
|
||||
interface ShareDialogProps {
|
||||
token: string;
|
||||
projectId: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
export function ShareDialog({ token, projectId, open, onClose }: ShareDialogProps) {
|
||||
const [shares, setShares] = useState<ProjectShare[]>([]);
|
||||
const [email, setEmail] = useState('');
|
||||
const [permission, setPermission] = useState('view');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const fetchShares = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await getProjectShares(token, projectId);
|
||||
setShares(data);
|
||||
} catch {
|
||||
setError('Fehler beim Laden der Freigaben');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) fetchShares();
|
||||
}, [open, fetchShares]);
|
||||
|
||||
const handleShare = async () => {
|
||||
if (!email.trim()) return;
|
||||
if (!EMAIL_REGEX.test(email.trim())) {
|
||||
setError('Ungültige E-Mail-Adresse');
|
||||
return;
|
||||
}
|
||||
// Check for duplicate share
|
||||
if (shares.some(s => s.shared_with_email === email.trim())) {
|
||||
setError('Projekt bereits mit dieser E-Mail geteilt');
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const newShare = await createProjectShare(token, projectId, {
|
||||
shared_with_email: email.trim(),
|
||||
permission,
|
||||
});
|
||||
setShares(prev => [newShare, ...prev]);
|
||||
setEmail('');
|
||||
setPermission('view');
|
||||
} catch (err: any) {
|
||||
setError(err.message || 'Freigabe fehlgeschlagen');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteProjectShare(token, id);
|
||||
setShares(prev => prev.filter(s => s.id !== id));
|
||||
} catch {
|
||||
setError('Fehler beim Entfernen der Freigabe');
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') handleShare();
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="share-dialog-overlay" onClick={onClose}>
|
||||
<div className="share-dialog" onClick={e => e.stopPropagation()}>
|
||||
<div className="share-dialog-header">
|
||||
<h3>Projekt teilen</h3>
|
||||
<button className="share-dialog-close" onClick={onClose}>×</button>
|
||||
</div>
|
||||
<div className="share-dialog-body">
|
||||
<div className="share-form">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="E-Mail-Adresse"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="share-email-input"
|
||||
/>
|
||||
<select value={permission} onChange={e => setPermission(e.target.value)} className="share-permission-select">
|
||||
<option value="view">Ansicht</option>
|
||||
<option value="edit">Bearbeitung</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
<button className="share-add-btn" onClick={handleShare} disabled={submitting}>
|
||||
{submitting ? '…' : 'Teilen'}
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="share-error">{error}</p>}
|
||||
{loading ? (
|
||||
<p className="share-loading">Lädt…</p>
|
||||
) : shares.length === 0 ? (
|
||||
<p className="share-empty">Noch niemandem geteilt</p>
|
||||
) : (
|
||||
<div className="share-list">
|
||||
{shares.map(s => (
|
||||
<div key={s.id} className="share-item">
|
||||
<div className="share-item-info">
|
||||
<span className="share-email">{s.shared_with_email}</span>
|
||||
<span className="share-permission-badge">{s.permission}</span>
|
||||
</div>
|
||||
<button className="share-remove-btn" onClick={() => handleDelete(s.id)} title="Freigabe entfernen">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import type { TopbarProps } from '../types/ui.types';
|
||||
|
||||
const Topbar: React.FC<TopbarProps> = ({ projectName, savedStatus, onUndo, onRedo, onThemeToggle, theme, onOpenSettings }) => {
|
||||
const Topbar: React.FC<TopbarProps> = ({ projectName, savedStatus, onUndo, onRedo, onThemeToggle, theme, onOpenSettings, onOpenLeftDrawer, onOpenRightDrawer }) => {
|
||||
return (
|
||||
<header className="topbar" role="banner">
|
||||
<div className="topbar-left">
|
||||
<button className="hamburger-btn" aria-label="Werkzeuge öffnen">
|
||||
<button className="hamburger-btn" aria-label="Werkzeuge öffnen" onClick={onOpenLeftDrawer}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||
</button>
|
||||
<div className="app-logo" aria-hidden="true">
|
||||
@@ -46,6 +46,9 @@ const Topbar: React.FC<TopbarProps> = ({ projectName, savedStatus, onUndo, onRed
|
||||
<option value="de">DE</option>
|
||||
<option value="en">EN</option>
|
||||
</select>
|
||||
<button className="icon-btn-top mobile-drawer-toggle" aria-label="Panel öffnen" title="Panel öffnen" onClick={onOpenRightDrawer}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="15" y1="3" x2="15" y2="21"/></svg>
|
||||
</button>
|
||||
<button className="icon-btn-top" aria-label="Benachrichtigungen" title="Benachrichtigungen">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user