Ctrl K
ComunidadGuías…

components/NavSidebar.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
'use client';

import NavBar from './NavBar';
import NavSearch from './NavSearch';
import Link from 'next/link';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { FAST } from '@/lib/motion';
import {
  ChevronLeftIcon,
  ChevronRightIcon,
  Bars3Icon,
  UserIcon,
  ArrowRightStartOnRectangleIcon,
  ShieldCheckIcon,
  MoonIcon,
  SunIcon,
  LanguageIcon,
  PlusIcon,
} from '@heroicons/react/24/outline';
import { Button } from '@/components/ui/button';
import NoteModalForm from '@/app/(authenticated)/boards/[id]/notes/components/NoteModalForm';
import UserAvatar from './UserAvatar';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
  Sheet,
  SheetContent,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from '@/components/ui/sheet';
import { signout } from '@/app/(authenticated)/lib/actions';
import { cn } from '@/lib/utils';
import { changeLocale } from '@/lib/locale';
import { locales } from '@/i18n/config';
import { useLocale, useTranslations } from 'next-intl';
import { useTheme } from 'next-themes';

import { User } from '@/prisma/generated/client';
import type { AvailableUpdate } from '@/app/(authenticated)/lib/version';
import type { QuickLinks } from '@/components/NavBar';
// '@/app/(authenticated)/lib/actions';
import UpdateAvailableMenuItem, { UpdateBadge } from './UpdateAvailableMenuItem';

interface NavSidebarProps {
  user: User;
  update?: AvailableUpdate | null;
  quickLinks?: QuickLinks;
  boards?: { id: number; title: string }[];
}

