Skip to content

Latest commit

 

History

80 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

The Produce Gallery

A gallery of fruits, vegetables, herbs, and spices - their colors, seasons, and the quiet stories behind them. Browse a hand-curated set, a frozen snapshot of Wikidata, or stream the full catalogue live.

Live demo: stalevski.github.io/produce-gallery

Built with Vite + React + TypeScript + Tailwind CSS.

Built AI-assisted, human-directed. Planned, generated, reviewed, and tested with AI tooling under my direction - the architecture, code review, and final decisions are mine.

Working on this across devices or with others? See PROGRESS.md for current focus, backlog, known issues, and the decision log, and AGENTS.md for the operator's manual.

Features

  • Three data sources, switchable in the header:
    • Curated - 114 hand-curated items across 9 categories with emoji art, origins, fun facts, seasons, botanical names, plant families, and food pairings
    • Snapshot - a frozen ~1,870-item Wikidata dump bundled with the app; instant, offline-safe, regenerated by npm run snapshot
    • Live - a fresh SPARQL query against the public Wikidata endpoint with photos from Wikimedia Commons
  • 9 categories: fruit, vegetable, herb, spice, nut, seed, mushroom, legume, grain
  • Click any card for a detail modal with botanical name, family, full description, food pairings, and a live-fetched Wikipedia lead paragraph
  • Pagination with size selector (25 / 50 / 100 / 200 / All) and first/prev/next/last navigation
  • 7-day localStorage cache for live Wikidata results, with a Refresh button to invalidate
  • Background cache-warming: 5s after page load, the live Wikidata fetch runs invisibly so the first toggle to Live is instant
  • Self-hosted fonts (Fraunces + Inter variable WOFF2) - zero third-party CDN dependency at runtime
  • Search across name, color, origin, description, and trivia, with live match highlighting in card titles and descriptions
  • Filter by category, season, color, and (in Snapshot or Live mode) has photo
  • Sort alphabetical (A→Z, Z→A), by category, or random
  • URL-encoded state: every filter, page, sort, source, and selected item is in the URL - links are shareable, browser refresh preserves view
  • Keyboard shortcuts: / focuses search, Esc closes the detail modal
  • Reset all chip appears when any filter is active
  • Skeleton loading cards during the live Wikidata fetch and the snapshot chunk download
  • Loading + error states with retry / fallback to snapshot or curated
  • Per-card image fallback to category emoji
  • Wikipedia link on each Wikidata-sourced item, plus the "this taxon is source of" bridge so common ingredients (rosemary, cinnamon, ginger) get their photos from the parent species
  • Adaptive UI: filters that don't apply to the current dataset auto-hide (e.g., season/color in Live or Snapshot mode where data is sparse)
  • Responsive: 1 → 2 → 3 → 4 columns from mobile to desktop

Getting started

npm install
npm run dev

Then open the URL Vite prints (default http://localhost:5173).

Build for production

npm run build
npm run preview

Project layout

src/
  App.tsx                       # Page composition, three-tier source toggle, fetch lifecycle, URL state
  main.tsx                      # React root
  index.css                     # Tailwind entry + base styles
  types.ts                      # ProduceItem / Category / Source / ImageAttribution types
  data/
    curated.ts                  # Hand-curated dataset (114 items, 9 categories)
    category-qids.json          # Wikidata QID per category, shared with the snapshot generator
    wikidata-snapshot.json      # Frozen Wikidata dump, regenerated by scripts/generate-snapshot.mjs
  assets/fonts/                 # Self-hosted variable WOFF2 (Fraunces + Inter)
  services/
    wikidata.ts                 # Live SPARQL queries + 7-day localStorage cache
    wikipedia.ts                # Lead-paragraph fetcher for the detail view
  components/
    ProduceCard.tsx             # Card (clickable, with search highlight + photo fallback)
    DetailView.tsx              # Modal with botanical name, pairings, Wikipedia summary
    FilterBar.tsx               # Search + category/season/color/photo/sort + reset chip
    Pagination.tsx              # Size selector (25/50/100/200/All) + page navigation
    SkeletonCard.tsx            # Pulsing placeholders during snapshot/live Wikidata load
    HighlightedText.tsx         # Highlights search-query matches inside text

How the Wikidata source works

src/services/wikidata.ts runs nine SPARQL queries (one per category) against https://query.wikidata.org/sparql. To capture both classification styles in Wikidata's ontology - and to inherit images / Wikipedia articles from a source taxon when the food entity itself lacks them - the query uses a hybrid path:

SELECT DISTINCT ?item ?itemLabel ?itemDescription
       (SAMPLE(?fImage) AS ?image)
       (SAMPLE(?fArticle) AS ?article)
WHERE {
  { ?item wdt:P279+ wd:${qid} . }                     # subclass-of+
  UNION
  { ?item wdt:P31/wdt:P279* wd:${qid} . }             # instance-of (any subclass)

  OPTIONAL { ?item wdt:P18 ?dImg }
  OPTIONAL {
    ?taxon wdt:P1672 ?item .                          # "this taxon is source of"
    ?taxon wdt:P18 ?tImg .
  }
  BIND(COALESCE(?dImg, ?tImg) AS ?fImage)

  OPTIONAL {
    ?dArt schema:about ?item ;
          schema:isPartOf <https://en.wikipedia.org/> .
  }
  OPTIONAL {
    ?taxon2 wdt:P1672 ?item .
    ?tArt schema:about ?taxon2 ;
          schema:isPartOf <https://en.wikipedia.org/> .
  }
  BIND(COALESCE(?dArt, ?tArt) AS ?fArticle)

  FILTER(BOUND(?fImage) || BOUND(?fArticle))
  SERVICE wikibase:label { bd:serviceParam wikibase:language "en" }
}
GROUP BY ?item ?itemLabel ?itemDescription
LIMIT 800

Categories use these QIDs:

Category QID Notes
fruit Q3314483 edible fruit
vegetable Q11004 vegetable
spice Q42527 spice
herb Q207123 herb
nut Q11009 nut
seed Q40763 seed
mushroom Q654236 edible mushroom
legume Q12034432 legume (fruit type)
grain Q12117 cereal

Results are deduplicated by Wikidata ID, sorted alphabetically, and cached in localStorage under produce-gallery:wikidata:v6 for 7 days. The cache is also warmed in the background 5 seconds after page load, so the first toggle to Wikidata mode is instant.

URL state

The current view is fully encoded in the URL search params. Open one of these in a fresh tab:

Example URL What it does
/?q=basil Search for "basil"
/?cat=spice&sort=random Spices in random order
/?src=snapshot Bundled Wikidata snapshot
/?src=wikidata&photo=1 Live Wikidata, only items with photos
/?cat=herb&item=rosemary Herbs filter, with the rosemary detail modal open

Adding more curated produce

Append a new entry to src/data/curated.ts matching the ProduceItem shape:

{
  id: "fennel",
  name: "Fennel",
  emoji: "\u{1F33F}",
  category: "vegetable",
  color: "Green",
  colorHex: "#9FBE6F",
  seasons: ["autumn", "winter"],
  origin: "Mediterranean",
  description: "...",
  funFact: "...",
  botanicalName: "Foeniculum vulgare",
  family: "Apiaceae",
  pairings: ["orange", "olive oil", "fish"],
}

The filter chips for color are derived automatically from the dataset, and the new item will appear in the appropriate category.

About

A produce gallery: 114 hand-curated fruits, vegetables, herbs, spices, nuts, seeds, mushrooms, legumes, and grains, plus hundreds more streamed live from Wikidata. Vite + React + TypeScript + Tailwind. Built AI-assisted, human-directed.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages