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:
@@ -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} />
|
||||
|
||||
99
src/components/UserStatusBadge.tsx
Normal file
99
src/components/UserStatusBadge.tsx
Normal 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
33
user_subscription_fix.sql
Normal 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;
|
||||
Reference in New Issue
Block a user