Add mobile-stacked card layout for all admin tables

Implemented responsive card layout for mobile devices across all admin pages:

- Desktop (md+): Shows traditional table layout
- Mobile (<md): Shows stacked card layout with better readability

AdminProducts.tsx:
- Mobile cards display title, type, price (with sale badge), status
- Action buttons (edit/delete) in header

AdminOrders.tsx:
- Mobile cards display order ID, email, status badge, total, payment method, date
- View detail button in header

AdminMembers.tsx:
- Mobile cards display name, email, role badge, join date
- Action buttons (detail/toggle admin) at bottom with full width

AdminConsulting.tsx (upcoming & past tabs):
- Mobile cards display date, time, client, category, status, meet link
- Action buttons (link/complete/cancel) stacked at bottom

AdminEvents.tsx (events & availability tabs):
- Mobile cards display title/event type or block type, dates, status, notes
- Action buttons (edit/delete) at bottom

This approach provides much better UX on mobile compared to horizontal scrolling,
especially for complex cells like sale prices with badges and multiple action buttons.
This commit is contained in:
dwindown
2025-12-25 09:53:33 +07:00
parent af40df2c9c
commit d07c32db1d
5 changed files with 369 additions and 7 deletions

View File

@@ -307,7 +307,8 @@ export default function AdminConsulting() {
<TabsContent value="upcoming">
<Card className="border-2 border-border">
<CardContent className="p-0">
<div className="overflow-x-auto">
{/* Desktop Table */}
<div className="hidden md:block overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
@@ -405,6 +406,93 @@ export default function AdminConsulting() {
</TableBody>
</Table>
</div>
{/* Mobile Card Layout */}
<div className="md:hidden space-y-3 p-4">
{upcomingSlots.map((slot) => (
<Card key={slot.id} className="border-2 border-border">
<CardContent className="p-4 space-y-3">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap mb-1">
<h3 className="font-semibold text-sm">
{format(parseISO(slot.date), 'd MMM yyyy', { locale: id })}
</h3>
<Badge variant={statusLabels[slot.status]?.variant || 'secondary'}>
{statusLabels[slot.status]?.label || slot.status}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)}
</p>
</div>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Klien:</span>
<div className="text-right">
<p className="text-sm font-medium">{slot.profiles?.name || '-'}</p>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Kategori:</span>
<Badge variant="outline" className="text-xs">{slot.topic_category}</Badge>
</div>
{slot.meet_link && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Meet:</span>
<a
href={slot.meet_link}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline text-sm flex items-center gap-1"
>
<ExternalLink className="w-3 h-3" />
Buka
</a>
</div>
)}
</div>
{slot.status === 'confirmed' && (
<div className="flex gap-2 pt-2 border-t border-border">
<Button
variant="outline"
size="sm"
onClick={() => openMeetDialog(slot)}
className="flex-1 border-2 text-xs"
>
<LinkIcon className="w-3 h-3 mr-1" />
{slot.meet_link ? 'Edit' : 'Link'}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => updateSlotStatus(slot.id, 'completed')}
className="flex-1 border-2 text-green-600 text-xs"
>
<CheckCircle className="w-3 h-3 mr-1" />
Selesai
</Button>
<Button
variant="outline"
size="sm"
onClick={() => updateSlotStatus(slot.id, 'cancelled')}
className="flex-1 border-2 text-destructive text-xs"
>
<XCircle className="w-3 h-3 mr-1" />
Batal
</Button>
</div>
)}
</CardContent>
</Card>
))}
{upcomingSlots.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Tidak ada jadwal mendatang
</div>
)}
</div>
</CardContent>
</Card>
</TabsContent>
@@ -412,7 +500,8 @@ export default function AdminConsulting() {
<TabsContent value="past">
<Card className="border-2 border-border">
<CardContent className="p-0">
<div className="overflow-x-auto">
{/* Desktop Table */}
<div className="hidden md:block overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
@@ -447,6 +536,44 @@ export default function AdminConsulting() {
</TableBody>
</Table>
</div>
{/* Mobile Card Layout */}
<div className="md:hidden space-y-3 p-4">
{pastSlots.slice(0, 20).map((slot) => (
<Card key={slot.id} className="border-2 border-border">
<CardContent className="p-4 space-y-2">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-sm">
{format(parseISO(slot.date), 'd MMM yyyy', { locale: id })}
</h3>
<p className="text-sm text-muted-foreground">
{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)}
</p>
</div>
<Badge variant={statusLabels[slot.status]?.variant || 'secondary'}>
{statusLabels[slot.status]?.label || slot.status}
</Badge>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Klien:</span>
<span className="text-sm">{slot.profiles?.name || '-'}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Kategori:</span>
<Badge variant="outline" className="text-xs">{slot.topic_category}</Badge>
</div>
</div>
</CardContent>
</Card>
))}
{pastSlots.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
Belum ada riwayat konsultasi
</div>
)}
</div>
</CardContent>
</Card>
</TabsContent>