Replace dropdown filters with tab buttons across admin pages
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string>('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;
|
||||
// Search filter
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
const firstSlot = order.slots[0];
|
||||
|
||||
return (
|
||||
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)
|
||||
);
|
||||
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,6 +393,8 @@ export default function AdminConsulting() {
|
||||
{/* Search */}
|
||||
<Card className="border-2 border-border mb-6">
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -386,9 +403,76 @@ export default function AdminConsulting() {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 border-2"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-muted-foreground">
|
||||
|
||||
{/* Status Filter */}
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm font-medium text-muted-foreground">Status:</span>
|
||||
<Button
|
||||
variant={filterStatus === 'all' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('all')}
|
||||
className={filterStatus === 'all' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Semua
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterStatus === 'pending_payment' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('pending_payment')}
|
||||
className={filterStatus === 'pending_payment' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Menunggu Pembayaran
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterStatus === 'confirmed' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('confirmed')}
|
||||
className={filterStatus === 'confirmed' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Dikonfirmasi
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterStatus === 'completed' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('completed')}
|
||||
className={filterStatus === 'completed' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Selesai
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterStatus === 'cancelled' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('cancelled')}
|
||||
className={filterStatus === 'cancelled' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Dibatalkan
|
||||
</Button>
|
||||
{(searchQuery || filterStatus !== 'all') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => { setSearchQuery(''); setFilterStatus('all'); }}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Result count */}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Menampilkan {filteredGroupedOrders.length} dari {groupedOrders.length} jadwal konsultasi
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -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 */}
|
||||
<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="space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -132,20 +136,58 @@ export default function AdminMembers() {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 border-2"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</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 className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm font-medium text-muted-foreground">Role:</span>
|
||||
<Button
|
||||
variant={filterRole === 'all' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterRole('all')}
|
||||
className={filterRole === 'all' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Semua
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterRole === 'admin' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterRole('admin')}
|
||||
className={filterRole === 'admin' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Admin
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterRole === 'member' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterRole('member')}
|
||||
className={filterRole === 'member' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Member
|
||||
</Button>
|
||||
{(searchQuery || filterRole !== 'all') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearFilters}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-muted-foreground">
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Menampilkan {filteredMembers.length} dari {members.length} member
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -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 */}
|
||||
<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="space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -404,21 +408,66 @@ export default function AdminOrders() {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 border-2"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</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 className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm font-medium text-muted-foreground">Status:</span>
|
||||
<Button
|
||||
variant={filterStatus === 'all' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('all')}
|
||||
className={filterStatus === 'all' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Semua
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterStatus === 'paid' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('paid')}
|
||||
className={filterStatus === 'paid' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Lunas
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterStatus === 'pending' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('pending')}
|
||||
className={filterStatus === 'pending' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Pending
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterStatus === 'refunded' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('refunded')}
|
||||
className={filterStatus === 'refunded' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Refunded
|
||||
</Button>
|
||||
{(searchQuery || filterStatus !== 'all') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearFilters}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-muted-foreground">
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Menampilkan {filteredOrders.length} dari {orders.length} order
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -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,9 +196,8 @@ export default function AdminProducts() {
|
||||
{/* Search and Filter */}
|
||||
<Card className="border-2 border-border mb-6">
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-4">
|
||||
{/* Search */}
|
||||
<div className="md:col-span-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
@@ -198,40 +206,76 @@ export default function AdminProducts() {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 border-2"
|
||||
/>
|
||||
</div>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Type Filter */}
|
||||
<Select value={filterType} onValueChange={setFilterType}>
|
||||
<SelectTrigger className="border-2">
|
||||
<SelectValue placeholder="Filter tipe" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Semua Tipe</SelectItem>
|
||||
<SelectItem value="webinar">Webinar</SelectItem>
|
||||
<SelectItem value="course">Course</SelectItem>
|
||||
<SelectItem value="consulting">Consulting</SelectItem>
|
||||
<SelectItem value="ebook">E-book</SelectItem>
|
||||
<SelectItem value="bootcamp">Bootcamp</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm font-medium text-muted-foreground">Tipe:</span>
|
||||
{productTypes.map((type) => (
|
||||
<Button
|
||||
key={type}
|
||||
variant={filterType === type ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterType(type)}
|
||||
className={filterType === type ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
{type === 'all' ? 'Semua' : type === 'webinar' ? 'Webinar' : type === 'bootcamp' ? 'Bootcamp' : type}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Status Filter */}
|
||||
<Select value={filterStatus} onValueChange={setFilterStatus}>
|
||||
<SelectTrigger className="border-2">
|
||||
<SelectValue placeholder="Filter status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Semua Status</SelectItem>
|
||||
<SelectItem value="active">Aktif</SelectItem>
|
||||
<SelectItem value="inactive">Nonaktif</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm font-medium text-muted-foreground">Status:</span>
|
||||
<Button
|
||||
variant={filterStatus === 'all' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('all')}
|
||||
className={filterStatus === 'all' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Semua
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterStatus === 'active' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('active')}
|
||||
className={filterStatus === 'active' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Aktif
|
||||
</Button>
|
||||
<Button
|
||||
variant={filterStatus === 'inactive' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilterStatus('inactive')}
|
||||
className={filterStatus === 'inactive' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Nonaktif
|
||||
</Button>
|
||||
{(searchQuery || filterType !== 'all' || filterStatus !== 'all') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearFilters}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Result count */}
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Menampilkan {filteredProducts.length} dari {products.length} produk
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
<Card className="border-2 border-border">
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="relative md:col-span-1">
|
||||
<div className="space-y-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 ulasan..."
|
||||
@@ -183,33 +186,102 @@ export default function AdminReviews() {
|
||||
onChange={(e) => setFilter({ ...filter, search: e.target.value })}
|
||||
className="pl-10 border-2"
|
||||
/>
|
||||
{filter.search && (
|
||||
<button
|
||||
onClick={() => setFilter({ ...filter, search: '' })}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<XIcon className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<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 })}>
|
||||
<SelectTrigger className="border-2">
|
||||
<SelectValue placeholder="Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Semua Status</SelectItem>
|
||||
<SelectItem value="pending">Menunggu</SelectItem>
|
||||
<SelectItem value="approved">Disetujui</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm font-medium text-muted-foreground">Tipe:</span>
|
||||
<Button
|
||||
variant={filter.type === 'all' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilter({ ...filter, type: 'all' })}
|
||||
className={filter.type === 'all' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Semua
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter.type === 'consulting' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilter({ ...filter, type: 'consulting' })}
|
||||
className={filter.type === 'consulting' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Konsultasi
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter.type === 'bootcamp' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilter({ ...filter, type: 'bootcamp' })}
|
||||
className={filter.type === 'bootcamp' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Bootcamp
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter.type === 'webinar' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilter({ ...filter, type: 'webinar' })}
|
||||
className={filter.type === 'webinar' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Webinar
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter.type === 'general' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilter({ ...filter, type: 'general' })}
|
||||
className={filter.type === 'general' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Umum
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-muted-foreground">
|
||||
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<span className="text-sm font-medium text-muted-foreground">Status:</span>
|
||||
<Button
|
||||
variant={filter.status === 'all' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilter({ ...filter, status: 'all' })}
|
||||
className={filter.status === 'all' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Semua
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter.status === 'pending' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilter({ ...filter, status: 'pending' })}
|
||||
className={filter.status === 'pending' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Menunggu
|
||||
</Button>
|
||||
<Button
|
||||
variant={filter.status === 'approved' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setFilter({ ...filter, status: 'approved' })}
|
||||
className={filter.status === 'approved' ? 'shadow-sm' : 'border-2'}
|
||||
>
|
||||
Disetujui
|
||||
</Button>
|
||||
{(filter.search || filter.type !== 'all' || filter.status !== 'all') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearFilters}
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<XIcon className="w-4 h-4 mr-1" />
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Menampilkan {filteredReviews.length} dari {reviews.length} ulasan
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user