Add Pinia persistence and scoreboard support (#15)

* Add scoreboard replicant and Pinia persistence

* Fix scoreboard replicant sync (#16)
This commit is contained in:
Pandipipas
2026-02-08 17:00:31 +01:00
committed by GitHub
parent e0323cca3f
commit b4a110fd1e
16 changed files with 712 additions and 64 deletions
+132
View File
@@ -0,0 +1,132 @@
import { defineStore } from 'pinia';
import { computed, ref, watch } from 'vue';
import type { Ref } from 'vue';
import { playersReplicant } from '../../../browser_shared/replicants';
import type { Schemas } from '../../../types';
type PlayersMap = Schemas.Players;
type Player = PlayersMap[string];
const STORAGE_KEY = 'scoreko-dev.players';
const normalizePlayer = (input: unknown): Player => {
const candidate = typeof input === 'object' && input !== null ? (input as Record<string, unknown>) : {};
return {
gamertag: typeof candidate.gamertag === 'string' ? candidate.gamertag : '',
team: typeof candidate.team === 'string' ? candidate.team : '',
country: typeof candidate.country === 'string' ? candidate.country : '',
twitter: typeof candidate.twitter === 'string' ? candidate.twitter : '',
realName: typeof candidate.realName === 'string' ? candidate.realName : '',
pronouns: typeof candidate.pronouns === 'string' ? candidate.pronouns : '',
twitch: typeof candidate.twitch === 'string' ? candidate.twitch : '',
notes: typeof candidate.notes === 'string' ? candidate.notes : '',
};
};
const normalizePlayers = (input: unknown): PlayersMap => {
if (typeof input !== 'object' || input === null) {
return {};
}
const result: PlayersMap = {};
Object.entries(input as Record<string, unknown>).forEach(([id, value]) => {
if (!id) {
return;
}
result[id] = normalizePlayer(value);
});
return result;
};
const readStorage = (): PlayersMap | null => {
if (typeof window === 'undefined') {
return null;
}
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) {
return null;
}
const parsed = JSON.parse(raw) as unknown;
return normalizePlayers(parsed);
} catch {
return null;
}
};
const writeStorage = (value: PlayersMap) => {
if (typeof window === 'undefined') {
return;
}
try {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
} catch {
// Ignore storage errors (quota, disabled, etc.)
}
};
export const usePlayersStore = defineStore('players', () => {
const players = ref<PlayersMap>({});
const replicantRef = playersReplicant?.data as unknown as Ref<PlayersMap | undefined> | undefined;
const storageSnapshot = readStorage();
if (storageSnapshot) {
players.value = storageSnapshot;
}
const isApplyingReplicant = ref(false);
watch(
() => replicantRef?.value,
(value) => {
if (!value) {
return;
}
isApplyingReplicant.value = true;
players.value = normalizePlayers(value);
isApplyingReplicant.value = false;
writeStorage(players.value);
},
{ deep: true, immediate: true }
);
watch(
players,
(value) => {
writeStorage(value);
if (isApplyingReplicant.value || !replicantRef) {
return;
}
replicantRef.value = normalizePlayers(value);
},
{ deep: true }
);
const setPlayers = (value: PlayersMap) => {
players.value = normalizePlayers(value);
};
const upsertPlayer = (id: string, player: Player) => {
players.value = {
...players.value,
[id]: normalizePlayer(player),
};
};
const removePlayer = (id: string) => {
const next = { ...players.value };
delete next[id];
players.value = next;
};
const rows = computed(() => Object.entries(players.value).map(([id, player]) => ({
id,
...player,
})));
return {
players,
rows,
setPlayers,
upsertPlayer,
removePlayer,
};
});