feat: Add UserStatusBadge showing subscription level and credits

New component in header shows:
- Subscription plan badge (Starter/Bronze/Silver/Gold with icons)
- AI credits balance with sparkle icon

Also includes SQL migration for user_subscriptions RLS fix
This commit is contained in:
2025-12-26 23:02:20 +01:00
parent 02bd025bce
commit 30a716f3e2
4 changed files with 135 additions and 1 deletions

View File

@@ -16,6 +16,7 @@ import { useSession } from "@/context/SessionContext";
import { Sparkles, X, Loader2 } from "lucide-react";
import { BottomNavigation } from '@/components/BottomNavigation';
import ScanAndTasteFlow from '@/components/ScanAndTasteFlow';
import UserStatusBadge from '@/components/UserStatusBadge';
export default function Home() {
const supabase = createClient();
@@ -224,6 +225,7 @@ export default function Home() {
)}
</div>
<div className="flex flex-wrap items-center justify-center sm:justify-end gap-3 md:gap-4">
<UserStatusBadge />
<OfflineIndicator />
<LanguageSwitcher />
<DramOfTheDay bottles={bottles} />

View File

@@ -0,0 +1,99 @@
'use client';
import { useState, useEffect } from 'react';
import { createClient } from '@/lib/supabase/client';
import { Sparkles, Zap, Crown, Star, Gift } from 'lucide-react';
interface UserStatus {
credits: number;
planName: string;
planDisplayName: string;
}
const PLAN_ICONS: Record<string, { icon: React.ReactNode; color: string }> = {
starter: { icon: <Gift size={12} />, color: 'text-zinc-400 border-zinc-600' },
bronze: { icon: <Star size={12} />, color: 'text-amber-600 border-amber-600/50' },
silver: { icon: <Zap size={12} />, color: 'text-zinc-300 border-zinc-400' },
gold: { icon: <Crown size={12} />, color: 'text-yellow-500 border-yellow-500/50' },
};
export default function UserStatusBadge() {
const [status, setStatus] = useState<UserStatus | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadStatus();
}, []);
const loadStatus = async () => {
try {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
setLoading(false);
return;
}
// Get credits
const { data: credits } = await supabase
.from('user_credits')
.select('balance')
.eq('user_id', user.id)
.single();
// Get subscription plan
const { data: subscription } = await supabase
.from('user_subscriptions')
.select(`
plan_id,
subscription_plans (
name,
display_name
)
`)
.eq('user_id', user.id)
.single();
// Extract plan from joined data
const planData = subscription?.subscription_plans;
const plan = Array.isArray(planData) ? planData[0] : planData;
setStatus({
credits: credits?.balance ?? 0,
planName: plan?.name ?? 'starter',
planDisplayName: plan?.display_name ?? 'Starter',
});
} catch (error) {
console.error('[UserStatusBadge] Error loading status:', error);
} finally {
setLoading(false);
}
};
if (loading || !status) {
return null;
}
const planConfig = PLAN_ICONS[status.planName] || PLAN_ICONS.starter;
return (
<div className="flex items-center gap-2">
{/* Plan Badge */}
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full border bg-zinc-900/50 ${planConfig.color}`}>
{planConfig.icon}
<span className="text-[10px] font-bold uppercase tracking-wider">
{status.planDisplayName}
</span>
</div>
{/* Credits */}
<div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full border border-orange-600/30 bg-orange-950/20">
<Sparkles size={12} className="text-orange-500" />
<span className="text-[10px] font-bold text-orange-400">
{status.credits}
</span>
</div>
</div>
);
}

File diff suppressed because one or more lines are too long

33
user_subscription_fix.sql Normal file
View File

@@ -0,0 +1,33 @@
-- Fix: Allow users to insert their own subscription on signup
-- Run this in Supabase SQL Editor
-- Option 1: Add INSERT policy for self-signup
CREATE POLICY "user_subscriptions_insert_self" ON user_subscriptions
FOR INSERT WITH CHECK (
(SELECT auth.uid()) = user_id
);
-- Option 2 (better): Extend the existing handle_new_user trigger
-- This automatically creates subscription when user registers
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger AS $$
BEGIN
-- Create profile
INSERT INTO public.profiles (id, username, avatar_url)
VALUES (
new.id,
COALESCE(new.raw_user_meta_data->>'username', 'user_' || substr(new.id::text, 1, 8)),
new.raw_user_meta_data->>'avatar_url'
)
ON CONFLICT (id) DO NOTHING;
-- Create subscription with starter plan
INSERT INTO public.user_subscriptions (user_id, plan_id)
SELECT
new.id,
(SELECT id FROM subscription_plans WHERE name = 'starter' LIMIT 1)
ON CONFLICT (user_id) DO NOTHING;
RETURN new;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;