export default function NavSidebar({
  user,
  update,
  quickLinks,
  boards = [],
}: NavSidebarProps) {
  const t = useTranslations('Navigation');
  const tRole = useTranslations('Roles');
  const tLocale = useTranslations('LocaleSwitcher');
  const tTheme = useTranslations('Theme');
  const tNote = useTranslations('NotesPage');
  const { theme, setTheme } = useTheme();
  const locale = useLocale();
  const [mounted, setMounted] = useState(false);
  const [isCollapsed, setIsCollapsed] = useState(false);
  const [isLocaleSubmitting, setIsLocaleSubmitting] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  const handleLocaleChange = async (nextLocale: string) => {
    setIsLocaleSubmitting(true);
    await changeLocale(nextLocale, locale);
  };

  useEffect(() => {
    const saved = localStorage.getItem('sidebar-collapsed');
    if (saved !== null) {
      setIsCollapsed(saved === 'true');
    }
  }, []);

  const toggleSidebar = () => {
    const newState = !isCollapsed;
    setIsCollapsed(newState);
    localStorage.setItem('sidebar-collapsed', String(newState));
  };

  if (!user) return null;

  return (
    <>
      {/* Desktop Sidebar */}
      <motion.aside
        initial={false}
        animate={{ width: isCollapsed ? 80 : 240 }}
        transition={FAST}
        className="hidden md:flex flex-col border-r border-border bg-background h-screen sticky top-0 z-50"
      >
        <div className="flex items-center justify-center h-20 border-b border-border relative transition-all duration-300">
          <AnimatePresence mode="wait">
            <motion.div
              key={isCollapsed ? 'collapsed' : 'expanded'}
              initial={{ opacity: 0, scale: 0.8 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0.8 }}
              transition={FAST}
              className="flex items-center justify-center"
            >
              <Link
                href="/"
                className="flex items-center justify-center transition-transform duration-300 hover:scale-105 px-4"
              >
                <Image
                  src={isCollapsed ? '/daylog-logo.svg' : '/daylog.svg'}
                  alt="daylog"
                  width={isCollapsed ? 40 : 150}
                  height={40}
                  className="h-10 w-auto logo-invert"
                  priority
                />
              </Link>
            </motion.div>
          </AnimatePresence>

          <Button
            variant="ghost"
            size="icon"
            onClick={toggleSidebar}
            className={cn(
              'rounded-full hover:bg-muted transition-all duration-300 z-50',
              isCollapsed
                ? 'absolute -right-3 top-16 bg-background border shadow-sm h-6 w-6'
                : 'absolute right-4',
            )}
          >
            {isCollapsed ? (
              <ChevronRightIcon className="h-4 w-4" />
            ) : (
              <ChevronLeftIcon className="h-5 w-5 text-muted-foreground" />
            )}
          </Button>
        </div>

        <div className="flex-1 flex flex-col py-6 overflow-y-auto overflow-x-hidden">
          {boards.length > 0 && (
            <div className={cn('mb-2', isCollapsed ? 'flex justify-center px-3' : 'px-4')}>
              <NoteModalForm
                modalId="quickNoteDesktop"
                mode="create"
                boardId={boards[0].id}
                boards={boards}
                trigger={
                  <button
                    type="button"
                    title={tNote('newNote')}
                    className={cn(
                      'group relative flex items-center rounded-xl border border-dashed border-muted-foreground/50 text-muted-foreground hover:bg-accent hover:text-foreground transition-all duration-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
                      isCollapsed ? 'justify-center h-12 w-12' : 'w-full gap-3 px-3 py-[9px]', // 9px + 1px border = py-2.5 of nav links
                    )}
                  >
                    <PlusIcon className="h-5 w-5 transition-transform duration-300 group-hover:scale-110" />
                    {isCollapsed ? (
                      <span className="sr-only">{tNote('newNote')}</span>
                    ) : (
                      <span className="font-bold text-sm tracking-tight whitespace-nowrap">
                        {tNote('newNote')}
                      </span>
                    )}
                  </button>
                }
              />
            </div>
          )}
          <NavBar user={user} isCollapsed={isCollapsed} quickLinks={quickLinks} />
        </div>
      </motion.aside>

      {/* Mobile Topbar */}
      <div className="md:hidden fixed top-0 left-0 right-0 z-50 flex items-center justify-between h-16 px-4 border-b bg-background/80 backdrop-blur-md">
        <Sheet>
          <SheetTrigger asChild>
            <Button variant="ghost" size="icon" className="rounded-full">
              <Bars3Icon className="h-6 w-6" />
            </Button>
          </SheetTrigger>
          <SheetContent
            side="left"
            className="w-[80%] max-w-sm p-0 flex flex-col border-r-0"
          >
            <SheetHeader className="h-20 px-6 border-b flex flex-row items-center justify-between shrink-0">
              <SheetTitle className="flex items-center">
                <Link href="/">
                  <Image
                    src="/daylog.svg"
                    alt="daylog"
                    width={150}
                    height={40}
                    className="h-10 w-auto logo-invert"
                    priority
                  />
                </Link>
              </SheetTitle>
            </SheetHeader>
            <div className="flex-1 flex flex-col py-6 overflow-y-auto">
              <NavBar user={user} quickLinks={quickLinks} />
            </div>
          </SheetContent>
        </Sheet>

        <Link href="/" className="absolute left-1/2 -translate-x-1/2">
          <Image
            src="/daylog-logo.svg"
            alt="daylog"
            width={40}
            height={40}
            className="h-10 w-auto logo-invert"
            priority
          />
        </Link>

        <div className="flex items-center gap-2">
          <NavSearch />
          <DropdownMenu>
            <DropdownMenuTrigger asChild>
              <button className="relative flex items-center p-0.5 rounded-full hover:bg-muted transition-all outline-none">
                <UserAvatar
                  name={user.name}
                  email={user.email}
                  userId={user.id}
                  profileImage={user.profileImage}
                  className="h-8 w-8 border-2 border-primary/10"
                  fallbackClassName="bg-primary text-xs font-bold text-primary-foreground"
                />
                {update && <UpdateBadge />}
              </button>
            </DropdownMenuTrigger>
            <DropdownMenuContent
              align="end"
              className="w-64 rounded-2xl p-2 shadow-2xl border-primary/5"
            >
              <DropdownMenuLabel className="px-3 py-3 border-b mb-1">
                <div className="flex flex-col">
                  <span className="text-sm font-bold tracking-tight">
                    {user?.name}
                  </span>
                  <span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground/60">
                    {tRole(user.role)}
                  </span>
                </div>
              </DropdownMenuLabel>
              <DropdownMenuItem asChild>
                <Link
                  href={`/profile/${user?.id}`}
                  className="rounded-xl flex items-center py-2.5 cursor-pointer"
                >
                  <UserIcon className="mr-3 h-4 w-4 opacity-60" />
                  <span className="font-medium">{t('profile')}</span>
                </Link>
              </DropdownMenuItem>
              {user?.role === 'admin' && (
                <DropdownMenuItem asChild>
                  <Link
                    href="/admin"
                    className="rounded-xl flex items-center py-2.5 cursor-pointer"
                  >
                    <ShieldCheckIcon className="mr-3 h-4 w-4 opacity-60" />
                    <span className="font-medium">{t('administration')}</span>
                  </Link>
                </DropdownMenuItem>
              )}
              {update && (
                <>
                  <DropdownMenuSeparator className="my-1" />
                  <UpdateAvailableMenuItem update={update} />
                </>
              )}
              <DropdownMenuSeparator className="my-1" />
              {mounted && (
                <DropdownMenuItem
                  onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
                  className="rounded-xl flex items-center py-2.5 cursor-pointer"
                >
                  {theme === 'light' ? (
                    <MoonIcon className="mr-3 h-4 w-4 opacity-60" />
                  ) : (
                    <SunIcon className="mr-3 h-4 w-4 opacity-60" />
                  )}
                  <span className="font-medium">{tTheme('toggle')}</span>
                </DropdownMenuItem>
              )}
              <DropdownMenuSeparator className="my-1" />
              <DropdownMenuLabel className="px-2 py-1.5 text-xs text-muted-foreground">
                {tLocale('title')}
              </DropdownMenuLabel>
              <DropdownMenuRadioGroup value={locale} onValueChange={handleLocaleChange}>
                {locales.map((optionLocale) => (
                  <DropdownMenuRadioItem
                    key={optionLocale}
                    value={optionLocale}
                    disabled={isLocaleSubmitting}
                    className="rounded-xl cursor-pointer"
                  >
                    <LanguageIcon className="mr-3 h-4 w-4 opacity-60" />
                    {tLocale(`options.${optionLocale}`)}
                  </DropdownMenuRadioItem>
                ))}
              </DropdownMenuRadioGroup>
              <DropdownMenuSeparator className="my-1" />
              <DropdownMenuItem
                onClick={() => signout()}
                className="rounded-xl flex items-center py-2.5 text-destructive focus:text-destructive cursor-pointer group"
              >
                <ArrowRightStartOnRectangleIcon className="mr-3 h-4 w-4 opacity-60 group-hover:opacity-100 transition-opacity" />
                <span className="font-medium">{t('logout')}</span>
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        </div>
      </div>
    </>
  );
}