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