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 <noreply@anthropic.com>
This commit is contained in:
parent
5e803f4547
commit
f34f03af51
|
|
@ -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 (
|
||||
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground/60">
|
||||
|
|
@ -55,15 +70,15 @@ function UpdateBadge({ status, loading }) {
|
|||
export default function AboutPage() {
|
||||
const { user } = useAuth();
|
||||
|
||||
const [about, setAbout] = useState(null);
|
||||
const [about, setAbout] = useState<AboutInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [updateStatus, setUpdateStatus] = useState(null);
|
||||
const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(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));
|
||||
}, []);
|
||||
|
|
@ -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<HTMLSpanElement>(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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<string, LucideIcon> = {
|
||||
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<PrivacyDoc | null>(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);
|
||||
}
|
||||
|
|
@ -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<ReleaseHistoryData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [error, setError] = useState<string | null>(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);
|
||||
}
|
||||
Loading…
Reference in New Issue