mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-29 14:50:13 -05:00
Show fallback UI when art fails to load
This commit is contained in:
@@ -56,6 +56,34 @@
|
||||
);
|
||||
}
|
||||
|
||||
/* nothing to look at, so the details take over the figure instead of floating over the image */
|
||||
.lightboxFigureFailed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(var(--lightbox-max-width), 22rem);
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
|
||||
.lightboxInfo {
|
||||
position: static;
|
||||
opacity: 1;
|
||||
padding-block-start: var(--s-3);
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lightboxFailed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--s-2);
|
||||
min-height: 10rem;
|
||||
padding: var(--s-4);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: var(--font-sm);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lightboxPlaceholder {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from "clsx";
|
||||
import { SquarePen, Trash, Unlink } from "lucide-react";
|
||||
import { ImageOff, SquarePen, Trash, Unlink } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
@@ -90,12 +90,15 @@ function BigImageDialog({ close, art }: { close: () => void; art: ListedArt }) {
|
||||
const [infoVisible, setInfoVisible] = React.useState(true);
|
||||
const [imageSettled, imageRef] = useImageSettled();
|
||||
const [aspectRatio, placeholderRef] = useImageAspectRatio();
|
||||
const { t } = useTranslation(["art"]);
|
||||
const { formatter } = useDateTimeFormat({
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
const imageFailed = aspectRatio === "FAILED";
|
||||
|
||||
const dateText = formatter.format(databaseTimestampToDate(art.createdAt));
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
@@ -120,10 +123,11 @@ function BigImageDialog({ close, art }: { close: () => void; art: ListedArt }) {
|
||||
<div className={styles.lightboxBody} onClick={handleClick}>
|
||||
<figure
|
||||
className={clsx(styles.lightboxFigure, {
|
||||
[styles.lightboxFigureSized]: aspectRatio,
|
||||
[styles.lightboxFigureSized]: typeof aspectRatio === "number",
|
||||
[styles.lightboxFigureFailed]: imageFailed,
|
||||
})}
|
||||
style={
|
||||
aspectRatio
|
||||
typeof aspectRatio === "number"
|
||||
? ({ "--aspect-ratio": aspectRatio } as React.CSSProperties)
|
||||
: undefined
|
||||
}
|
||||
@@ -134,14 +138,21 @@ function BigImageDialog({ close, art }: { close: () => void; art: ListedArt }) {
|
||||
className={styles.lightboxPlaceholder}
|
||||
ref={placeholderRef}
|
||||
/>
|
||||
<img
|
||||
alt=""
|
||||
src={art.url}
|
||||
className={clsx(styles.lightboxImg, {
|
||||
[styles.lightboxImgSettled]: imageSettled,
|
||||
})}
|
||||
ref={imageRef}
|
||||
/>
|
||||
{imageFailed ? (
|
||||
<div className={styles.lightboxFailed}>
|
||||
<ImageOff />
|
||||
{t("art:imageFailed")}
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
alt=""
|
||||
src={art.url}
|
||||
className={clsx(styles.lightboxImg, {
|
||||
[styles.lightboxImgSettled]: imageSettled,
|
||||
})}
|
||||
ref={imageRef}
|
||||
/>
|
||||
)}
|
||||
<figcaption
|
||||
className={clsx(styles.lightboxInfo, {
|
||||
[styles.lightboxInfoHidden]: !infoVisible,
|
||||
@@ -364,17 +375,24 @@ function useImageSettled() {
|
||||
return [imageSettled, imageRef] as const;
|
||||
}
|
||||
|
||||
/** Aspect ratio of the image once it has loaded, and the ref to give it. */
|
||||
/**
|
||||
* Aspect ratio of the image once it has loaded, "FAILED" if it never will,
|
||||
* and the ref to give it.
|
||||
*/
|
||||
function useImageAspectRatio() {
|
||||
const [aspectRatio, setAspectRatio] = React.useState<number | null>(null);
|
||||
const [aspectRatio, setAspectRatio] = React.useState<
|
||||
number | "FAILED" | null
|
||||
>(null);
|
||||
|
||||
const imageRef = (image: HTMLImageElement | null) => {
|
||||
if (!image) return;
|
||||
|
||||
const measure = () => {
|
||||
if (image.naturalWidth > 0 && image.naturalHeight > 0) {
|
||||
setAspectRatio(image.naturalWidth / image.naturalHeight);
|
||||
}
|
||||
setAspectRatio(
|
||||
image.naturalWidth > 0 && image.naturalHeight > 0
|
||||
? image.naturalWidth / image.naturalHeight
|
||||
: "FAILED",
|
||||
);
|
||||
};
|
||||
if (image.complete) {
|
||||
measure();
|
||||
@@ -382,7 +400,12 @@ function useImageAspectRatio() {
|
||||
}
|
||||
|
||||
image.addEventListener("load", measure);
|
||||
return () => image.removeEventListener("load", measure);
|
||||
image.addEventListener("error", measure);
|
||||
|
||||
return () => {
|
||||
image.removeEventListener("load", measure);
|
||||
image.removeEventListener("error", measure);
|
||||
};
|
||||
};
|
||||
|
||||
return [aspectRatio, imageRef] as const;
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
"forms.tags.maxReached": "Maks antal etiketter opnået",
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": ""
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
"forms.tags.maxReached": "",
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": ""
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
"forms.tags.maxReached": "Max tags reached",
|
||||
"delete.title": "Are you sure you want to delete the art?",
|
||||
"unlink.title": "Are you sure you want to remove this art from your profile (only {{username}} can add it back)?",
|
||||
"noArtForTag": "No uploaded art found for #{{tag}}"
|
||||
"noArtForTag": "No uploaded art found for #{{tag}}",
|
||||
"imageFailed": "Image could not be loaded"
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
"forms.tags.maxReached": "Límite máximo de etiquetas",
|
||||
"delete.title": "¿Seguro que quieres eliminar este arte?",
|
||||
"unlink.title": "¿Seguro que quieres desvincular este arte de tu perfil? (solo {{username}} puede volver a añadirlo)",
|
||||
"noArtForTag": "No se ha encontrado arte subido para #{{tag}}"
|
||||
"noArtForTag": "No se ha encontrado arte subido para #{{tag}}",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
"forms.tags.maxReached": "Límite máximo de etiquetas",
|
||||
"delete.title": "¿Seguro que quieres eliminar este arte?",
|
||||
"unlink.title": "¿Seguro que quieres desvincular este arte de tu perfil? (solo {{username}} puede volver a añadirlo)",
|
||||
"noArtForTag": "No se ha encontrado arte subido para #{{tag}}"
|
||||
"noArtForTag": "No se ha encontrado arte subido para #{{tag}}",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
"forms.tags.maxReached": "Limite de tags atteinte",
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": ""
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
"forms.tags.maxReached": "Limite de tags atteinte",
|
||||
"delete.title": "Êtes vous sûre de vouloir supprimer cette œuvre?",
|
||||
"unlink.title": "Êtes vous sûre de vouloir supprimer cette œuvre de votre profil? (Seulement {{username}} pourra le remettre)",
|
||||
"noArtForTag": ""
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
"forms.tags.maxReached": "הגעת לכמות תגים מקסימלית",
|
||||
"delete.title": "האם אתה בטוח שאתה רוצה למחוק את הציור?",
|
||||
"unlink.title": "האם אתה בטוח שאתה רוצה להסיר את הציור הזה מהפרופיל שלך (רק {{username}} יוכל להוסיף אותו חזרה)?",
|
||||
"noArtForTag": ""
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
"forms.tags.maxReached": "Numero massimo di tag raggiunto.",
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": ""
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -20,5 +20,6 @@
|
||||
"forms.tags.maxReached": "タグの最大数に到達しました",
|
||||
"delete.title": "本当にこの作品を削除しますか?",
|
||||
"unlink.title": "本当にこの作品をプロフィールから削除しますか?{{username}}しか再び追加することはできません。",
|
||||
"noArtForTag": "#{{tag}}の作品が見つかりませんでした。"
|
||||
"noArtForTag": "#{{tag}}の作品が見つかりませんでした。",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -20,5 +20,6 @@
|
||||
"forms.tags.maxReached": "",
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": ""
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
"forms.tags.maxReached": "",
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": ""
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
"forms.tags.maxReached": "",
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": ""
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@
|
||||
"forms.tags.maxReached": "Quantidade máxima de marcações atingida",
|
||||
"delete.title": "",
|
||||
"unlink.title": "",
|
||||
"noArtForTag": ""
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
"forms.tags.maxReached": "Максимальное кол-во тегов достигнуто",
|
||||
"delete.title": "Вы точно хотите удалить этот арт?",
|
||||
"unlink.title": "Вы точно хотите удалить этот арт с вашего профиля (только {{username}} может добавить его обратно)?",
|
||||
"noArtForTag": ""
|
||||
"noArtForTag": "",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
"forms.tags.maxReached": "已达到最大标签数",
|
||||
"delete.title": "您确定要删除这件作品吗?",
|
||||
"unlink.title": "您确定要将这件作品从个人资料中移除吗(只有 {{username}} 可以重新添加)?",
|
||||
"noArtForTag": "未能找到包含 #{{tag}} 的作品。"
|
||||
"noArtForTag": "未能找到包含 #{{tag}} 的作品。",
|
||||
"imageFailed": ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user