Untitled
user_0601895
plain_text
10 months ago
12 kB
18
Indexable
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { usePathname } from 'next/navigation';
import {
Users2,
Images as ImagesIcon,
MonitorSmartphone,
LogOut,
PanelLeftClose,
PanelLeftOpen,
} from 'lucide-react';
interface Body {
purpose: string;
value: string;
}
interface Annotation {
body?: Body[];
}
interface SidebarProps {
minimal?: boolean;
annotations?: Annotation[];
version?: string;
}
type ThumbItem = {
id: string;
thumb: string;
file: string;
tileSource: string;
};
function cn(...c: Array<string | false | null | undefined>) {
return c.filter(Boolean).join(' ');
}
function NavItem({
href,
label,
icon: Icon,
active,
collapsed,
}: {
href: string;
label: string;
icon: React.ElementType;
active: boolean;
collapsed: boolean;
}) {
return (
<Link
href={href}
className={cn(
'group flex items-center gap-3 rounded-xl px-3 py-2 text-sm font-medium transition-colors',
collapsed && 'justify-center',
active
? 'bg-[var(--sb-active-bg)] text-[var(--sb-fg)]'
: 'text-[var(--sb-muted)] hover:bg-[var(--sb-hover-bg)] hover:text-[var(--sb-fg)]'
)}
aria-current={active ? 'page' : undefined}
title={collapsed ? label : undefined}
>
<span
className={cn(
'h-1.5 w-1.5 rounded-full transition-colors',
active
? 'bg-[var(--sb-accent)]'
: 'bg-[color-mix(in_oklab,var(--sb-fg)_30%,transparent)] group-hover:bg-[color-mix(in_oklab,var(--sb-fg)_55%,transparent)]'
)}
aria-hidden
/>
<Icon className="h-4 w-4 opacity-90" />
{!collapsed && <span className="truncate">{label}</span>}
</Link>
);
}
export default function Sidebar({ minimal, annotations = [] }: SidebarProps) {
const pathname = usePathname();
const [activeId, setActiveId] = useState<string | null>(null);
const [collapsed, setCollapsed] = useState(false);
const [thumbnails, setThumbnails] = useState<ThumbItem[]>([]);
// 🔹 Dinamik slide fetch
useEffect(() => {
fetch('/slides')
.then((res) => res.json())
.then((files: string[]) => {
const items = files.map((f) => {
const id = f.replace('.tif', '');
return {
id,
file: f,
thumb: `/${id}.jpg`, // TODO: gerçek thumbnail varsa buraya bağla
tileSource: `/slides/${id}/dzi`,
};
});
setThumbnails(items);
})
.catch((err) => {
console.error('Slide fetch error:', err);
});
}, []);
// 🔹 LocalStorage state restore
useEffect(() => {
try {
const last = localStorage.getItem('lastSlideId');
if (last) setActiveId(last);
const col = localStorage.getItem('sidebarCollapsed');
if (col) setCollapsed(col === '1');
} catch {}
}, []);
const toggleCollapsed = () => {
setCollapsed((v) => {
const next = !v;
try {
localStorage.setItem('sidebarCollapsed', next ? '1' : '0');
} catch {}
return next;
});
};
// 🔹 Viewer’a event gönder
const sendClick = (item: ThumbItem) => {
setActiveId(item.id);
try {
localStorage.setItem('lastSlideId', item.id);
} catch {}
window.dispatchEvent(new CustomEvent('thumbnailClick', { detail: item.tileSource }));
};
const sidebarVars: React.CSSProperties = {
['--sb-bg' as any]: 'var(--surface-2)',
['--sb-fg' as any]: 'var(--foreground)',
['--sb-muted' as any]: 'color-mix(in oklab, var(--foreground) 72%, transparent)',
['--sb-border' as any]: 'var(--border-strong)',
['--sb-accent' as any]: 'var(--accent)',
['--sb-hover-bg' as any]: 'color-mix(in oklab, var(--foreground) 6%, transparent)',
['--sb-active-bg' as any]: 'color-mix(in oklab, var(--accent) 18%, transparent)',
['--sb-chip-bg' as any]: 'color-mix(in oklab, var(--surface-0) 75%, var(--accent) 8%)',
};
return (
<aside
className={cn(
'h-dvh shrink-0 overflow-y-auto border-r transition-[width] duration-200',
collapsed ? 'w-20' : 'w-72'
)}
style={{
...sidebarVars,
background: 'var(--sb-bg)',
color: 'var(--sb-fg)',
borderColor: 'var(--sb-border)',
}}
>
{/* HEADER */}
<div className="sticky top-0 z-10 border-b px-3 py-4 backdrop-blur-sm border-[var(--sb-border)]">
<div className="flex flex-col items-center justify-center">
<Image
src="/indir.png"
alt="Detagen Logo"
width={collapsed ? 36 : 48}
height={collapsed ? 36 : 48}
className="rounded-lg ring-1 ring-[color-mix(in_oklab,var(--sb-fg)_22%,transparent)]"
priority
/>
{!collapsed ? (
<div className="mt-2 leading-tight text-center">
<div className="text-sm font-semibold tracking-tight text-[var(--sb-fg)]">Detagen</div>
<div className="text-[11px] text-[var(--sb-muted)]">Patoloji Görüntüleme</div>
</div>
) : (
<div className="mt-2 text-[10px] font-medium uppercase tracking-widest text-[var(--sb-muted)] text-center">
Detagen
</div>
)}
</div>
{!minimal && (
<button
onClick={toggleCollapsed}
className={cn(
'mt-3 inline-flex items-center gap-2 rounded-lg border px-2 py-1.5 text-[11px] font-medium transition-colors',
'text-[var(--sb-fg)] border-[var(--sb-border)] hover:bg-[var(--sb-hover-bg)]',
collapsed && 'w-full justify-center'
)}
title={collapsed ? 'Genişlet' : 'Daralt'}
>
{collapsed ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
{!collapsed && <span>Paneli {collapsed ? 'aç' : 'daralt'}</span>}
</button>
)}
</div>
{/* NAV */}
{!minimal && (
<nav className="px-3 pt-3">
{!collapsed && (
<div className="mb-2 px-3 text-[11px] uppercase tracking-wider text-[var(--sb-muted)]">Menü</div>
)}
<div className="space-y-1">
<NavItem
href="/dashboard"
label="Hastalar"
icon={Users2}
active={pathname === '/dashboard'}
collapsed={collapsed}
/>
<NavItem
href="/images"
label="Görseller"
icon={ImagesIcon}
active={pathname === '/images'}
collapsed={collapsed}
/>
<NavItem
href="/viewer"
label="Görüntüleyici"
icon={MonitorSmartphone}
active={pathname === '/viewer'}
collapsed={collapsed}
/>
</div>
<div className="mt-6 h-px w-full bg-[color-mix(in_oklab,var(--sb-fg)_12%,transparent)]" />
<Link
href="/"
className={cn(
'mt-4 flex items-center gap-3 rounded-xl px-3 py-2 text-sm font-medium transition-colors',
'text-[color-mix(in_oklab,red_80%,var(--sb-fg)_20%)] hover:bg-[var(--sb-hover-bg)] hover:text-[color-mix(in_oklab,red_90%,var(--sb-fg)_10%)]',
collapsed && 'justify-center'
)}
title={collapsed ? 'Çıkış Yap' : undefined}
>
<LogOut className="h-4 w-4" />
{!collapsed && <span>Çıkış Yap</span>}
</Link>
</nav>
)}
{/* VIEWER PANELS */}
{pathname === '/viewer' && (
<div className={cn('px-3 py-5', collapsed && 'px-2')}>
{/* Görseller */}
<div
className={cn('rounded-2xl border p-3 shadow-sm backdrop-blur', collapsed && 'p-2')}
style={{
background: 'color-mix(in oklab, var(--surface-1) 92%, transparent)',
borderColor: 'var(--border-strong)',
color: 'var(--text-primary)',
}}
>
<div className={cn('mb-3 flex items-center', collapsed ? 'justify-center' : 'justify-between')}>
{!collapsed ? (
<>
<h2 className="text-sm font-semibold text-[var(--text-primary)]">Görseller</h2>
<span
className="rounded-full px-2 py-0.5 text-[10px] font-medium"
style={{ background: 'var(--sb-chip-bg)', color: 'var(--sb-fg)' }}
>
{thumbnails.length}
</span>
</>
) : (
<span className="text-[11px] font-semibold text-[var(--text-primary)]">Görseller</span>
)}
</div>
<div className={cn('grid gap-3', collapsed ? 'grid-cols-1' : 'grid-cols-2')}>
{thumbnails.map((item) => {
const isActive = activeId === item.id;
return (
<button
key={item.id}
onClick={() => sendClick(item)}
title={item.file}
className={cn(
'group relative overflow-hidden rounded-xl border transition shadow-sm hover:shadow-md focus:outline-none focus:ring-2',
isActive ? 'ring-2 ring-[var(--accent)]' : 'ring-0'
)}
style={{ borderColor: 'var(--border-strong)' }}
>
<div className={cn('relative', collapsed ? 'h-[76px] w-full' : 'aspect-square')}>
<Image
src={item.thumb}
alt={item.id}
fill
className="object-cover"
sizes={collapsed ? '160px' : '180px'}
priority={false}
/>
</div>
{!collapsed && (
<div className="absolute bottom-0 left-0 right-0 bg-[color-mix(in_oklab,black_55%,transparent)] px-2 py-1 text-center text-[10px] font-medium text-white">
{item.id}
</div>
)}
{isActive && (
<div className="absolute right-2 top-2 h-2 w-2 rounded-full bg-[var(--accent)]" aria-hidden />
)}
</button>
);
})}
</div>
</div>
{/* Anotasyonlar */}
<div
className={cn('mt-5 rounded-2xl border p-3 shadow-sm backdrop-blur', collapsed && 'p-2')}
style={{
background: 'color-mix(in oklab, var(--surface-1) 92%, transparent)',
borderColor: 'var(--border-strong)',
color: 'var(--text-primary)',
}}
>
{!collapsed && <h2 className="mb-3 text-sm font-semibold text-[var(--text-primary)]">Anotasyonlar</h2>}
{annotations.length === 0 ? (
<p className={cn('text-[11px] text-[var(--text-secondary)]', collapsed && 'text-center')}>
Henüz eklenmemiş.
</p>
) : (
<ul className="space-y-2">
{annotations.map((anno, idx) => (
<li key={idx} className="truncate text-[11px] text-[var(--text-primary)]">
{anno.body?.map((b, i) => (
<span key={i}>
{b.purpose}: {b.value}
{i < (anno.body?.length || 0) - 1 ? ', ' : ''}
</span>
))}
</li>
))}
</ul>
)}
</div>
</div>
)}
</aside>
);
}
Editor is loading...
Leave a Comment