This commit is contained in:
jjbrunton committed 2026-01-01 15:42:22 +00:00
1 parent ef7bc8c90d
commit 3fcbea8d60
68 files changed
+3579 -834

No files matched your search

+9
View File
@@ -15,6 +15,7 @@ import { AdminsPage } from '@/pages/admins/AdminsPage';
import { RedemptionCodesPage } from '@/pages/RedemptionCodesPage';
import { FeedbackPage } from '@/pages/FeedbackPage';
import { UsageInsightsPage } from '@/pages/UsageInsightsPage';
import { UserActivityPage } from '@/pages/activity/UserActivityPage';
import { AiSettingsPage } from '@/pages/AiSettingsPage';
import { AppSettingsPage } from '@/pages/AppSettingsPage';
@@ -96,6 +97,14 @@ function App() {
</ProtectedRoute>
}
/>
<Route
path="/activity"
element={
<ProtectedRoute requiredPermission={PERMISSION_KEYS.VIEW_ACTIVITY}>
<UserActivityPage />
</ProtectedRoute>
}
/>
{/* System */}
<Route
+21 -1
View File
@@ -15,6 +15,8 @@ import {
Bot,
Smartphone,
X,
Home,
Activity,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
@@ -29,6 +31,14 @@ interface NavItem {
requiredPermission?: PermissionKey;
}
const dashboardNav: NavItem[] = [
{
label: 'Dashboard',
href: '/',
icon: <Home className="h-5 w-5" />,
},
];
const contentNav: NavItem[] = [
{
label: 'Categories',
@@ -45,6 +55,12 @@ const usersNav: NavItem[] = [
icon: <Users className="h-5 w-5" />,
requiredPermission: PERMISSION_KEYS.VIEW_USERS,
},
{
label: 'User Activity',
href: '/activity',
icon: <Activity className="h-5 w-5" />,
requiredPermission: PERMISSION_KEYS.VIEW_ACTIVITY,
},
{
label: 'Usage Insights',
href: '/usage-insights',
@@ -112,7 +128,9 @@ export function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
const NavLink = ({ item }: { item: NavItem }) => {
if (item.requiredPermission && !hasPermission(item.requiredPermission)) return null;
const isActive = location.pathname.startsWith(item.href);
const isActive = item.href === '/'
? location.pathname === '/'
: location.pathname.startsWith(item.href);
return (
<Link
@@ -214,6 +232,7 @@ export function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
{/* Navigation */}
<div className="flex-1 space-y-6 overflow-auto px-3 py-2">
<NavSection title="Dashboard" items={dashboardNav} />
<NavSection title="Content" items={contentNav} />
<NavSection title="Users" items={usersNav} />
<NavSection title="System" items={systemNav} />
@@ -310,6 +329,7 @@ export function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
{/* Navigation */}
<div className="flex-1 space-y-6 overflow-auto px-3 py-4">
<NavSection title="Dashboard" items={dashboardNav} />
<NavSection title="Content" items={contentNav} />
<NavSection title="Users" items={usersNav} />
<NavSection title="System" items={systemNav} />
+2
View File
@@ -18,6 +18,7 @@ export const PERMISSION_KEYS = {
VIEW_MEDIA: 'view_media',
VIEW_MATCHES: 'view_matches',
VIEW_RESPONSES: 'view_responses',
VIEW_ACTIVITY: 'view_activity',
// System permissions
MANAGE_CODES: 'manage_codes',
MANAGE_ADMINS: 'manage_admins',
@@ -38,6 +39,7 @@ export const PERMISSION_METADATA: Record<PermissionKey, { label: string; descrip
[PERMISSION_KEYS.VIEW_MEDIA]: { label: 'Can view media', description: 'View user photos and media', group: 'users' },
[PERMISSION_KEYS.VIEW_MATCHES]: { label: 'Can view matches', description: 'View user matches and match details', group: 'users' },
[PERMISSION_KEYS.VIEW_RESPONSES]: { label: 'Can view responses', description: 'View user question responses', group: 'users' },
[PERMISSION_KEYS.VIEW_ACTIVITY]: { label: 'Can view activity', description: 'View user activity feed (responses, matches, messages, signups)', group: 'users' },
[PERMISSION_KEYS.MANAGE_CODES]: { label: 'Can manage redemption codes', description: 'Create and view redemption codes', group: 'system' },
[PERMISSION_KEYS.MANAGE_ADMINS]: { label: 'Can manage admins', description: 'Add/remove admin users and change permissions', group: 'system' },
[PERMISSION_KEYS.MANAGE_AI_CONFIG]: { label: 'Can manage AI settings', description: 'Configure AI models, API keys, and council mode', group: 'system' },
+20 -7
View File
@@ -16,7 +16,7 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Search, Crown, Users, ChevronRight, HardDrive } from 'lucide-react';
import { Search, Crown, Users, ChevronRight, HardDrive, MailWarning } from 'lucide-react';
import { format } from 'date-fns';
interface Profile {
@@ -27,6 +27,8 @@ interface Profile {
is_premium: boolean | null;
couple_id: string | null;
created_at: string | null;
last_sign_in_at: string | null;
email_confirmed_at: string | null;
storage_bytes?: number;
partner_is_premium?: boolean;
}
@@ -48,11 +50,9 @@ export function UsersPage() {
const fetchProfiles = useCallback(async () => {
setLoading(true);
try {
// Fetch profiles
// Fetch profiles with auth info (includes last_sign_in_at)
const { data: profilesData, error } = await supabase
.from('profiles')
.select('*')
.order('created_at', { ascending: false });
.rpc('get_profiles_with_auth_info');
if (error) throw error;
@@ -197,13 +197,14 @@ export function UsersPage() {
<TableHead>Couple</TableHead>
<TableHead>Storage</TableHead>
<TableHead>Joined</TableHead>
<TableHead>Last Active</TableHead>
<TableHead className="w-12"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredProfiles.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
{search ? 'No users found matching your search.' : 'No users yet.'}
</TableCell>
</TableRow>
@@ -224,7 +225,14 @@ export function UsersPage() {
</Link>
</TableCell>
<TableCell className="text-muted-foreground">
{profile.email || '—'}
<div className="flex items-center gap-1.5">
{profile.email || '—'}
{profile.email && !profile.email_confirmed_at && (
<span title="Email not verified">
<MailWarning className="h-3.5 w-3.5 text-amber-500" />
</span>
)}
</div>
</TableCell>
<TableCell>
{profile.is_premium ? (
@@ -268,6 +276,11 @@ export function UsersPage() {
? format(new Date(profile.created_at), 'MMM d, yyyy')
: '—'}
</TableCell>
<TableCell className="text-muted-foreground text-sm">
{profile.last_sign_in_at
? format(new Date(profile.last_sign_in_at), 'MMM d, yyyy h:mm a')
: 'Never'}
</TableCell>
<TableCell>
<Link to={`/users/${profile.id}`}>
<Button variant="ghost" size="icon">