Changes
This commit is contained in:
@@ -7,41 +7,20 @@ 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, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Plus, Pencil, Trash2 } from 'lucide-react';
|
||||
import { CurriculumEditor } from '@/components/admin/CurriculumEditor';
|
||||
|
||||
interface Product {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
description: string;
|
||||
content: string;
|
||||
meeting_link: string | null;
|
||||
recording_url: string | null;
|
||||
price: number;
|
||||
sale_price: number | null;
|
||||
is_active: boolean;
|
||||
}
|
||||
interface Product { id: string; title: string; slug: string; type: string; description: string; content: string; meeting_link: string | null; recording_url: string | null; price: number; sale_price: number | null; is_active: boolean; }
|
||||
|
||||
const emptyProduct = {
|
||||
title: '',
|
||||
slug: '',
|
||||
type: 'consulting',
|
||||
description: '',
|
||||
content: '',
|
||||
meeting_link: '',
|
||||
recording_url: '',
|
||||
price: 0,
|
||||
sale_price: null as number | null,
|
||||
is_active: true,
|
||||
};
|
||||
const emptyProduct = { title: '', slug: '', type: 'consulting', description: '', content: '', meeting_link: '', recording_url: '', price: 0, sale_price: null as number | null, is_active: true };
|
||||
|
||||
export default function Admin() {
|
||||
const { user, isAdmin, loading: authLoading } = useAuth();
|
||||
@@ -52,317 +31,111 @@ export default function Admin() {
|
||||
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
|
||||
const [form, setForm] = useState(emptyProduct);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('details');
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading) {
|
||||
if (!user) {
|
||||
navigate('/auth');
|
||||
} else if (!isAdmin) {
|
||||
toast({ title: 'Access denied', description: 'Admin access required', variant: 'destructive' });
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
fetchProducts();
|
||||
}
|
||||
if (!user) navigate('/auth');
|
||||
else if (!isAdmin) { toast({ title: 'Access denied', description: 'Admin access required', variant: 'destructive' }); navigate('/dashboard'); }
|
||||
else fetchProducts();
|
||||
}
|
||||
}, [user, isAdmin, authLoading, navigate]);
|
||||
|
||||
const fetchProducts = async () => {
|
||||
const { data, error } = await supabase
|
||||
.from('products')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (!error && data) {
|
||||
setProducts(data);
|
||||
}
|
||||
const { data, error } = await supabase.from('products').select('*').order('created_at', { ascending: false });
|
||||
if (!error && data) setProducts(data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const generateSlug = (title: string) => {
|
||||
return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
||||
};
|
||||
const generateSlug = (title: string) => title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
||||
|
||||
const handleEdit = (product: Product) => {
|
||||
setEditingProduct(product);
|
||||
setForm({
|
||||
title: product.title,
|
||||
slug: product.slug,
|
||||
type: product.type,
|
||||
description: product.description,
|
||||
content: product.content || '',
|
||||
meeting_link: product.meeting_link || '',
|
||||
recording_url: product.recording_url || '',
|
||||
price: product.price,
|
||||
sale_price: product.sale_price,
|
||||
is_active: product.is_active,
|
||||
});
|
||||
setForm({ title: product.title, slug: product.slug, type: product.type, description: product.description, content: product.content || '', meeting_link: product.meeting_link || '', recording_url: product.recording_url || '', price: product.price, sale_price: product.sale_price, is_active: product.is_active });
|
||||
setActiveTab('details');
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleNew = () => {
|
||||
setEditingProduct(null);
|
||||
setForm(emptyProduct);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
const handleNew = () => { setEditingProduct(null); setForm(emptyProduct); setActiveTab('details'); setDialogOpen(true); };
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.title || !form.slug || form.price <= 0) {
|
||||
toast({ title: 'Validation error', description: 'Please fill in all required fields', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!form.title || !form.slug || form.price <= 0) { toast({ title: 'Validation error', description: 'Please fill in all required fields', variant: 'destructive' }); return; }
|
||||
setSaving(true);
|
||||
|
||||
const productData = {
|
||||
title: form.title,
|
||||
slug: form.slug,
|
||||
type: form.type,
|
||||
description: form.description,
|
||||
content: form.content,
|
||||
meeting_link: form.meeting_link || null,
|
||||
recording_url: form.recording_url || null,
|
||||
price: form.price,
|
||||
sale_price: form.sale_price || null,
|
||||
is_active: form.is_active,
|
||||
};
|
||||
|
||||
const productData = { title: form.title, slug: form.slug, type: form.type, description: form.description, content: form.content, meeting_link: form.meeting_link || null, recording_url: form.recording_url || null, price: form.price, sale_price: form.sale_price || null, is_active: form.is_active };
|
||||
if (editingProduct) {
|
||||
const { error } = await supabase
|
||||
.from('products')
|
||||
.update(productData)
|
||||
.eq('id', editingProduct.id);
|
||||
|
||||
if (error) {
|
||||
toast({ title: 'Error', description: 'Failed to update product', variant: 'destructive' });
|
||||
} else {
|
||||
toast({ title: 'Success', description: 'Product updated' });
|
||||
setDialogOpen(false);
|
||||
fetchProducts();
|
||||
}
|
||||
const { error } = await supabase.from('products').update(productData).eq('id', editingProduct.id);
|
||||
if (error) toast({ title: 'Error', description: 'Failed to update product', variant: 'destructive' });
|
||||
else { toast({ title: 'Success', description: 'Product updated' }); setDialogOpen(false); fetchProducts(); }
|
||||
} else {
|
||||
const { error } = await supabase
|
||||
.from('products')
|
||||
.insert(productData);
|
||||
|
||||
if (error) {
|
||||
toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
||||
} else {
|
||||
toast({ title: 'Success', description: 'Product created' });
|
||||
setDialogOpen(false);
|
||||
fetchProducts();
|
||||
}
|
||||
const { error } = await supabase.from('products').insert(productData);
|
||||
if (error) toast({ title: 'Error', description: error.message, variant: 'destructive' });
|
||||
else { toast({ title: 'Success', description: 'Product created' }); setDialogOpen(false); fetchProducts(); }
|
||||
}
|
||||
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Are you sure you want to delete this product?')) return;
|
||||
|
||||
const { error } = await supabase.from('products').delete().eq('id', id);
|
||||
|
||||
if (error) {
|
||||
toast({ title: 'Error', description: 'Failed to delete product', variant: 'destructive' });
|
||||
} else {
|
||||
toast({ title: 'Success', description: 'Product deleted' });
|
||||
fetchProducts();
|
||||
}
|
||||
if (error) toast({ title: 'Error', description: 'Failed to delete product', variant: 'destructive' });
|
||||
else { toast({ title: 'Success', description: 'Product deleted' }); fetchProducts(); }
|
||||
};
|
||||
|
||||
if (authLoading || loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Skeleton className="h-10 w-1/3 mb-8" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
if (authLoading || loading) return (<Layout><div className="container mx-auto px-4 py-8"><Skeleton className="h-10 w-1/3 mb-8" /><Skeleton className="h-64 w-full" /></div></Layout>);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold">Admin Panel</h1>
|
||||
<p className="text-muted-foreground">Manage your products</p>
|
||||
</div>
|
||||
<div><h1 className="text-4xl font-bold">Admin Panel</h1><p className="text-muted-foreground">Manage your products</p></div>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={handleNew} className="shadow-sm">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Product
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto border-2 border-border">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingProduct ? 'Edit Product' : 'New Product'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Title *</Label>
|
||||
<Input
|
||||
value={form.title}
|
||||
onChange={(e) => {
|
||||
setForm({ ...form, title: e.target.value, slug: generateSlug(e.target.value) });
|
||||
}}
|
||||
className="border-2"
|
||||
/>
|
||||
<DialogTrigger asChild><Button onClick={handleNew} className="shadow-sm"><Plus className="w-4 h-4 mr-2" />Add Product</Button></DialogTrigger>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto border-2 border-border">
|
||||
<DialogHeader><DialogTitle>{editingProduct ? 'Edit Product' : 'New Product'}</DialogTitle></DialogHeader>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="mt-4">
|
||||
<TabsList className="border-2 border-border">
|
||||
<TabsTrigger value="details">Details</TabsTrigger>
|
||||
{editingProduct && form.type === 'bootcamp' && <TabsTrigger value="curriculum">Curriculum</TabsTrigger>}
|
||||
</TabsList>
|
||||
<TabsContent value="details" className="space-y-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2"><Label>Title *</Label><Input value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value, slug: generateSlug(e.target.value) })} className="border-2" /></div>
|
||||
<div className="space-y-2"><Label>Slug *</Label><Input value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className="border-2" /></div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Slug *</Label>
|
||||
<Input
|
||||
value={form.slug}
|
||||
onChange={(e) => setForm({ ...form, slug: e.target.value })}
|
||||
className="border-2"
|
||||
/>
|
||||
<div className="space-y-2"><Label>Type</Label><Select value={form.type} onValueChange={(v) => setForm({ ...form, type: v })}><SelectTrigger className="border-2"><SelectValue /></SelectTrigger><SelectContent><SelectItem value="consulting">Consulting</SelectItem><SelectItem value="webinar">Webinar</SelectItem><SelectItem value="bootcamp">Bootcamp</SelectItem></SelectContent></Select></div>
|
||||
<div className="space-y-2"><Label>Description</Label><Textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} className="border-2" rows={2} /></div>
|
||||
<div className="space-y-2"><Label>Content (HTML)</Label><Textarea value={form.content} onChange={(e) => setForm({ ...form, content: e.target.value })} className="border-2 font-mono text-sm" rows={6} /></div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2"><Label>Meeting Link</Label><Input value={form.meeting_link} onChange={(e) => setForm({ ...form, meeting_link: e.target.value })} placeholder="https://meet.google.com/..." className="border-2" /></div>
|
||||
<div className="space-y-2"><Label>Recording URL</Label><Input value={form.recording_url} onChange={(e) => setForm({ ...form, recording_url: e.target.value })} placeholder="https://youtube.com/..." className="border-2" /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Type</Label>
|
||||
<Select value={form.type} onValueChange={(v) => setForm({ ...form, type: v })}>
|
||||
<SelectTrigger className="border-2">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="consulting">Consulting</SelectItem>
|
||||
<SelectItem value="webinar">Webinar</SelectItem>
|
||||
<SelectItem value="bootcamp">Bootcamp</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Description</Label>
|
||||
<Textarea
|
||||
value={form.description}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
className="border-2"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Content (HTML)</Label>
|
||||
<Textarea
|
||||
value={form.content}
|
||||
onChange={(e) => setForm({ ...form, content: e.target.value })}
|
||||
className="border-2 font-mono text-sm"
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Meeting Link</Label>
|
||||
<Input
|
||||
value={form.meeting_link}
|
||||
onChange={(e) => setForm({ ...form, meeting_link: e.target.value })}
|
||||
placeholder="https://meet.google.com/..."
|
||||
className="border-2"
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2"><Label>Price *</Label><Input type="number" value={form.price} onChange={(e) => setForm({ ...form, price: parseFloat(e.target.value) || 0 })} className="border-2" /></div>
|
||||
<div className="space-y-2"><Label>Sale Price</Label><Input type="number" value={form.sale_price || ''} onChange={(e) => setForm({ ...form, sale_price: e.target.value ? parseFloat(e.target.value) : null })} placeholder="Leave empty if no sale" className="border-2" /></div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Recording URL</Label>
|
||||
<Input
|
||||
value={form.recording_url}
|
||||
onChange={(e) => setForm({ ...form, recording_url: e.target.value })}
|
||||
placeholder="https://youtube.com/..."
|
||||
className="border-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Price *</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={form.price}
|
||||
onChange={(e) => setForm({ ...form, price: parseFloat(e.target.value) || 0 })}
|
||||
className="border-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Sale Price</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={form.sale_price || ''}
|
||||
onChange={(e) => setForm({ ...form, sale_price: e.target.value ? parseFloat(e.target.value) : null })}
|
||||
placeholder="Leave empty if no sale"
|
||||
className="border-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={form.is_active}
|
||||
onCheckedChange={(checked) => setForm({ ...form, is_active: checked })}
|
||||
/>
|
||||
<Label>Active</Label>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleSave} className="w-full shadow-sm" disabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save Product'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2"><Switch checked={form.is_active} onCheckedChange={(checked) => setForm({ ...form, is_active: checked })} /><Label>Active</Label></div>
|
||||
<Button onClick={handleSave} className="w-full shadow-sm" disabled={saving}>{saving ? 'Saving...' : 'Save Product'}</Button>
|
||||
</TabsContent>
|
||||
{editingProduct && form.type === 'bootcamp' && <TabsContent value="curriculum" className="py-4"><CurriculumEditor productId={editingProduct.id} /></TabsContent>}
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<Card className="border-2 border-border">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableHeader><TableRow><TableHead>Title</TableHead><TableHead>Type</TableHead><TableHead>Price</TableHead><TableHead>Status</TableHead><TableHead className="text-right">Actions</TableHead></TableRow></TableHeader>
|
||||
<TableBody>
|
||||
{products.map((product) => (
|
||||
<TableRow key={product.id}>
|
||||
<TableCell className="font-medium">{product.title}</TableCell>
|
||||
<TableCell className="capitalize">{product.type}</TableCell>
|
||||
<TableCell>
|
||||
{product.sale_price ? (
|
||||
<span>
|
||||
<span className="font-bold">${product.sale_price}</span>
|
||||
<span className="text-muted-foreground line-through ml-2">${product.price}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="font-bold">${product.price}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className={product.is_active ? 'text-foreground' : 'text-muted-foreground'}>
|
||||
{product.is_active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(product)}>
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDelete(product.id)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell>{product.sale_price ? <span><span className="font-bold">${product.sale_price}</span><span className="text-muted-foreground line-through ml-2">${product.price}</span></span> : <span className="font-bold">${product.price}</span>}</TableCell>
|
||||
<TableCell><span className={product.is_active ? 'text-foreground' : 'text-muted-foreground'}>{product.is_active ? 'Active' : 'Inactive'}</span></TableCell>
|
||||
<TableCell className="text-right"><Button variant="ghost" size="sm" onClick={() => handleEdit(product)}><Pencil className="w-4 h-4" /></Button><Button variant="ghost" size="sm" onClick={() => handleDelete(product.id)}><Trash2 className="w-4 h-4" /></Button></TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{products.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
||||
No products yet. Create your first product!
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{products.length === 0 && <TableRow><TableCell colSpan={5} className="text-center py-8 text-muted-foreground">No products yet. Create your first product!</TableCell></TableRow>}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
@@ -370,4 +143,4 @@ export default function Admin() {
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user