From 0a299466d8af41bb9281cd548ae208ef8b2c6a8d Mon Sep 17 00:00:00 2001 From: dwindown Date: Sun, 28 Dec 2025 00:33:43 +0700 Subject: [PATCH] Replace dropdown filters with tab buttons across admin pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/pages/admin/AdminConsulting.tsx | 128 ++++++++++++++++++++++----- src/pages/admin/AdminMembers.tsx | 74 ++++++++++++---- src/pages/admin/AdminOrders.tsx | 83 ++++++++++++++---- src/pages/admin/AdminProducts.tsx | 122 +++++++++++++++++--------- src/pages/admin/AdminReviews.tsx | 130 +++++++++++++++++++++------- 5 files changed, 414 insertions(+), 123 deletions(-) diff --git a/src/pages/admin/AdminConsulting.tsx b/src/pages/admin/AdminConsulting.tsx index 317b577..bfb161d 100644 --- a/src/pages/admin/AdminConsulting.tsx +++ b/src/pages/admin/AdminConsulting.tsx @@ -14,7 +14,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { toast } from '@/hooks/use-toast'; 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 { id } from 'date-fns/locale'; @@ -73,6 +73,7 @@ export default function AdminConsulting() { const [creatingMeet, setCreatingMeet] = useState(false); const [activeTab, setActiveTab] = useState('upcoming'); const [searchQuery, setSearchQuery] = useState(''); + const [filterStatus, setFilterStatus] = useState('all'); useEffect(() => { if (!authLoading) { @@ -283,18 +284,32 @@ export default function AdminConsulting() { })).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 => { - if (!searchQuery) return true; - const query = searchQuery.toLowerCase(); - const firstSlot = order.slots[0]; + // Search filter + if (searchQuery) { + const query = searchQuery.toLowerCase(); + const firstSlot = order.slots[0]; - return ( - order.profile?.name?.toLowerCase().includes(query) || - order.profile?.email?.toLowerCase().includes(query) || - firstSlot.topic_category?.toLowerCase().includes(query) || - order.orderId?.toLowerCase().includes(query) - ); + const matchesSearch = + order.profile?.name?.toLowerCase().includes(query) || + order.profile?.email?.toLowerCase().includes(query) || + firstSlot.topic_category?.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]; @@ -378,17 +393,86 @@ export default function AdminConsulting() { {/* Search */} -
- - setSearchQuery(e.target.value)} - className="pl-10 border-2" - /> -
-
- Menampilkan {filteredGroupedOrders.length} dari {groupedOrders.length} jadwal konsultasi +
+ {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="pl-10 border-2" + /> + {searchQuery && ( + + )} +
+ + {/* Status Filter */} +
+ Status: + + + + + + {(searchQuery || filterStatus !== 'all') && ( + + )} +
+ + {/* Result count */} +

+ Menampilkan {filteredGroupedOrders.length} dari {groupedOrders.length} jadwal konsultasi +

diff --git a/src/pages/admin/AdminMembers.tsx b/src/pages/admin/AdminMembers.tsx index 7c6382f..b95e7ba 100644 --- a/src/pages/admin/AdminMembers.tsx +++ b/src/pages/admin/AdminMembers.tsx @@ -10,9 +10,8 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Skeleton } from "@/components/ui/skeleton"; import { Input } from "@/components/ui/input"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; 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"; interface Member { @@ -78,6 +77,11 @@ export default function AdminMembers() { return matchesSearch && matchesRole; }); + const clearFilters = () => { + setSearchQuery(''); + setFilterRole('all'); + }; + const viewMemberDetails = async (member: Member) => { setSelectedMember(member); 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 */} -
+
setSearchQuery(e.target.value)} className="pl-10 border-2" /> + {searchQuery && ( + + )}
- -
-
- Menampilkan {filteredMembers.length} dari {members.length} member + +
+ Role: + + + + {(searchQuery || filterRole !== 'all') && ( + + )} +
+ +

+ Menampilkan {filteredMembers.length} dari {members.length} member +

diff --git a/src/pages/admin/AdminOrders.tsx b/src/pages/admin/AdminOrders.tsx index c6b6bd3..1782a01 100644 --- a/src/pages/admin/AdminOrders.tsx +++ b/src/pages/admin/AdminOrders.tsx @@ -13,9 +13,8 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { getPaymentStatusLabel, getPaymentStatusColor, canRefundOrder, canCancelOrder, canMarkAsPaid } from "@/lib/statusHelpers"; import { convertToCSV, downloadCSV, formatExportDate, formatExportIDR } from "@/lib/exportCSV"; @@ -104,6 +103,11 @@ export default function AdminOrders() { return matchesSearch && matchesStatus; }); + const clearFilters = () => { + setSearchQuery(''); + setFilterStatus('all'); + }; + const viewOrderDetails = async (order: Order) => { setSelectedOrder(order); 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 */} -
+
setSearchQuery(e.target.value)} className="pl-10 border-2" /> + {searchQuery && ( + + )}
- -
-
- Menampilkan {filteredOrders.length} dari {orders.length} order + +
+ Status: + + + + + {(searchQuery || filterStatus !== 'all') && ( + + )} +
+ +

