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
|
- Estimate: 15m
|
||||||
- Files: backend/routers/techniques.py, frontend/src/api/public-client.ts
|
- 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
|
- 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):
|
**1. Featured Technique Spotlight** (above recently-added section):
|
||||||
- New useEffect fetching `fetchTechniques({ sort: 'random', limit: 1 })` into `featured` state
|
- 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 ───────────────────────────────────────────────── */
|
/* ── 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 {
|
.recent-section {
|
||||||
max-width: 36rem;
|
max-width: 42rem;
|
||||||
margin: 0 auto 2rem;
|
margin: 0 auto 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1185,8 +1255,8 @@ a.app-footer__repo:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-list {
|
.recent-list {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2209,6 +2279,14 @@ a.app-footer__repo:hover {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recent-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-featured {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.technique-header__title {
|
.technique-header__title {
|
||||||
font-size: 1.375rem;
|
font-size: 1.375rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export default function Home() {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [suggestions, setSuggestions] = useState<SearchResultItem[]>([]);
|
const [suggestions, setSuggestions] = useState<SearchResultItem[]>([]);
|
||||||
const [showDropdown, setShowDropdown] = useState(false);
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
|
const [featured, setFeatured] = useState<TechniqueListItem | null>(null);
|
||||||
const [recent, setRecent] = useState<TechniqueListItem[]>([]);
|
const [recent, setRecent] = useState<TechniqueListItem[]>([]);
|
||||||
const [recentLoading, setRecentLoading] = useState(true);
|
const [recentLoading, setRecentLoading] = useState(true);
|
||||||
const [popularTopics, setPopularTopics] = useState<{name: string; count: number}[]>([]);
|
const [popularTopics, setPopularTopics] = useState<{name: string; count: number}[]>([]);
|
||||||
|
|
@ -33,12 +34,28 @@ export default function Home() {
|
||||||
inputRef.current?.focus();
|
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
|
// Load recently added techniques
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetchTechniques({ limit: 5 });
|
const res = await fetchTechniques({ sort: "recent", limit: 6 });
|
||||||
if (!cancelled) setRecent(res.items);
|
if (!cancelled) setRecent(res.items);
|
||||||
} catch {
|
} catch {
|
||||||
// silently ignore — not critical
|
// silently ignore — not critical
|
||||||
|
|
@ -255,6 +272,37 @@ export default function Home() {
|
||||||
</Link>
|
</Link>
|
||||||
</section>
|
</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 */}
|
{/* Recently Added */}
|
||||||
<section className="recent-section">
|
<section className="recent-section">
|
||||||
<h3 className="recent-section__title">Recently Added</h3>
|
<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="empty-state">No techniques yet.</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="recent-list">
|
<div className="recent-list">
|
||||||
{recent.map((t) => (
|
{recent
|
||||||
|
.filter((t) => t.id !== featured?.id)
|
||||||
|
.slice(0, 4)
|
||||||
|
.map((t) => (
|
||||||
<Link
|
<Link
|
||||||
key={t.id}
|
key={t.id}
|
||||||
to={`/techniques/${t.slug}`}
|
to={`/techniques/${t.slug}`}
|
||||||
|
|
@ -285,8 +336,8 @@ export default function Home() {
|
||||||
))}
|
))}
|
||||||
{t.summary && (
|
{t.summary && (
|
||||||
<span className="recent-card__summary">
|
<span className="recent-card__summary">
|
||||||
{t.summary.length > 100
|
{t.summary.length > 150
|
||||||
? `${t.summary.slice(0, 100)}…`
|
? `${t.summary.slice(0, 150)}…`
|
||||||
: t.summary}
|
: t.summary}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue