mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Merge pull request #29 from jjbrunton/staging
fix(admin): prevent blank dashboard after login
This commit is contained in:
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');
|
||||
});
|
||||
});
|
||||
@@ -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(' ');
|
||||
}
|
||||
@@ -10,36 +10,22 @@ import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis, Tooltip } from 'recha
|
||||
import { format, subDays, startOfDay, endOfDay, formatDistanceToNow } from 'date-fns';
|
||||
import { useRealtimeSubscription } from '@/hooks/useRealtimeSubscription';
|
||||
import { RealtimeStatusIndicator } from '@/components/RealtimeStatusIndicator';
|
||||
import {
|
||||
formatFeatureName,
|
||||
featureInterestKey,
|
||||
toRecentFeatureInterest,
|
||||
type FeatureInterestRow,
|
||||
type RecentFeatureInterest,
|
||||
} from '@/lib/featureInterests';
|
||||
|
||||
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 = {
|
||||
feature_name: string;
|
||||
opt_in_count: 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() {
|
||||
const { hasPermission } = useAuth();
|
||||
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())
|
||||
: Promise.resolve({ data: [] }),
|
||||
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: [] }),
|
||||
]);
|
||||
|
||||
@@ -158,12 +144,7 @@ export function DashboardPage() {
|
||||
}))
|
||||
);
|
||||
|
||||
let recentActivity: RecentFeatureInterest[] = (recentInterests ?? []).map(item => ({
|
||||
id: item.id,
|
||||
user_id: item.user_id,
|
||||
feature_name: item.feature_name,
|
||||
created_at: item.created_at,
|
||||
}));
|
||||
let recentActivity: RecentFeatureInterest[] = (recentInterests ?? []).map(toRecentFeatureInterest);
|
||||
|
||||
if (canViewUsers && recentActivity.length > 0) {
|
||||
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) {
|
||||
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')
|
||||
.charAt(0)
|
||||
.toUpperCase();
|
||||
|
||||
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">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={item.profile?.avatar_url || undefined} />
|
||||
|
||||
Reference in new issue
Block a user