mirror of
https://github.com/mgeeky/decode-spam-headers.git
synced 2026-02-22 05:23:31 +01:00
MAESTRO: integrate browser cache UI
This commit is contained in:
@@ -1,15 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import AnalyseButton from "../components/AnalyseButton";
|
||||
import AnalysisResults from "../components/AnalysisResults";
|
||||
import FileDropZone from "../components/FileDropZone";
|
||||
import HeaderInput from "../components/HeaderInput";
|
||||
import ProgressIndicator from "../components/ProgressIndicator";
|
||||
import ReportContainer from "../components/report/ReportContainer";
|
||||
import useAnalysis from "../hooks/useAnalysis";
|
||||
import useAnalysisCache from "../hooks/useAnalysisCache";
|
||||
import { MAX_HEADER_INPUT_BYTES } from "../lib/header-validation";
|
||||
import type { AnalysisConfig } from "../types/analysis";
|
||||
import type { AnalysisConfig, AnalysisReport } from "../types/analysis";
|
||||
|
||||
const defaultConfig: AnalysisConfig = {
|
||||
testIds: [],
|
||||
@@ -18,22 +21,78 @@ const defaultConfig: AnalysisConfig = {
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const [headerInput, setHeaderInput] = useState("");
|
||||
const { status, progress, result, submit } = useAnalysis();
|
||||
const { status, progress, result, submit, cancel } = useAnalysis();
|
||||
const { save, load, clear, isNearLimit } = useAnalysisCache();
|
||||
const initialCache = useMemo(() => load(), [load]);
|
||||
const [headerInput, setHeaderInput] = useState(() => initialCache?.headers ?? "");
|
||||
const [analysisConfig, setAnalysisConfig] = useState<AnalysisConfig>(
|
||||
() => initialCache?.config ?? defaultConfig,
|
||||
);
|
||||
const [cachedReport, setCachedReport] = useState<AnalysisReport | null>(
|
||||
() => initialCache?.result ?? null,
|
||||
);
|
||||
const [cachedTimestamp, setCachedTimestamp] = useState<number | null>(
|
||||
() => initialCache?.timestamp ?? null,
|
||||
);
|
||||
const [isViewCleared, setIsViewCleared] = useState(false);
|
||||
const lastSubmissionRef = useRef<{ headers: string; config: AnalysisConfig } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = lastSubmissionRef.current ?? {
|
||||
headers: headerInput,
|
||||
config: analysisConfig,
|
||||
};
|
||||
|
||||
save({ headers: payload.headers, config: payload.config, result });
|
||||
}, [analysisConfig, headerInput, result, save]);
|
||||
|
||||
const hasHeaderInput = headerInput.trim().length > 0;
|
||||
const isOversized = headerInput.length > MAX_HEADER_INPUT_BYTES;
|
||||
const canAnalyse = hasHeaderInput && !isOversized;
|
||||
const isLoading = status === "submitting" || status === "analysing";
|
||||
const showProgress = status === "analysing" || status === "timeout";
|
||||
const incompleteTests = result?.metadata.incompleteTests ?? [];
|
||||
const allowCachedFallback =
|
||||
status === "idle" || status === "complete" || status === "error" || status === "timeout";
|
||||
const report = isViewCleared ? null : (result ?? (allowCachedFallback ? cachedReport : null));
|
||||
const isCachedView = Boolean(!isViewCleared && !result && allowCachedFallback && cachedReport);
|
||||
const hasCache = Boolean(result || cachedReport);
|
||||
const cacheTimestampLabel = useMemo(() => {
|
||||
if (!cachedTimestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date(cachedTimestamp).toLocaleString("en-US", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
});
|
||||
}, [cachedTimestamp]);
|
||||
|
||||
const handleAnalyse = useCallback(() => {
|
||||
if (!canAnalyse) {
|
||||
return;
|
||||
}
|
||||
|
||||
void submit({ headers: headerInput, config: defaultConfig });
|
||||
}, [canAnalyse, headerInput, submit]);
|
||||
const payload = { headers: headerInput, config: analysisConfig };
|
||||
lastSubmissionRef.current = payload;
|
||||
setIsViewCleared(false);
|
||||
void submit(payload);
|
||||
}, [analysisConfig, canAnalyse, headerInput, submit]);
|
||||
|
||||
const handleClearCache = useCallback(() => {
|
||||
clear();
|
||||
cancel();
|
||||
setHeaderInput("");
|
||||
setAnalysisConfig(defaultConfig);
|
||||
setCachedReport(null);
|
||||
setCachedTimestamp(null);
|
||||
setIsViewCleared(true);
|
||||
lastSubmissionRef.current = null;
|
||||
}, [cancel, clear]);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-background text-text">
|
||||
@@ -90,10 +149,53 @@ export default function Home() {
|
||||
incompleteTests={incompleteTests}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="rounded-2xl border border-info/10 bg-surface p-5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<p className="text-xs uppercase tracking-[0.2em] text-info/80">Browser Cache</p>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-info/20 bg-background/40 px-4 py-2 text-[11px] font-semibold uppercase tracking-[0.2em] text-text/70 transition hover:border-info/40 hover:text-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-info disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onClick={handleClearCache}
|
||||
disabled={!hasCache}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="text-xs" />
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-text/60">
|
||||
{result
|
||||
? "Latest analysis cached for this session."
|
||||
: hasCache
|
||||
? `Cached analysis saved ${cacheTimestampLabel ?? "recently"}.`
|
||||
: "No cached analysis yet. Run an analysis to save this session."}
|
||||
</p>
|
||||
{isNearLimit ? (
|
||||
<p className="mt-2 text-xs text-suspicious">
|
||||
Local storage is nearly full. Consider clearing cached data.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{result ? <AnalysisResults report={result} /> : null}
|
||||
{report ? (
|
||||
<section className="flex flex-col gap-3">
|
||||
{isCachedView ? (
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] text-text/60">
|
||||
<span className="rounded-full border border-info/20 bg-background/40 px-3 py-1 uppercase tracking-[0.2em] text-info/70">
|
||||
Cached Result
|
||||
</span>
|
||||
{cacheTimestampLabel ? (
|
||||
<span>Saved {cacheTimestampLabel}</span>
|
||||
) : (
|
||||
<span>Saved recently</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<ReportContainer report={report} />
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user