Some checks failed
CI / Lint (pull_request) Has been cancelled
CI / Schema map up to date (pull_request) Has been cancelled
CI / Forbidden patterns (9 checks) (pull_request) Has been cancelled
CI / Migrations apply (node-pg-migrate) (pull_request) Has been cancelled
Visual diff / Should run? (pull_request) Has been cancelled
CI / Unit tests (vitest) (pull_request) Has been cancelled
Convoy metrics gate / Require role-event telemetry on convoy PRs (pull_request) Has been cancelled
PR Health rollup / Aggregate gate status (pull_request) Has been cancelled
Preview smoke / Should run? (pull_request) Has been cancelled
Preview smoke / Playwright smoke (pull_request) Has been cancelled
Visual diff / Screenshot diff (pull_request) Has been cancelled
Add pricing schema, free API integrations (MTG bulk, TCGdex, Foolio), valuation and sort-by-price, card detail pricing UI, and nearby store discovery via home zip and OSM geo clients. Co-authored-by: Cursor <cursoragent@cursor.com>
119 lines
3.9 KiB
JavaScript
119 lines
3.9 KiB
JavaScript
import Link from 'next/link';
|
|
|
|
/**
|
|
* Nearby game stores subsection for the Purchase tab.
|
|
*/
|
|
export default function CardDetailLocalStores({
|
|
stores = [],
|
|
loading = false,
|
|
error = null,
|
|
noHomeZip = false,
|
|
}) {
|
|
if (loading) {
|
|
return (
|
|
<div className="glass-panel p-6 rounded-xl" aria-live="polite">
|
|
<p style={{ color: 'var(--text-secondary)' }}>Loading nearby stores…</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (noHomeZip) {
|
|
return (
|
|
<div className="glass-panel p-6 rounded-xl">
|
|
<h4 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
Game stores near you
|
|
</h4>
|
|
<p className="text-sm mb-3" style={{ color: 'var(--text-secondary)' }}>
|
|
Add your home ZIP in Settings to see nearby game and hobby stores.
|
|
</p>
|
|
<Link
|
|
href="/settings"
|
|
className="text-sm font-medium underline"
|
|
style={{ color: 'var(--accent-flame)' }}
|
|
>
|
|
Set home ZIP in Settings
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="glass-panel p-6 rounded-xl">
|
|
<h4 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
Game stores near you
|
|
</h4>
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
{error}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="glass-panel p-6 rounded-xl">
|
|
<h4 className="text-lg font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
|
Game stores near you
|
|
</h4>
|
|
|
|
{stores.length === 0 ? (
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
No game or hobby stores found in your area yet. Try again later — store data comes from OpenStreetMap.
|
|
</p>
|
|
) : (
|
|
<ul className="space-y-4" aria-label="Nearby game stores">
|
|
{stores.map((store) => (
|
|
<li
|
|
key={store.id ?? `${store.name}-${store.lat}-${store.lng}`}
|
|
className="p-4 rounded-xl border"
|
|
style={{ borderColor: 'var(--border)' }}
|
|
>
|
|
<div className="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between">
|
|
<div>
|
|
<p className="font-semibold" style={{ color: 'var(--text-primary)' }}>
|
|
{store.name}
|
|
</p>
|
|
{store.address ? (
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
{store.address}
|
|
</p>
|
|
) : null}
|
|
<p className="text-sm mt-1" style={{ color: 'var(--text-secondary)' }}>
|
|
{store.distanceMiles} mi away
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap gap-3 text-sm mt-2 sm:mt-0">
|
|
{store.phone ? (
|
|
<a href={`tel:${store.phone}`} style={{ color: 'var(--accent-flame)' }}>
|
|
Call {store.phone}
|
|
</a>
|
|
) : null}
|
|
{store.website ? (
|
|
<a
|
|
href={store.website}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{ color: 'var(--accent-flame)' }}
|
|
>
|
|
Visit website
|
|
</a>
|
|
) : null}
|
|
{store.osmUrl ? (
|
|
<a
|
|
href={store.osmUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{ color: 'var(--accent-flame)' }}
|
|
>
|
|
View on OpenStreetMap
|
|
</a>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|