Add search and filter features to admin pages
Implement comprehensive search and filter functionality across all admin dashboard pages: - AdminProducts: Search by title/description, filter by type/status - AdminBootcamp: Search by bootcamp title - AdminConsulting: Search by client name, email, category, or order ID - AdminOrders: Search by order ID/email, filter by payment status - AdminMembers: Search by name/email, filter by role (admin/member) - AdminReviews: Enhanced with search by title, body, reviewer, product; existing filters maintained Features: - Consistent UI pattern with search icon and border styling - Result count display showing filtered vs total items - Contextual empty state messages - Responsive grid layout for filters - Real-time filtering without page reload Also fix admin page reload redirect race condition where pages would redirect to /dashboard instead of staying on current page after reload. The loading state now properly waits for admin role check to complete. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,10 @@ import { Badge } from "@/components/ui/badge";
|
||||
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 } from "lucide-react";
|
||||
import { Eye, Shield, ShieldOff, Search } from "lucide-react";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
|
||||
interface Member {
|
||||
@@ -36,6 +38,8 @@ export default function AdminMembers() {
|
||||
const [selectedMember, setSelectedMember] = useState<Member | null>(null);
|
||||
const [memberAccess, setMemberAccess] = useState<UserAccess[]>([]);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterRole, setFilterRole] = useState<string>('all');
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading) {
|
||||
@@ -60,6 +64,20 @@ export default function AdminMembers() {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// Filter members based on search and role
|
||||
const filteredMembers = members.filter((member) => {
|
||||
const matchesSearch =
|
||||
member.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
member.email?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
const matchesRole =
|
||||
filterRole === 'all' ||
|
||||
(filterRole === 'admin' && adminIds.has(member.id)) ||
|
||||
(filterRole === 'member' && !adminIds.has(member.id));
|
||||
|
||||
return matchesSearch && matchesRole;
|
||||
});
|
||||
|
||||
const viewMemberDetails = async (member: Member) => {
|
||||
setSelectedMember(member);
|
||||
const { data } = await supabase.from("user_access").select("*, product:products(title)").eq("user_id", member.id);
|
||||
@@ -102,7 +120,49 @@ export default function AdminMembers() {
|
||||
<h1 className="text-4xl font-bold mb-2">Manajemen Member</h1>
|
||||
<p className="text-muted-foreground mb-8">Kelola semua pengguna</p>
|
||||
|
||||
<Card className="border-2 border-border hidden md:block">
|
||||
{/* Search & Filter */}
|
||||
<Card className="border-2 border-border mb-6">
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Cari nama atau email..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 border-2"
|
||||
/>
|
||||
</div>
|
||||
<Select value={filterRole} onValueChange={setFilterRole}>
|
||||
<SelectTrigger className="border-2">
|
||||
<SelectValue placeholder="Filter role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Semua Role</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-muted-foreground">
|
||||
Menampilkan {filteredMembers.length} dari {members.length} member
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{filteredMembers.length === 0 ? (
|
||||
<Card className="border-2 border-border">
|
||||
<CardContent className="py-12 text-center">
|
||||
<p className="text-muted-foreground">
|
||||
{searchQuery || filterRole !== 'all'
|
||||
? 'Tidak ada member yang cocok dengan filter'
|
||||
: 'Belum ada member'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
<Card className="border-2 border-border hidden md:block">
|
||||
<CardContent className="p-0">
|
||||
{/* Desktop Table */}
|
||||
<div className="overflow-x-auto">
|
||||
@@ -117,7 +177,7 @@ export default function AdminMembers() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{members.map((member) => (
|
||||
{filteredMembers.map((member) => (
|
||||
<TableRow key={member.id}>
|
||||
<TableCell>{member.email || "-"}</TableCell>
|
||||
<TableCell>{member.name || "-"}</TableCell>
|
||||
@@ -144,13 +204,6 @@ export default function AdminMembers() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{members.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
||||
Belum ada member
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
@@ -159,7 +212,7 @@ export default function AdminMembers() {
|
||||
|
||||
{/* Mobile Card Layout */}
|
||||
<div className="md:hidden space-y-3">
|
||||
{members.map((member) => (
|
||||
{filteredMembers.map((member) => (
|
||||
<div key={member.id} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card shadow-sm">
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
@@ -198,12 +251,9 @@ export default function AdminMembers() {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{members.length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
Belum ada member
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-lg border-2 border-border">
|
||||
|
||||
Reference in New Issue
Block a user