Written by: Marlon Colca
Posted on 22 Sep 2025 - 12 days ago
typescript nodejs vue games
One of the coolest parts of Cards Match is how players can tweak difficulty and swap image sources on the fly. Let us explore the supporting composables.
One of the coolest parts of Cards Match is how players can tweak difficulty and swap image sources on the fly. Let us explore the supporting composables.
useSettings.ts
keeps track of difficulty, source, and Giphy parameters, persisting everything to localStorage
:
const STORAGE_SETTINGS = "cm-settings";
const DEFAULTS: Settings = {
difficulty: "medium",
source: "emoji",
giphyApiKey: "",
giphyQuery: "cats",
};
function load(): Settings {
try {
return {
...DEFAULTS,
...(JSON.parse(localStorage.getItem(STORAGE_SETTINGS) || "null") || {}),
};
} catch {
return { ...DEFAULTS };
}
}
export function useSettings() {
const settings = reactive<Settings>(load());
watch(
settings,
() => {
localStorage.setItem(STORAGE_SETTINGS, JSON.stringify(settings));
},
{ deep: true }
);
return { settings };
}
load()
merges defaults with stored values and survives JSON parse errors.imageService.ts
decides whether to serve offline emojis or hit the Giphy API:
export async function getImages(settings: Settings, count: number) {
if (settings.source === "emoji") {
return EMOJI.slice(0, count).map((ch, i) => ({
id: `emoji-${i}-${ch}`,
url: emojiToDataUrl(ch),
}));
}
try {
const apiKey = (settings.giphyApiKey || "").trim();
const q = encodeURIComponent(settings.giphyQuery || "cats");
const limit = Math.min(Math.max(count, 1), 50);
const url = `https://api.giphy.com/v1/gifs/search?api_key=${apiKey}&q=${q}&limit=${limit}&rating=g`;
const res = await fetch(url);
if (!res.ok) throw new Error("Giphy request failed");
const data = await res.json();
const unique = (data.data || [])
.map((item: any) => ({
id: item.id,
url:
item.images?.downsized_still?.url ||
item.images?.fixed_width_still?.url ||
item.images?.original_still?.url ||
item.images?.preview_gif?.url ||
item.images?.downsized?.url,
}))
.filter(Boolean);
if (unique.length < count) throw new Error("Not enough images");
return unique.slice(0, count);
} catch (err) {
console.warn("Falling back to emoji images:", err);
return EMOJI.slice(0, count).map((ch, i) => ({
id: `emoji-${i}-${ch}`,
url: emojiToDataUrl(ch),
}));
}
}
Back in useGame
, saving from the modal triggers applySettings
, which mutates the shared state and immediately starts another round:
function applySettings(s: Settings) {
settings.difficulty = s.difficulty;
settings.source = s.source;
settings.giphyApiKey = s.giphyApiKey;
settings.giphyQuery = s.giphyQuery;
startNewGame();
}
By the end of this module you can offer the perfect balance between offline reliability and flashy GIFs. Let us wrap the course with persistence and polish! ✨
You made it to the finish line! Let us consolidate what keeps the experience sticky and outline where you can go from here.
22 Sep 2025 - 12 days ago