Add published filter to question analytics

This commit is contained in:
jjbrunton committed 2026-01-10 21:52:13 +00:00
1 parent 1698a7c148
commit 0dedf44e70
2 files changed
+54 -8

No files matched your search

+8 -3
View File
@@ -1,12 +1,17 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(wc:*)" "Bash(wc:*)",
"Bash(find:*)",
"Bash(for f in /Users/jjbrunton/Projects/Sauci/apps/supabase/migrations/*.sql)",
"Bash(do grep -q \"deleted_at\" \"$f\")",
"Bash(echo:*)",
"Bash(done)"
] ]
}, },
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": [ "enabledMcpjsonServers": [
"sauci-prod", "sauci-prod",
"sauci-non-prod" "sauci-non-prod"
], ]
"enableAllProjectMcpServers": true
} }
@@ -5,7 +5,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, Tooltip, Cell, Pie, PieChart as RechartsPieChart } from 'recharts'; import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, Tooltip, Cell, Pie, PieChart as RechartsPieChart } from 'recharts';
import { Crown, MessageCircle, PieChart, Sparkles, Tag } from 'lucide-react'; import { Crown, MessageCircle, PieChart, Sparkles, Tag, Eye, EyeOff } from 'lucide-react';
interface QuestionRow { interface QuestionRow {
id: string; id: string;
@@ -19,6 +19,7 @@ interface PackRow {
name: string; name: string;
is_premium: boolean; is_premium: boolean;
is_explicit: boolean; is_explicit: boolean;
is_public: boolean;
category_id: string | null; category_id: string | null;
category?: { name: string } | { name: string }[] | null; category?: { name: string } | { name: string }[] | null;
} }
@@ -75,8 +76,11 @@ const normalizeTopics = (topics?: PackTopicRow['topics']) => {
return Array.isArray(topics) ? topics : [topics]; return Array.isArray(topics) ? topics : [topics];
}; };
type PublishFilter = 'all' | 'published' | 'unpublished';
export function QuestionAnalyticsPage() { export function QuestionAnalyticsPage() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [publishFilter, setPublishFilter] = useState<PublishFilter>('published');
const [stats, setStats] = useState({ const [stats, setStats] = useState({
totalQuestions: 0, totalQuestions: 0,
premiumQuestions: 0, premiumQuestions: 0,
@@ -141,10 +145,19 @@ export function QuestionAnalyticsPage() {
const fetchAnalytics = async () => { const fetchAnalytics = async () => {
setLoading(true); setLoading(true);
try { try {
const [packsResult, topicsResult, packTopicsResult, questions] = await Promise.all([ // Build pack query with optional is_public filter
supabase let packQuery = supabase
.from('question_packs') .from('question_packs')
.select('id, name, is_premium, is_explicit, category_id, category:categories(name)'), .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 supabase
.from('topics') .from('topics')
.select('id, name') .select('id, name')
@@ -351,7 +364,7 @@ export function QuestionAnalyticsPage() {
}; };
fetchAnalytics(); fetchAnalytics();
}, []); }, [publishFilter]);
const premiumPercent = formatPercent(stats.premiumQuestions, stats.totalQuestions); const premiumPercent = formatPercent(stats.premiumQuestions, stats.totalQuestions);
const freePercent = formatPercent(stats.freeQuestions, stats.totalQuestions); const freePercent = formatPercent(stats.freeQuestions, stats.totalQuestions);
@@ -386,8 +399,15 @@ export function QuestionAnalyticsPage() {
const categoryData = categoryBreakdown[categorySegment]; const categoryData = categoryBreakdown[categorySegment];
const packData = packBreakdown[packSegment]; 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight flex items-center gap-2"> <h1 className="text-3xl font-bold tracking-tight flex items-center gap-2">
<PieChart className="h-8 w-8 text-primary" /> <PieChart className="h-8 w-8 text-primary" />
@@ -397,6 +417,27 @@ export function QuestionAnalyticsPage() {
Deep dive into premium coverage, tags, categories, and intensity distribution Deep dive into premium coverage, tags, categories, and intensity distribution
</p> </p>
</div> </div>
<div className="flex flex-col gap-2">
<p className="text-sm text-muted-foreground">Scope</p>
<div className="flex items-center gap-2">
{publishFilterOptions.map((option) => {
const Icon = option.icon;
return (
<Button
key={option.key}
variant={publishFilter === option.key ? 'default' : 'outline'}
size="sm"
onClick={() => setPublishFilter(option.key)}
className="gap-2"
>
<Icon className="h-4 w-4" />
{option.label}
</Button>
);
})}
</div>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4"> <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<Card> <Card>