Feature pack: Media, Gamification, Templates, Time Tracking, Kanban, AI Briefing, Webhooks, Icon UI
API features (separate files in /api/src/features/): - media-input: upload text/audio/photo/video, transcription - gamification: points, streaks, badges, leaderboard - templates: predefined task sets (sprint, study, moving) - time-tracking: start/stop timer, task/user reports - kanban: board view, drag-and-drop move - ai-briefing: daily AI summary with tasks/goals/reviews - webhooks-outgoing: notify external systems on events UI components (separate files in /components/features/): - IconButton: icon-only buttons with tooltip - CompactHeader, PageActionBar, InlineEditField - TaskDetailActions, GoalActionButtons, CollabActionButtons - DeleteIconButton, CollabBackButton All features modular — registry.js enables/disables each one. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,205 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import CompactHeader from "./features/CompactHeader";
|
||||
|
||||
export default function Header() {
|
||||
const { user, logout, token } = useAuth();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
function handleLogout() {
|
||||
logout();
|
||||
router.push("/login");
|
||||
setDrawerOpen(false);
|
||||
}
|
||||
|
||||
const closeDrawer = useCallback(() => setDrawerOpen(false), []);
|
||||
const openDrawer = useCallback(() => setDrawerOpen(true), []);
|
||||
|
||||
// Close drawer on escape
|
||||
useEffect(() => {
|
||||
if (!drawerOpen) return;
|
||||
function handleKey(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") closeDrawer();
|
||||
}
|
||||
document.addEventListener("keydown", handleKey);
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKey);
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [drawerOpen, closeDrawer]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="sticky top-0 z-50 bg-white/80 dark:bg-gray-950/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-800">
|
||||
<div className="max-w-4xl mx-auto px-4 h-11 flex items-center justify-end gap-2">
|
||||
{/* Right side only: avatar (no name text) + hamburger */}
|
||||
{token && user && (
|
||||
<button
|
||||
onClick={openDrawer}
|
||||
className="w-7 h-7 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-full flex items-center justify-center text-xs font-bold cursor-pointer hover:ring-2 hover:ring-blue-400 transition-all flex-shrink-0"
|
||||
aria-label={t("common.menu")}
|
||||
>
|
||||
{(user.name || user.email || "?").charAt(0).toUpperCase()}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={openDrawer}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors flex items-center justify-center flex-shrink-0"
|
||||
aria-label={t("common.menu")}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Slide-out drawer */}
|
||||
{drawerOpen && (
|
||||
<div className="fixed inset-0 z-[60]">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm animate-fadeIn"
|
||||
onClick={closeDrawer}
|
||||
/>
|
||||
|
||||
{/* Drawer panel */}
|
||||
<div className="absolute right-0 top-0 bottom-0 w-72 bg-white dark:bg-gray-900 shadow-2xl animate-slideInRight flex flex-col">
|
||||
{/* Close button */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800">
|
||||
<span className="font-semibold text-lg">{t("common.menu")}</span>
|
||||
<button
|
||||
onClick={closeDrawer}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 min-w-[44px] min-h-[44px] flex items-center justify-center"
|
||||
aria-label={t("common.closeMenu")}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* User info - full name + email */}
|
||||
{token && user && (
|
||||
<div className="p-4 border-b border-gray-200 dark:border-gray-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-full flex items-center justify-center text-sm font-bold">
|
||||
{(user.name || user.email || "?").charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-sm truncate">{user.name || t("settings.user")}</p>
|
||||
<p className="text-xs text-muted truncate">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Menu items */}
|
||||
<div className="p-2 space-y-1 flex-1 overflow-y-auto">
|
||||
<Link
|
||||
href="/tasks"
|
||||
onClick={closeDrawer}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors min-h-[48px]"
|
||||
>
|
||||
<svg className="w-5 h-5 text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="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 2" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">{t("nav.tasks")}</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/calendar"
|
||||
onClick={closeDrawer}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors min-h-[48px]"
|
||||
>
|
||||
<svg className="w-5 h-5 text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">{t("nav.calendar")}</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/chat"
|
||||
onClick={closeDrawer}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors min-h-[48px]"
|
||||
>
|
||||
<svg className="w-5 h-5 text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">{t("nav.chat")}</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/settings"
|
||||
onClick={closeDrawer}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors min-h-[48px]"
|
||||
>
|
||||
<svg className="w-5 h-5 text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="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.066 2.573c1.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.573 1.066c-.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.066-2.573c-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" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">{t("nav.settings")}</span>
|
||||
</Link>
|
||||
|
||||
{/* Install link */}
|
||||
<Link
|
||||
href="/settings#install"
|
||||
onClick={closeDrawer}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors min-h-[48px]"
|
||||
>
|
||||
<svg className="w-5 h-5 text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">{t("settings.install") || "Instalace"}</span>
|
||||
</Link>
|
||||
|
||||
{/* Theme toggle */}
|
||||
<button
|
||||
onClick={() => { toggleTheme(); }}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors w-full text-left min-h-[48px]"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<svg className="w-5 h-5 text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5 text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
)}
|
||||
<span className="text-sm font-medium">
|
||||
{theme === "dark" ? t("settings.light") : t("settings.dark")}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Logout icon at bottom */}
|
||||
{token && (
|
||||
<div className="p-4 border-t border-gray-200 dark:border-gray-800 safe-area-bottom">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
title={t("auth.logout")}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors min-w-[44px] min-h-[44px] flex items-center justify-center"
|
||||
aria-label={t("auth.logout")}
|
||||
>
|
||||
<svg className="w-5 h-5 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" 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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return <CompactHeader />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user