feat(frontend): add visual asset fallback strategy and QA notes
This commit is contained in:
parent
012a5362bb
commit
adf8386daf
|
|
@ -52,3 +52,11 @@ src/
|
|||
## Architecture
|
||||
|
||||
See `/ARCHITECTURE.md` for full system architecture and patterns.
|
||||
|
||||
|
||||
## Visual Assets + Fallbacks
|
||||
|
||||
- Visual asset definitions live in `src/assets/visualAssets.ts`.
|
||||
- The homepage hero uses a fallback chain: bundled `hero.png` first, then `/images/hero-fallback.svg`.
|
||||
- Keep fallback assets lightweight (SVG preferred) and store browser-served fallbacks under `public/images/`.
|
||||
- Any new UI-critical image should follow the same fallback pattern to avoid broken-image regressions in production.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<svg width="1600" height="900" viewBox="0 0 1600 900" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Recipe hero fallback illustration</title>
|
||||
<desc id="desc">Soft gradient with simple food-themed shapes used when the hero image is unavailable.</desc>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#fde68a"/>
|
||||
<stop offset="55%" stop-color="#fed7aa"/>
|
||||
<stop offset="100%" stop-color="#bfdbfe"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="1600" height="900" fill="url(#bg)"/>
|
||||
<g opacity="0.15" fill="#1e293b">
|
||||
<circle cx="250" cy="180" r="120"/>
|
||||
<circle cx="1430" cy="710" r="180"/>
|
||||
<circle cx="1210" cy="180" r="90"/>
|
||||
</g>
|
||||
<g fill="#0f172a" opacity="0.65" font-family="Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" text-anchor="middle">
|
||||
<text x="800" y="420" font-size="84" font-weight="700">Recipe Manager</text>
|
||||
<text x="800" y="500" font-size="34" font-weight="500">Fresh meals. Organized beautifully.</text>
|
||||
</g>
|
||||
<g font-size="84" text-anchor="middle">
|
||||
<text x="620" y="640">🥗</text>
|
||||
<text x="800" y="640">🍲</text>
|
||||
<text x="980" y="640">🍰</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -0,0 +1,13 @@
|
|||
import heroImage from './hero.png';
|
||||
|
||||
export const visualAssets = {
|
||||
hero: {
|
||||
primary: heroImage,
|
||||
fallbacks: ['/images/hero-fallback.svg'],
|
||||
alt: 'Fresh ingredients and plated food',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function heroImageChain(): string[] {
|
||||
return [visualAssets.hero.primary, ...visualAssets.hero.fallbacks];
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import heroImage from '../assets/hero.png';
|
||||
import { heroImageChain, visualAssets } from '../assets/visualAssets';
|
||||
import { useRecipes } from '../hooks/useRecipes';
|
||||
import { useTags } from '../hooks/useTags';
|
||||
import { RecipeCard } from '../components/RecipeCard';
|
||||
|
|
@ -18,6 +18,10 @@ export function RecipeListPage() {
|
|||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedTagId, setSelectedTagId] = useState<number | null>(null);
|
||||
const [heroCandidateIndex, setHeroCandidateIndex] = useState(0);
|
||||
|
||||
const heroCandidates = heroImageChain();
|
||||
const heroSrc = heroCandidates[Math.min(heroCandidateIndex, heroCandidates.length - 1)];
|
||||
|
||||
const { recipes, loading, error, hasMore, loadMore } = useRecipes({
|
||||
search: searchQuery,
|
||||
|
|
@ -81,7 +85,16 @@ export function RecipeListPage() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="relative min-h-[220px] bg-slate-100 md:min-h-full">
|
||||
<img src={heroImage} alt="Fresh ingredients and plated food" className="h-full w-full object-cover" />
|
||||
<img
|
||||
src={heroSrc}
|
||||
alt={visualAssets.hero.alt}
|
||||
className="h-full w-full object-cover"
|
||||
onError={() => {
|
||||
if (heroCandidateIndex < heroCandidates.length - 1) {
|
||||
setHeroCandidateIndex((current) => current + 1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-slate-900/20 via-transparent to-transparent" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue