Merge pull request #29 from jjbrunton/staging

fix(admin): prevent blank dashboard after login
This commit is contained in:
jjbrunton authored and GitHub committed 2026-09-05 17:12:45 +01:00
commit f4687f95fe
3 files changed
+65 -30

No files matched your search

@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { featureInterestKey, formatFeatureName, toRecentFeatureInterest } from './featureInterests';
describe('toRecentFeatureInterest', () => {
it('preserves the standalone API feature field for dashboard rendering', () => {
const interest = toRecentFeatureInterest({
user_id: 'user-1',
feature: 'better_chat',
created_at: '2026-09-05T12:00:00.000Z',
});
expect(interest).toEqual({
user_id: 'user-1',
feature: 'better_chat',
created_at: '2026-09-05T12:00:00.000Z',
});
expect(formatFeatureName(interest.feature)).toBe('Better Chat');
expect(featureInterestKey(interest)).toBe('user-1:better_chat');
});
});
+34
View File
@@ -0,0 +1,34 @@
export interface FeatureInterestRow {
user_id: string;
feature: string;
created_at: string;
}
export interface RecentFeatureInterest extends FeatureInterestRow {
profile?: {
id: string;
name: string | null;
email: string | null;
avatar_url: string | null;
};
}
export function toRecentFeatureInterest(row: FeatureInterestRow): RecentFeatureInterest {
return {
user_id: row.user_id,
feature: row.feature,
created_at: row.created_at,
};
}
export function featureInterestKey(row: Pick<FeatureInterestRow, 'user_id' | 'feature'>): string {
return `${row.user_id}:${row.feature}`;
}
export function formatFeatureName(feature: string): string {
return feature
.split('_')
.filter(Boolean)
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
+11 -30
View File
@@ -10,36 +10,22 @@ import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, Tooltip } from 'recha
import { format, subDays, startOfDay, endOfDay, formatDistanceToNow } from 'date-fns'; import { format, subDays, startOfDay, endOfDay, formatDistanceToNow } from 'date-fns';
import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription'; import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription';
import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator'; import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator';
import {
formatFeatureName,
featureInterestKey,
toRecentFeatureInterest,
type FeatureInterestRow,
type RecentFeatureInterest,
} from '@/lib/featureInterests';
type FeatureInterestChartPoint = { date: string; count: number }; type FeatureInterestChartPoint = { date: string; count: number };
type RecentFeatureInterest = {
id: string;
user_id: string;
feature_name: string;
created_at: string;
profile?: {
id: string;
name: string | null;
email: string | null;
avatar_url: string | null;
};
};
type FeatureInterestCountsRow = { type FeatureInterestCountsRow = {
feature_name: string; feature_name: string;
opt_in_count: number; opt_in_count: number;
opt_in_count_last_7_days: number; opt_in_count_last_7_days: number;
}; };
function formatFeatureName(featureName: string) {
return featureName
.split('_')
.filter(Boolean)
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
export function DashboardPage() { export function DashboardPage() {
const { hasPermission } = useAuth(); const { hasPermission } = useAuth();
const canViewUsers = hasPermission(PERMISSION_KEYS.VIEW_USERS); const canViewUsers = hasPermission(PERMISSION_KEYS.VIEW_USERS);
@@ -95,7 +81,7 @@ export function DashboardPage() {
? adminData.from('feature_interests').select('created_at, user_id').gte('created_at', startDate.toISOString()).lte('created_at', endDate.toISOString()) ? adminData.from('feature_interests').select('created_at, user_id').gte('created_at', startDate.toISOString()).lte('created_at', endDate.toISOString())
: Promise.resolve({ data: [] }), : Promise.resolve({ data: [] }),
canViewUsers canViewUsers
? adminData.from('feature_interests').select('id, user_id, feature_name, created_at').order('created_at', { ascending: false }).limit(10) ? adminData.from<FeatureInterestRow>('feature_interests').select('user_id, feature, created_at').order('created_at', { ascending: false }).limit(10)
: Promise.resolve({ data: [] }), : Promise.resolve({ data: [] }),
]); ]);
@@ -158,12 +144,7 @@ export function DashboardPage() {
})) }))
); );
let recentActivity: RecentFeatureInterest[] = (recentInterests ?? []).map(item => ({ let recentActivity: RecentFeatureInterest[] = (recentInterests ?? []).map(toRecentFeatureInterest);
id: item.id,
user_id: item.user_id,
feature_name: item.feature_name,
created_at: item.created_at,
}));
if (canViewUsers && recentActivity.length > 0) { if (canViewUsers && recentActivity.length > 0) {
const userIds = Array.from(new Set(recentActivity.map(item => item.user_id))); const userIds = Array.from(new Set(recentActivity.map(item => item.user_id)));
@@ -579,14 +560,14 @@ export function DashboardPage() {
if (item.profile?.name && item.profile.email) { if (item.profile?.name && item.profile.email) {
subtitleParts.push(item.profile.email); subtitleParts.push(item.profile.email);
} }
subtitleParts.push(formatFeatureName(item.feature_name)); subtitleParts.push(formatFeatureName(item.feature));
const fallbackChar = (item.profile?.name || item.profile?.email || 'U') const fallbackChar = (item.profile?.name || item.profile?.email || 'U')
.charAt(0) .charAt(0)
.toUpperCase(); .toUpperCase();
return ( return (
<div key={item.id} className="flex items-center justify-between gap-4 rounded-xl bg-white/5 p-3"> <div key={featureInterestKey(item)} className="flex items-center justify-between gap-4 rounded-xl bg-white/5 p-3">
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
<AvatarImage src={item.profile?.avatar_url || undefined} /> <AvatarImage src={item.profile?.avatar_url || undefined} />