Skip to content

Board game collection shelf visualization with photo-based input

pattern

Tracking large board game collections stored in spreadsheets lacks visual browsing and search

web-appvisualizationcollectionvibe-coding
18 views

Problem

Board game collectors typically track their games in spreadsheets or plain text lists. These flat formats make it impossible to browse visually, filter by player count or play time, or quickly find a game for a specific group size. The collection grows but the tooling stays stuck at rows and columns, and manually entering metadata for each game is tedious enough that the list falls out of date.

Solution

Step 1: Photo-based game entry

Use a phone camera to photograph your shelf. Pass the image to a vision model to extract game titles.

async function identifyGamesFromPhoto(imageBase64) {
  const response = await anthropic.messages.create({
    model: "claude-sonnet-4-5-20250929",
    max_tokens: 1024,
    messages: [{
      role: "user",
      content: [{
        type: "image",
        source: { type: "base64", media_type: "image/jpeg", data: imageBase64 },
      }, {
        type: "text",
        text: "List every board game visible on this shelf. Return JSON: [{\"title\": \"...\"}]",
      }],
    }],
  });
  return JSON.parse(response.content[0].text);
}

Step 2: Enrich with metadata from BoardGameGeek

async function enrichGame(title) {
  const searchUrl = `https://boardgamegeek.com/xmlapi2/search?query=${encodeURIComponent(title)}&type=boardgame`;
  const searchResult = await fetch(searchUrl).then((r) => r.text());
  const gameId = parseGameId(searchResult);

  const detailUrl = `https://boardgamegeek.com/xmlapi2/thing?id=${gameId}&stats=1`;
  const detail = await fetch(detailUrl).then((r) => r.text());

  return {
    title,
    minPlayers: parseField(detail, "minplayers"),
    maxPlayers: parseField(detail, "maxplayers"),
    playingTime: parseField(detail, "playingtime"),
    rating: parseField(detail, "average"),
    thumbnail: parseThumbnail(detail),
  };
}

Step 3: Render a virtual shelf UI

<div class="shelf">
  <div class="shelf-row" v-for="row in shelfRows" :key="row.id">
    <div
      class="game-spine"
      v-for="game in row.games"
      :key="game.title"
      :style="{ width: spineWidth(game), backgroundColor: game.color }"
      @click="showDetail(game)"
    >
      <span class="spine-title">{{ game.title }}</span>
    </div>
  </div>
</div>

Step 4: Add search and filter controls

const filteredGames = computed(() => {
  return games.value.filter((game) => {
    const matchesSearch = game.title.toLowerCase().includes(query.value.toLowerCase());
    const matchesPlayers = !playerCount.value
      || (game.minPlayers <= playerCount.value && game.maxPlayers >= playerCount.value);
    const matchesTime = !maxTime.value || game.playingTime <= maxTime.value;
    return matchesSearch && matchesPlayers && matchesTime;
  });
});

Why It Works

Vision models reliably read game titles from shelf photos, eliminating the manual data entry bottleneck that causes collections to go stale. BoardGameGeek's XML API provides rich metadata for free, so each game automatically gets player count, play time, and ratings. The shelf visualization matches the physical mental model collectors already have, making browse-and-pick faster than scanning a spreadsheet.

Context

  • BoardGameGeek's XML API v2 is free and unauthenticated but has rate limits so batch requests with delays
  • Vision models handle angled spines and partial occlusion surprisingly well for standard box sizes
  • The virtual shelf can be extended with sorting by rating, play count, or acquisition date
  • Similar patterns work for book collections, vinyl records, or any physical media with identifiable spines
  • Data persists in localStorage or a JSON file so no backend is needed for personal use
About this share
Contributormblode
Repositorymblode/shares
CreatedFeb 10, 2026
View on GitHub