Written by: Marlon Colca
Posted on 21 Sep 2025 - 13 days ago
typescript nodejs vue games
With the game logic solid, the UI binds everything together. Vue Single File Components keep the markup expressive while reusing the composables underneath.
With the game logic solid, the UI binds everything together. Vue Single File Components keep the markup expressive while reusing the composables underneath.
src/App.vue
wires the main composable, displays status chips, and renders the grid. Here is the setup script:
<script setup lang="ts">
import { ref } from "vue";
import CardItem from "./components/CardItem.vue";
import SettingsModal from "./components/SettingsModal.vue";
import ScoresList from "./components/ScoresList.vue";
import { useGame } from "./composables/useGame";
import GitHubBadge from "./components/GitHubBadge.vue";
const {
settings,
scores,
deck,
score,
busy,
gameOver,
showStart,
previewing,
previewLeftMs,
meta,
previewMs,
remaining,
startNewGame,
clickCard,
restart,
applySettings,
} = useGame();
const showSettings = ref(false);
</script>
useGame
—components stay lean.showSettings
ref toggles the modal visibility.Inside the template, the deck renders with a classic v-for
:
<div class="grid" :class="meta.gridClass">
<CardItem
v-for="(c, idx) in deck"
:key="c.id"
:flipped="c.flipped"
:matched="c.matched"
:image-url="c.imageUrl"
:disabled="busy"
@click="clickCard(idx)"
/>
</div>
meta.gridClass
maps each difficulty to a responsive grid.busy
disables clicks during previews or while checking matches.Each card is a button-like component that reacts to props. Focus on accessibility and simple states:
<template>
<button
class="card"
:class="{ flipped, matched }"
:disabled="disabled"
@click="$emit('click')"
>
<img v-if="flipped" :src="imageUrl" alt="Card image" />
<span v-else>❓</span>
</button>
</template>
<button>
gives keyboard support out of the box.SettingsModal.vue
keeps a temporary copy of the user selection, then emits save
only when the player confirms:
const state = reactive({
difficulty: props.difficulty,
source: props.source,
giphyApiKey: props.giphyApiKey ?? "",
giphyQuery: props.giphyQuery ?? "cats",
});
function onSave() {
emit("save", { ...state });
}
applySettings
).ScoresList
receives a reactive scores
array and renders recent runs. Pair it with the “Play again” button to encourage replays.
With components assembled, the user experience already feels polished. Next up: real personalization through settings and image sources! 🛠️
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.
22 Sep 2025 - 12 days ago