From 8789f26b36135b028157cb1f9e98e98e5edac179 Mon Sep 17 00:00:00 2001 From: Michele Date: Sat, 31 Jan 2026 13:42:45 +0100 Subject: [PATCH] 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 --- src/app/actions/subscription.ts | 80 +++++++++++++++ src/components/subscription/plan-card.tsx | 118 ++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 src/app/actions/subscription.ts create mode 100644 src/components/subscription/plan-card.tsx diff --git a/src/app/actions/subscription.ts b/src/app/actions/subscription.ts new file mode 100644 index 0000000..6f4a764 --- /dev/null +++ b/src/app/actions/subscription.ts @@ -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 { + 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 +} diff --git a/src/components/subscription/plan-card.tsx b/src/components/subscription/plan-card.tsx new file mode 100644 index 0000000..55c6e83 --- /dev/null +++ b/src/components/subscription/plan-card.tsx @@ -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 ( + + {isCurrentPlan && ( +
+ + Piano attuale + +
+ )} + + +
+ + {plan.display_name_it} + +
+ + {formatPrice(plan.price_monthly)} + + + {plan.name === 'free' && 'Perfetto per iniziare'} + {plan.name === 'creator' && 'Per creator seri'} + {plan.name === 'pro' && 'Per professionisti'} + +
+ + +
    + {displayFeatures.map((featureKey) => { + const value = features[featureKey] + const isIncluded = value !== false + + return ( +
  • + + {isIncluded ? '\u2713' : '\u2014'} + + + + {formatFeatureValue(featureKey, value)} + + {' '} + + {FEATURE_LABELS[featureKey].toLowerCase()} + + +
  • + ) + })} +
+
+ + + {isCurrentPlan ? ( + + ) : ( + + )} + +
+ ) +}