import { useQuery } from '@tanstack/react-query'
import { useCallback, useMemo, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
const DEFAULT_WEBSITE_URL = 'https://www.communityone.com/'
type LighthouseScores = {
performance: number | null
accessibility: number | null
best_practices: number | null
seo: number | null
}
export type LighthouseReportPayload = {
scan_key: string
batch_id: string
jurisdiction_id: string
website_url: string
final_url: string | null
scanned_at: string | null
status: string
lighthouse_version: string | null
requested_url: string | null
scores: LighthouseScores
run_warnings: string[]
screenshot_data_url: string | null
}
function scoreStroke(score: number | null): string {
if (score == null) return '#94a3b8'
if (score >= 90) return '#15803d'
if (score >= 50) return '#ca8a04'
return '#b91c1c'
}
function LighthouseGauge({
label,
score,
size = 'sm',
}: {
label: string
score: number | null
size?: 'sm' | 'lg'
}) {
const r = size === 'lg' ? 54 : 38
const c = 2 * Math.PI * r
const pct = score == null ? 0 : Math.min(100, Math.max(0, score)) / 100
const offset = c * (1 - pct)
const stroke = scoreStroke(score)
const dim = size === 'lg' ? 136 : 100
const view = dim
const cx = view / 2
const cy = view / 2
return (
{score == null ? '—' : score}
{label}
)
}
function WarningBox({ items }: { items: string[] }) {
if (!items.length) return null
return (
There were issues affecting this run of Lighthouse:
{items.map((w) => (
- {w}
))}
)
}
function formatErrorDetail(detail: unknown): string {
if (typeof detail === 'string') return detail
if (Array.isArray(detail)) {
return detail
.map((x) => (typeof x === 'object' && x && 'msg' in x ? String((x as { msg: string }).msg) : String(x)))
.join('; ')
}
if (detail && typeof detail === 'object' && 'detail' in detail) {
return formatErrorDetail((detail as { detail: unknown }).detail)
}
return 'Request failed'
}
async function fetchReport(websiteUrl: string, batchId?: string): Promise {
const u = new URL('/api/lighthouse/report', window.location.origin)
u.searchParams.set('website_url', websiteUrl.trim())
if (batchId?.trim()) u.searchParams.set('batch_id', batchId.trim())
const res = await fetch(u.toString(), { credentials: 'include' })
if (!res.ok) {
let body: unknown = null
try {
body = await res.json()
} catch {
/* ignore */
}
const msg =
body && typeof body === 'object' && 'detail' in body
? formatErrorDetail((body as { detail: unknown }).detail)
: `HTTP ${res.status}`
throw new Error(msg)
}
return (await res.json()) as LighthouseReportPayload
}
export default function LighthouseReportPage() {
const [searchParams, setSearchParams] = useSearchParams()
const initialUrl = useMemo(() => {
const q = searchParams.get('url')?.trim()
return q && q.length >= 4 ? q : DEFAULT_WEBSITE_URL
}, [searchParams])
const [urlInput, setUrlInput] = useState(initialUrl)
const [batchInput, setBatchInput] = useState(() => searchParams.get('batch_id') ?? '')
const [submittedUrl, setSubmittedUrl] = useState(initialUrl)
const [submittedBatch, setSubmittedBatch] = useState(() => searchParams.get('batch_id') ?? '')
const syncQuery = useCallback(
(nextUrl: string, nextBatch: string) => {
const sp = new URLSearchParams(searchParams)
sp.set('url', nextUrl.trim())
if (nextBatch.trim()) sp.set('batch_id', nextBatch.trim())
else sp.delete('batch_id')
setSearchParams(sp, { replace: true })
},
[searchParams, setSearchParams],
)
const {
data,
isPending,
isError,
error,
isFetching,
} = useQuery({
queryKey: ['lighthouse-report', submittedUrl, submittedBatch || null],
queryFn: () => fetchReport(submittedUrl, submittedBatch),
staleTime: 60_000,
})
function onSubmit(e: React.FormEvent) {
e.preventDefault()
const u = urlInput.trim()
if (u.length < 4) return
setSubmittedUrl(u)
setSubmittedBatch(batchInput)
syncQuery(u, batchInput)
}
const scores = data?.scores
return (
{isPending && (
Loading Lighthouse data…
)}
{isError && (
{(error as Error)?.message ?? 'Failed to load report'}
)}
{data && scores && (
Values are estimated and may vary. The performance score is calculated directly from these metrics.{' '}
See calculator
.
- Requested URL:
- {data.requested_url ?? data.website_url}
{data.final_url && data.final_url !== data.requested_url && (
- Final URL:
- {data.final_url}
)}
- Scanned at:
- {data.scanned_at ?? '—'}
{data.lighthouse_version && (
- Lighthouse:
- {data.lighthouse_version}
)}
{data.screenshot_data_url ? (
Final screenshot (from audit JSON)
) : (
No embedded screenshot in this run (run a full Lighthouse categories audit, not accessibility-only, to
capture final-screenshot in stored JSON).
)}
)}
)
}