QR-master/src/app/(app)/layout.tsx

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>
);
}