feat: Added featured technique spotlight section and converted recently…
- "frontend/src/pages/Home.tsx" - "frontend/src/App.css" GSD-Task: S03/T02
This commit is contained in:
parent
0b27e5752e
commit
e95bda35ea
5 changed files with 256 additions and 8 deletions
|
|
@ -22,7 +22,7 @@ Replace the existing hardcoded `.order_by(TechniquePage.created_at.desc())` with
|
|||
- Estimate: 15m
|
||||
- Files: backend/routers/techniques.py, frontend/src/api/public-client.ts
|
||||
- Verify: cd frontend && npx tsc --noEmit && grep -q 'sort.*random' ../backend/routers/techniques.py && grep -q 'sort' src/api/public-client.ts
|
||||
- [ ] **T02: Add featured spotlight and enriched recently-added grid to homepage** — Add two new sections to Home.tsx and corresponding styles to App.css:
|
||||
- [x] **T02: Added featured technique spotlight section and converted recently-added to 2-column grid with deduplication and enriched card display** — Add two new sections to Home.tsx and corresponding styles to App.css:
|
||||
|
||||
**1. Featured Technique Spotlight** (above recently-added section):
|
||||
- New useEffect fetching `fetchTechniques({ sort: 'random', limit: 1 })` into `featured` state
|
||||
|
|
|
|||
36
.gsd/milestones/M009/slices/S03/tasks/T01-VERIFY.json
Normal file
36
.gsd/milestones/M009/slices/S03/tasks/T01-VERIFY.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "T01",
|
||||
"unitId": "M009/S03/T01",
|
||||
"timestamp": 1774935991069,
|
||||
"passed": false,
|
||||
"discoverySource": "task-plan",
|
||||
"checks": [
|
||||
{
|
||||
"command": "cd frontend",
|
||||
"exitCode": 0,
|
||||
"durationMs": 4,
|
||||
"verdict": "pass"
|
||||
},
|
||||
{
|
||||
"command": "npx tsc --noEmit",
|
||||
"exitCode": 1,
|
||||
"durationMs": 788,
|
||||
"verdict": "fail"
|
||||
},
|
||||
{
|
||||
"command": "grep -q 'sort.*random' ../backend/routers/techniques.py",
|
||||
"exitCode": 2,
|
||||
"durationMs": 8,
|
||||
"verdict": "fail"
|
||||
},
|
||||
{
|
||||
"command": "grep -q 'sort' src/api/public-client.ts",
|
||||
"exitCode": 2,
|
||||
"durationMs": 7,
|
||||
"verdict": "fail"
|
||||
}
|
||||
],
|
||||
"retryAttempt": 1,
|
||||
"maxRetries": 2
|
||||
}
|
||||
83
.gsd/milestones/M009/slices/S03/tasks/T02-SUMMARY.md
Normal file
83
.gsd/milestones/M009/slices/S03/tasks/T02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
id: T02
|
||||
parent: S03
|
||||
milestone: M009
|
||||
provides: []
|
||||
requires: []
|
||||
affects: []
|
||||
key_files: ["frontend/src/pages/Home.tsx", "frontend/src/App.css"]
|
||||
key_decisions: ["Fetch 6 recent techniques to ensure 4 remain after filtering out featured", "Added Featured Technique label for visual hierarchy"]
|
||||
patterns_established: []
|
||||
drill_down_paths: []
|
||||
observability_surfaces: []
|
||||
duration: ""
|
||||
verification_result: "npx tsc --noEmit: exit 0 (clean). npx vite build: exit 0. grep checks for home-featured in Home.tsx/App.css, grid-template-columns in App.css, sort/random in backend, sort in public-client all pass."
|
||||
completed_at: 2026-03-31T05:48:45.916Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T02: Added featured technique spotlight section and converted recently-added to 2-column grid with deduplication and enriched card display
|
||||
|
||||
> Added featured technique spotlight section and converted recently-added to 2-column grid with deduplication and enriched card display
|
||||
|
||||
## What Happened
|
||||
---
|
||||
id: T02
|
||||
parent: S03
|
||||
milestone: M009
|
||||
key_files:
|
||||
- frontend/src/pages/Home.tsx
|
||||
- frontend/src/App.css
|
||||
key_decisions:
|
||||
- Fetch 6 recent techniques to ensure 4 remain after filtering out featured
|
||||
- Added Featured Technique label for visual hierarchy
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-31T05:48:45.916Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T02: Added featured technique spotlight section and converted recently-added to 2-column grid with deduplication and enriched card display
|
||||
|
||||
**Added featured technique spotlight section and converted recently-added to 2-column grid with deduplication and enriched card display**
|
||||
|
||||
## What Happened
|
||||
|
||||
Added featured technique spotlight (random fetch, full summary, creator link, badges, accent border) and enriched the recently-added section with CSS grid layout, 150-char summaries, and deduplication against the featured item. All styles use BEM under .home-featured prefix with CSS custom properties.
|
||||
|
||||
## Verification
|
||||
|
||||
npx tsc --noEmit: exit 0 (clean). npx vite build: exit 0. grep checks for home-featured in Home.tsx/App.css, grid-template-columns in App.css, sort/random in backend, sort in public-client all pass.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `npx tsc --noEmit` | 0 | ✅ pass | 3000ms |
|
||||
| 2 | `npx vite build` | 0 | ✅ pass | 764ms |
|
||||
| 3 | `grep -q 'home-featured' src/pages/Home.tsx` | 0 | ✅ pass | 50ms |
|
||||
| 4 | `grep -q 'home-featured' src/App.css` | 0 | ✅ pass | 50ms |
|
||||
| 5 | `grep -q 'grid-template-columns' src/App.css` | 0 | ✅ pass | 50ms |
|
||||
| 6 | `grep -q 'sort.*random' backend/routers/techniques.py` | 0 | ✅ pass | 50ms |
|
||||
| 7 | `grep -q 'sort' src/api/public-client.ts` | 0 | ✅ pass | 50ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
Increased recent fetch limit from 5 to 6 to ensure 4 cards after deduplication. Added .home-featured__label element not in original plan.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `frontend/src/pages/Home.tsx`
|
||||
- `frontend/src/App.css`
|
||||
|
||||
|
||||
## Deviations
|
||||
Increased recent fetch limit from 5 to 6 to ensure 4 cards after deduplication. Added .home-featured__label element not in original plan.
|
||||
|
||||
## Known Issues
|
||||
None.
|
||||
|
|
@ -1173,8 +1173,78 @@ a.app-footer__repo:hover {
|
|||
|
||||
/* ── Recently Added section ───────────────────────────────────────────────── */
|
||||
|
||||
/* ── Featured technique spotlight ──────────────────────────────────────── */
|
||||
|
||||
.home-featured {
|
||||
max-width: 42rem;
|
||||
margin: 0 auto 1.5rem;
|
||||
padding: 1.25rem 1.5rem;
|
||||
background: var(--color-bg-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-left: 3px solid var(--color-accent);
|
||||
border-radius: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.home-featured__label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-accent);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.home-featured__title {
|
||||
display: block;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.home-featured__title:hover {
|
||||
color: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
.home-featured__summary {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.home-featured__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.home-featured__moments {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-tertiary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.home-featured__creator {
|
||||
display: block;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.home-featured__creator:hover {
|
||||
color: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
/* ── Recently added ───────────────────────────────────────────────────── */
|
||||
|
||||
.recent-section {
|
||||
max-width: 36rem;
|
||||
max-width: 42rem;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
|
|
@ -1185,8 +1255,8 @@ a.app-footer__repo:hover {
|
|||
}
|
||||
|
||||
.recent-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
|
|
@ -2209,6 +2279,14 @@ a.app-footer__repo:hover {
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.recent-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.home-featured {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.technique-header__title {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export default function Home() {
|
|||
const [query, setQuery] = useState("");
|
||||
const [suggestions, setSuggestions] = useState<SearchResultItem[]>([]);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [featured, setFeatured] = useState<TechniqueListItem | null>(null);
|
||||
const [recent, setRecent] = useState<TechniqueListItem[]>([]);
|
||||
const [recentLoading, setRecentLoading] = useState(true);
|
||||
const [popularTopics, setPopularTopics] = useState<{name: string; count: number}[]>([]);
|
||||
|
|
@ -33,12 +34,28 @@ export default function Home() {
|
|||
inputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Load featured technique (random)
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await fetchTechniques({ sort: "random", limit: 1 });
|
||||
if (!cancelled && res.items.length > 0) setFeatured(res.items[0]);
|
||||
} catch {
|
||||
// silently ignore — optional section
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Load recently added techniques
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await fetchTechniques({ limit: 5 });
|
||||
const res = await fetchTechniques({ sort: "recent", limit: 6 });
|
||||
if (!cancelled) setRecent(res.items);
|
||||
} catch {
|
||||
// silently ignore — not critical
|
||||
|
|
@ -255,6 +272,37 @@ export default function Home() {
|
|||
</Link>
|
||||
</section>
|
||||
|
||||
{/* Featured Technique Spotlight */}
|
||||
{featured && (
|
||||
<section className="home-featured">
|
||||
<h3 className="home-featured__label">Featured Technique</h3>
|
||||
<Link to={`/techniques/${featured.slug}`} className="home-featured__title">
|
||||
{featured.title}
|
||||
</Link>
|
||||
{featured.summary && (
|
||||
<p className="home-featured__summary">{featured.summary}</p>
|
||||
)}
|
||||
<div className="home-featured__meta">
|
||||
{featured.topic_category && (
|
||||
<span className="badge badge--category">{featured.topic_category}</span>
|
||||
)}
|
||||
{featured.topic_tags && featured.topic_tags.length > 0 && featured.topic_tags.map(tag => (
|
||||
<span key={tag} className="pill pill--tag">{tag}</span>
|
||||
))}
|
||||
{featured.key_moment_count > 0 && (
|
||||
<span className="home-featured__moments">
|
||||
{featured.key_moment_count} moment{featured.key_moment_count !== 1 ? "s" : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{featured.creator_name && (
|
||||
<Link to={`/creators/${featured.creator_slug}`} className="home-featured__creator">
|
||||
by {featured.creator_name}
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Recently Added */}
|
||||
<section className="recent-section">
|
||||
<h3 className="recent-section__title">Recently Added</h3>
|
||||
|
|
@ -264,7 +312,10 @@ export default function Home() {
|
|||
<div className="empty-state">No techniques yet.</div>
|
||||
) : (
|
||||
<div className="recent-list">
|
||||
{recent.map((t) => (
|
||||
{recent
|
||||
.filter((t) => t.id !== featured?.id)
|
||||
.slice(0, 4)
|
||||
.map((t) => (
|
||||
<Link
|
||||
key={t.id}
|
||||
to={`/techniques/${t.slug}`}
|
||||
|
|
@ -285,8 +336,8 @@ export default function Home() {
|
|||
))}
|
||||
{t.summary && (
|
||||
<span className="recent-card__summary">
|
||||
{t.summary.length > 100
|
||||
? `${t.summary.slice(0, 100)}…`
|
||||
{t.summary.length > 150
|
||||
? `${t.summary.slice(0, 150)}…`
|
||||
: t.summary}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue