Replace dropdown filters with tab buttons across admin pages

Update all admin pages to use tab button filters instead of dropdown selects, following the pattern from MemberAccess.tsx:

Changes:
- AdminProducts: Tab buttons for product type (only bootcamp/webinar shown) and status (active/inactive)
- AdminConsulting: Added status filter with tab buttons (pending payment, confirmed, completed, cancelled)
- AdminOrders: Tab buttons for status filter (all, paid, pending, refunded)
- AdminMembers: Tab buttons for role filter (all, admin, member)
- AdminReviews: Tab buttons for both type (all, consulting, bootcamp, webinar, general) and status (all, pending, approved)

Features added:
- Clear button (X) on search input when text is present
- Reset button appears when any filter is active
- Consistent styling with shadow-sm for active tabs and border-2 for outline tabs
- All filters in vertical stack layout for better mobile responsiveness
- Active state visually distinct with default variant and shadow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
dwindown
2025-12-28 00:33:43 +07:00
parent c993abe1e9
commit 0a299466d8
5 changed files with 414 additions and 123 deletions

View File

@@ -14,7 +14,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { formatIDR } from '@/lib/format'; import { formatIDR } from '@/lib/format';
import { Video, Calendar, Clock, User, Link as LinkIcon, ExternalLink, CheckCircle, XCircle, Loader2, Search } from 'lucide-react'; import { Video, Calendar, Clock, User, Link as LinkIcon, ExternalLink, CheckCircle, XCircle, Loader2, Search, X } from 'lucide-react';
import { format, parseISO, isToday, isTomorrow, isPast } from 'date-fns'; import { format, parseISO, isToday, isTomorrow, isPast } from 'date-fns';
import { id } from 'date-fns/locale'; import { id } from 'date-fns/locale';
@@ -73,6 +73,7 @@ export default function AdminConsulting() {
const [creatingMeet, setCreatingMeet] = useState(false); const [creatingMeet, setCreatingMeet] = useState(false);
const [activeTab, setActiveTab] = useState('upcoming'); const [activeTab, setActiveTab] = useState('upcoming');
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [filterStatus, setFilterStatus] = useState<string>('all');
useEffect(() => { useEffect(() => {
if (!authLoading) { if (!authLoading) {
@@ -283,18 +284,32 @@ export default function AdminConsulting() {
})).sort((a, b) => new Date(a.firstDate).getTime() - new Date(b.firstDate).getTime()); })).sort((a, b) => new Date(a.firstDate).getTime() - new Date(b.firstDate).getTime());
})(); })();
// Filter orders based on search query // Filter orders based on search query and status
const filteredGroupedOrders = groupedOrders.filter(order => { const filteredGroupedOrders = groupedOrders.filter(order => {
if (!searchQuery) return true; // Search filter
const query = searchQuery.toLowerCase(); if (searchQuery) {
const firstSlot = order.slots[0]; const query = searchQuery.toLowerCase();
const firstSlot = order.slots[0];
return ( const matchesSearch =
order.profile?.name?.toLowerCase().includes(query) || order.profile?.name?.toLowerCase().includes(query) ||
order.profile?.email?.toLowerCase().includes(query) || order.profile?.email?.toLowerCase().includes(query) ||
firstSlot.topic_category?.toLowerCase().includes(query) || firstSlot.topic_category?.toLowerCase().includes(query) ||
order.orderId?.toLowerCase().includes(query) order.orderId?.toLowerCase().includes(query);
);
if (!matchesSearch) return false;
}
// Status filter
if (filterStatus !== 'all') {
const firstSlot = order.slots[0];
if (filterStatus === 'confirmed' && firstSlot.status !== 'confirmed') return false;
if (filterStatus === 'pending_payment' && firstSlot.status !== 'pending_payment') return false;
if (filterStatus === 'completed' && firstSlot.status !== 'completed') return false;
if (filterStatus === 'cancelled' && firstSlot.status !== 'cancelled') return false;
}
return true;
}); });
const today = new Date().toISOString().split('T')[0]; const today = new Date().toISOString().split('T')[0];
@@ -378,17 +393,86 @@ export default function AdminConsulting() {
{/* Search */} {/* Search */}
<Card className="border-2 border-border mb-6"> <Card className="border-2 border-border mb-6">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="relative"> <div className="space-y-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" /> {/* Search */}
<Input <div className="relative">
placeholder="Cari nama klien, email, kategori, atau order ID..." <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
value={searchQuery} <Input
onChange={(e) => setSearchQuery(e.target.value)} placeholder="Cari nama klien, email, kategori, atau order ID..."
className="pl-10 border-2" value={searchQuery}
/> onChange={(e) => setSearchQuery(e.target.value)}
</div> className="pl-10 border-2"
<div className="mt-2 text-sm text-muted-foreground"> />
Menampilkan {filteredGroupedOrders.length} dari {groupedOrders.length} jadwal konsultasi {searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="w-4 h-4" />
</button>
)}
</div>
{/* Status Filter */}
<div className="flex flex-wrap gap-2 items-center">
<span className="text-sm font-medium text-muted-foreground">Status:</span>
<Button
variant={filterStatus === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilterStatus('all')}
className={filterStatus === 'all' ? 'shadow-sm' : 'border-2'}
>
Semua
</Button>
<Button
variant={filterStatus === 'pending_payment' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilterStatus('pending_payment')}
className={filterStatus === 'pending_payment' ? 'shadow-sm' : 'border-2'}
>
Menunggu Pembayaran
</Button>
<Button
variant={filterStatus === 'confirmed' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilterStatus('confirmed')}
className={filterStatus === 'confirmed' ? 'shadow-sm' : 'border-2'}
>
Dikonfirmasi
</Button>
<Button
variant={filterStatus === 'completed' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilterStatus('completed')}
className={filterStatus === 'completed' ? 'shadow-sm' : 'border-2'}
>
Selesai
</Button>
<Button
variant={filterStatus === 'cancelled' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilterStatus('cancelled')}
className={filterStatus === 'cancelled' ? 'shadow-sm' : 'border-2'}
>
Dibatalkan
</Button>
{(searchQuery || filterStatus !== 'all') && (
<Button
variant="ghost"
size="sm"
onClick={() => { setSearchQuery(''); setFilterStatus('all'); }}
className="text-muted-foreground hover:text-destructive"
>
<X className="w-4 h-4 mr-1" />
Reset
</Button>
)}
</div>
{/* Result count */}
<p className="text-sm text-muted-foreground">
Menampilkan {filteredGroupedOrders.length} dari {groupedOrders.length} jadwal konsultasi
</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -10,9 +10,8 @@ import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { formatDateTime } from "@/lib/format"; import { formatDateTime } from "@/lib/format";
import { Eye, Shield, ShieldOff, Search } from "lucide-react"; import { Eye, Shield, ShieldOff, Search, X } from "lucide-react";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/use-toast";
interface Member { interface Member {
@@ -78,6 +77,11 @@ export default function AdminMembers() {
return matchesSearch && matchesRole; return matchesSearch && matchesRole;
}); });
const clearFilters = () => {
setSearchQuery('');
setFilterRole('all');
};
const viewMemberDetails = async (member: Member) => { const viewMemberDetails = async (member: Member) => {
setSelectedMember(member); setSelectedMember(member);
const { data } = await supabase.from("user_access").select("*, product:products(title)").eq("user_id", member.id); const { data } = await supabase.from("user_access").select("*, product:products(title)").eq("user_id", member.id);
@@ -123,7 +127,7 @@ export default function AdminMembers() {
{/* Search & Filter */} {/* Search & Filter */}
<Card className="border-2 border-border mb-6"> <Card className="border-2 border-border mb-6">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="space-y-4">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input <Input
@@ -132,20 +136,58 @@ export default function AdminMembers() {
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 border-2" className="pl-10 border-2"
/> />
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="w-4 h-4" />
</button>
)}
</div> </div>
<Select value={filterRole} onValueChange={setFilterRole}>
<SelectTrigger className="border-2"> <div className="flex flex-wrap gap-2 items-center">
<SelectValue placeholder="Filter role" /> <span className="text-sm font-medium text-muted-foreground">Role:</span>
</SelectTrigger> <Button
<SelectContent> variant={filterRole === 'all' ? 'default' : 'outline'}
<SelectItem value="all">Semua Role</SelectItem> size="sm"
<SelectItem value="admin">Admin</SelectItem> onClick={() => setFilterRole('all')}
<SelectItem value="member">Member</SelectItem> className={filterRole === 'all' ? 'shadow-sm' : 'border-2'}
</SelectContent> >
</Select> Semua
</div> </Button>
<div className="mt-2 text-sm text-muted-foreground"> <Button
Menampilkan {filteredMembers.length} dari {members.length} member variant={filterRole === 'admin' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilterRole('admin')}
className={filterRole === 'admin' ? 'shadow-sm' : 'border-2'}
>
Admin
</Button>
<Button
variant={filterRole === 'member' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilterRole('member')}
className={filterRole === 'member' ? 'shadow-sm' : 'border-2'}
>
Member
</Button>
{(searchQuery || filterRole !== 'all') && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="text-muted-foreground hover:text-destructive"
>
<X className="w-4 h-4 mr-1" />
Reset
</Button>
)}
</div>
<p className="text-sm text-muted-foreground">
Menampilkan {filteredMembers.length} dari {members.length} member
</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -13,9 +13,8 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { formatIDR, formatDateTime } from "@/lib/format"; import { formatIDR, formatDateTime } from "@/lib/format";
import { Eye, CheckCircle, XCircle, Video, ExternalLink, Trash2, AlertTriangle, RefreshCw, Link as LinkIcon, Download, Search } from "lucide-react"; import { Eye, CheckCircle, XCircle, Video, ExternalLink, Trash2, AlertTriangle, RefreshCw, Link as LinkIcon, Download, Search, X } from "lucide-react";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/use-toast";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { getPaymentStatusLabel, getPaymentStatusColor, canRefundOrder, canCancelOrder, canMarkAsPaid } from "@/lib/statusHelpers"; import { getPaymentStatusLabel, getPaymentStatusColor, canRefundOrder, canCancelOrder, canMarkAsPaid } from "@/lib/statusHelpers";
import { convertToCSV, downloadCSV, formatExportDate, formatExportIDR } from "@/lib/exportCSV"; import { convertToCSV, downloadCSV, formatExportDate, formatExportIDR } from "@/lib/exportCSV";
@@ -104,6 +103,11 @@ export default function AdminOrders() {
return matchesSearch && matchesStatus; return matchesSearch && matchesStatus;
}); });
const clearFilters = () => {
setSearchQuery('');
setFilterStatus('all');
};
const viewOrderDetails = async (order: Order) => { const viewOrderDetails = async (order: Order) => {
setSelectedOrder(order); setSelectedOrder(order);
const { data: itemsData } = await supabase.from("order_items").select("*, product:products(title, type)").eq("order_id", order.id); const { data: itemsData } = await supabase.from("order_items").select("*, product:products(title, type)").eq("order_id", order.id);
@@ -395,7 +399,7 @@ export default function AdminOrders() {
{/* Search & Filter */} {/* Search & Filter */}
<Card className="border-2 border-border mb-6"> <Card className="border-2 border-border mb-6">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="space-y-4">
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input <Input
@@ -404,21 +408,66 @@ export default function AdminOrders() {
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 border-2" className="pl-10 border-2"
/> />
{searchQuery && (
<button
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="w-4 h-4" />
</button>
)}
</div> </div>
<Select value={filterStatus} onValueChange={setFilterStatus}>
<SelectTrigger className="border-2"> <div className="flex flex-wrap gap-2 items-center">
<SelectValue placeholder="Filter status" /> <span className="text-sm font-medium text-muted-foreground">Status:</span>
</SelectTrigger> <Button
<SelectContent> variant={filterStatus === 'all' ? 'default' : 'outline'}
<SelectItem value="all">Semua Status</SelectItem> size="sm"
<SelectItem value="paid">Lunas</SelectItem> onClick={() => setFilterStatus('all')}
<SelectItem value="pending">Pending</SelectItem> className={filterStatus === 'all' ? 'shadow-sm' : 'border-2'}
<SelectItem value="refunded">Refunded</SelectItem> >
</SelectContent> Semua
</Select> </Button>
</div> <Button
<div className="mt-2 text-sm text-muted-foreground"> variant={filterStatus === 'paid' ? 'default' : 'outline'}
Menampilkan {filteredOrders.length} dari {orders.length} order size="sm"
onClick={() => setFilterStatus('paid')}
className={filterStatus === 'paid' ? 'shadow-sm' : 'border-2'}
>
Lunas
</Button>
<Button
variant={filterStatus === 'pending' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilterStatus('pending')}
className={filterStatus === 'pending' ? 'shadow-sm' : 'border-2'}
>
Pending
</Button>
<Button
variant={filterStatus === 'refunded' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilterStatus('refunded')}
className={filterStatus === 'refunded' ? 'shadow-sm' : 'border-2'}
>
Refunded
</Button>
{(searchQuery || filterStatus !== 'all') && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="text-muted-foreground hover:text-destructive"
>
<X className="w-4 h-4 mr-1" />
Reset
</Button>
)}
</div>
<p className="text-sm text-muted-foreground">
Menampilkan {filteredOrders.length} dari {orders.length} order
</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -15,7 +15,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { Plus, Pencil, Trash2, Search } from 'lucide-react'; import { Plus, Pencil, Trash2, Search, X } from 'lucide-react';
import { CurriculumEditor } from '@/components/admin/CurriculumEditor'; import { CurriculumEditor } from '@/components/admin/CurriculumEditor';
import { RichTextEditor } from '@/components/RichTextEditor'; import { RichTextEditor } from '@/components/RichTextEditor';
import { formatIDR } from '@/lib/format'; import { formatIDR } from '@/lib/format';
@@ -90,6 +90,15 @@ export default function AdminProducts() {
return matchesSearch && matchesType && matchesStatus; return matchesSearch && matchesType && matchesStatus;
}); });
// Get unique product types from actual products
const productTypes = ['all', ...Array.from(new Set(products.map(p => p.type)))];
const clearFilters = () => {
setSearchQuery('');
setFilterType('all');
setFilterStatus('all');
};
const generateSlug = (title: string) => 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) => { const handleEdit = (product: Product) => {
@@ -187,51 +196,86 @@ export default function AdminProducts() {
{/* Search and Filter */} {/* Search and Filter */}
<Card className="border-2 border-border mb-6"> <Card className="border-2 border-border mb-6">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="space-y-4">
{/* Search */} {/* Search */}
<div className="md:col-span-2"> <div className="relative">
<div className="relative"> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" /> <Input
<Input placeholder="Cari judul atau deskripsi produk..."
placeholder="Cari judul atau deskripsi produk..." value={searchQuery}
value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)}
onChange={(e) => setSearchQuery(e.target.value)} className="pl-10 border-2"
className="pl-10 border-2" />
/> {searchQuery && (
</div> <button
onClick={() => setSearchQuery('')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="w-4 h-4" />
</button>
)}
</div> </div>
{/* Type Filter */} {/* Type Filter */}
<Select value={filterType} onValueChange={setFilterType}> <div className="flex flex-wrap gap-2 items-center">
<SelectTrigger className="border-2"> <span className="text-sm font-medium text-muted-foreground">Tipe:</span>
<SelectValue placeholder="Filter tipe" /> {productTypes.map((type) => (
</SelectTrigger> <Button
<SelectContent> key={type}
<SelectItem value="all">Semua Tipe</SelectItem> variant={filterType === type ? 'default' : 'outline'}
<SelectItem value="webinar">Webinar</SelectItem> size="sm"
<SelectItem value="course">Course</SelectItem> onClick={() => setFilterType(type)}
<SelectItem value="consulting">Consulting</SelectItem> className={filterType === type ? 'shadow-sm' : 'border-2'}
<SelectItem value="ebook">E-book</SelectItem> >
<SelectItem value="bootcamp">Bootcamp</SelectItem> {type === 'all' ? 'Semua' : type === 'webinar' ? 'Webinar' : type === 'bootcamp' ? 'Bootcamp' : type}
</SelectContent> </Button>
</Select> ))}
</div>
{/* Status Filter */} {/* Status Filter */}
<Select value={filterStatus} onValueChange={setFilterStatus}> <div className="flex flex-wrap gap-2 items-center">
<SelectTrigger className="border-2"> <span className="text-sm font-medium text-muted-foreground">Status:</span>
<SelectValue placeholder="Filter status" /> <Button
</SelectTrigger> variant={filterStatus === 'all' ? 'default' : 'outline'}
<SelectContent> size="sm"
<SelectItem value="all">Semua Status</SelectItem> onClick={() => setFilterStatus('all')}
<SelectItem value="active">Aktif</SelectItem> className={filterStatus === 'all' ? 'shadow-sm' : 'border-2'}
<SelectItem value="inactive">Nonaktif</SelectItem> >
</SelectContent> Semua
</Select> </Button>
</div> <Button
variant={filterStatus === 'active' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilterStatus('active')}
className={filterStatus === 'active' ? 'shadow-sm' : 'border-2'}
>
Aktif
</Button>
<Button
variant={filterStatus === 'inactive' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilterStatus('inactive')}
className={filterStatus === 'inactive' ? 'shadow-sm' : 'border-2'}
>
Nonaktif
</Button>
{(searchQuery || filterType !== 'all' || filterStatus !== 'all') && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="text-muted-foreground hover:text-destructive"
>
<X className="w-4 h-4 mr-1" />
Reset
</Button>
)}
</div>
{/* Result count */} {/* Result count */}
<div className="mt-4 text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Menampilkan {filteredProducts.length} dari {products.length} produk Menampilkan {filteredProducts.length} dari {products.length} produk
</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -6,12 +6,11 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/use-toast";
import { Star, Check, X, Edit, Trash2, Search } from "lucide-react"; import { Star, Check, X, Edit, Trash2, Search, X as XIcon } from "lucide-react";
interface Review { interface Review {
id: string; id: string;
@@ -125,6 +124,10 @@ export default function AdminReviews() {
return true; return true;
}); });
const clearFilters = () => {
setFilter({ ...filter, type: 'all', status: 'all', search: '' });
};
const pendingReviews = reviews.filter((r) => !r.is_approved); const pendingReviews = reviews.filter((r) => !r.is_approved);
const renderStars = (rating: number) => ( const renderStars = (rating: number) => (
@@ -174,8 +177,8 @@ export default function AdminReviews() {
<Card className="border-2 border-border"> <Card className="border-2 border-border">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="space-y-4">
<div className="relative md:col-span-1"> <div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input <Input
placeholder="Cari ulasan..." placeholder="Cari ulasan..."
@@ -183,33 +186,102 @@ export default function AdminReviews() {
onChange={(e) => setFilter({ ...filter, search: e.target.value })} onChange={(e) => setFilter({ ...filter, search: e.target.value })}
className="pl-10 border-2" className="pl-10 border-2"
/> />
{filter.search && (
<button
onClick={() => setFilter({ ...filter, search: '' })}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<XIcon className="w-4 h-4" />
</button>
)}
</div> </div>
<Select value={filter.type} onValueChange={(v) => setFilter({ ...filter, type: v })}>
<SelectTrigger className="border-2">
<SelectValue placeholder="Tipe" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Semua Tipe</SelectItem>
<SelectItem value="consulting">Konsultasi</SelectItem>
<SelectItem value="bootcamp">Bootcamp</SelectItem>
<SelectItem value="webinar">Webinar</SelectItem>
<SelectItem value="general">Umum</SelectItem>
</SelectContent>
</Select>
<Select value={filter.status} onValueChange={(v) => setFilter({ ...filter, status: v })}> <div className="flex flex-wrap gap-2 items-center">
<SelectTrigger className="border-2"> <span className="text-sm font-medium text-muted-foreground">Tipe:</span>
<SelectValue placeholder="Status" /> <Button
</SelectTrigger> variant={filter.type === 'all' ? 'default' : 'outline'}
<SelectContent> size="sm"
<SelectItem value="all">Semua Status</SelectItem> onClick={() => setFilter({ ...filter, type: 'all' })}
<SelectItem value="pending">Menunggu</SelectItem> className={filter.type === 'all' ? 'shadow-sm' : 'border-2'}
<SelectItem value="approved">Disetujui</SelectItem> >
</SelectContent> Semua
</Select> </Button>
</div> <Button
<div className="mt-2 text-sm text-muted-foreground"> variant={filter.type === 'consulting' ? 'default' : 'outline'}
Menampilkan {filteredReviews.length} dari {reviews.length} ulasan size="sm"
onClick={() => setFilter({ ...filter, type: 'consulting' })}
className={filter.type === 'consulting' ? 'shadow-sm' : 'border-2'}
>
Konsultasi
</Button>
<Button
variant={filter.type === 'bootcamp' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter({ ...filter, type: 'bootcamp' })}
className={filter.type === 'bootcamp' ? 'shadow-sm' : 'border-2'}
>
Bootcamp
</Button>
<Button
variant={filter.type === 'webinar' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter({ ...filter, type: 'webinar' })}
className={filter.type === 'webinar' ? 'shadow-sm' : 'border-2'}
>
Webinar
</Button>
<Button
variant={filter.type === 'general' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter({ ...filter, type: 'general' })}
className={filter.type === 'general' ? 'shadow-sm' : 'border-2'}
>
Umum
</Button>
</div>
<div className="flex flex-wrap gap-2 items-center">
<span className="text-sm font-medium text-muted-foreground">Status:</span>
<Button
variant={filter.status === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter({ ...filter, status: 'all' })}
className={filter.status === 'all' ? 'shadow-sm' : 'border-2'}
>
Semua
</Button>
<Button
variant={filter.status === 'pending' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter({ ...filter, status: 'pending' })}
className={filter.status === 'pending' ? 'shadow-sm' : 'border-2'}
>
Menunggu
</Button>
<Button
variant={filter.status === 'approved' ? 'default' : 'outline'}
size="sm"
onClick={() => setFilter({ ...filter, status: 'approved' })}
className={filter.status === 'approved' ? 'shadow-sm' : 'border-2'}
>
Disetujui
</Button>
{(filter.search || filter.type !== 'all' || filter.status !== 'all') && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="text-muted-foreground hover:text-destructive"
>
<XIcon className="w-4 h-4 mr-1" />
Reset
</Button>
)}
</div>
<p className="text-sm text-muted-foreground">
Menampilkan {filteredReviews.length} dari {reviews.length} ulasan
</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>