diff --git a/frontend/src/features/upload/UploadPage.tsx b/frontend/src/features/upload/UploadPage.tsx index aa208e4..791e53c 100644 --- a/frontend/src/features/upload/UploadPage.tsx +++ b/frontend/src/features/upload/UploadPage.tsx @@ -1,6 +1,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { CheckCircle2, + ImageOff, ListChecks, Trash2, UploadCloud, @@ -310,6 +311,54 @@ function TempCard({ ); } +/** Image with a spinner/broken-image placeholder instead of alt-text fallback. */ +function ThumbImage({ + src, + className, +}: { + src: string; + className?: string; +}) { + const [loadedSrc, setLoadedSrc] = useState(null); + const [failedSrc, setFailedSrc] = useState(null); + // Derived from the current src: no effect needed when it changes. + const ready = Boolean(src) && loadedSrc === src; + const failed = !src || failedSrc === src; + + return ( + + {src ? ( + setLoadedSrc(src)} + onError={() => setFailedSrc(src)} + className={cn( + "h-full w-full object-cover transition-opacity duration-200", + ready ? "opacity-100" : "opacity-0", + )} + /> + ) : null} + {!ready && !failed ? ( + + + + ) : null} + {failed ? ( + + + + ) : null} + + ); +} + /** * A "J-x uploaded" notification. * @@ -378,7 +427,6 @@ function MetadataForm({ }) { const queryClient = useQueryClient(); const credentials = useE621((state) => state.credentials); - const [tab, setTab] = useState<"link" | "custom">("link"); const [postId, setPostId] = useState(""); const [selected, setSelected] = useState(null); const [rating, setRating] = useState(temp.custom_rating); @@ -485,8 +533,8 @@ function MetadataForm({ } return ( - <> -
+
+

Set metadata @@ -495,288 +543,279 @@ function MetadataForm({ {temp.original_filename}

- +
+ {temp.processing ? ( + + working + + ) : null} + +
-
-
- {mediaUrl ? ( - isVideo ? ( -