refactor(ts): convert AdminPage + SubscriptionCatalogPage to TSX (B12)

First two pages — confirms the page pattern (self-contained, untyped .jsx
feature-children accept any props). AdminPage: User/boolean|null/unknown[] state
+ API-response casts. SubscriptionCatalogPage: consumes the now-typed BillModal
(Bill/Category), casts the partial fallback bill. typecheck 0, build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-04 20:14:30 -05:00
parent 73be95d12d
commit 5e803f4547
2 changed files with 23 additions and 19 deletions

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '@/api'; import { api } from '@/api';
import { errMessage } from '@/lib/utils';
import AppNavigation from '@/components/layout/Sidebar'; import AppNavigation from '@/components/layout/Sidebar';
import OnboardingWizard from '@/components/admin/OnboardingWizard'; import OnboardingWizard from '@/components/admin/OnboardingWizard';
import EmailNotifCard from '@/components/admin/EmailNotifCard'; import EmailNotifCard from '@/components/admin/EmailNotifCard';
@ -11,20 +12,21 @@ import UsersTable from '@/components/admin/UsersTable';
import AddUserCard from '@/components/admin/AddUserCard'; import AddUserCard from '@/components/admin/AddUserCard';
import BackupManagementCard from '@/components/admin/BackupManagementCard'; import BackupManagementCard from '@/components/admin/BackupManagementCard';
import CleanupPanel from '@/components/admin/CleanupPanel'; import CleanupPanel from '@/components/admin/CleanupPanel';
import type { User } from '@/hooks/useAuth';
export default function AdminPage() { export default function AdminPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [me, setMe] = useState(null); const [me, setMe] = useState<User | null>(null);
const [hasUsers, setHasUsers] = useState(null); const [hasUsers, setHasUsers] = useState<boolean | null>(null);
const [loadError, setLoadError] = useState(''); const [loadError, setLoadError] = useState('');
const [users, setUsers] = useState([]); const [users, setUsers] = useState<unknown[]>([]);
const [authMode, setAuthMode] = useState('multi'); const [authMode, setAuthMode] = useState('multi');
const loadMe = useCallback(async () => { const loadMe = useCallback(async () => {
try { try {
const d = await api.me(); const d = await api.me() as { user?: User };
setMe(d.user); setMe(d.user ?? null);
} catch { } catch {
navigate('/login', { replace: true }); navigate('/login', { replace: true });
} }
@ -32,18 +34,18 @@ export default function AdminPage() {
const loadUsers = useCallback(async () => { const loadUsers = useCallback(async () => {
try { try {
const d = await api.adminUsers(); const d = await api.adminUsers() as { users?: unknown[] } | unknown[];
setUsers(d.users || d); setUsers(Array.isArray(d) ? d : (d.users || []));
} catch {} } catch { /* ignore */ }
}, []); }, []);
const loadHasUsers = useCallback(async () => { const loadHasUsers = useCallback(async () => {
try { try {
const d = await api.hasUsers(); const d = await api.hasUsers() as { has_users?: boolean };
setHasUsers(d.has_users); setHasUsers(!!d.has_users);
if (d.has_users) loadUsers(); if (d.has_users) loadUsers();
} catch (err) { } catch (err) {
setLoadError(err.message || 'Failed to load admin page'); setLoadError(errMessage(err, 'Failed to load admin page'));
setHasUsers(false); setHasUsers(false);
} }
}, [loadUsers]); }, [loadUsers]);

View File

@ -3,26 +3,28 @@ import { Link } from 'react-router-dom';
import { ArrowLeft, Link2, RefreshCw } from 'lucide-react'; import { ArrowLeft, Link2, RefreshCw } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { api } from '@/api'; import { api } from '@/api';
import { errMessage } from '@/lib/utils';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import BillModal from '@/components/BillModal'; import BillModal from '@/components/BillModal';
import SubscriptionCatalogSection from '@/components/SubscriptionCatalogSection'; import SubscriptionCatalogSection from '@/components/SubscriptionCatalogSection';
import type { Bill, Category } from '@/types';
export default function SubscriptionCatalogPage() { export default function SubscriptionCatalogPage() {
const [catalogKey, setCatalogKey] = useState(0); const [catalogKey, setCatalogKey] = useState(0);
const [subscriptions, setSubscriptions] = useState([]); const [subscriptions, setSubscriptions] = useState<Bill[]>([]);
const [categories, setCategories] = useState([]); const [categories, setCategories] = useState<Category[]>([]);
const [modal, setModal] = useState(null); const [modal, setModal] = useState<{ bill: Bill } | null>(null);
const refreshSubscriptions = useCallback(async () => { const refreshSubscriptions = useCallback(async () => {
try { try {
const [subscriptionData, categoryData] = await Promise.all([ const [subscriptionData, categoryData] = await Promise.all([
api.subscriptions(), api.subscriptions() as Promise<{ subscriptions?: Bill[] }>,
api.categories(), api.categories(),
]); ]);
setSubscriptions(subscriptionData?.subscriptions || []); setSubscriptions(subscriptionData?.subscriptions || []);
setCategories(Array.isArray(categoryData) ? categoryData : []); setCategories(Array.isArray(categoryData) ? categoryData : []);
} catch (err) { } catch (err) {
toast.error(err.message || 'Subscriptions could not be refreshed.'); toast.error(errMessage(err, 'Subscriptions could not be refreshed.'));
} }
}, []); }, []);
@ -30,8 +32,8 @@ export default function SubscriptionCatalogPage() {
refreshSubscriptions(); refreshSubscriptions();
}, [refreshSubscriptions]); }, [refreshSubscriptions]);
function openBillEditor(billId) { function openBillEditor(billId: number) {
const bill = subscriptions.find(item => item.id === billId) || { id: billId }; const bill = subscriptions.find(item => item.id === billId) || ({ id: billId } as Bill);
setModal({ bill }); setModal({ bill });
} }