From b955445dea31e9a4f200395c3d8833a5bf26dda5 Mon Sep 17 00:00:00 2001 From: dwindown Date: Fri, 26 Dec 2025 16:10:26 +0700 Subject: [PATCH] Add filters to Member Access and Member Orders pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Member Access: Add product kind filter pills + search input - Member Orders: Add order status filter pills with counts - Both pages show results count and empty state when no results - Include reset filter button and clear search functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/pages/member/MemberAccess.tsx | 107 +++++++++++++++++++++++++++++- src/pages/member/MemberOrders.tsx | 106 ++++++++++++++++++++++++----- 2 files changed, 194 insertions(+), 19 deletions(-) diff --git a/src/pages/member/MemberAccess.tsx b/src/pages/member/MemberAccess.tsx index 38aede5..d8511ca 100644 --- a/src/pages/member/MemberAccess.tsx +++ b/src/pages/member/MemberAccess.tsx @@ -7,7 +7,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; -import { Video, Calendar, BookOpen, ArrowRight } from 'lucide-react'; +import { Video, Calendar, BookOpen, ArrowRight, Search, X } from 'lucide-react'; +import { Input } from '@/components/ui/input'; interface UserAccess { id: string; @@ -29,6 +30,8 @@ export default function MemberAccess() { const navigate = useNavigate(); const [access, setAccess] = useState([]); const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedType, setSelectedType] = useState('all'); useEffect(() => { if (!authLoading && !user) navigate('/auth'); @@ -81,6 +84,38 @@ export default function MemberAccess() { setLoading(false); }; + const getTypeLabel = (type: string) => { + switch (type) { + case 'consulting': return 'Konsultasi'; + case 'webinar': return 'Webinar'; + case 'bootcamp': return 'Bootcamp'; + default: return type; + } + }; + + // Strip HTML tags for search + const stripHtml = (html: string) => { + const tmp = document.createElement('div'); + tmp.innerHTML = html; + return tmp.textContent || tmp.innerText || ''; + }; + + // Filter access based on search and type + const filteredAccess = access.filter((item) => { + const matchesSearch = item.product.title.toLowerCase().includes(searchQuery.toLowerCase()) || + stripHtml(item.product.description).toLowerCase().includes(searchQuery.toLowerCase()); + const matchesType = selectedType === 'all' || item.product.type === selectedType; + return matchesSearch && matchesType; + }); + + // Get unique product types for filter + const productTypes = ['all', ...Array.from(new Set(access.map(a => a.product.type)))]; + + const clearFilters = () => { + setSearchQuery(''); + setSelectedType('all'); + }; + const renderAccessActions = (item: UserAccess) => { switch (item.product.type) { case 'consulting': @@ -157,6 +192,63 @@ export default function MemberAccess() {

Akses Saya

Semua produk yang dapat Anda akses

+ {/* Search and Filter */} + {!loading && access.length > 0 && ( +
+ {/* Search Bar */} +
+ + setSearchQuery(e.target.value)} + className="pl-10 border-2" + /> + {searchQuery && ( + + )} +
+ + {/* Category Filter */} +
+ Kategori: + {productTypes.map((type) => ( + + ))} + {(searchQuery || selectedType !== 'all') && ( + + )} +
+ + {/* Results Count */} +

+ Menampilkan {filteredAccess.length} dari {access.length} produk +

+
+ )} + {access.length === 0 ? ( @@ -168,7 +260,18 @@ export default function MemberAccess() { ) : (
- {access.map((item) => ( + {filteredAccess.length === 0 && access.length > 0 && ( +
+ +

Tidak Ada Produk Ditemukan

+

Coba kata kunci atau kategori lain.

+ +
+ )} + {filteredAccess.map((item) => (
diff --git a/src/pages/member/MemberOrders.tsx b/src/pages/member/MemberOrders.tsx index efc0f39..4307575 100644 --- a/src/pages/member/MemberOrders.tsx +++ b/src/pages/member/MemberOrders.tsx @@ -5,9 +5,10 @@ import { useAuth } from "@/hooks/useAuth"; import { supabase } from "@/integrations/supabase/client"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { formatIDR, formatDate } from "@/lib/format"; -import { ChevronRight } from "lucide-react"; +import { ChevronRight, X } from "lucide-react"; interface Order { id: string; @@ -23,6 +24,7 @@ export default function MemberOrders() { const navigate = useNavigate(); const [orders, setOrders] = useState([]); const [loading, setLoading] = useState(true); + const [selectedStatus, setSelectedStatus] = useState('all'); useEffect(() => { if (!authLoading && !user) navigate("/auth"); @@ -39,20 +41,7 @@ export default function MemberOrders() { setLoading(false); }; - const getStatusColor = (status: string) => { - switch (status) { - case "paid": - return "bg-brand-accent text-white"; - case "pending": - return "bg-amber-500 text-white"; - case "cancelled": - return "bg-destructive text-white"; - default: - return "bg-secondary"; - } - }; - - const getPaymentStatusLabel = (status: string | null) => { + const getStatusLabel = (status: string | null) => { switch (status) { case "paid": return "Lunas"; @@ -67,6 +56,39 @@ export default function MemberOrders() { } }; + // Filter orders based on status + const filteredOrders = orders.filter((order) => { + const status = order.payment_status || order.status; + return selectedStatus === 'all' || status === selectedStatus; + }); + + // Get order counts by status + const statusCounts = orders.reduce((acc, order) => { + const status = order.payment_status || order.status; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, {} as Record); + + // Available status filters + const statusFilters = ['all', 'paid', 'pending', 'failed', 'cancelled']; + + const clearFilters = () => { + setSelectedStatus('all'); + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "paid": + return "bg-brand-accent text-white"; + case "pending": + return "bg-amber-500 text-white"; + case "cancelled": + return "bg-destructive text-white"; + default: + return "bg-secondary"; + } + }; + if (authLoading || loading) { return ( @@ -88,6 +110,46 @@ export default function MemberOrders() {

Riwayat Order

Semua pesanan Anda

+ {/* Status Filter */} + {!loading && orders.length > 0 && ( +
+
+ Status: + {statusFilters.map((status) => { + const count = status === 'all' ? orders.length : (statusCounts[status] || 0); + return ( + + ); + })} + {selectedStatus !== 'all' && ( + + )} +
+ + {/* Results Count */} +

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

+
+ )} + {orders.length === 0 ? ( @@ -96,7 +158,17 @@ export default function MemberOrders() { ) : (
- {orders.map((order) => ( + {filteredOrders.length === 0 && orders.length > 0 && ( +
+

Tidak Ada Pesanan

+

Tidak ada pesanan dengan status yang dipilih.

+ +
+ )} + {filteredOrders.map((order) => (
- {getPaymentStatusLabel(order.payment_status || order.status)} + {getStatusLabel(order.payment_status || order.status)} {formatIDR(order.total_amount)}