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 { 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { formatDateTime } from "@/lib/format";
|
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";
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
interface Member {
|
interface Member {
|
||||||
@@ -36,6 +38,8 @@ export default function AdminMembers() {
|
|||||||
const [selectedMember, setSelectedMember] = useState<Member | null>(null);
|
const [selectedMember, setSelectedMember] = useState<Member | null>(null);
|
||||||
const [memberAccess, setMemberAccess] = useState<UserAccess[]>([]);
|
const [memberAccess, setMemberAccess] = useState<UserAccess[]>([]);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [filterRole, setFilterRole] = useState<string>('all');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authLoading) {
|
if (!authLoading) {
|
||||||
@@ -60,6 +64,20 @@ export default function AdminMembers() {
|
|||||||
setLoading(false);
|
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) => {
|
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);
|
||||||
@@ -102,7 +120,49 @@ export default function AdminMembers() {
|
|||||||
<h1 className="text-4xl font-bold mb-2">Manajemen Member</h1>
|
<h1 className="text-4xl font-bold mb-2">Manajemen Member</h1>
|
||||||
<p className="text-muted-foreground mb-8">Kelola semua pengguna</p>
|
<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">
|
<CardContent className="p-0">
|
||||||
{/* Desktop Table */}
|
{/* Desktop Table */}
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -117,7 +177,7 @@ export default function AdminMembers() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{members.map((member) => (
|
{filteredMembers.map((member) => (
|
||||||
<TableRow key={member.id}>
|
<TableRow key={member.id}>
|
||||||
<TableCell>{member.email || "-"}</TableCell>
|
<TableCell>{member.email || "-"}</TableCell>
|
||||||
<TableCell>{member.name || "-"}</TableCell>
|
<TableCell>{member.name || "-"}</TableCell>
|
||||||
@@ -144,13 +204,6 @@ export default function AdminMembers() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
{members.length === 0 && (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
|
||||||
Belum ada member
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +212,7 @@ export default function AdminMembers() {
|
|||||||
|
|
||||||
{/* Mobile Card Layout */}
|
{/* Mobile Card Layout */}
|
||||||
<div className="md:hidden space-y-3">
|
<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 key={member.id} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card shadow-sm">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
@@ -198,12 +251,9 @@ export default function AdminMembers() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{members.length === 0 && (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
Belum ada member
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<DialogContent className="max-w-lg border-2 border-border">
|
<DialogContent className="max-w-lg border-2 border-border">
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ 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 } from "lucide-react";
|
import { Eye, CheckCircle, XCircle, Video, ExternalLink, Trash2, AlertTriangle, RefreshCw, Link as LinkIcon, Download, Search } 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";
|
||||||
|
|
||||||
@@ -68,6 +69,8 @@ export default function AdminOrders() {
|
|||||||
const [newMeetLink, setNewMeetLink] = useState("");
|
const [newMeetLink, setNewMeetLink] = useState("");
|
||||||
const [creatingMeetLink, setCreatingMeetLink] = useState(false);
|
const [creatingMeetLink, setCreatingMeetLink] = useState(false);
|
||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [filterStatus, setFilterStatus] = useState<string>('all');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authLoading) {
|
if (!authLoading) {
|
||||||
@@ -86,6 +89,21 @@ export default function AdminOrders() {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Filter orders based on search and status
|
||||||
|
const filteredOrders = orders.filter((order) => {
|
||||||
|
const matchesSearch =
|
||||||
|
order.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
order.profile?.email?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
|
||||||
|
const matchesStatus =
|
||||||
|
filterStatus === 'all' ||
|
||||||
|
(filterStatus === 'paid' && order.payment_status === 'paid') ||
|
||||||
|
(filterStatus === 'pending' && order.payment_status === 'pending') ||
|
||||||
|
(filterStatus === 'refunded' && order.refunded_at);
|
||||||
|
|
||||||
|
return matchesSearch && matchesStatus;
|
||||||
|
});
|
||||||
|
|
||||||
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);
|
||||||
@@ -374,7 +392,50 @@ export default function AdminOrders() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 ID order atau email..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="pl-10 border-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Select value={filterStatus} onValueChange={setFilterStatus}>
|
||||||
|
<SelectTrigger className="border-2">
|
||||||
|
<SelectValue placeholder="Filter status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Semua Status</SelectItem>
|
||||||
|
<SelectItem value="paid">Lunas</SelectItem>
|
||||||
|
<SelectItem value="pending">Pending</SelectItem>
|
||||||
|
<SelectItem value="refunded">Refunded</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Menampilkan {filteredOrders.length} dari {orders.length} order
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{filteredOrders.length === 0 ? (
|
||||||
|
<Card className="border-2 border-border">
|
||||||
|
<CardContent className="py-12 text-center">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{searchQuery || filterStatus !== 'all'
|
||||||
|
? 'Tidak ada order yang cocok dengan filter'
|
||||||
|
: 'Belum ada order'}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Card className="border-2 border-border hidden md:block">
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
{/* Desktop Table */}
|
{/* Desktop Table */}
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
@@ -391,7 +452,7 @@ export default function AdminOrders() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{orders.map((order) => (
|
{filteredOrders.map((order) => (
|
||||||
<TableRow key={order.id}>
|
<TableRow key={order.id}>
|
||||||
<TableCell className="font-mono text-sm">{order.id.slice(0, 8)}</TableCell>
|
<TableCell className="font-mono text-sm">{order.id.slice(0, 8)}</TableCell>
|
||||||
<TableCell>{order.profile?.email || "-"}</TableCell>
|
<TableCell>{order.profile?.email || "-"}</TableCell>
|
||||||
@@ -406,13 +467,6 @@ export default function AdminOrders() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
{orders.length === 0 && (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
|
|
||||||
Belum ada order
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
@@ -421,7 +475,7 @@ export default function AdminOrders() {
|
|||||||
|
|
||||||
{/* Mobile Card Layout */}
|
{/* Mobile Card Layout */}
|
||||||
<div className="md:hidden space-y-3">
|
<div className="md:hidden space-y-3">
|
||||||
{orders.map((order) => (
|
{filteredOrders.map((order) => (
|
||||||
<div key={order.id} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card shadow-sm">
|
<div key={order.id} className="border-2 border-border rounded-lg p-4 space-y-3 bg-card shadow-sm">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
@@ -453,14 +507,11 @@ export default function AdminOrders() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{orders.length === 0 && (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
Belum ada order
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<DialogContent className="max-w-lg border-2 border-border">
|
<DialogContent className="max-w-lg border-2 border-border">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Detail Order</DialogTitle>
|
<DialogTitle>Detail Order</DialogTitle>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
|
|||||||
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 } from "lucide-react";
|
import { Star, Check, X, Edit, Trash2, Search } from "lucide-react";
|
||||||
|
|
||||||
interface Review {
|
interface Review {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -30,7 +30,7 @@ interface Review {
|
|||||||
export default function AdminReviews() {
|
export default function AdminReviews() {
|
||||||
const [reviews, setReviews] = useState<Review[]>([]);
|
const [reviews, setReviews] = useState<Review[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [filter, setFilter] = useState({ type: "all", status: "all" });
|
const [filter, setFilter] = useState({ type: "all", status: "all", search: "" });
|
||||||
const [editReview, setEditReview] = useState<Review | null>(null);
|
const [editReview, setEditReview] = useState<Review | null>(null);
|
||||||
const [editForm, setEditForm] = useState({ title: "", body: "" });
|
const [editForm, setEditForm] = useState({ title: "", body: "" });
|
||||||
|
|
||||||
@@ -112,6 +112,16 @@ export default function AdminReviews() {
|
|||||||
if (filter.type !== "all" && r.type !== filter.type) return false;
|
if (filter.type !== "all" && r.type !== filter.type) return false;
|
||||||
if (filter.status === "approved" && !r.is_approved) return false;
|
if (filter.status === "approved" && !r.is_approved) return false;
|
||||||
if (filter.status === "pending" && r.is_approved) return false;
|
if (filter.status === "pending" && r.is_approved) return false;
|
||||||
|
if (filter.search) {
|
||||||
|
const query = filter.search.toLowerCase();
|
||||||
|
const matchesSearch =
|
||||||
|
r.title.toLowerCase().includes(query) ||
|
||||||
|
r.body.toLowerCase().includes(query) ||
|
||||||
|
r.profiles?.name?.toLowerCase().includes(query) ||
|
||||||
|
r.profiles?.email?.toLowerCase().includes(query) ||
|
||||||
|
r.products?.title.toLowerCase().includes(query);
|
||||||
|
if (!matchesSearch) return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -162,31 +172,47 @@ export default function AdminReviews() {
|
|||||||
<p className="text-muted-foreground">Kelola ulasan dari member</p>
|
<p className="text-muted-foreground">Kelola ulasan dari member</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-4 flex-wrap">
|
<Card className="border-2 border-border">
|
||||||
<Select value={filter.type} onValueChange={(v) => setFilter({ ...filter, type: v })}>
|
<CardContent className="pt-6">
|
||||||
<SelectTrigger className="w-40 border-2">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<SelectValue placeholder="Tipe" />
|
<div className="relative md:col-span-1">
|
||||||
</SelectTrigger>
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
<SelectContent>
|
<Input
|
||||||
<SelectItem value="all">Semua Tipe</SelectItem>
|
placeholder="Cari ulasan..."
|
||||||
<SelectItem value="consulting">Konsultasi</SelectItem>
|
value={filter.search}
|
||||||
<SelectItem value="bootcamp">Bootcamp</SelectItem>
|
onChange={(e) => setFilter({ ...filter, search: e.target.value })}
|
||||||
<SelectItem value="webinar">Webinar</SelectItem>
|
className="pl-10 border-2"
|
||||||
<SelectItem value="general">Umum</SelectItem>
|
/>
|
||||||
</SelectContent>
|
</div>
|
||||||
</Select>
|
<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 })}>
|
<Select value={filter.status} onValueChange={(v) => setFilter({ ...filter, status: v })}>
|
||||||
<SelectTrigger className="w-40 border-2">
|
<SelectTrigger className="border-2">
|
||||||
<SelectValue placeholder="Status" />
|
<SelectValue placeholder="Status" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Semua Status</SelectItem>
|
<SelectItem value="all">Semua Status</SelectItem>
|
||||||
<SelectItem value="pending">Menunggu</SelectItem>
|
<SelectItem value="pending">Menunggu</SelectItem>
|
||||||
<SelectItem value="approved">Disetujui</SelectItem>
|
<SelectItem value="approved">Disetujui</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Menampilkan {filteredReviews.length} dari {reviews.length} ulasan
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Tabs defaultValue="list">
|
<Tabs defaultValue="list">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
@@ -202,9 +228,15 @@ export default function AdminReviews() {
|
|||||||
<Card className="border-2 border-border">
|
<Card className="border-2 border-border">
|
||||||
<CardContent className="py-12 text-center">
|
<CardContent className="py-12 text-center">
|
||||||
<Star className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
|
<Star className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
|
||||||
<h3 className="text-lg font-semibold mb-2">Belum ada ulasan</h3>
|
<h3 className="text-lg font-semibold mb-2">
|
||||||
|
{filter.search || filter.type !== 'all' || filter.status !== 'all'
|
||||||
|
? 'Tidak ada ulasan yang cocok'
|
||||||
|
: 'Belum ada ulasan'}
|
||||||
|
</h3>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Ulasan dari member akan muncul di sini setelah mereka mengirimkan ulasan.
|
{filter.search || filter.type !== 'all' || filter.status !== 'all'
|
||||||
|
? 'Coba ubah filter atau kata kunci pencarian'
|
||||||
|
: 'Ulasan dari member akan muncul di sini setelah mereka mengirimkan ulasan.'}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user