195 lines
8.2 KiB
JavaScript
195 lines
8.2 KiB
JavaScript
import React from "react";
|
||
import { ROLE_LABELS } from "../constants/roles";
|
||
import { Badge } from "../components/UI/Badge";
|
||
import { Button } from "../components/UI/Button";
|
||
import { Panel } from "../components/UI/Panel";
|
||
import { ThemeToggle } from "../components/UI/ThemeToggle";
|
||
import { PwaInstallButton } from "../components/UI/PwaInstallButton";
|
||
import { NotificationBell } from "../components/notifications/NotificationBell";
|
||
import { NotificationSettings } from "../components/notifications/NotificationSettings";
|
||
|
||
export const AppShell = ({
|
||
user,
|
||
onInstallApp,
|
||
isInstalled,
|
||
isInstallAvailable,
|
||
onSignOut,
|
||
onOpenGuide,
|
||
isGuideOpen = false,
|
||
navItems,
|
||
activeSection,
|
||
onSectionChange,
|
||
sectionMeta,
|
||
notifications = [],
|
||
unreadCount = 0,
|
||
onMarkNotificationRead,
|
||
onMarkAllNotificationsRead,
|
||
children,
|
||
}) => {
|
||
const shouldShowMobileNav = !isGuideOpen && navItems.length > 1;
|
||
const [showNotifSettings, setShowNotifSettings] = React.useState(false);
|
||
|
||
if (showNotifSettings) {
|
||
return (
|
||
<div className="min-h-screen px-3 py-4 sm:px-4 md:px-6 md:py-8">
|
||
<div className="mx-auto max-w-2xl">
|
||
<NotificationSettings
|
||
userId={user?.id}
|
||
userRole={user?.role}
|
||
onBack={() => setShowNotifSettings(false)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen px-3 py-4 sm:px-4 md:px-6 md:py-8">
|
||
<div className="mx-auto max-w-[1540px] space-y-4 xl:grid xl:grid-cols-[220px_1fr] xl:gap-8 xl:space-y-0">
|
||
{/* Desktop sidebar */}
|
||
<Panel className="hidden h-fit flex-col gap-5 p-4 xl:flex">
|
||
<div>
|
||
<p className="text-xs uppercase tracking-[0.24em] text-[var(--color-text-muted)]">
|
||
Панель
|
||
</p>
|
||
<h1 className="mt-2 text-lg font-semibold leading-tight">Управление доставкой</h1>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
{navItems.map((item) => (
|
||
<button
|
||
key={item.key}
|
||
className={[
|
||
"flex w-full items-center justify-between rounded-[18px] px-3 py-3 text-left text-sm transition",
|
||
activeSection === item.key
|
||
? "bg-[var(--color-accent-soft)] text-[var(--color-text)]"
|
||
: "text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)] hover:text-[var(--color-text)]",
|
||
].join(" ")}
|
||
onClick={() => onSectionChange(item.key)}
|
||
type="button"
|
||
>
|
||
<span className="font-medium">{item.label}</span>
|
||
{item.badge ? <Badge tone="accent">{item.badge}</Badge> : null}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="mt-auto">
|
||
{onOpenGuide ? (
|
||
<Button variant="ghost" className="mb-2 w-full justify-start" onClick={onOpenGuide}>
|
||
{isGuideOpen ? "К рабочей области" : "Справка"}
|
||
</Button>
|
||
) : null}
|
||
<Button variant="ghost" className="w-full justify-start" onClick={onSignOut}>
|
||
Выйти
|
||
</Button>
|
||
</div>
|
||
</Panel>
|
||
|
||
{/* Main content area */}
|
||
<div className="min-w-0 space-y-5 pb-20 xl:space-y-8 xl:pb-0">
|
||
{/* Mobile header */}
|
||
<Panel className="p-4 xl:hidden">
|
||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||
<div className="min-w-0 flex-1 space-y-1">
|
||
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-text-muted)]">
|
||
Рабочая область
|
||
</p>
|
||
<h2 className="text-lg font-semibold leading-tight sm:text-xl md:text-2xl">
|
||
{sectionMeta?.label || "Панель"}
|
||
</h2>
|
||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
||
{user.name} · {ROLE_LABELS[user.role] || user.role}
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-1 md:flex-shrink-0">
|
||
<NotificationBell
|
||
notifications={notifications}
|
||
unreadCount={unreadCount}
|
||
onMarkAsRead={onMarkNotificationRead}
|
||
onMarkAllAsRead={onMarkAllNotificationsRead}
|
||
onOpenSettings={() => setShowNotifSettings(true)}
|
||
/>
|
||
{onOpenGuide ? (
|
||
<Button size="sm" variant="ghost" onClick={onOpenGuide} aria-label="Справка">
|
||
?
|
||
</Button>
|
||
) : null}
|
||
<PwaInstallButton onInstall={onInstallApp} isInstalled={isInstalled} isInstallAvailable={isInstallAvailable} />
|
||
<ThemeToggle />
|
||
<Button size="sm" variant="ghost" onClick={onSignOut}>
|
||
Выйти
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Panel>
|
||
|
||
{/* Mobile tab navigation — STICKY TOP */}
|
||
{shouldShowMobileNav && (
|
||
<div className="sticky inset-x-0 top-0 z-40 -mx-3 -mt-4 border-b border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 backdrop-blur xl:hidden sm:-mx-4 md:-mx-6">
|
||
<div className="flex gap-1 overflow-x-auto" style={{ WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none' }}>
|
||
{navItems.map((item) => (
|
||
<button
|
||
key={item.key}
|
||
className={[
|
||
"flex flex-shrink-0 items-center gap-1.5 rounded-[14px] px-3 py-2 text-sm transition",
|
||
activeSection === item.key
|
||
? "bg-[var(--color-accent)] text-[var(--color-accent-contrast)]"
|
||
: "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)]",
|
||
].join(" ")}
|
||
onClick={() => onSectionChange(item.key)}
|
||
type="button"
|
||
>
|
||
<span className="truncate font-medium">{item.label}</span>
|
||
{item.badge ? (
|
||
<Badge tone={activeSection === item.key ? "neutral" : "accent"}>{item.badge}</Badge>
|
||
) : null}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Desktop header */}
|
||
<Panel className="hidden p-4 md:p-5 xl:block">
|
||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||
<div>
|
||
<p className="text-sm uppercase tracking-[0.2em] text-[var(--color-text-muted)]">
|
||
Рабочая область
|
||
</p>
|
||
<h2 className="mt-2 text-2xl font-semibold">{sectionMeta?.label || "Панель"}</h2>
|
||
{sectionMeta?.description ? (
|
||
<p className="mt-2 max-w-3xl text-sm leading-6 text-[var(--color-text-muted)]">
|
||
{sectionMeta.description}
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
<div className="flex flex-wrap items-center gap-3">
|
||
<NotificationBell
|
||
notifications={notifications}
|
||
unreadCount={unreadCount}
|
||
onMarkAsRead={onMarkNotificationRead}
|
||
onMarkAllAsRead={onMarkAllNotificationsRead}
|
||
onOpenSettings={() => setShowNotifSettings(true)}
|
||
/>
|
||
<div className="text-right">
|
||
<div className="text-sm font-medium">{user.name}</div>
|
||
<div className="text-sm text-[var(--color-text-muted)]">{ROLE_LABELS[user.role] || user.role}</div>
|
||
</div>
|
||
{onOpenGuide ? (
|
||
<Button size="sm" variant="ghost" onClick={onOpenGuide} aria-label="Справка">
|
||
{isGuideOpen ? "Назад" : "?"}
|
||
</Button>
|
||
) : null}
|
||
<PwaInstallButton onInstall={onInstallApp} isInstalled={isInstalled} isInstallAvailable={isInstallAvailable} />
|
||
<ThemeToggle />
|
||
</div>
|
||
</div>
|
||
</Panel>
|
||
|
||
{children}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}; |