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:
parent
73be95d12d
commit
5e803f4547
|
|
@ -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]);
|
||||||
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue