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