Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/lib/components/pokemon/pokemon-art.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { imageSrcset, optimizedImage, type ImageWidth } from "#lib/images.js";
import { imageSrcset, optimizedImage, retryOriginalImage, type ImageWidth } from "#lib/images.js";
import { artworkUrl } from "#lib/pokemon/sprites.js";
import { cn } from "#lib/utils.js";

Expand Down Expand Up @@ -55,7 +55,9 @@
loading={eager ? "eager" : "lazy"}
fetchpriority={eager ? "high" : undefined}
decoding="async"
onerror={() => (failed = true)}
onerror={(event) => {
if (!retryOriginalImage(event.currentTarget as HTMLImageElement, url)) failed = true;
}}
class={cn("aspect-square h-auto w-full object-contain", className)}
/>
{/if}
4 changes: 2 additions & 2 deletions src/lib/components/pokemon/pokemon-picker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import * as Command from "#lib/components/ui/command/index.js";
import { loadDexList } from "#lib/dex/client.js";
import { DEFAULT_QUERY, queryDex, type DexListEntry } from "#lib/dex/list.js";
import { optimizedImage } from "#lib/images.js";
import { optimizedImage, retryOriginalImage } from "#lib/images.js";
import { dexNumber } from "#lib/pokemon/format.js";
import { artworkUrl } from "#lib/pokemon/sprites.js";
import TypeBadge from "./type-badge.svelte";
Expand Down Expand Up @@ -37,7 +37,7 @@
<Command.Empty>{entries.length ? "No Pokémon match that search." : "Loading the Pokédex…"}</Command.Empty>
{#each results as entry (entry.id)}
<Command.Item value="pick-{entry.slug}" onSelect={() => pick(entry)} class="gap-3">
<img src={optimizedImage(artworkUrl(entry.id), 96)} alt="" width="36" height="36" loading="lazy" class="size-9 object-contain" />
<img src={optimizedImage(artworkUrl(entry.id), 96)} onerror={(event) => retryOriginalImage(event.currentTarget as HTMLImageElement, artworkUrl(entry.id))} alt="" width="36" height="36" loading="lazy" class="size-9 object-contain" />
<span class="flex min-w-0 flex-1 flex-col">
<span class="truncate font-medium">{entry.name}</span>
<span class="text-xs text-muted-foreground tabular">{dexNumber(entry.speciesId)}</span>
Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/site/command-palette.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { parseSearch } from "#lib/dex/filters.js";
import { DEFAULT_QUERY, queryDex, type DexListEntry } from "#lib/dex/list.js";
import { matchScore } from "#lib/dex/search.js";
import { optimizedImage } from "#lib/images.js";
import { optimizedImage, retryOriginalImage } from "#lib/images.js";
import { navItems } from "#lib/nav.js";
import { dexNumber, titleCase } from "#lib/pokemon/format.js";
import { artworkUrl } from "#lib/pokemon/sprites.js";
Expand Down Expand Up @@ -84,6 +84,7 @@
<Command.Item value="pokemon-{entry.slug}" onSelect={() => open(`/pokemon/${entry.slug}`)} class="gap-3">
<img
src={optimizedImage(artworkUrl(entry.id), 96)}
onerror={(event) => retryOriginalImage(event.currentTarget as HTMLImageElement, artworkUrl(entry.id))}
alt=""
width="36"
height="36"
Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/team/saved-teams.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import * as Dialog from "#lib/components/ui/dialog/index.js";
import { Input } from "#lib/components/ui/input/index.js";
import { Label } from "#lib/components/ui/label/index.js";
import { optimizedImage } from "#lib/images.js";
import { optimizedImage, retryOriginalImage } from "#lib/images.js";
import { artworkUrl } from "#lib/pokemon/sprites.js";
import type { TeamMember } from "#lib/team/analysis.js";
import { MAX_TEAM_NAME } from "#lib/team/validate.js";
Expand Down Expand Up @@ -139,6 +139,7 @@
{#if id}
<img
src={optimizedImage(artworkUrl(id), 96)}
onerror={(event) => retryOriginalImage(event.currentTarget as HTMLImageElement, artworkUrl(id))}
alt=""
width="32"
height="32"
Expand Down
26 changes: 26 additions & 0 deletions src/lib/images.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, it } from "vite-plus/test";
import { retryOriginalImage } from "./images.js";

const url = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png";

describe("retryOriginalImage", () => {
it("retries the original when the optimizer fails and clears responsive candidates", () => {
const image = {
src: "https://dexa.voltcrash.com/_vercel/image?url=artwork",
srcset: "optimized 384w",
};
expect(retryOriginalImage(image, url)).toBe(true);
expect(image).toEqual({ src: url, srcset: "" });
});

it("stops retrying when the original also fails", () => {
const image = { src: "/_vercel/image?url=artwork", srcset: "" };
retryOriginalImage(image, url);
expect(retryOriginalImage(image, url)).toBe(false);
});

it("does not retry an image already served directly", () => {
const image = { src: url, srcset: "" };
expect(retryOriginalImage(image, url)).toBe(false);
});
});
10 changes: 10 additions & 0 deletions src/lib/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,13 @@ export function imageSrcset(url: string, widths: readonly ImageWidth[]): string
if (!__VERCEL_IMAGES__) return undefined;
return widths.map((w) => `${optimizedImage(url, w)} ${w}w`).join(", ");
}

export function retryOriginalImage(
image: Pick<HTMLImageElement, "src" | "srcset">,
url: string,
): boolean {
if (image.src === url) return false;
image.srcset = "";
image.src = url;
return true;
}
3 changes: 2 additions & 1 deletion src/routes/collection/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { Skeleton } from "#lib/components/ui/skeleton/index.js";
import { loadDexList } from "#lib/dex/client.js";
import type { DexListEntry } from "#lib/dex/list.js";
import { optimizedImage } from "#lib/images.js";
import { optimizedImage, retryOriginalImage } from "#lib/images.js";
import { dexNumber } from "#lib/pokemon/format.js";
import { GENERATIONS } from "#lib/pokemon/generations.js";
import { artworkUrl } from "#lib/pokemon/sprites.js";
Expand Down Expand Up @@ -148,6 +148,7 @@
>
<img
src={optimizedImage(artworkUrl(entry.id), 96)}
onerror={(event) => retryOriginalImage(event.currentTarget as HTMLImageElement, artworkUrl(entry.id))}
alt=""
width="72"
height="72"
Expand Down
Loading