Files
WooNooW/customer-spa/src/pages/Account/components/AccountLayout.tsx

222 lines
7.7 KiB
TypeScript

import React, { ReactNode, useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { LayoutDashboard, ShoppingBag, Download, MapPin, Heart, User, LogOut, Key } from 'lucide-react';
import { useModules } from '@/hooks/useModules';
import { api } from '@/lib/api/client';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
interface AccountLayoutProps {
children: ReactNode;
}
export function AccountLayout({ children }: AccountLayoutProps) {
const location = useLocation();
const user = (window as any).woonoowCustomer?.user;
const { isEnabled } = useModules();
const wishlistEnabled = (window as any).woonoowCustomer?.settings?.wishlist_enabled !== false;
const [isLoggingOut, setIsLoggingOut] = useState(false);
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
// Fetch avatar settings
useEffect(() => {
const fetchAvatar = async () => {
try {
const data = await api.get<{ current_avatar: string | null; gravatar_url: string }>('/account/avatar-settings');
setAvatarUrl(data.current_avatar || data.gravatar_url);
} catch (error) {
console.error('Failed to fetch avatar:', error);
}
};
fetchAvatar();
// Listen for avatar updates
const handleAvatarUpdate = (e: CustomEvent) => {
setAvatarUrl(e.detail?.avatar_url || null);
};
window.addEventListener('woonoow:avatar-updated' as any, handleAvatarUpdate);
return () => window.removeEventListener('woonoow:avatar-updated' as any, handleAvatarUpdate);
}, []);
const allMenuItems = [
{ id: 'dashboard', label: 'Dashboard', path: '/my-account', icon: LayoutDashboard },
{ id: 'orders', label: 'Orders', path: '/my-account/orders', icon: ShoppingBag },
{ id: 'downloads', label: 'Downloads', path: '/my-account/downloads', icon: Download },
{ id: 'addresses', label: 'Addresses', path: '/my-account/addresses', icon: MapPin },
{ id: 'wishlist', label: 'Wishlist', path: '/my-account/wishlist', icon: Heart },
{ id: 'licenses', label: 'Licenses', path: '/my-account/licenses', icon: Key },
{ id: 'account-details', label: 'Account Details', path: '/my-account/account-details', icon: User },
];
// Filter out wishlist if module disabled or settings disabled, licenses if licensing disabled
const menuItems = allMenuItems.filter(item => {
if (item.id === 'wishlist') return isEnabled('wishlist') && wishlistEnabled;
if (item.id === 'licenses') return isEnabled('licensing');
return true;
});
const handleLogout = async () => {
setIsLoggingOut(true);
try {
const apiRoot = (window as any).woonoowCustomer?.apiRoot || '/wp-json/woonoow/v1';
const nonce = (window as any).woonoowCustomer?.nonce || '';
await fetch(`${apiRoot}/auth/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': nonce,
},
credentials: 'include',
});
// Full page reload to clear cookies and refresh state
const basePath = (window as any).woonoowCustomer?.basePath || '/store';
window.location.href = window.location.origin + basePath + '/';
} catch (error) {
// Even on error, try to redirect and let server handle session
const basePath = (window as any).woonoowCustomer?.basePath || '/store';
window.location.href = window.location.origin + basePath + '/';
}
};
const isActive = (path: string) => {
if (path === '/my-account') {
return location.pathname === '/my-account';
}
return location.pathname.startsWith(path);
};
// Logout Button with AlertDialog
const LogoutButton = () => (
<AlertDialog>
<AlertDialogTrigger asChild>
<button
disabled={isLoggingOut}
className="w-full font-[inherit] flex items-center gap-3 px-4 py-2.5 rounded-lg text-gray-700 hover:bg-gray-100 transition-colors disabled:opacity-50"
>
<LogOut className="w-5 h-5" />
<span className="font-medium">{isLoggingOut ? 'Logging out...' : 'Logout'}</span>
</button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Log out?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to log out of your account? You'll need to sign in again to access your orders and account details.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleLogout}
className="bg-red-600 hover:bg-red-700 text-white"
>
Log Out
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
// Sidebar Navigation
const SidebarNav = () => (
<aside className="bg-white rounded-lg border p-4">
<div className="mb-6">
<div className="flex items-center gap-3 pb-4 border-b">
{avatarUrl ? (
<img
src={avatarUrl}
alt={user?.display_name || 'User'}
className="w-12 h-12 rounded-full object-cover"
/>
) : (
<div className="w-12 h-12 rounded-full bg-gray-200 flex items-center justify-center">
<User className="w-6 h-6 text-gray-600" />
</div>
)}
<div>
<p className="font-semibold">{user?.display_name || 'User'}</p>
<p className="text-sm text-gray-500">{user?.email}</p>
</div>
</div>
</div>
<nav className="space-y-1">
{menuItems.map((item) => {
const Icon = item.icon;
return (
<Link
key={item.id}
to={item.path}
className={`flex items-center gap-3 px-4 py-2.5 rounded-lg transition-colors ${isActive(item.path)
? 'bg-primary text-primary-foreground'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<Icon className="w-5 h-5" />
<span className="font-medium">{item.label}</span>
</Link>
);
})}
<LogoutButton />
</nav>
</aside>
);
// Tab Navigation (Mobile)
const TabNav = () => (
<div className="bg-white rounded-lg border mb-6 lg:hidden">
<nav className="flex overflow-x-auto">
{menuItems.map((item) => {
const Icon = item.icon;
return (
<Link
key={item.id}
to={item.path}
className={`flex items-center gap-2 px-6 py-4 border-b-2 transition-colors whitespace-nowrap text-sm ${isActive(item.path)
? 'border-primary text-primary font-medium'
: 'border-transparent text-gray-600 hover:text-gray-900'
}`}
>
<Icon className="w-5 h-5" />
<span>{item.label}</span>
</Link>
);
})}
</nav>
</div>
);
// Responsive layout: Tabs on mobile, Sidebar on desktop
return (
<div className="py-8">
{/* Mobile: Tab Navigation */}
<TabNav />
{/* Desktop: Sidebar + Content */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div className="hidden lg:block lg:col-span-1">
<SidebarNav />
</div>
<div className="lg:col-span-3">
<div className="bg-white rounded-lg border p-6">
{children}
</div>
</div>
</div>
</div>
);
}