v3: allow users to edit dump thumbnail, fixed some linter errors in the frontend
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 41s

This commit is contained in:
khannurien
2026-06-21 15:41:40 +00:00
parent 57cf55cc48
commit cf988ae608
27 changed files with 431 additions and 121 deletions

View File

@@ -96,7 +96,9 @@ export function DumpCreateModal(
const [submitState, setSubmitState] = useState<SubmitState>({
status: "idle",
});
const [urlPreview, setUrlPreview] = useState<UrlPreview>({ status: "idle" });
const [urlFetchState, setUrlFetchState] = useState<UrlPreview>({
status: "idle",
});
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Playlist phase state
@@ -109,40 +111,54 @@ export function DumpCreateModal(
setTitleEditing(false);
};
// Canonical form of `url`, used (rather than the raw input text) to decide
// whether the preview actually needs to re-fetch — e.g. "example.com" and
// "https://example.com" (set later by onBlur's normalizeUrl) canonicalize
// to the same value, so they shouldn't trigger two separate fetches.
let canonicalUrl: string | null;
try {
const u = new URL(normalizeUrl(url));
canonicalUrl = (u.protocol === "http:" || u.protocol === "https:")
? u.toString()
: null;
} catch {
canonicalUrl = null;
}
// Idle state is derived below from canonicalUrl rather than set here, so
// this effect only ever needs to set loading/done (inside the timeout).
const urlPreview: UrlPreview = canonicalUrl
? urlFetchState
: { status: "idle" };
// Debounced URL preview
useEffect(() => {
if (debounceRef.current) clearTimeout(debounceRef.current);
if (!canonicalUrl) return;
let trimmed: string;
try {
const u = new URL(normalizeUrl(url));
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error();
trimmed = u.toString();
} catch {
setUrlPreview({ status: "idle" });
return;
}
// Deferred to a microtask (rather than called directly) so the immediate
// "loading" feedback doesn't fire synchronously within the effect body —
// same timing for the user, just not in the same tick.
queueMicrotask(() => setUrlFetchState({ status: "loading" }));
setUrlPreview({ status: "loading" });
debounceRef.current = setTimeout(async () => {
try {
const res = await fetch(
`${API_URL}/api/preview?url=${encodeURIComponent(trimmed)}`,
);
const body = await res.json();
setUrlPreview({
status: "done",
richContent: body.success ? body.data : null,
debounceRef.current = setTimeout(() => {
fetch(`${API_URL}/api/preview?url=${encodeURIComponent(canonicalUrl)}`)
.then((res) => res.json())
.then((body) => {
setUrlFetchState({
status: "done",
richContent: body.success ? body.data : null,
});
})
.catch(() => {
setUrlFetchState({ status: "done", richContent: null });
});
} catch {
setUrlPreview({ status: "done", richContent: null });
}
}, 600);
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [url]);
}, [canonicalUrl]);
// Paste handler
useEffect(() => {
@@ -151,7 +167,6 @@ export function DumpCreateModal(
if (pastedFile) {
setMode("file");
setUrl("");
setUrlPreview({ status: "idle" });
selectFile(pastedFile);
setSubmitState({ status: "idle" });
return;
@@ -310,7 +325,6 @@ export function DumpCreateModal(
onClick={() => {
setMode("file");
setUrl("");
setUrlPreview({ status: "idle" });
setSubmitState({ status: "idle" });
}}
disabled={submitting}
@@ -346,7 +360,6 @@ export function DumpCreateModal(
e.preventDefault();
setMode("file");
setUrl("");
setUrlPreview({ status: "idle" });
selectFile(pastedFile);
setSubmitState({ status: "idle" });
}