From 3e418759a1c1bb3cb899d27c119cb5f0a2b9bd1b Mon Sep 17 00:00:00 2001 From: dwindown Date: Sun, 28 Dec 2025 00:07:07 +0700 Subject: [PATCH] feat: add search and filter to admin pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add search and filter (type, status) to AdminProducts - Add search to AdminBootcamp - Change mobile admin nav "Pesanan" to "Order" - Show result counts for filtered data - Handle empty states with helpful messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/components/AppLayout.tsx | 2 +- src/pages/admin/AdminBootcamp.tsx | 43 +++++++++++++--- src/pages/admin/AdminProducts.tsx | 84 ++++++++++++++++++++++++++++--- 3 files changed, 114 insertions(+), 15 deletions(-) diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 21b5e54..6dc61fe 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -62,7 +62,7 @@ const mobileUserNav: NavItem[] = [ const mobileAdminNav: NavItem[] = [ { label: 'Dashboard', href: '/admin', icon: LayoutDashboard }, { label: 'Produk', href: '/admin/products', icon: Package }, - { label: 'Pesanan', href: '/admin/orders', icon: Receipt }, + { label: 'Order', href: '/admin/orders', icon: Receipt }, { label: 'Pengguna', href: '/admin/members', icon: Users }, ]; diff --git a/src/pages/admin/AdminBootcamp.tsx b/src/pages/admin/AdminBootcamp.tsx index 0dd6734..3c45b52 100644 --- a/src/pages/admin/AdminBootcamp.tsx +++ b/src/pages/admin/AdminBootcamp.tsx @@ -8,7 +8,8 @@ import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; import { CurriculumEditor } from '@/components/admin/CurriculumEditor'; -import { BookOpen } from 'lucide-react'; +import { BookOpen, Search } from 'lucide-react'; +import { Input } from '@/components/ui/input'; interface Product { id: string; @@ -21,6 +22,7 @@ export default function AdminBootcamp() { const navigate = useNavigate(); const [bootcamps, setBootcamps] = useState([]); const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); useEffect(() => { if (!authLoading) { @@ -40,6 +42,11 @@ export default function AdminBootcamp() { setLoading(false); }; + // Filter bootcamps based on search + const filteredBootcamps = bootcamps.filter((bootcamp) => + bootcamp.title.toLowerCase().includes(searchQuery.toLowerCase()) + ); + if (authLoading || loading) { return ( @@ -62,18 +69,40 @@ export default function AdminBootcamp() { - {bootcamps.length === 0 ? ( + {/* Search */} + + +
+ + setSearchQuery(e.target.value)} + className="pl-10 border-2" + /> +
+
+ Menampilkan {filteredBootcamps.length} dari {bootcamps.length} bootcamp +
+
+
+ + {filteredBootcamps.length === 0 ? ( -

Belum ada bootcamp. Buat produk dengan tipe bootcamp terlebih dahulu.

- +

+ {searchQuery ? 'Tidak ada bootcamp yang cocok dengan pencarian' : 'Belum ada bootcamp. Buat produk dengan tipe bootcamp terlebih dahulu.'} +

+ {!searchQuery && ( + + )}
) : ( - {bootcamps.map((bootcamp) => ( + {filteredBootcamps.map((bootcamp) => ( {bootcamp.title} diff --git a/src/pages/admin/AdminProducts.tsx b/src/pages/admin/AdminProducts.tsx index edc18b8..3171564 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 } from 'lucide-react'; +import { Plus, Pencil, Trash2, Search } from 'lucide-react'; import { CurriculumEditor } from '@/components/admin/CurriculumEditor'; import { RichTextEditor } from '@/components/RichTextEditor'; import { formatIDR } from '@/lib/format'; @@ -61,6 +61,9 @@ export default function AdminProducts() { const [form, setForm] = useState(emptyProduct); const [saving, setSaving] = useState(false); const [activeTab, setActiveTab] = useState('details'); + const [searchQuery, setSearchQuery] = useState(''); + const [filterType, setFilterType] = useState('all'); + const [filterStatus, setFilterStatus] = useState('all'); useEffect(() => { if (!authLoading) { @@ -76,6 +79,17 @@ export default function AdminProducts() { setLoading(false); }; + // Filter products based on search and filters + const filteredProducts = products.filter((product) => { + const matchesSearch = product.title.toLowerCase().includes(searchQuery.toLowerCase()) || + product.description.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesType = filterType === 'all' || product.type === filterType; + const matchesStatus = filterStatus === 'all' || + (filterStatus === 'active' && product.is_active) || + (filterStatus === 'inactive' && !product.is_active); + return matchesSearch && matchesType && matchesStatus; + }); + const generateSlug = (title: string) => title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); const handleEdit = (product: Product) => { @@ -170,6 +184,58 @@ export default function AdminProducts() { + {/* Search and Filter */} + + +
+ {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-10 border-2" + /> +
+
+ + {/* Type Filter */} + + + {/* Status Filter */} + +
+ + {/* Result count */} +
+ Menampilkan {filteredProducts.length} dari {products.length} produk +
+
+
+ {/* Desktop Table */} @@ -185,7 +251,7 @@ export default function AdminProducts() { - {products.map((product) => ( + {filteredProducts.map((product) => ( {product.title} {product.type} @@ -214,10 +280,12 @@ export default function AdminProducts() { ))} - {products.length === 0 && ( + {filteredProducts.length === 0 && ( - Belum ada produk + {searchQuery || filterType !== 'all' || filterStatus !== 'all' + ? 'Tidak ada produk yang cocok dengan filter' + : 'Belum ada produk'} )} @@ -229,7 +297,7 @@ export default function AdminProducts() { {/* Mobile Card Layout */}
- {products.map((product) => ( + {filteredProducts.map((product) => (
@@ -268,9 +336,11 @@ export default function AdminProducts() {
))} - {products.length === 0 && ( + {filteredProducts.length === 0 && (
- Belum ada produk + {searchQuery || filterType !== 'all' || filterStatus !== 'all' + ? 'Tidak ada produk yang cocok dengan filter' + : 'Belum ada produk'}
)}