This commit is contained in:
gpt-engineer-app[bot]
2025-12-18 08:06:31 +00:00
parent cbc0992554
commit bf7a9fad99
11 changed files with 1441 additions and 20 deletions

373
src/pages/Admin.tsx Normal file
View File

@@ -0,0 +1,373 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Layout } from '@/components/Layout';
import { useAuth } from '@/hooks/useAuth';
import { supabase } from '@/integrations/supabase/client';
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 { 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 { toast } from '@/hooks/use-toast';
import { Skeleton } from '@/components/ui/skeleton';
import { Plus, Pencil, Trash2 } from 'lucide-react';
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,
};
export default function Admin() {
const { user, isAdmin, loading: authLoading } = useAuth();
const navigate = useNavigate();
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [dialogOpen, setDialogOpen] = useState(false);
const [editingProduct, setEditingProduct] = useState<Product | null>(null);
const [form, setForm] = useState(emptyProduct);
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!authLoading) {
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);
}
setLoading(false);
};
const generateSlug = (title: string) => {
return 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,
});
setDialogOpen(true);
};
const handleNew = () => {
setEditingProduct(null);
setForm(emptyProduct);
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;
}
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,
};
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();
}
} 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();
}
}
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 (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>
<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"
/>
</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>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 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>
</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>
<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>
</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>
</Card>
</div>
</Layout>
);
}