feat(35-02): add image skeleton loading states to all card types
- Add useState(false) loaded state to ItemCard, CandidateCard, GlobalItemCard
- Show bg-gray-100 animate-pulse skeleton overlay while image loads
- Fade in image via transition-opacity duration-200 on onLoad callback
- No-image placeholders (icon on bg-gray-50) unchanged
- Add import { useState } from react to all three files with correct Biome import order
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFormatters } from "../hooks/useFormatters";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import type { CandidateDelta } from "../hooks/useImpactDeltas";
|
import type { CandidateDelta } from "../hooks/useImpactDeltas";
|
||||||
@@ -56,6 +57,7 @@ export function CandidateCard({
|
|||||||
rank,
|
rank,
|
||||||
delta,
|
delta,
|
||||||
}: CandidateCardProps) {
|
}: CandidateCardProps) {
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
const { t } = useTranslation("threads");
|
const { t } = useTranslation("threads");
|
||||||
const { weight, price } = useFormatters();
|
const { weight, price } = useFormatters();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -169,6 +171,10 @@ export function CandidateCard({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
{!loaded && (
|
||||||
|
<div className="absolute inset-0 bg-gray-100 animate-pulse" />
|
||||||
|
)}
|
||||||
<GearImage
|
<GearImage
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={name}
|
alt={name}
|
||||||
@@ -176,7 +182,10 @@ export function CandidateCard({
|
|||||||
cropZoom={cropZoom}
|
cropZoom={cropZoom}
|
||||||
cropX={cropX}
|
cropX={cropX}
|
||||||
cropY={cropY}
|
cropY={cropY}
|
||||||
|
onLoad={() => setLoaded(true)}
|
||||||
|
className={`transition-opacity duration-200 ${loaded ? "opacity-100" : "opacity-0"}`}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full bg-gray-50 flex flex-col items-center justify-center">
|
<div className="w-full h-full bg-gray-50 flex flex-col items-center justify-center">
|
||||||
<LucideIcon
|
<LucideIcon
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
import { useFormatters } from "../hooks/useFormatters";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import { GearImage, imageContainerBg } from "./GearImage";
|
import { GearImage, imageContainerBg } from "./GearImage";
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ export function GlobalItemCard({
|
|||||||
cropX,
|
cropX,
|
||||||
cropY,
|
cropY,
|
||||||
}: GlobalItemCardProps) {
|
}: GlobalItemCardProps) {
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
const { weight, price } = useFormatters();
|
const { weight, price } = useFormatters();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,6 +48,10 @@ export function GlobalItemCard({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
{!loaded && (
|
||||||
|
<div className="absolute inset-0 bg-gray-100 animate-pulse" />
|
||||||
|
)}
|
||||||
<GearImage
|
<GearImage
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={`${brand} ${model}`}
|
alt={`${brand} ${model}`}
|
||||||
@@ -53,7 +59,10 @@ export function GlobalItemCard({
|
|||||||
cropZoom={cropZoom}
|
cropZoom={cropZoom}
|
||||||
cropX={cropX}
|
cropX={cropX}
|
||||||
cropY={cropY}
|
cropY={cropY}
|
||||||
|
onLoad={() => setLoaded(true)}
|
||||||
|
className={`transition-opacity duration-200 ${loaded ? "opacity-100" : "opacity-0"}`}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full bg-gray-50 flex flex-col items-center justify-center">
|
<div className="w-full h-full bg-gray-50 flex flex-col items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFormatters } from "../hooks/useFormatters";
|
import { useFormatters } from "../hooks/useFormatters";
|
||||||
import { useDuplicateItem } from "../hooks/useItems";
|
import { useDuplicateItem } from "../hooks/useItems";
|
||||||
@@ -52,6 +53,7 @@ export function ItemCard({
|
|||||||
linkTo,
|
linkTo,
|
||||||
priceCurrency,
|
priceCurrency,
|
||||||
}: ItemCardProps) {
|
}: ItemCardProps) {
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
const { t } = useTranslation("collection");
|
const { t } = useTranslation("collection");
|
||||||
const { weight, price } = useFormatters();
|
const { weight, price } = useFormatters();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -194,6 +196,10 @@ export function ItemCard({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
{!loaded && (
|
||||||
|
<div className="absolute inset-0 bg-gray-100 animate-pulse" />
|
||||||
|
)}
|
||||||
<GearImage
|
<GearImage
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={name}
|
alt={name}
|
||||||
@@ -201,7 +207,10 @@ export function ItemCard({
|
|||||||
cropZoom={cropZoom}
|
cropZoom={cropZoom}
|
||||||
cropX={cropX}
|
cropX={cropX}
|
||||||
cropY={cropY}
|
cropY={cropY}
|
||||||
|
onLoad={() => setLoaded(true)}
|
||||||
|
className={`transition-opacity duration-200 ${loaded ? "opacity-100" : "opacity-0"}`}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full h-full bg-gray-50 flex flex-col items-center justify-center">
|
<div className="w-full h-full bg-gray-50 flex flex-col items-center justify-center">
|
||||||
<LucideIcon
|
<LucideIcon
|
||||||
|
|||||||
Reference in New Issue
Block a user