fix: remediate branch review findings

This commit is contained in:
Josh Creek
2026-09-14 11:47:58 +01:00
parent 4af33709a3
commit 86f1c21e4d
38 changed files with 65833 additions and 58545 deletions
+6 -47
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { PUBLIC_USE_LOCAL_POKEMON_SPRITE_FOLDER } from '$env/static/public';
import { inView } from '$lib/actions/inView';
import { resolveSpriteUrl } from '$lib/utils/spriteUrl';
export let pokemonName: string;
export let pokedexNumber: string | number;
@@ -12,46 +13,7 @@
let imagePath = null as string | null;
let isInView = false;
function isFemaleForm(value?: string) {
return /^female\b/i.test((value ?? '').trim());
}
function buildFallbackKey() {
const strippedPokedexNumber = pokedexNumber.toString().replace(/^0+/, '') || '0';
if (!form) return strippedPokedexNumber;
let formValue = form.trim();
formValue = formValue.replace(/^female[-\s]*/i, '');
formValue = formValue
.replace(/\s*\(.*?\)/g, '')
.replace(/\s*\[.*?\]/g, '')
.trim();
if (!formValue || formValue.toLowerCase() === 'male') return strippedPokedexNumber;
formValue = formValue
.toLowerCase()
.replace(/%/g, '')
.replace(/\balolan\b/g, 'alola')
.replace(/\bgalarian\b/g, 'galar')
.replace(/\bhisuian\b/g, 'hisui')
.replace(/\bpaldean\b/g, 'paldea')
.replace(/\bform(e)?$/, '')
.replace(/\bability$/, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
.replace(/2/g, 'two')
.replace(/3/g, 'three')
.replace(/4/g, 'four');
return formValue ? `${strippedPokedexNumber}-${formValue}` : strippedPokedexNumber;
}
$: {
const rootFolderBase =
PUBLIC_USE_LOCAL_POKEMON_SPRITE_FOLDER === 'true'
? '/sprites-small/home'
: 'https://raw.githubusercontent.com/jcreek/LivingDexTracker/master/static/sprites-small/home';
const resolvedSpriteKey = spriteKey?.trim() || buildFallbackKey();
if (!spriteKey?.trim()) {
console.warn('Missing sprite key for pokemon entry', {
pokemonName,
@@ -60,14 +22,11 @@
});
}
let rootFolder = rootFolderBase;
if (shiny) {
rootFolder += '/shiny';
}
if (isFemaleForm(form)) {
rootFolder += '/female';
}
imagePath = `${rootFolder}/${resolvedSpriteKey}.webp`;
imagePath = resolveSpriteUrl(
{ pokedexNumber: Number(pokedexNumber), form, spriteKey },
!!shiny,
PUBLIC_USE_LOCAL_POKEMON_SPRITE_FOLDER === 'true'
);
}
$: if (loadingStrategy !== 'inView') {
+29 -9
View File
@@ -2,7 +2,10 @@
import type { SupabaseClient } from '@supabase/supabase-js';
import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation';
import { clearOfflineData } from '$lib/stores/offlineSync';
const dispatch = createEventDispatcher();
let errorMessage = '';
let isSigningOut = false;
function emitSignedOutEvent() {
dispatch('signedOut', {});
@@ -12,15 +15,32 @@
export let supabase: SupabaseClient;
async function signOut() {
// `.then(() => {...})` resolved to undefined, so destructuring `error` off it threw a
// TypeError on every sign-out - after the event had already been emitted.
const { error } = await supabase.auth.signOut();
if (error) console.error('Sign out failed', error);
emitSignedOutEvent();
// Signing out used to leave the user sitting on the protected page they were on, still
// showing its content. Send them to the public home page and re-run the server loads.
await goto('/', { invalidateAll: true });
errorMessage = '';
isSigningOut = true;
try {
const { error } = await supabase.auth.signOut();
if (error) {
errorMessage = `Sign out failed: ${error.message || 'Please try again.'}`;
dispatch('signOutFailed', { message: errorMessage });
return;
}
try {
await clearOfflineData();
} catch (cacheError) {
console.error('Signed out, but failed to clear offline data', cacheError);
}
emitSignedOutEvent();
await goto('/', { invalidateAll: true });
} catch (error) {
console.error('Sign out failed', error);
errorMessage = 'Sign out failed. Please try again.';
dispatch('signOutFailed', { message: errorMessage });
} finally {
isSigningOut = false;
}
}
</script>
<button on:click={signOut}>Sign Out</button>
<button on:click={signOut} disabled={isSigningOut}>
{isSigningOut ? 'Signing Out…' : 'Sign Out'}
</button>
+35490 -29191
View File
File diff suppressed because it is too large Load Diff
+29197 -29101
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
import type { CombinedData } from './CombinedData';
import type { Pokedex } from './Pokedex';
export const OFFLINE_SNAPSHOT_VERSION = 1;
export type OfflinePokedexSnapshot = {
pokedex: Pokedex;
entries: CombinedData[];
};
export type OfflineSnapshot = {
version: typeof OFFLINE_SNAPSHOT_VERSION;
generatedAt: string;
userId: string;
pokedexes: OfflinePokedexSnapshot[];
};
+169
View File
@@ -0,0 +1,169 @@
import { writable } from 'svelte/store';
import { PUBLIC_USE_LOCAL_POKEMON_SPRITE_FOLDER } from '$env/static/public';
import type { OfflineSnapshot } from '$lib/models/OfflineSnapshot';
import { resolveSpriteUrl } from '$lib/utils/spriteUrl';
export type OfflineSyncStatus = {
state: 'idle' | 'syncing' | 'ready' | 'partial' | 'error';
generatedAt: string | null;
message: string | null;
};
export const offlineSyncStatus = writable<OfflineSyncStatus>({
state: 'idle',
generatedAt: null,
message: null
});
const SYNC_EVENT = 'livingdex:offline-sync';
const OFFLINE_CACHE_PREFIX = 'livingdex-offline-';
const OFFLINE_META_CACHE = `${OFFLINE_CACHE_PREFIX}meta-v1`;
const OFFLINE_META_URL = '/__offline/current';
async function workerMessage(
message: unknown,
timeoutMs = 120_000,
waitForReady = true
): Promise<Record<string, unknown>> {
if (!('serviceWorker' in navigator)) throw new Error('Service workers are unavailable');
const registration = waitForReady
? await navigator.serviceWorker.ready
: await navigator.serviceWorker.getRegistration();
if (!registration?.active) throw new Error('Offline worker is not active');
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
const timeout = window.setTimeout(
() => reject(new Error('Offline worker timed out')),
timeoutMs
);
channel.port1.onmessage = (event) => {
window.clearTimeout(timeout);
const result = event.data as Record<string, unknown>;
if (result?.ok) resolve(result);
else reject(new Error(String(result?.error ?? 'Offline worker failed')));
};
registration.active?.postMessage(message, [channel.port2]);
});
}
export function requestOfflineSync(): void {
if (typeof window !== 'undefined') window.dispatchEvent(new Event(SYNC_EVENT));
}
async function deleteOfflineCaches(): Promise<void> {
if (typeof window === 'undefined' || !('caches' in window)) return;
const names = await caches.keys();
await Promise.all(
names.filter((name) => name.startsWith(OFFLINE_CACHE_PREFIX)).map((name) => caches.delete(name))
);
}
export async function claimOfflineData(userId: string): Promise<void> {
if (typeof window === 'undefined') return;
if ('caches' in window) {
const names = await caches.keys();
if (names.includes(OFFLINE_META_CACHE)) {
const response = await (await caches.open(OFFLINE_META_CACHE)).match(OFFLINE_META_URL);
const meta = await response?.json().catch(() => null);
if (typeof meta?.userId !== 'string' || meta.userId !== userId) await deleteOfflineCaches();
}
}
if ('serviceWorker' in navigator && (await navigator.serviceWorker.getRegistration())?.active) {
await workerMessage({ type: 'CLAIM_OFFLINE_USER', userId }, 10_000, false);
}
}
export async function clearOfflineData(): Promise<void> {
if (typeof window === 'undefined') return;
await deleteOfflineCaches();
if ('serviceWorker' in navigator && (await navigator.serviceWorker.getRegistration())?.active) {
await workerMessage({ type: 'CLEAR_OFFLINE_DATA' }, 10_000, false);
}
offlineSyncStatus.set({ state: 'idle', generatedAt: null, message: null });
}
export function startOfflineSync(getUserId: () => string | null): () => void {
let timer: number | null = null;
let stopped = false;
let running = false;
let rerun = false;
let lastGeneratedAt: string | null = null;
const synchronize = async () => {
if (stopped || !navigator.onLine) return;
if (running) {
rerun = true;
return;
}
const userId = getUserId();
if (!userId || !('serviceWorker' in navigator)) return;
running = true;
offlineSyncStatus.set({ state: 'syncing', generatedAt: null, message: null });
try {
await claimOfflineData(userId);
const response = await fetch('/api/offline-snapshot', {
credentials: 'include',
headers: { Accept: 'application/json' }
});
if (!response.ok) throw new Error(`Snapshot request failed (${response.status})`);
const snapshot = (await response.json()) as OfflineSnapshot;
if (snapshot.userId !== userId) throw new Error('Snapshot owner did not match the session');
const useLocal = PUBLIC_USE_LOCAL_POKEMON_SPRITE_FOLDER === 'true';
const artworkUrls = Array.from(
new Set(
snapshot.pokedexes.flatMap(({ pokedex, entries }) =>
entries.map(({ pokedexEntry }) => {
const url = resolveSpriteUrl(pokedexEntry, pokedex.isShinyDex, useLocal);
(
pokedexEntry as typeof pokedexEntry & { offlineSpriteUrl: string }
).offlineSpriteUrl = url;
return url;
})
)
)
);
const result = await workerMessage({
type: 'SYNC_OFFLINE_SNAPSHOT',
snapshot,
artworkUrls
});
const failed = Number(result.failedArtwork ?? 0);
offlineSyncStatus.set({
state: failed > 0 ? 'partial' : 'ready',
generatedAt: snapshot.generatedAt,
message: failed > 0 ? `${failed} artwork files could not be cached` : null
});
lastGeneratedAt = snapshot.generatedAt;
} catch (error) {
offlineSyncStatus.set({
state: 'error',
generatedAt: lastGeneratedAt,
message: error instanceof Error ? error.message : String(error)
});
} finally {
running = false;
if (rerun && !stopped) {
rerun = false;
schedule();
}
}
};
const schedule = () => {
if (timer !== null) window.clearTimeout(timer);
timer = window.setTimeout(() => {
timer = null;
void synchronize();
}, 1_000);
};
window.addEventListener(SYNC_EVENT, schedule);
window.addEventListener('online', schedule);
schedule();
return () => {
stopped = true;
if (timer !== null) window.clearTimeout(timer);
window.removeEventListener(SYNC_EVENT, schedule);
window.removeEventListener('online', schedule);
};
}
+43
View File
@@ -0,0 +1,43 @@
export function resolveSpriteUrl(
entry: { pokedexNumber: number; form?: string; spriteKey?: string },
shiny: boolean,
useLocalSprites: boolean
): string {
const form = entry.form?.trim() ?? '';
const strippedNumber = String(entry.pokedexNumber).replace(/^0+/, '') || '0';
let key = entry.spriteKey?.trim();
if (!key) {
let formKey = form
.replace(/^female[-\s]*/i, '')
.replace(/\s*\(.*?\)/g, '')
.replace(/\s*\[.*?\]/g, '')
.trim();
if (!formKey || formKey.toLowerCase() === 'male') {
key = strippedNumber;
} else {
formKey = formKey
.toLowerCase()
.replace(/%/g, '')
.replace(/\balolan\b/g, 'alola')
.replace(/\bgalarian\b/g, 'galar')
.replace(/\bhisuian\b/g, 'hisui')
.replace(/\bpaldean\b/g, 'paldea')
.replace(/\bform(e)?$/, '')
.replace(/\bability$/, '')
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
.replace(/2/g, 'two')
.replace(/3/g, 'three')
.replace(/4/g, 'four');
key = formKey ? `${strippedNumber}-${formKey}` : strippedNumber;
}
}
let root = useLocalSprites
? '/sprites-small/home'
: 'https://raw.githubusercontent.com/jcreek/LivingDexTracker/master/static/sprites-small/home';
if (shiny) root += '/shiny';
if (/^female\b/i.test(form)) root += '/female';
return `${root}/${key}.webp`;
}