From f34f03af512f4512ae385e4d12b55b7e76626210 Mon Sep 17 00:00:00 2001 From: null Date: Sat, 4 Jul 2026 20:32:06 -0500 Subject: [PATCH] refactor(ts): convert content pages to TSX (B13) PrivacyPage (PrivacyDoc/PrivacySection + Record-typed iconByTitle), NotFoundPage (typed GlitchDigit rAF loop, HTMLSpanElement ref), AboutPage (UpdateStatus/ AboutInfo + UpdateBadge), ReleaseNotesPage (ReleaseHistoryData, typed markdown line parser). typecheck 0, lint 0 errors (35 warns), build green. Co-Authored-By: Claude Opus 4.8 --- client/pages/{AboutPage.jsx => AboutPage.tsx} | 27 ++++++++++++++---- .../{NotFoundPage.jsx => NotFoundPage.tsx} | 14 +++++----- .../{PrivacyPage.jsx => PrivacyPage.tsx} | 22 +++++++++++---- ...easeNotesPage.jsx => ReleaseNotesPage.tsx} | 28 +++++++++++-------- 4 files changed, 62 insertions(+), 29 deletions(-) rename client/pages/{AboutPage.jsx => AboutPage.tsx} (89%) rename client/pages/{NotFoundPage.jsx => NotFoundPage.tsx} (93%) rename client/pages/{PrivacyPage.jsx => PrivacyPage.tsx} (91%) rename client/pages/{ReleaseNotesPage.jsx => ReleaseNotesPage.tsx} (85%) diff --git a/client/pages/AboutPage.jsx b/client/pages/AboutPage.tsx similarity index 89% rename from client/pages/AboutPage.jsx rename to client/pages/AboutPage.tsx index 1a4c2b9..53b8691 100644 --- a/client/pages/AboutPage.jsx +++ b/client/pages/AboutPage.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { ArrowLeft, ArrowUpCircle, CheckCircle2, Info, Loader2, Sparkles, AlertCircle } from 'lucide-react'; import { api } from '@/api'; @@ -9,7 +9,22 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com const REPOSITORY_URL = 'https://dream.scheller.ltd/null/BillTracker'; -function UpdateBadge({ status, loading }) { +interface UpdateStatus { + has_update?: boolean; + latest_release_url?: string | null; + latest_version?: string; + up_to_date?: boolean | null; + error?: string | null; +} + +interface AboutInfo { + version?: string; + name?: string; + description?: string; + stack?: { backend?: string; database?: string }; +} + +function UpdateBadge({ status, loading }: { status: UpdateStatus | null; loading: boolean }) { if (loading) { return ( @@ -55,15 +70,15 @@ function UpdateBadge({ status, loading }) { export default function AboutPage() { const { user } = useAuth(); - const [about, setAbout] = useState(null); + const [about, setAbout] = useState(null); const [loading, setLoading] = useState(true); - const [updateStatus, setUpdateStatus] = useState(null); + const [updateStatus, setUpdateStatus] = useState(null); const [updateLoading, setUpdateLoading] = useState(true); const load = useCallback(async () => { setLoading(true); try { - setAbout(await api.about()); + setAbout(await api.about() as AboutInfo); } finally { setLoading(false); } @@ -74,7 +89,7 @@ export default function AboutPage() { useEffect(() => { setUpdateLoading(true); api.updateStatus() - .then(setUpdateStatus) + .then(d => setUpdateStatus(d as UpdateStatus)) .catch(() => setUpdateStatus(null)) .finally(() => setUpdateLoading(false)); }, []); diff --git a/client/pages/NotFoundPage.jsx b/client/pages/NotFoundPage.tsx similarity index 93% rename from client/pages/NotFoundPage.jsx rename to client/pages/NotFoundPage.tsx index ab7945e..807909b 100644 --- a/client/pages/NotFoundPage.jsx +++ b/client/pages/NotFoundPage.tsx @@ -5,8 +5,8 @@ import { Button } from '@/components/ui/button'; import { useAuth } from '@/hooks/useAuth'; // Animated digit — cycles through random numbers before settling -function GlitchDigit({ value, delay = 0 }) { - const ref = useRef(null); +function GlitchDigit({ value, delay = 0 }: { value: string; delay?: number }) { + const ref = useRef(null); useEffect(() => { const el = ref.current; @@ -15,20 +15,20 @@ function GlitchDigit({ value, delay = 0 }) { const frames = 14; const digits = '0123456789'; let frame = 0; - let start = null; + let start: number | null = null; - function tick(ts) { + function tick(ts: number) { if (!start) start = ts + delay; const elapsed = ts - start; if (elapsed < 0) { requestAnimationFrame(tick); return; } if (frame < frames) { - el.textContent = digits[Math.floor(Math.random() * 10)]; + el!.textContent = digits[Math.floor(Math.random() * 10)] ?? '0'; frame++; setTimeout(() => requestAnimationFrame(tick), 40 + frame * 6); } else { - el.textContent = value; - el.classList.add('settled'); + el!.textContent = value; + el!.classList.add('settled'); } } diff --git a/client/pages/PrivacyPage.jsx b/client/pages/PrivacyPage.tsx similarity index 91% rename from client/pages/PrivacyPage.jsx rename to client/pages/PrivacyPage.tsx index 59f7dec..aab4e42 100644 --- a/client/pages/PrivacyPage.jsx +++ b/client/pages/PrivacyPage.tsx @@ -1,15 +1,15 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { ArrowLeft, BadgeCheck, Database, EyeOff, FileText, Fingerprint, - LockKeyhole, RadioTower, ShieldCheck, UserRoundCog, + LockKeyhole, RadioTower, ShieldCheck, UserRoundCog, type LucideIcon, } from 'lucide-react'; import { api } from '@/api'; import { useAuth } from '@/hooks/useAuth'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -const iconByTitle = { +const iconByTitle: Record = { Overview: ShieldCheck, 'Data Collection': Database, 'Version Checking': RadioTower, @@ -21,15 +21,27 @@ const iconByTitle = { Contact: Fingerprint, }; +interface PrivacySection { + title: string; + items: string[]; +} + +interface PrivacyDoc { + name?: string; + summary?: string; + last_updated?: string; + sections?: PrivacySection[]; +} + export default function PrivacyPage() { const { user } = useAuth(); - const [privacy, setPrivacy] = useState(null); + const [privacy, setPrivacy] = useState(null); const [loading, setLoading] = useState(true); const load = useCallback(async () => { setLoading(true); try { - setPrivacy(await api.privacy()); + setPrivacy(await api.privacy() as PrivacyDoc); } finally { setLoading(false); } diff --git a/client/pages/ReleaseNotesPage.jsx b/client/pages/ReleaseNotesPage.tsx similarity index 85% rename from client/pages/ReleaseNotesPage.jsx rename to client/pages/ReleaseNotesPage.tsx index 367cd5e..eb47f3e 100644 --- a/client/pages/ReleaseNotesPage.jsx +++ b/client/pages/ReleaseNotesPage.tsx @@ -1,26 +1,32 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { ArrowLeft, RefreshCw } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; -import { cn } from '@/lib/utils'; +import { cn, errMessage } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { MarkdownText } from '@/components/MarkdownText'; -function formatDateTime(value) { +interface ReleaseHistoryData { + history?: string; + version?: string; + updated_at?: string; +} + +function formatDateTime(value: string | null | undefined): string | null { if (!value) return null; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString(); } -function parseImageLine(line) { +function parseImageLine(line: string): { alt: string; src: string } | null { const match = line.match(/^!\[([^\]\n]*)\]\(([^)\s]+)\)$/); if (!match) return null; - return { alt: match[1], src: match[2] }; + return { alt: match[1] ?? '', src: match[2] ?? '' }; } -function HistoryLine({ line, index }) { +function HistoryLine({ line, index }: { line: string; index: number }) { const trimmed = line.trim(); const image = parseImageLine(trimmed); @@ -82,18 +88,18 @@ function HistoryLine({ line, index }) { } export default function ReleaseNotesPage() { - const [data, setData] = useState(null); + const [data, setData] = useState(null); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const load = useCallback(async () => { setLoading(true); setError(null); try { - setData(await api.releaseHistory()); + setData(await api.releaseHistory() as ReleaseHistoryData); } catch (err) { - setError(err.message || 'Failed to load release notes.'); - toast.error(err.message || 'Failed to load release notes.'); + setError(errMessage(err, 'Failed to load release notes.')); + toast.error(errMessage(err, 'Failed to load release notes.')); } finally { setLoading(false); }