|
@@ -0,0 +1,349 @@
|
|
|
+// src/app/dashboard/layout.tsx
|
|
|
+'use client'
|
|
|
+
|
|
|
+import Onboarding from '@/components/profiles/addProfile';
|
|
|
+import InviteCollaborators from '@/components/shared/inviteCollaborators';
|
|
|
+import { useEffect, useState, ReactNode, useCallback } from 'react';
|
|
|
+import { useRouter } from 'next/navigation';
|
|
|
+import { Plus } from 'lucide-react';
|
|
|
+import { userMe, getProfilesByWorkspace, type Profile } from '@/lib/api/core';
|
|
|
+import { Workspace } from '@/types/auth';
|
|
|
+
|
|
|
+import Link from 'next/link'
|
|
|
+import {
|
|
|
+ TrendingUp,
|
|
|
+ Palette,
|
|
|
+ Megaphone,
|
|
|
+ Sparkles,
|
|
|
+ ChevronDown,
|
|
|
+ PlusCircle,
|
|
|
+ Lightbulb,
|
|
|
+ Sun,
|
|
|
+ Moon,
|
|
|
+ Home,
|
|
|
+ LogOut,
|
|
|
+ User
|
|
|
+} from 'lucide-react'
|
|
|
+import { useTheme } from '@/contexts/themecontext'
|
|
|
+
|
|
|
+interface DashboardLayoutProps {
|
|
|
+ children: ReactNode
|
|
|
+}
|
|
|
+
|
|
|
+// Helper function to get workspace initial
|
|
|
+const getInitial = (name: string | undefined) => {
|
|
|
+ if (!name) return '?';
|
|
|
+ return name.charAt(0).toUpperCase();
|
|
|
+};
|
|
|
+
|
|
|
+// Workspace selector component
|
|
|
+const WorkspaceSelector = ({ workspaces, onWorkspaceChange }: { workspaces: Workspace[], onWorkspaceChange: (workspace: Workspace) => void }) => {
|
|
|
+ const [selectedWorkspace, setSelectedWorkspace] = useState(workspaces[0]);
|
|
|
+ const [isOpen, setIsOpen] = useState(false);
|
|
|
+ const initial = getInitial(selectedWorkspace?.name);
|
|
|
+ const router = useRouter();
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (selectedWorkspace) {
|
|
|
+ localStorage.setItem('selectedWorkspaceId', selectedWorkspace.id.toString());
|
|
|
+ onWorkspaceChange(selectedWorkspace);
|
|
|
+ }
|
|
|
+ }, [selectedWorkspace, onWorkspaceChange]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="p-4 border-b border-gray-200 dark:border-gray-800 relative">
|
|
|
+ <button
|
|
|
+ onClick={() => setIsOpen(!isOpen)}
|
|
|
+ className="w-full flex items-center justify-between px-2 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md"
|
|
|
+ >
|
|
|
+ <div className="flex items-center">
|
|
|
+ <div className="w-8 h-8 rounded-md bg-blue-500 text-white flex items-center justify-center mr-3">
|
|
|
+ {initial}
|
|
|
+ </div>
|
|
|
+ <span>{selectedWorkspace?.name || 'Workspace'}</span>
|
|
|
+ </div>
|
|
|
+ <ChevronDown className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
|
|
+ </button>
|
|
|
+
|
|
|
+ {isOpen && (
|
|
|
+ <>
|
|
|
+ <div
|
|
|
+ className="fixed inset-0 z-10"
|
|
|
+ onClick={() => setIsOpen(false)}
|
|
|
+ />
|
|
|
+ <div className="absolute left-0 right-0 mt-2 mx-4 py-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg z-20 border border-gray-200 dark:border-gray-700">
|
|
|
+ {workspaces.map((workspace) => (
|
|
|
+ <button
|
|
|
+ key={workspace.id}
|
|
|
+ onClick={() => {
|
|
|
+ setSelectedWorkspace(workspace);
|
|
|
+ setIsOpen(false);
|
|
|
+ }}
|
|
|
+ className={`w-full px-3 py-2 flex items-center space-x-2 hover:bg-gray-100 dark:hover:bg-gray-700 ${
|
|
|
+ selectedWorkspace?.id === workspace.id ? 'bg-gray-50 dark:bg-gray-700' : ''
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ <div className="w-6 h-6 rounded bg-blue-500 text-white flex items-center justify-center">
|
|
|
+ {getInitial(workspace.name)}
|
|
|
+ </div>
|
|
|
+ <span className="text-gray-900 dark:text-white">{workspace.name}</span>
|
|
|
+ </button>
|
|
|
+ ))}
|
|
|
+ <div className="border-t border-gray-200 dark:border-gray-700 mt-2 pt-2">
|
|
|
+ <button
|
|
|
+ onClick={() => router.push('/setup')}
|
|
|
+ className="w-full px-3 py-2 flex items-center space-x-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
|
+ >
|
|
|
+ <PlusCircle className="w-4 h-4" />
|
|
|
+ <span>Create New Workspace</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default function DashboardLayout({ children }: DashboardLayoutProps) {
|
|
|
+ const { theme, toggleTheme } = useTheme()
|
|
|
+ const [showOnboarding, setShowOnboarding] = useState(false);
|
|
|
+ const [showInviteModal, setShowInviteModal] = useState(false);
|
|
|
+ const [showUserDropdown, setShowUserDropdown] = useState(false);
|
|
|
+ const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
|
|
|
+ const [profiles, setProfiles] = useState<Profile[]>([]);
|
|
|
+ const [isLoading, setIsLoading] = useState(true);
|
|
|
+ const router = useRouter();
|
|
|
+
|
|
|
+ const handleOnboardingComplete = () => {
|
|
|
+ setShowOnboarding(false);
|
|
|
+ // Refresh profiles after creating a new one
|
|
|
+ const workspaceId = localStorage.getItem('selectedWorkspaceId');
|
|
|
+ if (workspaceId) {
|
|
|
+ getProfilesByWorkspace(workspaceId).then(setProfiles);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleLogout = () => {
|
|
|
+ localStorage.clear();
|
|
|
+ router.push('/login');
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleWorkspaceChange = useCallback(async (workspace: Workspace) => {
|
|
|
+ try {
|
|
|
+ const profiles = await getProfilesByWorkspace(workspace.id.toString());
|
|
|
+ setProfiles(profiles);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Failed to fetch profiles', error);
|
|
|
+ setProfiles([]);
|
|
|
+ }
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const fetchWorkspaces = async () => {
|
|
|
+ try {
|
|
|
+ const response = await userMe();
|
|
|
+ setWorkspaces(response.workspaces);
|
|
|
+
|
|
|
+ // Fetch profiles for the first workspace
|
|
|
+ if (response.workspaces.length > 0) {
|
|
|
+ await handleWorkspaceChange(response.workspaces[0]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.error('Failed to fetch workspaces', error);
|
|
|
+ setWorkspaces([]);
|
|
|
+ }
|
|
|
+ finally {
|
|
|
+ setIsLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ fetchWorkspaces();
|
|
|
+ }, [handleWorkspaceChange]);
|
|
|
+
|
|
|
+ if (isLoading) {
|
|
|
+ return (
|
|
|
+ <div className="flex justify-center items-center min-h-screen bg-gray-900">
|
|
|
+ <div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (workspaces.length === 0) {
|
|
|
+ return (
|
|
|
+ <div className={`flex flex-col items-center justify-center min-h-screen ${theme === 'dark' ? 'bg-gray-900' : 'bg-gray-50'} px-4 relative`}>
|
|
|
+ <button
|
|
|
+ onClick={toggleTheme}
|
|
|
+ className={`absolute top-4 right-4 p-2 rounded-lg ${
|
|
|
+ theme === 'dark'
|
|
|
+ ? 'bg-gray-800 hover:bg-gray-700'
|
|
|
+ : 'bg-white hover:bg-gray-100'
|
|
|
+ } transition-colors shadow-lg`}
|
|
|
+ aria-label="Toggle theme"
|
|
|
+ >
|
|
|
+ {theme === 'dark' ? (
|
|
|
+ <Sun className="w-5 h-5 text-yellow-500" />
|
|
|
+ ) : (
|
|
|
+ <Moon className="w-5 h-5 text-gray-600" />
|
|
|
+ )}
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div className="text-center max-w-md">
|
|
|
+ <h2 className={`text-2xl font-bold ${theme === 'dark' ? 'text-white' : 'text-gray-900'} mb-3`}>
|
|
|
+ Bienvenue sur votre tableau de bord
|
|
|
+ </h2>
|
|
|
+ <p className={`${theme === 'dark' ? 'text-gray-400' : 'text-gray-600'} mb-8`}>
|
|
|
+ Vous n'avez pas encore d'espaces de travail. Créez votre premier espace de travail pour commencer.
|
|
|
+ </p>
|
|
|
+ <button
|
|
|
+ onClick={() => router.push('/setup')}
|
|
|
+ className={`inline-flex items-center justify-center rounded-md ${
|
|
|
+ theme === 'dark' ? 'bg-blue-600 hover:bg-blue-500' : 'bg-blue-500 hover:bg-blue-400'
|
|
|
+ } px-4 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500`}
|
|
|
+ >
|
|
|
+ <Plus className="w-5 h-5 mr-2" />
|
|
|
+ Créer un espace de travail
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="flex h-screen bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-300">
|
|
|
+ {showOnboarding && <Onboarding onComplete={handleOnboardingComplete} />}
|
|
|
+ <aside className="w-64 border-r border-gray-200 dark:border-gray-800">
|
|
|
+ <div className="h-full flex flex-col">
|
|
|
+ <WorkspaceSelector workspaces={workspaces} onWorkspaceChange={handleWorkspaceChange} />
|
|
|
+
|
|
|
+ <nav className="flex-1 p-4 space-y-1">
|
|
|
+ <Link href="/dashboard"
|
|
|
+ className="flex items-center px-2 py-2 rounded text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800">
|
|
|
+ <Home className="w-4 h-4 mr-3" />
|
|
|
+ Accueil
|
|
|
+ </Link>
|
|
|
+ <Link href="/dashboard/trends"
|
|
|
+ className="flex items-center px-2 py-2 rounded text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800">
|
|
|
+ <TrendingUp className="w-4 h-4 mr-3" />
|
|
|
+ Tendances
|
|
|
+ </Link>
|
|
|
+ <Link href="/dashboard/suggestions"
|
|
|
+ className="flex items-center px-2 py-2 rounded text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800">
|
|
|
+ <Lightbulb className="w-4 h-4 mr-3" />
|
|
|
+ Suggestions
|
|
|
+ </Link>
|
|
|
+ <Link href="/dashboard/design"
|
|
|
+ className="flex items-center px-2 py-2 rounded text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800">
|
|
|
+ <Palette className="w-4 h-4 mr-3" />
|
|
|
+ Design
|
|
|
+ </Link>
|
|
|
+ <Link href="/dashboard/ads"
|
|
|
+ className="flex items-center px-2 py-2 rounded text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800">
|
|
|
+ <Megaphone className="w-4 h-4 mr-3" />
|
|
|
+ Publicités
|
|
|
+ </Link>
|
|
|
+ <Link href="/dashboard/showcase"
|
|
|
+ className="flex items-center px-2 py-2 rounded text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800">
|
|
|
+ <Sparkles className="w-4 h-4 mr-3" />
|
|
|
+ Vitrine
|
|
|
+ </Link>
|
|
|
+
|
|
|
+ {/* Profiles Section */}
|
|
|
+ <div className="mt-8">
|
|
|
+ <div className="px-2 flex items-center justify-between mb-2">
|
|
|
+ <h2 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
|
+ Profils
|
|
|
+ </h2>
|
|
|
+ <button
|
|
|
+ onClick={() => setShowOnboarding(true)}
|
|
|
+ className="p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md"
|
|
|
+ >
|
|
|
+ <PlusCircle className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div className="mt-2 space-y-1">
|
|
|
+ {profiles.length > 0 ? (
|
|
|
+ profiles.map((profile) => (
|
|
|
+ <Link
|
|
|
+ key={profile.id}
|
|
|
+ href={`/dashboard/profile/${profile.name.toLowerCase()}`}
|
|
|
+ className="flex items-center px-2 py-2 rounded text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
|
+ >
|
|
|
+ <User className="w-4 h-4 mr-3 text-blue-500" />
|
|
|
+ @{profile.name}
|
|
|
+ </Link>
|
|
|
+ ))
|
|
|
+ ) : (
|
|
|
+ <div className="px-2 py-2 text-sm text-gray-500 dark:text-gray-400">
|
|
|
+ Aucun profil
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </nav>
|
|
|
+ </div>
|
|
|
+ </aside>
|
|
|
+
|
|
|
+ <div className="flex-1 flex flex-col">
|
|
|
+ <header className="h-16 border-b border-gray-200 dark:border-gray-800">
|
|
|
+ <div className="h-full px-4 flex items-center justify-between">
|
|
|
+ <div className="flex items-center space-x-4 ml-auto">
|
|
|
+ <button
|
|
|
+ onClick={() => setShowInviteModal(true)}
|
|
|
+ className="text-sm text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">
|
|
|
+ Inviter des collaborateurs
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ onClick={toggleTheme}
|
|
|
+ className="p-2 rounded-md bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
|
|
+ aria-label="Toggle theme"
|
|
|
+ >
|
|
|
+ {theme === 'dark' ? (
|
|
|
+ <Sun className="w-4 h-4" />
|
|
|
+ ) : (
|
|
|
+ <Moon className="w-4 h-4" />
|
|
|
+ )}
|
|
|
+ </button>
|
|
|
+ <div className="relative">
|
|
|
+ <button
|
|
|
+ onClick={() => setShowUserDropdown(!showUserDropdown)}
|
|
|
+ className="w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded-full flex items-center justify-center text-gray-900 dark:text-white hover:ring-2 hover:ring-gray-300 dark:hover:ring-gray-600 transition-all"
|
|
|
+ >
|
|
|
+ U
|
|
|
+ </button>
|
|
|
+
|
|
|
+ {showUserDropdown && (
|
|
|
+ <>
|
|
|
+ <div
|
|
|
+ className="fixed inset-0 z-10"
|
|
|
+ onClick={() => setShowUserDropdown(false)}
|
|
|
+ />
|
|
|
+
|
|
|
+ <div className={`absolute right-0 mt-2 w-48 py-2 ${theme === 'dark' ? 'bg-gray-800' : 'bg-white'} rounded-lg shadow-lg border ${theme === 'dark' ? 'border-gray-700' : 'border-gray-200'} z-20`}>
|
|
|
+ <button
|
|
|
+ onClick={handleLogout}
|
|
|
+ className="w-full px-4 py-2 text-left flex items-center space-x-2 text-red-500 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
|
+ >
|
|
|
+ <LogOut className="w-4 h-4" />
|
|
|
+ <span>Se déconnecter</span>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <main className="flex-1 overflow-y-auto">
|
|
|
+ {children}
|
|
|
+ </main>
|
|
|
+
|
|
|
+ {showInviteModal && (
|
|
|
+ <InviteCollaborators onClose={() => setShowInviteModal(false)} />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|