feat: complete redesign of admin plan cards

Major UI Overhaul:
 Modern card design with gradients
 Hover effects: lift up + shadow
 Inline layout: grip icon + title + badge
 No wasted vertical space
 Larger price display (text-4xl)
 Gradient price section with border
 2-column stats grid with cards
 Animated pulse dot for active status
 Full-width visibility button + icon actions
 Indonesian text throughout

Layout Changes:
- Grip icon inline with title (left side)
- Badge inline with title text
- Trial badge inline with duration
- Stats in grid layout
- Action buttons with hover fills

Design Elements:
- rounded-2xl cards
- Gradient backgrounds
- Border color changes on hover
- Smooth transitions (300ms)
- Better spacing and typography
- Consistent card heights
This commit is contained in:
dwindown
2025-10-11 21:59:13 +07:00
parent bef2344ddc
commit 1e3d320716

View File

@@ -125,114 +125,137 @@ export function AdminPlans() {
{plans.map((plan) => (
<div
key={plan.id}
className="bg-card rounded-lg shadow border border-border overflow-hidden"
className="group relative bg-gradient-to-br from-card to-card/50 rounded-2xl border-2 border-border/50 hover:border-primary/30 overflow-hidden transition-all duration-300 hover:shadow-2xl hover:-translate-y-1"
>
{/* Header */}
<div className="p-6 border-b border-border">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center">
<GripVertical className="h-5 w-5 text-muted-foreground mr-2 cursor-move" />
<h3 className="text-lg font-semibold text-foreground">
{plan.name}
</h3>
{/* Content */}
<div className="p-6">
{/* Header: Drag Icon + Title/Description + Badge */}
<div className="flex items-start gap-3 mb-6">
{/* Drag Handle */}
<div className="opacity-0 group-hover:opacity-100 transition-opacity pt-1">
<GripVertical className="h-5 w-5 mt-1 text-muted-foreground/50 cursor-move" />
</div>
{plan.badge && (
<span
className={`text-xs font-semibold inline-flex items-center rounded-md px-2.5 py-0.5 ${
plan.badgeColor === 'blue'
? 'bg-primary/20 text-primary ring-1 ring-primary'
: plan.badgeColor === 'green'
? 'bg-green-500/20 text-green-600 ring-1 ring-green-600'
: 'bg-muted text-muted-foreground ring-1 ring-border'
}`}
>
{plan.badge}
{/* Title & Description */}
<div className="flex-1 min-w-0">
{/* Plan Name + Badge */}
<div className="flex items-center gap-2 mb-2 flex-wrap">
<h3 className="text-2xl font-bold text-foreground">
{plan.name}
</h3>
{plan.badge && (
<span
className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold shadow-sm ${
plan.badgeColor === 'blue'
? 'bg-gradient-to-r from-blue-500 to-blue-600 text-white'
: plan.badgeColor === 'green'
? 'bg-gradient-to-r from-green-500 to-green-600 text-white'
: 'bg-muted text-muted-foreground'
}`}
>
{plan.badge}
</span>
)}
</div>
{/* Description */}
<p className="text-sm text-muted-foreground line-clamp-2">
{plan.description}
</p>
</div>
</div>
{/* Price - Hero Section */}
<div className="mb-6 p-6 rounded-xl bg-gradient-to-br from-primary/5 to-primary/10 border border-primary/20">
<div className="flex items-baseline gap-2 mb-1">
<span className="text-4xl font-black text-foreground tracking-tight">
{formatPrice(plan.price, plan.currency)}
</span>
)}
</div>
<p className="text-sm text-muted-foreground">
{plan.description}
</p>
</div>
{/* Price */}
<div className="p-6 bg-muted">
<div className="text-3xl font-bold text-foreground">
{formatPrice(plan.price, plan.currency)}
</div>
<div className="text-sm text-muted-foreground mt-1">
{plan.durationType === 'lifetime'
? 'Lifetime access'
: plan.durationType === 'monthly'
? 'per month'
: plan.durationType === 'yearly'
? 'per year'
: plan.durationType}
</div>
{plan.trialDays > 0 && (
<div className="text-sm text-primary mt-1">
{plan.trialDays} days free trial
</div>
)}
</div>
{/* Stats */}
<div className="p-6 border-t border-border">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
Subscriptions:
</span>
<span className="font-medium text-foreground">
{plan._count.subscriptions}
</span>
</div>
<div className="flex items-center justify-between text-sm mt-2">
<span className="text-muted-foreground">Status:</span>
<div className="flex items-center space-x-2">
{plan.isActive && (
<span className="px-2 py-1 text-xs bg-green-500/20 text-green-600 rounded">
Active
<div className="flex items-center gap-2 flex-wrap">
<p className="text-sm font-medium text-muted-foreground">
{plan.durationType === 'lifetime'
? 'Akses Selamanya'
: plan.durationType === 'monthly'
? 'per bulan'
: plan.durationType === 'yearly'
? 'per tahun'
: plan.durationType}
</p>
{plan.trialDays > 0 && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-primary/20 text-primary text-xs font-semibold">
🎁 {plan.trialDays} hari gratis
</span>
)}
{plan.isVisible ? (
<Eye className="h-4 w-4 text-green-600" />
) : (
<EyeOff className="h-4 w-4 text-muted-foreground" />
)}
</div>
</div>
</div>
{/* Actions */}
<div className="p-4 bg-muted border-t border-border flex items-center justify-end space-x-2">
<button
onClick={() => toggleVisibility(plan)}
className="p-2 text-muted-foreground hover:text-foreground transition-colors"
title={plan.isVisible ? 'Hide' : 'Show'}
>
{plan.isVisible ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
<button
onClick={() => {
setEditingPlan(plan)
setShowModal(true)
}}
className="p-2 text-primary hover:text-primary/80 transition-colors"
title="Edit"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => handleDelete(plan.id)}
className="p-2 text-destructive hover:text-destructive/80 transition-colors"
title="Delete"
>
<Trash2 className="h-4 w-4" />
</button>
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="p-3 rounded-lg bg-muted/50 border border-border/50">
<div className="text-xs text-muted-foreground mb-1">Subscribers</div>
<div className="text-2xl font-bold text-foreground">
{plan._count.subscriptions}
</div>
</div>
<div className="p-3 rounded-lg bg-muted/50 border border-border/50">
<div className="text-xs text-muted-foreground mb-1">Status</div>
<div className="flex items-center gap-2">
{plan.isActive ? (
<>
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
<span className="text-sm font-semibold text-green-600">Active</span>
</>
) : (
<>
<div className="h-2 w-2 rounded-full bg-gray-400" />
<span className="text-sm font-semibold text-muted-foreground">Inactive</span>
</>
)}
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-2">
<button
onClick={() => toggleVisibility(plan)}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all ${
plan.isVisible
? 'bg-primary/10 text-primary hover:bg-primary/20'
: 'bg-muted text-muted-foreground hover:bg-muted/80'
}`}
>
{plan.isVisible ? (
<>
<Eye className="h-4 w-4" />
<span>Visible</span>
</>
) : (
<>
<EyeOff className="h-4 w-4" />
<span>Hidden</span>
</>
)}
</button>
<button
onClick={() => {
setEditingPlan(plan)
setShowModal(true)
}}
className="p-2.5 rounded-lg bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground transition-all"
title="Edit Plan"
>
<Edit className="h-4 w-4" />
</button>
<button
onClick={() => handleDelete(plan.id)}
className="p-2.5 rounded-lg bg-destructive/10 text-destructive hover:bg-destructive hover:text-destructive-foreground transition-all"
title="Hapus Plan"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
</div>
))}