import { useEffect, useState } from 'react'; import { supabase } from '@/config'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, Tooltip, Cell, Pie, PieChart as RechartsPieChart } from 'recharts'; import { Crown, MessageCircle, PieChart, Sparkles, Tag, Eye, EyeOff } from 'lucide-react'; type QuestionType = 'swipe' | 'text_answer' | 'audio' | 'photo' | 'who_likely'; interface QuestionRow { id: string; pack_id: string; intensity: number | null; partner_text: string | null; inverse_of?: string | null; question_type?: QuestionType | null; } interface PackRow { id: string; name: string; is_premium: boolean; is_explicit: boolean; is_public: boolean; category_id: string | null; category?: { name: string } | { name: string }[] | null; } interface TopicRow { id: string; name: string; } interface PackTopicRow { pack_id: string; topic_id: string; topics: TopicRow | TopicRow[] | null; } interface BreakdownItem { id: string; name: string; count: number; } interface PackBreakdownItem { id: string; name: string; count: number; isPremium: boolean; category: string; } interface ChartDatum { name: string; value: number; color: string; [key: string]: string | number; } type SegmentKey = 'all' | 'premium' | 'free'; const QUESTION_TYPE_CONFIG: Record = { swipe: { label: 'Swipe', color: '#38bdf8' }, text_answer: { label: 'Text', color: '#f59e0b' }, audio: { label: 'Audio', color: '#a78bfa' }, photo: { label: 'Photo', color: '#f472b6' }, who_likely: { label: 'Who Likely', color: '#22c55e' }, }; const formatPercent = (count: number, total: number) => (total > 0 ? Math.round((count / total) * 100) : 0); const getCategoryName = (category?: PackRow['category']) => { if (!category) return 'Uncategorized'; if (Array.isArray(category)) { return category[0]?.name || 'Uncategorized'; } return category.name || 'Uncategorized'; }; const normalizeTopics = (topics?: PackTopicRow['topics']) => { if (!topics) return [] as TopicRow[]; return Array.isArray(topics) ? topics : [topics]; }; type PublishFilter = 'all' | 'published' | 'unpublished'; export function QuestionAnalyticsPage() { const [loading, setLoading] = useState(true); const [publishFilter, setPublishFilter] = useState('published'); const [stats, setStats] = useState({ totalQuestions: 0, premiumQuestions: 0, freeQuestions: 0, taggedQuestions: 0, }); const [premiumBreakdown, setPremiumBreakdown] = useState([]); const [questionTypeBreakdown, setQuestionTypeBreakdown] = useState([]); const [topicBreakdown, setTopicBreakdown] = useState>({ all: [], premium: [], free: [], }); const [categoryBreakdown, setCategoryBreakdown] = useState>({ all: [], premium: [], free: [], }); const [packBreakdown, setPackBreakdown] = useState>({ all: [], premium: [], free: [], }); const [tagSegment, setTagSegment] = useState('all'); const [categorySegment, setCategorySegment] = useState('all'); const [packSegment, setPackSegment] = useState('all'); useEffect(() => { const fetchAllQuestions = async () => { const pageSize = 1000; let page = 0; const allQuestions: QuestionRow[] = []; while (true) { const from = page * pageSize; const to = from + pageSize - 1; const { data, error } = await supabase .from('questions') .select('id, pack_id, intensity, partner_text, inverse_of, question_type') .range(from, to); if (error) throw error; const batch = data || []; allQuestions.push(...batch); if (batch.length < pageSize) { break; } page += 1; } return allQuestions; }; const fetchAnalytics = async () => { setLoading(true); try { // Build pack query with optional is_public filter let packQuery = supabase .from('question_packs') .select('id, name, is_premium, is_explicit, is_public, category_id, category:categories(name)'); if (publishFilter === 'published') { packQuery = packQuery.eq('is_public', true); } else if (publishFilter === 'unpublished') { packQuery = packQuery.eq('is_public', false); } const [packsResult, topicsResult, packTopicsResult, questions] = await Promise.all([ packQuery, supabase .from('topics') .select('id, name') .order('name'), supabase .from('pack_topics') .select('pack_id, topic_id, topics(id, name)'), fetchAllQuestions(), ]); if (packsResult.error) throw packsResult.error; if (topicsResult.error) throw topicsResult.error; if (packTopicsResult.error) throw packTopicsResult.error; const packs = (packsResult.data || []) as PackRow[]; const topics = (topicsResult.data || []) as TopicRow[]; const packTopics = (packTopicsResult.data || []) as PackTopicRow[]; const packById = new Map(); packs.forEach((pack) => { packById.set(pack.id, pack); }); const seedTopicCounts = () => new Map( topics.map((topic) => [topic.id, { id: topic.id, name: topic.name, count: 0 }]) ); const topicCountsBySegment: Record> = { all: seedTopicCounts(), premium: seedTopicCounts(), free: seedTopicCounts(), }; const packTopicsMap = new Map(); packTopics.forEach((relation) => { const topicsForPack = packTopicsMap.get(relation.pack_id) || []; const topicsList = normalizeTopics(relation.topics); if (topicsList.length === 0) return; topicsForPack.push(...topicsList); packTopicsMap.set(relation.pack_id, topicsForPack); }); const untaggedCounts: Record = { all: 0, premium: 0, free: 0 }; const questionTypeCounts: Record = { swipe: 0, text_answer: 0, audio: 0, photo: 0, who_likely: 0, }; const categoryCountsBySegment: Record> = { all: new Map(), premium: new Map(), free: new Map(), }; const packCountsBySegment: Record> = { all: new Map(), premium: new Map(), free: new Map(), }; let premiumCount = 0; let freeCount = 0; let taggedCount = 0; const primaryQuestions = questions.filter((question) => question.inverse_of == null); primaryQuestions.forEach((question) => { const pack = packById.get(question.pack_id); const isPremium = pack?.is_premium ?? false; const segmentKey: SegmentKey = isPremium ? 'premium' : 'free'; const categoryName = getCategoryName(pack?.category); const categoryId = pack?.category_id || 'uncategorized'; const questionType = question.question_type ?? 'swipe'; questionTypeCounts[questionType] = (questionTypeCounts[questionType] || 0) + 1; if (isPremium) { premiumCount += 1; } else { freeCount += 1; } const updatePackCounts = (key: SegmentKey) => { const map = packCountsBySegment[key]; const packEntry = map.get(question.pack_id) || { id: question.pack_id, name: pack?.name || 'Unknown Pack', count: 0, isPremium: isPremium, category: categoryName, }; packEntry.count += 1; map.set(question.pack_id, packEntry); }; updatePackCounts('all'); updatePackCounts(segmentKey); const updateCategoryCounts = (key: SegmentKey) => { const map = categoryCountsBySegment[key]; const categoryEntry = map.get(categoryId) || { id: categoryId, name: categoryName, count: 0, }; categoryEntry.count += 1; map.set(categoryId, categoryEntry); }; updateCategoryCounts('all'); updateCategoryCounts(segmentKey); const topicsForPack = packTopicsMap.get(question.pack_id) || []; if (topicsForPack.length > 0) { taggedCount += 1; topicsForPack.forEach((topic) => { const updateCounts = (map: Map) => { const topicEntry = map.get(topic.id) || { id: topic.id, name: topic.name, count: 0, }; topicEntry.count += 1; map.set(topic.id, topicEntry); }; updateCounts(topicCountsBySegment.all); updateCounts(topicCountsBySegment[segmentKey]); }); } else { untaggedCounts.all += 1; untaggedCounts[segmentKey] += 1; } }); const totalQuestions = primaryQuestions.length; const premiumData: ChartDatum[] = [ { name: 'Premium', value: premiumCount, color: '#f59e0b' }, { name: 'Free', value: freeCount, color: '#22c55e' }, ]; const questionTypeData: ChartDatum[] = (Object.entries(QUESTION_TYPE_CONFIG) as Array< [QuestionType, { label: string; color: string }] >).map(([key, config]) => ({ name: config.label, value: questionTypeCounts[key], color: config.color, })); const buildTopicData = (key: SegmentKey) => { const data = Array.from(topicCountsBySegment[key].values()); if (untaggedCounts[key] > 0 || data.length === 0) { data.push({ id: 'untagged', name: 'Untagged', count: untaggedCounts[key] }); } data.sort((a, b) => b.count - a.count); return data; }; const topicData = { all: buildTopicData('all'), premium: buildTopicData('premium'), free: buildTopicData('free'), }; const buildCategoryData = (key: SegmentKey) => Array.from(categoryCountsBySegment[key].values()).sort((a, b) => b.count - a.count); const buildPackData = (key: SegmentKey) => Array.from(packCountsBySegment[key].values()).sort((a, b) => b.count - a.count); const categoryData = { all: buildCategoryData('all'), premium: buildCategoryData('premium'), free: buildCategoryData('free'), }; const packData = { all: buildPackData('all'), premium: buildPackData('premium'), free: buildPackData('free'), }; setStats({ totalQuestions, premiumQuestions: premiumCount, freeQuestions: freeCount, taggedQuestions: taggedCount, }); setPremiumBreakdown(premiumData); setQuestionTypeBreakdown(questionTypeData); setTopicBreakdown(topicData); setCategoryBreakdown(categoryData); setPackBreakdown(packData); } catch (error) { console.error('Failed to fetch question analytics:', error); } finally { setLoading(false); } }; fetchAnalytics(); }, [publishFilter]); const premiumPercent = formatPercent(stats.premiumQuestions, stats.totalQuestions); const freePercent = formatPercent(stats.freeQuestions, stats.totalQuestions); const taggedPercent = formatPercent(stats.taggedQuestions, stats.totalQuestions); const segmentOptions: { key: SegmentKey; label: string }[] = [ { key: 'all', label: 'All' }, { key: 'premium', label: 'Premium' }, { key: 'free', label: 'Free' }, ]; const segmentLabelByKey: Record = { all: 'All', premium: 'Premium', free: 'Free', }; const totalBySegment: Record = { all: stats.totalQuestions, premium: stats.premiumQuestions, free: stats.freeQuestions, }; const segmentShareByKey: Record = { all: '100', premium: stats.totalQuestions ? ((stats.premiumQuestions / stats.totalQuestions) * 100).toFixed(0) : '0', free: stats.totalQuestions ? ((stats.freeQuestions / stats.totalQuestions) * 100).toFixed(0) : '0', }; const tagData = topicBreakdown[tagSegment]; const categoryData = categoryBreakdown[categorySegment]; const packData = packBreakdown[packSegment]; const publishFilterOptions: { key: PublishFilter; label: string; icon: typeof Eye }[] = [ { key: 'all', label: 'All Packs', icon: Eye }, { key: 'published', label: 'Published Only', icon: Eye }, { key: 'unpublished', label: 'Unpublished Only', icon: EyeOff }, ]; return (

Question Analytics

Deep dive into premium coverage, tags, categories, and question types

Scope

{publishFilterOptions.map((option) => { const Icon = option.icon; return ( ); })}
Total Questions {loading ? ( ) : (
{stats.totalQuestions.toLocaleString()}
)}
Premium Questions {loading ? ( ) : ( <>
{stats.premiumQuestions.toLocaleString()}

{premiumPercent}% of all questions

)}
Free Questions {loading ? ( ) : ( <>
{stats.freeQuestions.toLocaleString()}

{freePercent}% of all questions

)}
Tagged Questions {loading ? ( ) : ( <>
{stats.taggedQuestions.toLocaleString()}

{taggedPercent}% of all questions

)}
Premium vs Free Question count split by pack premium status {loading ? ( ) : stats.totalQuestions === 0 ? (
No question data available yet
) : (
`${name}: ${((percent ?? 0) * 100).toFixed(0)}%` } > {premiumBreakdown.map((entry, index) => ( ))}
)}
Question Types Distribution of questions by answer format {loading ? ( ) : stats.totalQuestions === 0 ? (
No question data available yet
) : (
`${value}`} /> {questionTypeBreakdown.map((entry, index) => ( ))}
)}
Tag Breakdown Tags are derived from pack topics; questions in multi-tag packs count toward each tag
{segmentOptions.map((option) => ( ))}

{segmentLabelByKey[tagSegment]}: {totalBySegment[tagSegment].toLocaleString()} questions ({segmentShareByKey[tagSegment]}%)

{loading ? (
{Array.from({ length: 6 }).map((_, index) => ( ))}
) : totalBySegment[tagSegment] === 0 ? (
No questions in this segment yet
) : tagData.length === 0 ? (
No tag data available yet
) : (
{tagData.map((item) => { const percentage = formatPercent(item.count, totalBySegment[tagSegment]); return (

{item.name}

{item.count.toLocaleString()}
{percentage}%
); })}
)}
Category Breakdown Distribution of questions across pack categories
{segmentOptions.map((option) => ( ))}

{segmentLabelByKey[categorySegment]}: {totalBySegment[categorySegment].toLocaleString()} questions ({segmentShareByKey[categorySegment]}%)

{loading ? (
{Array.from({ length: 5 }).map((_, index) => ( ))}
) : totalBySegment[categorySegment] === 0 ? (
No questions in this segment yet
) : categoryData.length === 0 ? (
No category data available yet
) : (
{categoryData.map((item) => { const percentage = formatPercent(item.count, totalBySegment[categorySegment]); return (

{item.name}

{percentage}% of questions

{item.count.toLocaleString()}

questions

); })}
)}
Top Packs Packs with the most questions right now
{segmentOptions.map((option) => ( ))}

{segmentLabelByKey[packSegment]}: {totalBySegment[packSegment].toLocaleString()} questions ({segmentShareByKey[packSegment]}%)

{loading ? (
{Array.from({ length: 5 }).map((_, index) => ( ))}
) : totalBySegment[packSegment] === 0 ? (
No questions in this segment yet
) : packData.length === 0 ? (
No pack data available yet
) : (
{packData.slice(0, 8).map((pack) => (

{pack.name}

{pack.category} {pack.isPremium && ( Premium )}

{pack.count.toLocaleString()}

questions

))}
)}
); }