mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-01 18:19:43 +00:00
Add published filter to question analytics
This commit is contained in:
1 parent
1698a7c148
commit
0dedf44e70
2 files changed
+54
-8
No files matched your search
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user