703 lines
27 KiB
TypeScript
703 lines
27 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { useAuth } from "@/hooks/useAuth";
|
|
import ImageGallery from "@/components/ImageGallery";
|
|
import { ImageFile } from "@/types";
|
|
import { fetchUserPictures, deletePicture } from "@/modules/posts/client-pictures";
|
|
import { toast } from "sonner";
|
|
import { Navigate, useNavigate } from "react-router-dom";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
import { User, Images, Save, Camera, Upload, Check, Key, Globe, Hash, MapPin, Building2, ShoppingBag } from "lucide-react";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { T, translate, getCurrentLang, supportedLanguages, setLanguage } from "@/i18n";
|
|
import { uploadImage } from '@/lib/uploadUtils';
|
|
import { getUserSecrets, updateUserSecrets, getUserVariables, updateUserVariables, fetchProfileAPI, updateProfileAPI, updateUserEmail } from '@/modules/user/client-user';
|
|
import { VariablesEditor } from '@/components/variables/VariablesEditor';
|
|
import { ShippingAddressManager } from '@/components/ShippingAddressManager';
|
|
import { VendorProfileManager } from '@/components/VendorProfileManager';
|
|
import {
|
|
Sidebar,
|
|
SidebarContent,
|
|
SidebarGroup,
|
|
SidebarGroupContent,
|
|
SidebarGroupLabel,
|
|
SidebarMenu,
|
|
SidebarMenuButton,
|
|
SidebarMenuItem,
|
|
SidebarProvider,
|
|
useSidebar
|
|
} from "@/components/ui/sidebar";
|
|
import { AnalyticsDashboard } from "./analytics";
|
|
|
|
const LazyPurchasesList = React.lazy(() =>
|
|
import("@polymech/ecommerce").then(m => ({ default: m.PurchasesList }))
|
|
);
|
|
|
|
type ActiveSection = 'general' | 'api-keys' | 'variables' | 'addresses' | 'vendor' | 'gallery' | 'purchases';
|
|
|
|
const Profile = () => {
|
|
const { user, loading, resetPassword } = useAuth();
|
|
const navigate = useNavigate();
|
|
const [images, setImages] = useState<ImageFile[]>([]);
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const [fetchingImages, setFetchingImages] = useState(true);
|
|
const [activeSection, setActiveSection] = useState<ActiveSection>('general');
|
|
|
|
// Profile data state
|
|
const [profile, setProfile] = useState({
|
|
username: '',
|
|
display_name: '',
|
|
bio: '',
|
|
avatar_url: '',
|
|
settings: {}
|
|
});
|
|
const [secrets, setSecrets] = useState<Record<string, string>>({});
|
|
const [email, setEmail] = useState('');
|
|
const [updatingProfile, setUpdatingProfile] = useState(false);
|
|
const [avatarDialogOpen, setAvatarDialogOpen] = useState(false);
|
|
const [uploadingAvatar, setUploadingAvatar] = useState(false);
|
|
const [selectedLanguage, setSelectedLanguage] = useState(getCurrentLang());
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
fetchUserImages();
|
|
fetchProfile();
|
|
setEmail(user.email || '');
|
|
}
|
|
}, [user]);
|
|
|
|
|
|
|
|
const fetchProfile = async () => {
|
|
try {
|
|
console.log('Fetching profile for user:', user?.id);
|
|
const result = await fetchProfileAPI(user!.id);
|
|
|
|
if (result?.profile) {
|
|
const data = result.profile;
|
|
setProfile({
|
|
username: data.username || '',
|
|
display_name: data.display_name || '',
|
|
bio: data.bio || '',
|
|
avatar_url: data.avatar_url || '',
|
|
settings: data.settings || {}
|
|
});
|
|
|
|
// Fetch secrets
|
|
const fetchedSecrets = await getUserSecrets(user!.id);
|
|
if (fetchedSecrets) {
|
|
setSecrets(fetchedSecrets);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching profile:', error);
|
|
toast.error(translate('Failed to load profile data'));
|
|
}
|
|
};
|
|
|
|
const handleProfileUpdate = async () => {
|
|
if (!user) return;
|
|
|
|
setUpdatingProfile(true);
|
|
try {
|
|
// Update profile via API
|
|
await updateProfileAPI({
|
|
username: profile.username || null,
|
|
display_name: profile.display_name || null,
|
|
bio: profile.bio || null,
|
|
avatar_url: profile.avatar_url || null,
|
|
settings: profile.settings || {}
|
|
});
|
|
|
|
// Update secrets
|
|
await updateUserSecrets(user.id, secrets);
|
|
|
|
// Update email if changed
|
|
if (email !== user.email && email.trim()) {
|
|
await updateUserEmail(email);
|
|
}
|
|
|
|
toast.success(translate('Profile updated successfully'));
|
|
} catch (error) {
|
|
console.error('Error updating profile:', error);
|
|
toast.error(translate('Failed to update profile'));
|
|
} finally {
|
|
setUpdatingProfile(false);
|
|
}
|
|
};
|
|
|
|
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = event.target.files?.[0];
|
|
if (!file || !user) return;
|
|
|
|
// Validate file type
|
|
if (!file.type.startsWith('image/')) {
|
|
toast.error(translate('Please select an image file'));
|
|
return;
|
|
}
|
|
|
|
// Validate file size (max 5MB)
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
toast.error(translate('Image must be less than 5MB'));
|
|
return;
|
|
}
|
|
|
|
setUploadingAvatar(true);
|
|
try {
|
|
// Upload to storage (direct or via proxy)
|
|
const { publicUrl } = await uploadImage(file, user.id);
|
|
|
|
// Update profile with new avatar URL
|
|
setProfile(prev => ({ ...prev, avatar_url: publicUrl }));
|
|
setAvatarDialogOpen(false);
|
|
toast.success(translate('Avatar updated successfully'));
|
|
} catch (error) {
|
|
console.error('Error uploading avatar:', error);
|
|
toast.error(translate('Failed to upload avatar'));
|
|
} finally {
|
|
setUploadingAvatar(false);
|
|
}
|
|
};
|
|
|
|
const handleSelectFromGallery = (imageUrl: string) => {
|
|
setProfile(prev => ({ ...prev, avatar_url: imageUrl }));
|
|
setAvatarDialogOpen(false);
|
|
toast.success(translate('Avatar updated successfully'));
|
|
};
|
|
|
|
const fetchUserImages = async () => {
|
|
try {
|
|
const pictures = await fetchUserPictures(user!.id);
|
|
// Filter client-side for is_selected (API returns all)
|
|
const selected = pictures.filter((p: any) => p.is_selected);
|
|
|
|
const imageFiles: ImageFile[] = selected.map((picture: any) => ({
|
|
path: picture.title,
|
|
src: picture.image_url,
|
|
isGenerated: false,
|
|
selected: false
|
|
}));
|
|
|
|
setImages(imageFiles);
|
|
} catch (error) {
|
|
console.error('Error fetching user images:', error);
|
|
toast.error(translate('Failed to load your images'));
|
|
} finally {
|
|
setFetchingImages(false);
|
|
}
|
|
};
|
|
|
|
const handleImageDelete = async (imagePath: string) => {
|
|
try {
|
|
// Find the image to delete
|
|
const imageToDelete = images.find(img => img.path === imagePath);
|
|
if (!imageToDelete) return;
|
|
|
|
// Find the picture ID from our fetched data by matching title
|
|
const allPics = await fetchUserPictures(user!.id);
|
|
const picture = allPics.find((p: any) => p.title === imagePath);
|
|
|
|
if (!picture) {
|
|
toast.error(translate('Failed to find image to delete'));
|
|
return;
|
|
}
|
|
|
|
await deletePicture(picture.id);
|
|
|
|
// Update local state
|
|
setImages(prevImages => prevImages.filter(img => img.path !== imagePath));
|
|
toast.success(translate('Image deleted successfully'));
|
|
} catch (error) {
|
|
console.error('Error deleting image:', error);
|
|
toast.error(translate('Failed to delete image'));
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
<div className="text-muted-foreground"><T>Loading...</T></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!user) {
|
|
return <Navigate to="/auth" replace />;
|
|
}
|
|
|
|
return (
|
|
<SidebarProvider>
|
|
<div className="min-h-screen flex w-full bg-background pt-14">
|
|
<ProfileSidebar activeSection={activeSection} onSectionChange={setActiveSection} />
|
|
|
|
<main className="flex-1 p-4 overflow-auto">
|
|
<div className="mx-auto">
|
|
<div className="mb-8">
|
|
<h1 className="text-4xl font-bold mb-2">
|
|
<span className="bg-gradient-primary bg-clip-text text-transparent">
|
|
<T>Profile Settings</T>
|
|
</span>
|
|
</h1>
|
|
<p className="text-muted-foreground text-lg">
|
|
<T>Manage your account settings and preferences</T>
|
|
</p>
|
|
</div>
|
|
|
|
{activeSection === 'general' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle><T>General Settings</T></CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Avatar Section */}
|
|
<div className="flex flex-col items-center space-y-4 pb-6 border-b">
|
|
<Avatar className="h-32 w-32">
|
|
<AvatarImage src={profile.avatar_url} alt={translate("Profile picture")} />
|
|
<AvatarFallback className="bg-gradient-primary text-white text-4xl">
|
|
<User className="h-16 w-16" />
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
|
|
<Dialog open={avatarDialogOpen} onOpenChange={setAvatarDialogOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="outline" size="sm">
|
|
<Camera className="h-4 w-4 mr-2" />
|
|
<T>Change Avatar</T>
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle><T>Choose Avatar</T></DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-6">
|
|
{/* Upload New Image */}
|
|
<div className="space-y-3">
|
|
<h4 className="font-medium"><T>Upload New Image</T></h4>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
asChild
|
|
disabled={uploadingAvatar}
|
|
>
|
|
<label htmlFor="avatar-upload" className="cursor-pointer">
|
|
<Upload className="h-4 w-4 mr-2" />
|
|
{uploadingAvatar ? translate('Uploading...') : translate('Choose File')}
|
|
<input
|
|
id="avatar-upload"
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleAvatarUpload}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
</Button>
|
|
<span className="text-sm text-muted-foreground">
|
|
<T>Max 5MB, JPG/PNG/WebP</T>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Select from Gallery */}
|
|
{images.length > 0 && (
|
|
<div className="space-y-3">
|
|
<h4 className="font-medium"><T>Select from Gallery</T></h4>
|
|
<div className="grid grid-cols-4 gap-3 max-h-64 overflow-y-auto">
|
|
{images.map((image) => (
|
|
<button
|
|
key={image.path}
|
|
onClick={() => handleSelectFromGallery(image.src)}
|
|
className="relative aspect-square rounded-lg overflow-hidden bg-muted hover:ring-2 hover:ring-primary transition-all group"
|
|
>
|
|
<img
|
|
src={image.src}
|
|
alt={image.path}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
|
<Check className="h-6 w-6 text-white" />
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
{/* Profile Form Fields */}
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email"><T>Email</T></Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder={translate("your.email@example.com")}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="username"><T>Username</T></Label>
|
|
<Input
|
|
id="username"
|
|
value={profile.username}
|
|
onChange={(e) => setProfile(prev => ({ ...prev, username: e.target.value }))}
|
|
placeholder={translate("Enter username")}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="display_name"><T>Display Name</T></Label>
|
|
<Input
|
|
id="display_name"
|
|
value={profile.display_name}
|
|
onChange={(e) => setProfile(prev => ({ ...prev, display_name: e.target.value }))}
|
|
placeholder={translate("Enter display name")}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="bio"><T>Bio</T></Label>
|
|
<Textarea
|
|
id="bio"
|
|
value={profile.bio}
|
|
onChange={(e) => setProfile(prev => ({ ...prev, bio: e.target.value }))}
|
|
placeholder={translate("Tell us about yourself...")}
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="language" className="flex items-center gap-2">
|
|
<Globe className="h-4 w-4" />
|
|
<T>Language</T>
|
|
</Label>
|
|
<Select
|
|
value={selectedLanguage}
|
|
onValueChange={(value) => setSelectedLanguage(value as any)}
|
|
>
|
|
<SelectTrigger id="language">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{supportedLanguages.map((lang) => (
|
|
<SelectItem key={lang.code} value={lang.code}>
|
|
{lang.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-sm text-muted-foreground">
|
|
<T>Your preferred language for the interface</T>
|
|
</p>
|
|
</div>
|
|
|
|
{/* Change Password */}
|
|
<div className="space-y-2 pt-4 border-t">
|
|
<Label className="flex items-center gap-2">
|
|
<Key className="h-4 w-4" />
|
|
<T>Password</T>
|
|
</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
<T>Send a password reset link to your email address</T>
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => resetPassword(user.email || email)}
|
|
>
|
|
<T>Change Password</T>
|
|
</Button>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={() => {
|
|
handleProfileUpdate();
|
|
// Apply language change if it changed
|
|
if (selectedLanguage !== getCurrentLang()) {
|
|
setLanguage(selectedLanguage as any);
|
|
}
|
|
}}
|
|
disabled={updatingProfile}
|
|
className="w-full"
|
|
>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
<T>{updatingProfile ? 'Saving...' : 'Save Changes'}</T>
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
|
|
|
|
{activeSection === 'api-keys' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle><T>API Keys</T></CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="google_api_key"><T>Google API Key</T></Label>
|
|
<Input
|
|
id="google_api_key"
|
|
type="password"
|
|
value={secrets.google_api_key || ''}
|
|
onChange={(e) => setSecrets(prev => ({ ...prev, google_api_key: e.target.value }))}
|
|
placeholder={translate("Enter your Google API key")}
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
<T>For Google services (stored securely)</T>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="openai_api_key"><T>OpenAI API Key</T></Label>
|
|
<Input
|
|
id="openai_api_key"
|
|
type="password"
|
|
value={secrets.openai_api_key || ''}
|
|
onChange={(e) => setSecrets(prev => ({ ...prev, openai_api_key: e.target.value }))}
|
|
placeholder={translate("Enter your OpenAI API key")}
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
<T>For AI image generation (stored securely)</T>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="replicate_api_key"><T>Replicate API Key</T></Label>
|
|
<Input
|
|
id="replicate_api_key"
|
|
type="password"
|
|
value={secrets.replicate_api_key || ''}
|
|
onChange={(e) => setSecrets(prev => ({ ...prev, replicate_api_key: e.target.value }))}
|
|
placeholder={translate("Enter your Replicate API key")}
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
<T>For Replicate AI models (stored securely)</T>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="bria_api_key"><T>Bria API Key</T></Label>
|
|
<Input
|
|
id="bria_api_key"
|
|
type="password"
|
|
value={secrets.bria_api_key || ''}
|
|
onChange={(e) => setSecrets(prev => ({ ...prev, bria_api_key: e.target.value }))}
|
|
placeholder={translate("Enter your Bria API key")}
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
<T>For Bria AI services (stored securely)</T>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="huggingface_api_key"><T>HuggingFace API Key</T></Label>
|
|
<Input
|
|
id="huggingface_api_key"
|
|
type="password"
|
|
value={secrets.huggingface_api_key || ''}
|
|
onChange={(e) => setSecrets(prev => ({ ...prev, huggingface_api_key: e.target.value }))}
|
|
placeholder={translate("Enter your HuggingFace API key")}
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
<T>For HuggingFace models (stored securely)</T>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="aimlapi_api_key"><T>AIMLAPI API Key</T></Label>
|
|
<Input
|
|
id="aimlapi_api_key"
|
|
type="password"
|
|
value={secrets.aimlapi_api_key || ''}
|
|
onChange={(e) => setSecrets(prev => ({ ...prev, aimlapi_api_key: e.target.value }))}
|
|
placeholder={translate("Enter your AIMLAPI API key")}
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
<T>For AIMLAPI services (stored securely)</T>
|
|
</p>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={handleProfileUpdate}
|
|
disabled={updatingProfile}
|
|
className="w-full"
|
|
>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
<T>{updatingProfile ? 'Saving...' : 'Save API Keys'}</T>
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{activeSection === 'variables' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle><T>Variables</T></CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<VariablesEditor
|
|
onLoad={async () => {
|
|
if (!user?.id) return {};
|
|
return await getUserVariables(user.id) || {};
|
|
}}
|
|
onSave={async (data) => {
|
|
if (!user?.id) return;
|
|
await updateUserVariables(user.id, data);
|
|
}}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{activeSection === 'addresses' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle><T>Shipping Addresses</T></CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ShippingAddressManager userId={user.id} />
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{activeSection === 'vendor' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle><T>Vendor Profiles</T></CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<VendorProfileManager userId={user.id} />
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{activeSection === 'purchases' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle><T>My Purchases</T></CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<React.Suspense fallback={<div className="flex items-center justify-center py-12 text-muted-foreground"><T>Loading...</T></div>}>
|
|
<LazyPurchasesList
|
|
onFetchTransactions={async () => {
|
|
const { listTransactions } = await import('@/modules/ecommerce/client-ecommerce');
|
|
return listTransactions();
|
|
}}
|
|
onNavigate={navigate}
|
|
toast={{ error: (msg: string) => toast.error(msg) }}
|
|
/>
|
|
</React.Suspense>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
|
|
{activeSection === 'gallery' && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle><T>My Gallery</T></CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{fetchingImages ? (
|
|
<div className="flex items-center justify-center py-16">
|
|
<div className="text-muted-foreground"><T>Loading your images...</T></div>
|
|
</div>
|
|
) : (
|
|
<ImageGallery
|
|
images={images}
|
|
currentIndex={currentIndex}
|
|
setCurrentIndex={setCurrentIndex}
|
|
onImageDelete={handleImageDelete}
|
|
showSelection={false}
|
|
onDoubleClick={(imagePath) => {
|
|
navigate(`/post/${imagePath}`);
|
|
}}
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</SidebarProvider>
|
|
);
|
|
};
|
|
|
|
const ProfileSidebar = ({
|
|
activeSection,
|
|
onSectionChange
|
|
}: {
|
|
activeSection: ActiveSection;
|
|
onSectionChange: (section: ActiveSection) => void;
|
|
}) => {
|
|
const { state } = useSidebar();
|
|
const navigate = useNavigate();
|
|
const isCollapsed = state === "collapsed";
|
|
|
|
const menuItems = [
|
|
{ id: 'general' as ActiveSection, label: translate('General'), icon: User },
|
|
|
|
{ id: 'api-keys' as ActiveSection, label: translate('API Keys'), icon: Key },
|
|
{ id: 'variables' as ActiveSection, label: translate('Variables'), icon: Hash },
|
|
{ id: 'addresses' as ActiveSection, label: translate('Shipping Addresses'), icon: MapPin },
|
|
{ id: 'vendor' as ActiveSection, label: translate('Vendor Profiles'), icon: Building2 },
|
|
{ id: 'purchases' as ActiveSection, label: translate('Purchases'), icon: ShoppingBag },
|
|
];
|
|
|
|
|
|
|
|
return (
|
|
<Sidebar collapsible="icon">
|
|
<SidebarContent>
|
|
<SidebarGroup>
|
|
<SidebarGroupLabel><T>Profile</T></SidebarGroupLabel>
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
{menuItems.map((item) => (
|
|
<SidebarMenuItem key={item.id}>
|
|
<SidebarMenuButton
|
|
onClick={() => onSectionChange(item.id)}
|
|
className={activeSection === item.id ? "bg-muted text-primary font-medium" : "hover:bg-muted/50"}
|
|
>
|
|
<item.icon className="h-4 w-4" />
|
|
{!isCollapsed && <span>{item.label}</span>}
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
))}
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
|
|
<SidebarGroup className="mt-auto">
|
|
<SidebarGroupContent>
|
|
<SidebarMenu>
|
|
<SidebarMenuItem>
|
|
<SidebarMenuButton
|
|
onClick={() => onSectionChange('gallery')}
|
|
className={activeSection === 'gallery' ? "bg-muted text-primary font-medium" : "hover:bg-muted/50"}
|
|
>
|
|
<Images className="h-4 w-4" />
|
|
{!isCollapsed && <span><T>Gallery</T></span>}
|
|
</SidebarMenuButton>
|
|
</SidebarMenuItem>
|
|
</SidebarMenu>
|
|
</SidebarGroupContent>
|
|
</SidebarGroup>
|
|
</SidebarContent>
|
|
</Sidebar>
|
|
);
|
|
};
|
|
|
|
export default Profile; |