feat(01-06): add plan card component and switch action

- Create switchPlan server action for plan changes
- Create getCurrentPlan utility function
- Build PlanCard component with feature display
- Handle plan switching with loading state
- Revalidate dashboard and subscription pages on change
This commit is contained in:
Michele
2026-01-31 13:42:45 +01:00
parent 7bdc6d3d0a
commit 8789f26b36
2 changed files with 198 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
export type SubscriptionActionState = {
success?: boolean
error?: string
message?: string
}
export async function switchPlan(planId: string): Promise<SubscriptionActionState> {
const supabase = await createClient()
// Get current user
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) {
return { error: 'Devi essere autenticato per cambiare piano' }
}
// Verify plan exists
const { data: plan, error: planError } = await supabase
.from('plans')
.select('id, name, display_name_it')
.eq('id', planId)
.single()
if (planError || !plan) {
return { error: 'Piano non trovato' }
}
// Update user's plan
const { error: updateError } = await supabase
.from('profiles')
.update({ plan_id: planId })
.eq('id', user.id)
if (updateError) {
console.error('Failed to update plan:', updateError)
return { error: 'Errore durante il cambio piano. Riprova.' }
}
// Revalidate pages that show plan info
revalidatePath('/dashboard')
revalidatePath('/subscription')
return {
success: true,
message: `Piano cambiato a ${plan.display_name_it}`
}
}
export async function getCurrentPlan() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return null
}
const { data: profile } = await supabase
.from('profiles')
.select(`
plan_id,
plans (
id,
name,
display_name,
display_name_it,
price_monthly,
features
)
`)
.eq('id', user.id)
.single()
return profile?.plans || null
}

View File

@@ -0,0 +1,118 @@
'use client'
import { Plan, PlanFeatures } from '@/types/database'
import { formatFeatureValue, formatPrice, FEATURE_LABELS, getPlanBadgeColor } from '@/lib/plans'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { switchPlan } from '@/app/actions/subscription'
import { useTransition } from 'react'
import { useRouter } from 'next/navigation'
interface PlanCardProps {
plan: Plan
isCurrentPlan: boolean
onPlanChange?: () => void
}
export function PlanCard({ plan, isCurrentPlan, onPlanChange }: PlanCardProps) {
const [isPending, startTransition] = useTransition()
const router = useRouter()
const features = plan.features as PlanFeatures
function handleSwitchPlan() {
startTransition(async () => {
const result = await switchPlan(plan.id)
if (result.success) {
router.refresh()
if (onPlanChange) {
onPlanChange()
}
}
})
}
// Highlight features to show
const displayFeatures: (keyof PlanFeatures)[] = [
'posts_per_month',
'social_accounts',
'ai_models',
'image_generation',
'automation',
]
return (
<Card className={`relative ${isCurrentPlan ? 'ring-2 ring-blue-500' : ''}`}>
{isCurrentPlan && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<span className="bg-blue-500 text-white text-xs font-medium px-3 py-1 rounded-full">
Piano attuale
</span>
</div>
)}
<CardHeader className="text-center pt-8">
<div className="mb-2">
<span className={`inline-block px-3 py-1 text-xs font-medium rounded-full border ${getPlanBadgeColor(plan.name)}`}>
{plan.display_name_it}
</span>
</div>
<CardTitle className="text-3xl">
{formatPrice(plan.price_monthly)}
</CardTitle>
<CardDescription>
{plan.name === 'free' && 'Perfetto per iniziare'}
{plan.name === 'creator' && 'Per creator seri'}
{plan.name === 'pro' && 'Per professionisti'}
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-3">
{displayFeatures.map((featureKey) => {
const value = features[featureKey]
const isIncluded = value !== false
return (
<li key={featureKey} className="flex items-center gap-2">
<span className={`w-5 h-5 rounded-full flex items-center justify-center text-xs ${
isIncluded ? 'bg-green-100 text-green-600' : 'bg-gray-100 text-gray-400'
}`}>
{isIncluded ? '\u2713' : '\u2014'}
</span>
<span className="text-sm">
<span className="font-medium">
{formatFeatureValue(featureKey, value)}
</span>
{' '}
<span className="text-gray-500">
{FEATURE_LABELS[featureKey].toLowerCase()}
</span>
</span>
</li>
)
})}
</ul>
</CardContent>
<CardFooter>
{isCurrentPlan ? (
<Button variant="outline" className="w-full" disabled>
Piano attuale
</Button>
) : (
<Button
className="w-full"
onClick={handleSwitchPlan}
disabled={isPending}
variant={plan.name === 'pro' ? 'default' : 'outline'}
>
{isPending ? 'Cambio in corso...' : (
plan.price_monthly === 0 ? 'Passa a Gratuito' : `Passa a ${plan.display_name_it}`
)}
</Button>
)}
</CardFooter>
</Card>
)
}