224 lines
7.8 KiB
TypeScript
224 lines
7.8 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, Repeat } 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: 'subscriptions', label: 'Subscriptions', path: '/my-account/subscriptions', icon: Repeat },
|
|
{ id: 'licenses', label: 'Licenses', path: '/my-account/licenses', icon: Key },
|
|
{ id: 'downloads', label: 'Downloads', path: '/my-account/downloads', icon: Download },
|
|
{ id: 'addresses', label: 'Addresses', path: '/my-account/addresses', icon: MapPin },
|
|
{ id: 'account-details', label: 'Account Details', path: '/my-account/account-details', icon: User },
|
|
{ id: 'wishlist', label: 'Wishlist', path: '/my-account/wishlist', icon: Heart },
|
|
];
|
|
|
|
// 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');
|
|
if (item.id === 'subscriptions') return isEnabled('subscription');
|
|
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>
|
|
);
|
|
}
|
|
|