+ Menampilkan {filteredOrders.length} dari {orders.length} order +

diff --git a/src/pages/admin/AdminProducts.tsx b/src/pages/admin/AdminProducts.tsx index 3171564..b4bc71d 100644 --- a/src/pages/admin/AdminProducts.tsx +++ b/src/pages/admin/AdminProducts.tsx @@ -15,7 +15,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@ 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, Search } from 'lucide-react'; +import { Plus, Pencil, Trash2, Search, X } from 'lucide-react'; import { CurriculumEditor } from '@/components/admin/CurriculumEditor'; import { RichTextEditor } from '@/components/RichTextEditor'; import { formatIDR } from '@/lib/format'; @@ -90,6 +90,15 @@ export default function AdminProducts() { 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 handleEdit = (product: Product) => { @@ -187,51 +196,86 @@ export default function AdminProducts() { {/* Search and Filter */} -
+
{/* Search */} -
-
- - setSearchQuery(e.target.value)} - className="pl-10 border-2" - /> -
+
+ + setSearchQuery(e.target.value)} + className="pl-10 border-2" + /> + {searchQuery && ( + + )}
{/* Type Filter */} - +
+ Tipe: + {productTypes.map((type) => ( + + ))} +
{/* Status Filter */} - -
+
+ Status: + + + + {(searchQuery || filterType !== 'all' || filterStatus !== 'all') && ( + + )} +
- {/* Result count */} -
- Menampilkan {filteredProducts.length} dari {products.length} produk + {/* Result count */} +

+ Menampilkan {filteredProducts.length} dari {products.length} produk +

diff --git a/src/pages/admin/AdminReviews.tsx b/src/pages/admin/AdminReviews.tsx index 6b232f1..8e731d2 100644 --- a/src/pages/admin/AdminReviews.tsx +++ b/src/pages/admin/AdminReviews.tsx @@ -6,12 +6,11 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Skeleton } from "@/components/ui/skeleton"; 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 { id: string; @@ -125,6 +124,10 @@ export default function AdminReviews() { return true; }); + const clearFilters = () => { + setFilter({ ...filter, type: 'all', status: 'all', search: '' }); + }; + const pendingReviews = reviews.filter((r) => !r.is_approved); const renderStars = (rating: number) => ( @@ -174,8 +177,8 @@ export default function AdminReviews() { -
-
+
+
setFilter({ ...filter, search: e.target.value })} className="pl-10 border-2" /> + {filter.search && ( + + )}
- - -
-
- Menampilkan {filteredReviews.length} dari {reviews.length} ulasan +
+ Tipe: + + + + + +
+ +
+ Status: + + + + {(filter.search || filter.type !== 'all' || filter.status !== 'all') && ( + + )} +
+ +

+ Menampilkan {filteredReviews.length} dari {reviews.length} ulasan +