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:
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user