diff --git a/src/components/Pill.tsx b/src/components/Pill.tsx index b2fd4bf..5dfae47 100644 --- a/src/components/Pill.tsx +++ b/src/components/Pill.tsx @@ -1,4 +1,4 @@ -import { AppSettingStatus } from '@/service/protontweaks'; +import { AppSettingStatus } from '@/service/protontweaks.service'; import { cn } from '@/utils/cn'; import type { FC, ReactNode } from 'react'; diff --git a/src/pages/apps/AppPage.tsx b/src/pages/apps/AppPage.tsx index 4df9d7c..9659fde 100644 --- a/src/pages/apps/AppPage.tsx +++ b/src/pages/apps/AppPage.tsx @@ -6,7 +6,7 @@ import { Label } from '../../components/Label'; import { Pill, appSettingStatustoVariant } from '../../components/Pill'; import { Card } from '../../components/Card'; import { Code } from '../../components/Code'; -import { getApp, getAppSettingStatus, toLaunchOptions } from '@/service/protontweaks'; +import { getApp, getAppSettingStatus, toLaunchOptions } from '@/service/protontweaks.service'; import type { App } from '@/types'; import { Button } from '@/components/Button'; import { ButtonGroup } from '@/components/ButtonGroup'; diff --git a/src/pages/apps/SearchPage.tsx b/src/pages/apps/SearchPage.tsx index 5ae1beb..bf6a438 100644 --- a/src/pages/apps/SearchPage.tsx +++ b/src/pages/apps/SearchPage.tsx @@ -5,7 +5,8 @@ import { Button } from '@/components/Button'; import { ButtonGroup } from '@/components/ButtonGroup'; import type { App } from '@/types'; import { PageSpinner } from '@/components/PageSpinner'; -import { SearchService } from '@/service/search'; +import { SearchService } from '@/service/search.service'; +import { ImageService } from '@/service/image.service'; export const Component: FC = () => { const search = useSearch(); @@ -16,7 +17,12 @@ export const Component: FC = () => { setLoading(true); SearchService.query(search) - .then((apps) => setFilteredApps(apps)) + .then(async (apps) => { + // Preload the images to prevent content from jumping + await ImageService.preload(...apps.map((app) => app.image_url)); + + setFilteredApps(apps); + }) .catch(() => console.log('debounced...')) .finally(() => { setLoading(false); diff --git a/src/service/image.service.ts b/src/service/image.service.ts new file mode 100644 index 0000000..1f8732a --- /dev/null +++ b/src/service/image.service.ts @@ -0,0 +1,24 @@ +export class ImageService { + static async preload(...srcs: string[]): Promise { + return Promise.all( + srcs.map((src) => { + const image = new Image(); + + const promise = new Promise((resolve, reject) => { + image.addEventListener('load', () => resolve(), { + once: true, + }); + + image.addEventListener('error', reject, { + once: true, + }); + }); + + // Set the SRC here to prevent race conditions + image.src = src; + + return promise; + }) + ); + } +} diff --git a/src/service/protontweaks.ts b/src/service/protontweaks.service.ts similarity index 100% rename from src/service/protontweaks.ts rename to src/service/protontweaks.service.ts diff --git a/src/service/search.ts b/src/service/search.service.ts similarity index 68% rename from src/service/search.ts rename to src/service/search.service.ts index 4c4b62c..a7f807f 100644 --- a/src/service/search.ts +++ b/src/service/search.service.ts @@ -1,10 +1,10 @@ -import type { App } from '@/types'; +import type { App, ComputedApp } from '@/types'; import SearchWorker from '@/workers/search.worker?worker'; let previousWorker: Worker; export class SearchService { - static async query(value: string): Promise { + static async query(value: string): Promise { if (previousWorker) { previousWorker.terminate(); } @@ -14,8 +14,13 @@ export class SearchService { return new Promise((resolve, reject) => { previousWorker.addEventListener( 'message', - (event) => { - resolve(event.data); + (event: MessageEvent) => { + resolve( + event.data.map((app) => ({ + ...app, + image_url: `https://steamcdn-a.akamaihd.net/steam/apps/${app.id}/header.jpg`, + })) + ); }, { once: true, diff --git a/src/types/apps.ts b/src/types/apps.ts index 5f340df..5903c46 100644 --- a/src/types/apps.ts +++ b/src/types/apps.ts @@ -26,3 +26,7 @@ export type App = { created_at: string; updated_at: string; }; + +export type ComputedApp = App & { + image_url: string; +}; diff --git a/src/workers/search.worker.ts b/src/workers/search.worker.ts index f210219..9bd445c 100644 --- a/src/workers/search.worker.ts +++ b/src/workers/search.worker.ts @@ -1,4 +1,4 @@ -import { getApps } from '@/service/protontweaks'; +import { getApps } from '@/service/protontweaks.service'; import { delay } from '@ribbon-studios/js-utils'; self.onmessage = async (event) => { diff --git a/vite.config.ts b/vite.config.ts index ffb9a4c..f71e540 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,7 +3,7 @@ import Sitemap from 'vite-plugin-sitemap'; import { viteStaticCopy } from 'vite-plugin-static-copy'; import path from 'path'; import { defineConfig } from 'vitest/config'; -import { getAppRoutes } from './src/service/protontweaks'; +import { getAppRoutes } from './src/service/protontweaks.service'; // https://vitejs.dev/config/ export default defineConfig(async () => {