readysite / website / frontend / layouts / AdminLayout.jsx
14.8 KB
AdminLayout.jsx
import * as React from 'react';
import { useState, useEffect, useCallback, createContext, useContext } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { ChatPanel } from '../components/ChatPanel.jsx';

const CHAT_OPEN_KEY = 'admin_chat_open';

// Chat context for sharing chat state across components
const ChatContext = createContext(null);
export const useChat = () => useContext(ChatContext);

export function AdminLayout({ user, tourCompleted, children }) {
  const location = useLocation();
  const navigate = useNavigate();
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [chatOpen, setChatOpen] = useState(() => {
    const stored = localStorage.getItem(CHAT_OPEN_KEY);
    return stored === 'true';
  });

  const isActive = (path) => {
    if (path === '/') return location.pathname === '/';
    return location.pathname.startsWith(path);
  };

  const toggleChat = useCallback(() => {
    setChatOpen(prev => {
      const newValue = !prev;
      localStorage.setItem(CHAT_OPEN_KEY, String(newValue));
      return newValue;
    });
  }, []);

  // Keyboard shortcut: Cmd/Ctrl+K to toggle chat
  useEffect(() => {
    const handleKeyDown = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault();
        toggleChat();
      }
    };
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [toggleChat]);

  // Handle navigation from chat (AI tool calls)
  const handleChatNavigate = useCallback((url) => {
    // Convert absolute URLs to relative for React Router
    if (url.startsWith('/admin/')) {
      navigate(url.replace('/admin', ''));
    } else if (url.startsWith('/')) {
      // Other absolute URLs open in new tab (public pages, etc.)
      window.open(url, '_blank');
    }
  }, [navigate]);

  return (
    <div className="flex h-screen bg-base-200">
      {/* Mobile hamburger menu button */}
      <button
        className="fixed top-2 left-2 z-50 btn btn-ghost btn-sm btn-square lg:hidden"
        onClick={() => setSidebarOpen(!sidebarOpen)}
      >
        <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16" />
        </svg>
      </button>

      {/* Sidebar overlay for mobile */}
      {sidebarOpen && (
        <div className="fixed inset-0 bg-black/50 z-30 lg:hidden" onClick={() => setSidebarOpen(false)} />
      )}

      {/* Sidebar */}
      <aside className={`w-16 flex-shrink-0 bg-gradient-to-b from-base-300/80 to-base-300/40 border-r border-base-300/80 flex flex-col fixed lg:relative inset-y-0 left-0 z-40 transition-transform duration-200 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}`}>
        {/* Logo */}
        <div className="p-3 flex justify-center">
          <Link to="/" className="w-10 h-10 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center text-white font-bold text-lg shadow-lg hover:shadow-xl transition-shadow">
            R
          </Link>
        </div>

        {/* Main Navigation */}
        <nav className="flex-1 flex flex-col items-center gap-1 py-4">
          {/* Dashboard */}
          <NavItem path="/" icon="home" label="Dashboard" active={isActive('/')} onClick={() => setSidebarOpen(false)} />

          <div className="w-6 h-px bg-base-300 my-2" />

          {/* Users */}
          <NavItem path="/users" icon="users" label="Users" active={isActive('/users')} onClick={() => setSidebarOpen(false)} />

          {/* Pages */}
          <NavItem path="/pages" icon="pages" label="Pages" active={isActive('/pages')} onClick={() => setSidebarOpen(false)} />

          {/* Collections */}
          <NavItem path="/collections" icon="collections" label="Collections" active={isActive('/collections')} onClick={() => setSidebarOpen(false)} />

          {/* Files */}
          <NavItem path="/files" icon="files" label="Files" active={isActive('/files')} onClick={() => setSidebarOpen(false)} />
        </nav>

        {/* Bottom Actions */}
        <div className="flex flex-col items-center gap-1 py-4 border-t border-base-300/80">

          {/* Audit Logs */}
          <NavItem path="/audit" icon="audit" label="Audit Logs" active={isActive('/audit')} onClick={() => setSidebarOpen(false)} />

          {/* Settings */}
          <NavItem path="/settings" icon="settings" label="Settings" active={isActive('/settings')} onClick={() => setSidebarOpen(false)} />

          {/* View Site */}
          <div className="tooltip tooltip-right" data-tip="View Site">
            <a href="/" target="_blank" className="btn btn-ghost btn-square btn-sm hover:bg-base-200/80">
              <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
              </svg>
            </a>
          </div>

          {/* Sign Out */}
          <div className="tooltip tooltip-right" data-tip="Sign Out">
            <button
              onClick={() => {
                if (confirm('Sign out?')) {
                  fetch('/auth/signout', { method: 'POST' }).then(() => {
                    window.location.href = '/signin';
                  });
                }
              }}
              className="btn btn-ghost btn-square btn-sm text-base-content/50 hover:text-error hover:bg-error/10"
            >
              <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" 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>
      </aside>

      {/* Main Content Area + Chat Panel */}
      <div className="flex flex-1 min-w-0">
        {/* Page Content */}
        <div className={`flex-1 flex flex-col min-w-0 bg-base-100 transition-[margin] duration-200 ${chatOpen ? 'mr-[320px] lg:mr-[480px]' : ''}`}>
          <ChatContext.Provider value={{ chatOpen, toggleChat }}>
            {children}
          </ChatContext.Provider>
        </div>

        {/* Chat Panel Overlay for Mobile */}
        {chatOpen && (
          <div
            className="fixed inset-0 bg-black/50 z-40 lg:hidden"
            onClick={toggleChat}
          />
        )}

        {/* Chat Panel */}
        <div className={`fixed inset-y-0 right-0 z-50 transition-transform duration-200 ${chatOpen ? 'translate-x-0' : 'translate-x-full'}`}>
          <ChatPanel
            open={chatOpen}
            onToggle={toggleChat}
            onNavigate={handleChatNavigate}
          />
        </div>
      </div>

      {/* Welcome Tour */}
      <WelcomeTour tourCompleted={tourCompleted} onOpenChat={toggleChat} />
    </div>
  );
}

