254 lines
9.4 KiB
TypeScript
254 lines
9.4 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import { usePathname, useRouter } from 'next/navigation';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { Dropdown, DropdownItem } from '@/components/ui/Dropdown';
|
|
import { Footer } from '@/components/ui/Footer';
|
|
import { useTranslation } from '@/hooks/useTranslation';
|
|
|
|
interface User {
|
|
id: string;
|
|
name: string | null;
|
|
email: string;
|
|
plan: string | null;
|
|
}
|
|
|
|
export default function AppLayout({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
const { t } = useTranslation();
|
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
const [user, setUser] = useState<User | null>(null);
|
|
|
|
// Fetch user data on mount
|
|
useEffect(() => {
|
|
const fetchUser = async () => {
|
|
try {
|
|
const response = await fetch('/api/user');
|
|
if (response.ok) {
|
|
const userData = await response.json();
|
|
setUser(userData);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching user:', error);
|
|
}
|
|
};
|
|
|
|
fetchUser();
|
|
}, []);
|
|
|
|
const handleSignOut = async () => {
|
|
// Track logout event before clearing data
|
|
try {
|
|
const { trackEvent, resetUser } = await import('@/components/PostHogProvider');
|
|
trackEvent('user_logout');
|
|
resetUser(); // Reset PostHog user session
|
|
} catch (error) {
|
|
console.error('PostHog tracking error:', error);
|
|
}
|
|
|
|
// Clear all cookies
|
|
document.cookie.split(";").forEach(c => {
|
|
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
|
|
});
|
|
// Clear localStorage
|
|
localStorage.clear();
|
|
// Redirect to home
|
|
router.push('/');
|
|
};
|
|
|
|
// Get user initials for avatar (e.g., "Timo Schmidt" -> "TS")
|
|
const getUserInitials = () => {
|
|
if (!user) return 'U';
|
|
|
|
if (user.name) {
|
|
const names = user.name.trim().split(' ');
|
|
if (names.length >= 2) {
|
|
return (names[0][0] + names[names.length - 1][0]).toUpperCase();
|
|
}
|
|
return user.name.substring(0, 2).toUpperCase();
|
|
}
|
|
|
|
// Fallback to email
|
|
return user.email.substring(0, 1).toUpperCase();
|
|
};
|
|
|
|
// Get display name (first name or full name)
|
|
const getDisplayName = () => {
|
|
if (!user) return 'User';
|
|
|
|
if (user.name) {
|
|
return user.name;
|
|
}
|
|
|
|
// Fallback to email without domain
|
|
return user.email.split('@')[0];
|
|
};
|
|
|
|
const navigation = [
|
|
{
|
|
name: t('nav.dashboard'),
|
|
href: '/dashboard',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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>
|
|
),
|
|
},
|
|
{
|
|
name: t('nav.create_qr'),
|
|
href: '/create',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
name: t('nav.bulk_creation'),
|
|
href: '/bulk-creation',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
name: t('nav.analytics'),
|
|
href: '/analytics',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
name: t('nav.pricing'),
|
|
href: '/pricing',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
name: t('nav.settings'),
|
|
href: '/settings',
|
|
icon: (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
{/* Mobile sidebar backdrop */}
|
|
{sidebarOpen && (
|
|
<div
|
|
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
|
|
onClick={() => setSidebarOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Sidebar */}
|
|
<aside
|
|
className={`fixed top-0 left-0 z-50 h-full w-64 bg-white border-r border-gray-200 transform transition-transform lg:translate-x-0 ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
|
<Link href="/" className="flex items-center space-x-2">
|
|
<img src="/logo.svg" alt="QR Master" className="w-8 h-8" />
|
|
<span className="text-xl font-bold text-gray-900">QR Master</span>
|
|
</Link>
|
|
<button
|
|
className="lg:hidden"
|
|
onClick={() => setSidebarOpen(false)}
|
|
>
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<nav className="p-4 space-y-1">
|
|
{navigation.map((item) => {
|
|
const isActive = pathname === item.href;
|
|
return (
|
|
<Link
|
|
key={item.name}
|
|
href={item.href}
|
|
className={`flex items-center space-x-3 px-3 py-2 rounded-lg transition-colors ${isActive
|
|
? 'bg-primary-50 text-primary-600'
|
|
: 'text-gray-700 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
{item.icon}
|
|
<span className="font-medium">{item.name}</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
</aside>
|
|
|
|
{/* Main content */}
|
|
<div className="lg:ml-64">
|
|
{/* Top bar */}
|
|
<header className="bg-white border-b border-gray-200">
|
|
<div className="flex items-center justify-between px-4 py-3">
|
|
<button
|
|
className="lg:hidden"
|
|
onClick={() => setSidebarOpen(true)}
|
|
>
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div className="flex items-center space-x-4 ml-auto">
|
|
{/* User Menu */}
|
|
<Dropdown
|
|
align="right"
|
|
trigger={
|
|
<button className="flex items-center space-x-2 text-gray-700 hover:text-gray-900">
|
|
<div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
|
|
<span className="text-sm font-medium text-primary-600">
|
|
{getUserInitials()}
|
|
</span>
|
|
</div>
|
|
<span className="hidden md:block font-medium">
|
|
{getDisplayName()}
|
|
</span>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
}
|
|
>
|
|
<DropdownItem onClick={handleSignOut}>
|
|
Sign Out
|
|
</DropdownItem>
|
|
</Dropdown>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Page content */}
|
|
<main className="p-6">
|
|
{children}
|
|
</main>
|
|
|
|
{/* Footer */}
|
|
<Footer variant="dashboard" />
|
|
</div>
|
|
</div>
|
|
);
|
|
} |