mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Show opt-in counts by feature
This commit is contained in:
1 parent
e1b1800f03
commit
e1ff87dc03
2 files changed
+90
No files matched your search
@@ -26,6 +26,12 @@ type RecentFeatureInterest = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FeatureInterestCountsRow = {
|
||||||
|
feature_name: string;
|
||||||
|
opt_in_count: number;
|
||||||
|
opt_in_count_last_7_days: number;
|
||||||
|
};
|
||||||
|
|
||||||
function formatFeatureName(featureName: string) {
|
function formatFeatureName(featureName: string) {
|
||||||
return featureName
|
return featureName
|
||||||
.split('_')
|
.split('_')
|
||||||
@@ -45,6 +51,7 @@ export function DashboardPage() {
|
|||||||
});
|
});
|
||||||
const [messageStats, setMessageStats] = useState<{ date: string; count: number }[]>([]);
|
const [messageStats, setMessageStats] = useState<{ date: string; count: number }[]>([]);
|
||||||
const [featureInterestStats, setFeatureInterestStats] = useState<FeatureInterestChartPoint[]>([]);
|
const [featureInterestStats, setFeatureInterestStats] = useState<FeatureInterestChartPoint[]>([]);
|
||||||
|
const [featureInterestCounts, setFeatureInterestCounts] = useState<FeatureInterestCountsRow[]>([]);
|
||||||
const [recentFeatureInterests, setRecentFeatureInterests] = useState<RecentFeatureInterest[]>([]);
|
const [recentFeatureInterests, setRecentFeatureInterests] = useState<RecentFeatureInterest[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@@ -95,6 +102,19 @@ export function DashboardPage() {
|
|||||||
.limit(10),
|
.limit(10),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (canViewUsers) {
|
||||||
|
const { data: countsData, error: countsError } = await supabase.rpc('get_feature_interest_counts');
|
||||||
|
|
||||||
|
if (countsError) {
|
||||||
|
console.error('Failed to fetch feature interest counts:', countsError);
|
||||||
|
setFeatureInterestCounts([]);
|
||||||
|
} else {
|
||||||
|
setFeatureInterestCounts((countsData ?? []) as FeatureInterestCountsRow[]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFeatureInterestCounts([]);
|
||||||
|
}
|
||||||
|
|
||||||
// Process messages for chart
|
// Process messages for chart
|
||||||
const messageDaysMap = new Map<string, number>();
|
const messageDaysMap = new Map<string, number>();
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
@@ -483,6 +503,47 @@ export function DashboardPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 pt-6 border-t border-white/5">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-sm font-medium">Opt-ins by feature</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">All time • last 7d</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Array.from({ length: 6 }).map((_, idx) => (
|
||||||
|
<div key={idx} className="flex items-center justify-between gap-4 rounded-xl bg-white/5 p-3">
|
||||||
|
<Skeleton className="h-4 w-40" />
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Skeleton className="h-4 w-10" />
|
||||||
|
<Skeleton className="h-3 w-12" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : featureInterestCounts.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">No opt-ins yet.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{featureInterestCounts.slice(0, 12).map((row) => (
|
||||||
|
<div key={row.feature_name} className="flex items-center justify-between gap-4 rounded-xl bg-white/5 p-3">
|
||||||
|
<p className="text-sm font-medium truncate">
|
||||||
|
{formatFeatureName(row.feature_name)}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-baseline gap-2 whitespace-nowrap">
|
||||||
|
<span className="text-sm font-semibold tabular-nums">
|
||||||
|
{row.opt_in_count}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground tabular-nums">
|
||||||
|
{row.opt_in_count_last_7_days} 7d
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 pt-6 border-t border-white/5">
|
<div className="mt-6 pt-6 border-t border-white/5">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-sm font-medium">Recent opt-ins</h3>
|
<h3 className="text-sm font-medium">Recent opt-ins</h3>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- Expose per-feature opt-in counts for admins
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION public.get_feature_interest_counts()
|
||||||
|
RETURNS TABLE (
|
||||||
|
feature_name TEXT,
|
||||||
|
opt_in_count BIGINT,
|
||||||
|
opt_in_count_last_7_days BIGINT
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = 'public'
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT has_permission('view_users') THEN
|
||||||
|
RAISE EXCEPTION 'Access denied';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
fi.feature_name,
|
||||||
|
COUNT(*)::BIGINT AS opt_in_count,
|
||||||
|
COUNT(*) FILTER (WHERE fi.created_at >= now() - interval '7 days')::BIGINT AS opt_in_count_last_7_days
|
||||||
|
FROM public.feature_interests fi
|
||||||
|
GROUP BY fi.feature_name
|
||||||
|
ORDER BY opt_in_count DESC, fi.feature_name ASC;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
GRANT EXECUTE ON FUNCTION public.get_feature_interest_counts() TO authenticated;
|
||||||
Reference in new issue
Block a user