Loeschen in der Akquise war ein echtes DELETE: der Eintrag stand danach in keiner Liste, auch nicht im Archiv, und war nicht zurueckholbar. Jetzt setzt es `deleted_at`/`deleted_by`, die Zeile bleibt 90 Tage stehen. Der Papierkorb ist die eine Ausnahme von der Regel, dass diese Tabelle keinen Zustand fuehrt: Loeschen von Hand ist eine Entscheidung, kein Ablauf. Was dort liegt, blenden alle anderen Abfragen ueber NOT_DELETED aus - Eingang, Archiv, Menuezahlen und Dashboard. Der Aufraeumlauf ueber `found_at` laesst geloeschte Zeilen ausdruecklich liegen, sonst waere ein alter Fund im Papierkorb sofort weg; der Papierkorb raeumt ab `deleted_at`. Zwei Rueckwege, deshalb zwei Routen. `/reopen` (bisher `/restore`) holt aus dem Archiv in den Eingang und setzt das Funddatum auf heute. `/restore` holt aus dem Papierkorb zurueck und laesst das Funddatum in Ruhe - der Eintrag soll dort auftauchen, wo er hingehoert, also im Archiv, wenn er alt ist. Die Meldung nennt das Ziel. Dazu `DELETE /:id/permanent` und `POST /trash/empty`. Im Papierkorb wird nicht gepflegt: Bearbeiten und Uebernehmen antworten 409. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
459 lines
17 KiB
TypeScript
459 lines
17 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
ACQUISITION_VIEWS,
|
|
CONTACT_VIEWS,
|
|
MAIL_VIEWS,
|
|
OPPORTUNITY_VIEWS,
|
|
SCANNER_VIEWS,
|
|
type ViewState,
|
|
} from '../types';
|
|
import { UserAvatar } from './ui/UserAvatar';
|
|
import { useAuth } from '../lib/auth';
|
|
|
|
interface SidebarProps {
|
|
currentView: ViewState;
|
|
onViewChange: (view: ViewState) => void;
|
|
isOpen: boolean;
|
|
onToggle: () => void;
|
|
/**
|
|
* Zahlen an den Unterpunkten. Was auf eine Entscheidung wartet oder sich
|
|
* ansammelt, soll man sehen, ohne die Ansicht zu oeffnen. 0 zeigt nichts an -
|
|
* eine Null waere nur Rauschen.
|
|
*/
|
|
counts?: Partial<Record<ViewState, number | null>>;
|
|
}
|
|
|
|
interface MenuItem {
|
|
name: ViewState;
|
|
icon: string;
|
|
/** Abweichende Beschriftung; ohne Angabe wird der Ansichtsname gezeigt. */
|
|
label?: string;
|
|
}
|
|
|
|
/**
|
|
* Aufklappbarer Menuepunkt: der Kopf wechselt auf `item.name` und zeigt die
|
|
* Unterpunkte. `views` sind alle Ansichten der Gruppe - liegt die aktuelle
|
|
* darunter, bleibt die Gruppe offen und hervorgehoben.
|
|
*/
|
|
interface MenuGroup {
|
|
item: MenuItem;
|
|
children: MenuItem[];
|
|
views: ViewState[];
|
|
}
|
|
|
|
const isGroup = (entry: MenuItem | MenuGroup): entry is MenuGroup => 'children' in entry;
|
|
|
|
const CONTACTS_ICON =
|
|
'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z';
|
|
const MAIL_ICON =
|
|
'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z';
|
|
|
|
/**
|
|
* Kontakte. Der Menuepunkt klappt den gepflegten Bestand und den Vorbereich
|
|
* "Aus Mails" darunter auf - dort sammelt der Postfachabgleich Personen, die
|
|
* noch niemand gesichtet hat.
|
|
*/
|
|
const CONTACTS_GROUP: MenuGroup = {
|
|
item: { name: 'Kontakte', icon: CONTACTS_ICON },
|
|
views: CONTACT_VIEWS,
|
|
children: [
|
|
{ name: 'Kontakte', label: 'Alle Kontakte', icon: CONTACTS_ICON },
|
|
{ name: 'Aus Mails', icon: MAIL_ICON },
|
|
],
|
|
};
|
|
|
|
const DASHBOARD_ICON =
|
|
'M4 5a1 1 0 011-1h4a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v3a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 16a1 1 0 011-1h4a1 1 0 011 1v3a1 1 0 01-1 1H5a1 1 0 01-1-1v-3zm10-4a1 1 0 011-1h4a1 1 0 011 1v7a1 1 0 01-1 1h-4a1 1 0 01-1-1v-7z';
|
|
|
|
const ACQUISITION_ICON =
|
|
'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01';
|
|
const ARCHIVE_ICON =
|
|
'M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4';
|
|
|
|
const TRASH_ICON =
|
|
'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16';
|
|
|
|
/**
|
|
* Akquise. Der Eingang zeigt die Funde der letzten 14 Tage, darunter klappt das
|
|
* Archiv mit allem Aelteren auf. Beides ist dieselbe Liste - geteilt wird nach
|
|
* dem Funddatum, nicht durch Verschieben. Der Papierkorb darunter ist die
|
|
* Ausnahme: dort liegt, was jemand von Hand geloescht hat.
|
|
*/
|
|
const ACQUISITION_GROUP: MenuGroup = {
|
|
item: { name: 'Akquise', icon: ACQUISITION_ICON },
|
|
views: ACQUISITION_VIEWS,
|
|
children: [
|
|
{ name: 'Akquise', label: 'Eingang', icon: ACQUISITION_ICON },
|
|
{ name: 'Archiv', icon: ARCHIVE_ICON },
|
|
{ name: 'Akquise-Papierkorb', label: 'Papierkorb', icon: TRASH_ICON },
|
|
],
|
|
};
|
|
|
|
const OPPORTUNITY_ICON =
|
|
'M9 17V7m4 10V11m4 6v-3m4 7H5a2 2 0 01-2-2V5a2 2 0 012-2h14a2 2 0 012 2v14a2 2 0 01-2 2z';
|
|
|
|
/**
|
|
* Opportunities mit ihrem Papierkorb. Geloeschte Opportunities liegen dort 90
|
|
* Tage, damit ein Fehlklick nicht Verlauf, Ansprechpartner und Mailbezuege
|
|
* kostet.
|
|
*/
|
|
const OPPORTUNITIES_GROUP: MenuGroup = {
|
|
item: { name: 'Opportunities', icon: OPPORTUNITY_ICON },
|
|
views: OPPORTUNITY_VIEWS,
|
|
children: [
|
|
{ name: 'Opportunities', label: 'Alle', icon: OPPORTUNITY_ICON },
|
|
{ name: 'Opportunity-Papierkorb', label: 'Papierkorb', icon: TRASH_ICON },
|
|
],
|
|
};
|
|
|
|
/**
|
|
* Mails. Darunter klappt der Papierkorb auf: Mails, die im CRM geloescht wurden.
|
|
* Im Postfach bleiben sie stehen - der Abgleich holt sie aber nicht wieder.
|
|
*/
|
|
const MAILS_GROUP: MenuGroup = {
|
|
item: { name: 'Mails', icon: MAIL_ICON },
|
|
views: MAIL_VIEWS,
|
|
children: [
|
|
{ name: 'Mails', label: 'Postfach', icon: MAIL_ICON },
|
|
{ name: 'Papierkorb', icon: TRASH_ICON },
|
|
],
|
|
};
|
|
|
|
/**
|
|
* Projekt Scanner. Wie bei den Kontakten klappen die Reiter darunter auf -
|
|
* "Scanner" selbst ist dabei das Dashboard. Die Einstellungen des Scanners
|
|
* stehen nicht hier, sondern als Reiter in den CRM-Einstellungen.
|
|
*/
|
|
const SCANNER_GROUP: MenuGroup = {
|
|
item: { name: 'Scanner', icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' },
|
|
views: SCANNER_VIEWS,
|
|
children: [
|
|
{
|
|
name: 'Scanner',
|
|
label: 'Dashboard',
|
|
icon: 'M9 17V7m4 10V11m4 6v-3m4 7H5a2 2 0 01-2-2V5a2 2 0 012-2h14a2 2 0 012 2v14a2 2 0 01-2 2z',
|
|
},
|
|
{
|
|
name: 'Unpassend',
|
|
icon: 'M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636',
|
|
},
|
|
{
|
|
name: 'Suchagenten',
|
|
icon: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z',
|
|
},
|
|
{
|
|
name: 'Börsen-Konten',
|
|
icon: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9',
|
|
},
|
|
],
|
|
};
|
|
|
|
// Kategorien in Anzeigereihenfolge, einzelne Punkte und Gruppen gemischt.
|
|
// Einstellungen bleibt separat am unteren Rand.
|
|
const CATEGORY_ENTRIES: (MenuItem | MenuGroup)[] = [
|
|
{ name: 'Dashboard', icon: DASHBOARD_ICON },
|
|
ACQUISITION_GROUP,
|
|
OPPORTUNITIES_GROUP,
|
|
{
|
|
// Der Terminkalender heisst im CRM "Interviews": genutzt wird er fuer
|
|
// Gespraechstermine, nicht als allgemeiner Kalender.
|
|
name: 'Interviews',
|
|
icon: 'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z',
|
|
},
|
|
CONTACTS_GROUP,
|
|
MAILS_GROUP,
|
|
SCANNER_GROUP,
|
|
];
|
|
|
|
/*
|
|
* Whisper hat keinen eigenen Menuepunkt mehr - es ist ein Reiter in den
|
|
* Einstellungen. Deshalb sind die Einstellungen jetzt fuer jede Rolle sichtbar:
|
|
* jeder pflegt sein Konto und richtet sein eigenes Diktat ein. Welche Reiter
|
|
* erscheinen, entscheidet die Rolle innerhalb der Einstellungen.
|
|
*/
|
|
const SETTINGS_ITEM: MenuItem = {
|
|
name: 'Einstellungen',
|
|
icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
|
|
};
|
|
|
|
function useIsMobile() {
|
|
const [isMobile, setIsMobile] = useState(() =>
|
|
typeof window === 'undefined' ? false : window.innerWidth < 1024
|
|
);
|
|
|
|
useEffect(() => {
|
|
const onResize = () => setIsMobile(window.innerWidth < 1024);
|
|
window.addEventListener('resize', onResize);
|
|
return () => window.removeEventListener('resize', onResize);
|
|
}, []);
|
|
|
|
return isMobile;
|
|
}
|
|
|
|
export const Sidebar: React.FC<SidebarProps> = ({
|
|
currentView,
|
|
onViewChange,
|
|
isOpen,
|
|
onToggle,
|
|
counts = {},
|
|
}) => {
|
|
const { user, logout } = useAuth();
|
|
const isMobile = useIsMobile();
|
|
const showLabels = isOpen || isMobile;
|
|
|
|
/*
|
|
* Offene Gruppen, nach dem Namen des Kopfeintrags. Wer per Klick in einer
|
|
* Gruppenansicht landet, soll die Unterpunkte auch sehen - deshalb oeffnet der
|
|
* Effekt die Gruppe, zu der die aktuelle Ansicht gehoert.
|
|
*/
|
|
const [openGroups, setOpenGroups] = useState<ViewState[]>(() =>
|
|
CATEGORY_ENTRIES.filter(isGroup)
|
|
.filter((group) => group.views.includes(currentView))
|
|
.map((group) => group.item.name)
|
|
);
|
|
|
|
useEffect(() => {
|
|
const active = CATEGORY_ENTRIES.filter(isGroup).find((group) => group.views.includes(currentView));
|
|
if (!active) return;
|
|
setOpenGroups((open) => (open.includes(active.item.name) ? open : [...open, active.item.name]));
|
|
}, [currentView]);
|
|
|
|
const renderItem = (
|
|
item: MenuItem,
|
|
options: { indented?: boolean; badge?: number | null } = {}
|
|
) => {
|
|
const isActive = currentView === item.name;
|
|
return (
|
|
<button
|
|
key={item.name}
|
|
onClick={() => {
|
|
onViewChange(item.name);
|
|
if (isMobile) onToggle();
|
|
}}
|
|
title={!showLabels ? item.label ?? item.name : undefined}
|
|
aria-current={isActive ? 'page' : undefined}
|
|
className={`group flex w-full items-center rounded-xl transition ${
|
|
showLabels
|
|
? `gap-3 py-2.5 ${options.indented ? 'pl-9 pr-3' : 'px-3'}`
|
|
: 'justify-center px-0 py-2.5'
|
|
} ${
|
|
isActive
|
|
? 'bg-brand-500 text-white shadow-card'
|
|
: 'text-ink-600 hover:bg-brand-50 hover:text-brand-700'
|
|
}`}
|
|
>
|
|
<svg
|
|
className={`shrink-0 ${options.indented && showLabels ? 'h-4 w-4' : 'h-5 w-5'}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={isActive ? 2.2 : 1.7}
|
|
d={item.icon}
|
|
/>
|
|
</svg>
|
|
{showLabels && (
|
|
<span className={`truncate font-bold ${options.indented ? 'text-[13px]' : 'text-sm'}`}>
|
|
{item.label ?? item.name}
|
|
</span>
|
|
)}
|
|
{showLabels && options.badge ? (
|
|
<span
|
|
className={`ml-auto shrink-0 rounded-md px-1.5 py-0.5 text-[10px] font-extrabold ${
|
|
isActive ? 'bg-white/25 text-white' : 'border border-amber-200 bg-amber-50 text-amber-700'
|
|
}`}
|
|
>
|
|
{options.badge}
|
|
</span>
|
|
) : null}
|
|
</button>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Aufklappbare Gruppe: der Klick oeffnet die Unterpunkte und wechselt gleich
|
|
* auf die Hauptansicht der Gruppe. Bei eingeklappter Leiste gibt es nur das
|
|
* Symbol, die Unterpunkte erscheinen erst mit den Beschriftungen.
|
|
*/
|
|
const renderGroup = (group: MenuGroup) => {
|
|
const { item, children, views } = group;
|
|
const isActive = currentView === item.name;
|
|
const isInGroup = views.includes(currentView);
|
|
const isGroupOpen = openGroups.includes(item.name);
|
|
|
|
return (
|
|
<div key={`group-${item.name}`}>
|
|
<button
|
|
onClick={() => {
|
|
setOpenGroups((open) =>
|
|
isInGroup && open.includes(item.name)
|
|
? open.filter((name) => name !== item.name)
|
|
: [...open.filter((name) => name !== item.name), item.name]
|
|
);
|
|
onViewChange(item.name);
|
|
if (isMobile) onToggle();
|
|
}}
|
|
title={!showLabels ? item.label ?? item.name : undefined}
|
|
aria-expanded={isGroupOpen}
|
|
aria-current={isActive ? 'page' : undefined}
|
|
className={`group flex w-full items-center rounded-xl transition ${
|
|
showLabels ? 'gap-3 px-3 py-2.5' : 'justify-center px-0 py-2.5'
|
|
} ${
|
|
isActive
|
|
? 'bg-brand-500 text-white shadow-card'
|
|
: isInGroup
|
|
? 'bg-brand-50 text-brand-700'
|
|
: 'text-ink-600 hover:bg-brand-50 hover:text-brand-700'
|
|
}`}
|
|
>
|
|
<svg className="h-5 w-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={isActive ? 2.2 : 1.7}
|
|
d={item.icon}
|
|
/>
|
|
</svg>
|
|
{showLabels && (
|
|
<>
|
|
<span className="truncate text-sm font-bold">{item.label ?? item.name}</span>
|
|
<svg
|
|
className={`ml-auto h-4 w-4 shrink-0 transition-transform ${
|
|
isGroupOpen ? 'rotate-180' : ''
|
|
}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
{showLabels && isGroupOpen && (
|
|
<div className="mt-1 space-y-1">
|
|
{children.map((child) =>
|
|
renderItem(child, { indented: true, badge: counts[child.name] ?? undefined })
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{isOpen && isMobile && (
|
|
<div className="fixed inset-0 z-20 bg-ink-950/40 backdrop-blur-sm lg:hidden" onClick={onToggle} />
|
|
)}
|
|
|
|
<aside
|
|
className={`fixed inset-y-0 left-0 z-30 flex h-full flex-col overflow-hidden border-r border-ink-200 bg-surface transition-all duration-300 lg:static ${
|
|
isOpen ? 'w-64 translate-x-0' : 'w-0 -translate-x-full lg:w-[76px] lg:translate-x-0'
|
|
}`}
|
|
>
|
|
{/* Logo */}
|
|
<button
|
|
onClick={() => {
|
|
onViewChange('Dashboard');
|
|
if (isMobile) onToggle();
|
|
}}
|
|
title="Zum Dashboard"
|
|
aria-label="Zum Dashboard"
|
|
className={`flex shrink-0 items-center border-b border-ink-100 transition hover:bg-ink-50 ${
|
|
showLabels ? 'px-5 py-5' : 'justify-center px-0 py-5'
|
|
}`}
|
|
>
|
|
{showLabels ? (
|
|
<img src="/logo.png" alt="perinco CRM" className="logo-adaptive h-9 w-auto" />
|
|
) : (
|
|
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-brand-500 text-base font-extrabold text-white">
|
|
p
|
|
</div>
|
|
)}
|
|
</button>
|
|
|
|
{/* Kategorien */}
|
|
<nav className="scrollbar-slim flex-1 space-y-1 overflow-y-auto overflow-x-hidden px-3 py-4">
|
|
{showLabels && (
|
|
<p className="mb-2 px-3 text-[10px] font-extrabold uppercase tracking-[0.18em] text-ink-400">
|
|
Kategorien
|
|
</p>
|
|
)}
|
|
{CATEGORY_ENTRIES.map((entry) => (isGroup(entry) ? renderGroup(entry) : renderItem(entry)))}
|
|
</nav>
|
|
|
|
{/* Einstellungen unten - Inhalt richtet sich nach der Rolle */}
|
|
<div className="shrink-0 space-y-1 border-t border-ink-100 px-3 py-3">
|
|
{renderItem(SETTINGS_ITEM)}
|
|
</div>
|
|
|
|
{/* Benutzer */}
|
|
<div
|
|
className={`shrink-0 border-t border-ink-100 ${
|
|
showLabels ? 'p-4' : 'flex flex-col items-center gap-3 p-3'
|
|
}`}
|
|
>
|
|
<div className="flex w-full min-w-0 items-center justify-between gap-3">
|
|
<div className="flex min-w-0 items-center gap-3">
|
|
{user ? (
|
|
<UserAvatar user={user} size={36} />
|
|
) : (
|
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-brand-100 text-sm font-extrabold text-brand-700">
|
|
?
|
|
</div>
|
|
)}
|
|
{showLabels && (
|
|
<div className="min-w-0">
|
|
<p className="truncate text-sm font-bold text-ink-900">{user?.name}</p>
|
|
<p className="truncate text-xs text-ink-400">
|
|
{user?.role === 'admin' ? 'Administrator' : 'Benutzer'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{showLabels && (
|
|
<button
|
|
onClick={logout}
|
|
title="Abmelden"
|
|
className="shrink-0 rounded-lg p-2 text-ink-400 transition hover:bg-red-50 hover:text-red-600"
|
|
>
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{!showLabels && (
|
|
<button
|
|
onClick={logout}
|
|
title="Abmelden"
|
|
className="rounded-lg p-2 text-ink-400 transition hover:bg-red-50 hover:text-red-600"
|
|
>
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
</>
|
|
);
|
|
};
|