Hero Card Category Icon Implementation Plan โ
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the hardcoded Flame icon in HeroOfferCard with a dynamic venue category icon.
Architecture: Extract the existing categoryIconMap from VenuesMap.jsx into venueConfig.js as a shared utility. HeroOfferCard calls the new getCategoryIcon() to render the appropriate lucide-react icon based on offer.venue.category. Fallback is Flame to preserve the amber motif.
Tech Stack: React, lucide-react, Vitest
Spec: docs/superpowers/specs/2026-03-30-hero-card-category-icon-design.md
Task 1: Add getCategoryIcon to venueConfig.js โ
Files:
Modify:
apps/web/src/lib/venueConfig.js(add export at bottom)Test:
apps/web/src/lib/__tests__/venueConfig.test.js[ ] Step 1: Write the failing test
Add to apps/web/src/lib/__tests__/venueConfig.test.js:
import {
// ... existing imports ...
getCategoryIcon,
} from '../venueConfig'
// Add inside the existing describe('venueConfig', ...) block:
describe('getCategoryIcon', () => {
it('returns Coffee for cafe category', () => {
const Icon = getCategoryIcon('cafe')
expect(Icon.displayName || Icon.name).toMatch(/Coffee/i)
})
it('returns Coffee for coffee_shop category', () => {
const Icon = getCategoryIcon('coffee_shop')
expect(Icon.displayName || Icon.name).toMatch(/Coffee/i)
})
it('returns Beer for bar category', () => {
const Icon = getCategoryIcon('bar')
expect(Icon.displayName || Icon.name).toMatch(/Beer/i)
})
it('returns Utensils for restaurant category', () => {
const Icon = getCategoryIcon('restaurant')
expect(Icon.displayName || Icon.name).toMatch(/Utensils/i)
})
it('is case-insensitive', () => {
const Icon = getCategoryIcon('BAR')
expect(Icon.displayName || Icon.name).toMatch(/Beer/i)
})
it('returns Flame fallback for unknown category', () => {
const Icon = getCategoryIcon('unknown_type')
expect(Icon.displayName || Icon.name).toMatch(/Flame/i)
})
it('returns Flame fallback for null/undefined', () => {
const Icon = getCategoryIcon(null)
expect(Icon.displayName || Icon.name).toMatch(/Flame/i)
const Icon2 = getCategoryIcon(undefined)
expect(Icon2.displayName || Icon2.name).toMatch(/Flame/i)
})
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
- [ ] Step 2: Run test to verify it fails
Run: npx vitest run apps/web/src/lib/__tests__/venueConfig.test.js Expected: FAIL โ getCategoryIcon is not exported from ../venueConfig
- [ ] Step 3: Write the implementation
Add to the bottom of apps/web/src/lib/venueConfig.js, before any existing default export:
// ============================================================================
// VENUE CATEGORY ICONS (lucide-react components by category)
// ============================================================================
import { Coffee, Beer, Utensils, Gamepad2, BookOpen, Music, Flame } from 'lucide-react'
/**
* Maps venue categories to lucide-react icon components.
* Used by HeroOfferCard and VenuesMap for category-specific visuals.
*/
const CATEGORY_ICON_MAP = {
cafe: Coffee,
coffee_shop: Coffee,
bar: Beer,
pub: Beer,
food: Utensils,
restaurant: Utensils,
activities: Gamepad2,
library: BookOpen,
music: Music,
shop: Music,
}
/**
* Get the lucide-react icon component for a venue category.
* @param {string|null|undefined} category - Venue category string
* @returns {import('lucide-react').LucideIcon} Icon component (Flame as fallback)
*/
export const getCategoryIcon = (category) =>
CATEGORY_ICON_MAP[category?.toLowerCase()] || Flame2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Note: The import statement for lucide-react icons should go at the top of the file with the other imports, not inline. Move it up to the import section.
- [ ] Step 4: Run test to verify it passes
Run: npx vitest run apps/web/src/lib/__tests__/venueConfig.test.js Expected: All tests PASS including the new getCategoryIcon tests
- [ ] Step 5: Commit
git add apps/web/src/lib/venueConfig.js apps/web/src/lib/__tests__/venueConfig.test.js
git commit -m "feat: add getCategoryIcon utility to venueConfig"2
Task 2: Update HeroOfferCard to use category icon โ
Files:
Modify:
apps/web/src/components/dashboard/OfferCards.jsx:82-125(HeroOfferCard)[ ] Step 1: Update the import
In apps/web/src/components/dashboard/OfferCards.jsx, change the lucide-react import:
// Before:
import { Sparkles, Flame, MessageCircle } from 'lucide-react'
// After:
import { Sparkles, MessageCircle } from 'lucide-react'
import { getCategoryIcon } from '@/lib/venueConfig'2
3
4
5
6
- [ ] Step 2: Replace the Flame icon in HeroOfferCard
In the HeroOfferCard component, replace the icon area (line 123-125):
// Before:
<div className="hidden sm:flex w-14 h-14 rounded-xl bg-amber-500/20 border border-amber-500/40 items-center justify-center text-amber-100 shadow-inner">
<Flame size={22} className="fill-amber-200/30" />
</div>
// After:
<div className="hidden sm:flex w-14 h-14 rounded-xl bg-amber-500/20 border border-amber-500/40 items-center justify-center text-amber-100 shadow-inner">
{(() => { const Icon = getCategoryIcon(offer.venue?.category); return <Icon size={22} className="fill-amber-200/30" /> })()}
</div>2
3
4
5
6
7
8
9
- [ ] Step 3: Run lint to verify no errors
Run: npm run lint -w apps/web 2>&1 | grep -E "error|OfferCards" Expected: No new errors (only pre-existing warnings)
- [ ] Step 4: Commit
git add apps/web/src/components/dashboard/OfferCards.jsx
git commit -m "feat: use venue category icon in HeroOfferCard"2
Task 3: Migrate VenuesMap to use shared getCategoryIcon โ
Files:
Modify:
apps/web/src/components/VenuesMap.jsx:1-13,698-711[ ] Step 1: Replace the local categoryIconMap with the shared import
In apps/web/src/components/VenuesMap.jsx, update imports (lines 1-13):
// Before:
import {
MapPin,
Navigation,
Coffee,
Beer,
Utensils,
Gamepad2,
BookOpen,
Music,
Flame,
Search,
} from 'lucide-react'
// After:
import {
MapPin,
Navigation,
Flame,
Search,
} from 'lucide-react'
import { getCategoryIcon } from '@/lib/venueConfig'2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Note: Keep MapPin (used elsewhere in VenuesMap), Navigation, Flame, and Search โ only remove the category-specific icons that are now provided by getCategoryIcon.
- [ ] Step 2: Remove the local categoryIconMap and getCategoryIcon
Delete lines 698-711:
// DELETE these lines:
const categoryIconMap = {
cafe: Coffee,
coffee_shop: Coffee,
bar: Beer,
pub: Beer,
food: Utensils,
restaurant: Utensils,
activities: Gamepad2,
library: BookOpen,
music: Music,
shop: Music,
}
const getCategoryIcon = (category) => categoryIconMap[category?.toLowerCase()] || MapPin2
3
4
5
6
7
8
9
10
11
12
13
14
15
Note: The VenuesMap version falls back to MapPin, while the shared version falls back to Flame. Check all call sites of getCategoryIcon in VenuesMap.jsx to confirm Flame is an acceptable fallback for map markers. If not, wrap the call: getCategoryIcon(category) || MapPin โ but since getCategoryIcon always returns something (never null), the Flame fallback is fine for map context too.
- [ ] Step 3: Run lint and verify the app builds
Run: npm run lint -w apps/web 2>&1 | grep "error" and npm run build -w apps/web 2>&1 | tail -5 Expected: No new errors, build succeeds
- [ ] Step 4: Commit
git add apps/web/src/components/VenuesMap.jsx
git commit -m "refactor: use shared getCategoryIcon in VenuesMap"2
Task 4: Add category to offerNormalizer preview venue โ
Files:
Modify:
apps/web/src/lib/offerNormalizer.js:40[ ] Step 1: Update the default venue object in normalizeFormToOffer
In apps/web/src/lib/offerNormalizer.js, update the venue fallback (line 40):
// Before:
venue: options.venue || { name: 'Your Venue', distance: 'nearby' },
// After:
venue: options.venue || { name: 'Your Venue', distance: 'nearby', category: 'cafe' },2
3
4
5
This gives the preview hero card a sample category icon (coffee cup) instead of the Flame fallback.
- [ ] Step 2: Verify the preview renders correctly
Run: npm run build -w apps/web 2>&1 | tail -5 Expected: Build succeeds
- [ ] Step 3: Commit
git add apps/web/src/lib/offerNormalizer.js
git commit -m "feat: add default category to preview venue for icon display"2
Task 5: Update Storybook stories with category data โ
Files:
Modify:
apps/web/src/components/dashboard/AdSlot.stories.jsx:6-13[ ] Step 1: Add category to the base offer fixture
In apps/web/src/components/dashboard/AdSlot.stories.jsx, update baseOffer:
// Before:
const baseOffer = {
id: 'offer-demo',
headline: 'Free pastry with any latte tonight',
body: 'Show this at the counter. Limited to the first 50 redemptions after 6pm.',
incentive: 'Free pastry',
venue: { name: 'The Midnight Bean', distance: '0.1 mi' },
showDisclaimerWhileSuppliesLast: false,
}
// After:
const baseOffer = {
id: 'offer-demo',
headline: 'Free pastry with any latte tonight',
body: 'Show this at the counter. Limited to the first 50 redemptions after 6pm.',
incentive: 'Free pastry',
venue: { name: 'The Midnight Bean', distance: '0.1 mi', category: 'coffee_shop' },
showDisclaimerWhileSuppliesLast: false,
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- [ ] Step 2: Run full validation
Run: npm run validate -- --workspace apps/web --scope lint,test Expected: All checks pass
- [ ] Step 3: Commit
git add apps/web/src/components/dashboard/AdSlot.stories.jsx
git commit -m "chore: add venue category to story fixtures for icon display"2
Task 6: Final validation โ
- [ ] Step 1: Run full validation suite
Run: npm run validate -- --workspace apps/web Expected: All sections pass (lint, format, test, build)
- [ ] Step 2: Verify Storybook renders
Run: npm run storybook and check Components/Dashboard/AdSlot stories โ hero card should show a coffee cup icon instead of flame.