mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 10:19:43 +00:00
Compare commits
3
Commits
c776859183
...
f4687f95fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4687f95fe | ||
|
|
53a48ab6d5 | ||
|
|
a6233d0175 |
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 { 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} />
|
||||||
|
|||||||
Reference in new issue
Block a user