function WelcomeTour({ tourCompleted, onOpenChat }) {
  const [visible, setVisible] = useState(!tourCompleted);
  const [step, setStep] = useState(0);

  if (!visible) return null;

  const steps = [
    {
      title: 'Welcome to ReadySite!',
      description: "Let's take a quick look around. ReadySite is your AI-powered website builder — create pages, manage content, and launch your site from right here.",
    },
    {
      title: 'Sidebar Navigation',
      description: 'The sidebar on the left has your main tools: Pages, Collections, Files, Users, Audit Logs, and Settings. Click any icon to navigate.',
    },
    {
      title: 'AI Assistant',
      description: 'Press Cmd+K (or Ctrl+K) to open the AI assistant. It can create pages, set up collections, manage content, and build your entire site through conversation.',
    },
    {
      title: "You're all set!",
      description: 'Try asking the AI to create your first page, or explore the sidebar to see what\'s here. Need help? Check out the docs at readysite.org.',
    },
  ];

  const current = steps[step];
  const isLast = step === steps.length - 1;

  const complete = () => {
    setVisible(false);
    fetch('/api/admin/tour/complete', { method: 'POST' });
  };

  return (
    <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60">
      <div className="bg-base-200 border border-base-300 rounded-2xl shadow-2xl w-full max-w-md mx-4 p-6">
        {/* Step indicator */}
        <div className="flex items-center justify-between mb-4">
          <div className="flex gap-1.5">
            {steps.map((_, i) => (
              <div key={i} className={`w-2 h-2 rounded-full transition-colors ${i === step ? 'bg-primary' : 'bg-base-content/20'}`} />
            ))}
          </div>
          <button onClick={complete} className="text-xs text-base-content/40 hover:text-base-content/70 transition-colors">
            Skip tour
          </button>
        </div>

        {/* Content */}
        <div className="mb-6">
          <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-primary/20 to-secondary/20 flex items-center justify-center mb-4">
            {step === 0 && <span className="text-2xl">&#128075;</span>}
            {step === 1 && (
              <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M4 6h16M4 12h16M4 18h16" />
              </svg>
            )}
            {step === 2 && (
              <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" 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>
            )}
            {step === 3 && (
              <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M13 10V3L4 14h7v7l9-11h-7z" />
              </svg>
            )}
          </div>
          <h3 className="text-lg font-semibold mb-2">{current.title}</h3>
          <p className="text-base-content/60 text-sm leading-relaxed">{current.description}</p>
        </div>

        {/* Actions */}
        <div className="flex items-center justify-between">
          <button
            onClick={() => setStep(s => s - 1)}
            className={`btn btn-ghost btn-sm ${step === 0 ? 'invisible' : ''}`}
          >
            Back
          </button>
          <button
            onClick={() => {
              if (isLast) {
                complete();
              } else {
                setStep(s => s + 1);
              }
            }}
            className="btn btn-primary btn-sm"
          >
            {isLast ? 'Get Started' : 'Next'}
          </button>
        </div>
      </div>
    </div>
  );
}

function NavItem({ path, icon, label, active, onClick }) {
  return (
    <div className="tooltip tooltip-right" data-tip={label}>
      <Link
        to={path}
        onClick={onClick}
        className={`btn btn-ghost btn-square btn-sm ${active ? 'bg-primary/20 text-primary' : 'hover:bg-base-200/80'}`}
      >
        <NavIcon name={icon} />
      </Link>
    </div>
  );
}

function NavIcon({ name }) {
  const icons = {
    home: <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>,
    pages: <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>,
    files: <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>,
    collections: <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" /></svg>,
    partials: <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" /></svg>,
    users: <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>,
    audit: <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" 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 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" /></svg>,
    settings: <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" 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.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" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>,
  };
  return icons[name] || null;
}
← Back