feat: Added SubTopicPage with breadcrumbs and creator-grouped technique…
- "frontend/src/pages/SubTopicPage.tsx" - "frontend/src/api/public-client.ts" - "frontend/src/App.tsx" - "frontend/src/pages/TopicsBrowse.tsx" - "frontend/src/App.css" - "frontend/src/pages/Home.tsx" GSD-Task: S01/T02
This commit is contained in:
parent
8661549ab1
commit
5e52dd5e9c
10 changed files with 426 additions and 6 deletions
|
|
@ -8,7 +8,7 @@
|
||||||
- Estimate: 45m
|
- Estimate: 45m
|
||||||
- Files: backend/routers/topics.py, backend/tests/test_public_api.py
|
- Files: backend/routers/topics.py, backend/tests/test_public_api.py
|
||||||
- Verify: cd backend && python -m pytest tests/test_public_api.py -k 'subtopic' -v --timeout=30
|
- Verify: cd backend && python -m pytest tests/test_public_api.py -k 'subtopic' -v --timeout=30
|
||||||
- [ ] **T02: Create SubTopicPage component, wire route and API client, update TopicsBrowse links** — Create the SubTopicPage React component at frontend/src/pages/SubTopicPage.tsx following the CreatorDetail pattern. Add fetchSubTopicTechniques function to public-client.ts that calls GET /topics/{category}/{subtopic}. Register route /topics/:category/:subtopic in App.tsx (before the /topics catch-all). Update TopicsBrowse.tsx sub-topic links from /search?q=... to /topics/{cat}/{subtopic}. Add CSS for breadcrumbs and creator-grouped layout to App.css.
|
- [x] **T02: Added SubTopicPage with breadcrumbs and creator-grouped technique list, wired /topics/:category/:subtopic route, updated TopicsBrowse links to dedicated pages** — Create the SubTopicPage React component at frontend/src/pages/SubTopicPage.tsx following the CreatorDetail pattern. Add fetchSubTopicTechniques function to public-client.ts that calls GET /topics/{category}/{subtopic}. Register route /topics/:category/:subtopic in App.tsx (before the /topics catch-all). Update TopicsBrowse.tsx sub-topic links from /search?q=... to /topics/{cat}/{subtopic}. Add CSS for breadcrumbs and creator-grouped layout to App.css.
|
||||||
|
|
||||||
SubTopicPage must:
|
SubTopicPage must:
|
||||||
- Extract category and subtopic from URL params
|
- Extract category and subtopic from URL params
|
||||||
|
|
|
||||||
24
.gsd/milestones/M010/slices/S01/tasks/T01-VERIFY.json
Normal file
24
.gsd/milestones/M010/slices/S01/tasks/T01-VERIFY.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"taskId": "T01",
|
||||||
|
"unitId": "M010/S01/T01",
|
||||||
|
"timestamp": 1774936776440,
|
||||||
|
"passed": false,
|
||||||
|
"discoverySource": "task-plan",
|
||||||
|
"checks": [
|
||||||
|
{
|
||||||
|
"command": "cd backend",
|
||||||
|
"exitCode": 0,
|
||||||
|
"durationMs": 5,
|
||||||
|
"verdict": "pass"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "python -m pytest tests/test_public_api.py -k 'subtopic' -v --timeout=30",
|
||||||
|
"exitCode": 4,
|
||||||
|
"durationMs": 199,
|
||||||
|
"verdict": "fail"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"retryAttempt": 1,
|
||||||
|
"maxRetries": 2
|
||||||
|
}
|
||||||
86
.gsd/milestones/M010/slices/S01/tasks/T02-SUMMARY.md
Normal file
86
.gsd/milestones/M010/slices/S01/tasks/T02-SUMMARY.md
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
---
|
||||||
|
id: T02
|
||||||
|
parent: S01
|
||||||
|
milestone: M010
|
||||||
|
provides: []
|
||||||
|
requires: []
|
||||||
|
affects: []
|
||||||
|
key_files: ["frontend/src/pages/SubTopicPage.tsx", "frontend/src/api/public-client.ts", "frontend/src/App.tsx", "frontend/src/pages/TopicsBrowse.tsx", "frontend/src/App.css", "frontend/src/pages/Home.tsx"]
|
||||||
|
key_decisions: ["Grouped techniques by creator with Map-based first-appearance ordering", "Breadcrumb pattern: nav.breadcrumbs with link/text/current spans for reuse"]
|
||||||
|
patterns_established: []
|
||||||
|
drill_down_paths: []
|
||||||
|
observability_surfaces: []
|
||||||
|
duration: ""
|
||||||
|
verification_result: "TypeScript check (npx tsc --noEmit) passed with exit 0. Production build (npm run build) passed with exit 0, 48 modules transformed."
|
||||||
|
completed_at: 2026-03-31T06:03:01.860Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T02: Added SubTopicPage with breadcrumbs and creator-grouped technique list, wired /topics/:category/:subtopic route, updated TopicsBrowse links to dedicated pages
|
||||||
|
|
||||||
|
> Added SubTopicPage with breadcrumbs and creator-grouped technique list, wired /topics/:category/:subtopic route, updated TopicsBrowse links to dedicated pages
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
---
|
||||||
|
id: T02
|
||||||
|
parent: S01
|
||||||
|
milestone: M010
|
||||||
|
key_files:
|
||||||
|
- frontend/src/pages/SubTopicPage.tsx
|
||||||
|
- frontend/src/api/public-client.ts
|
||||||
|
- frontend/src/App.tsx
|
||||||
|
- frontend/src/pages/TopicsBrowse.tsx
|
||||||
|
- frontend/src/App.css
|
||||||
|
- frontend/src/pages/Home.tsx
|
||||||
|
key_decisions:
|
||||||
|
- Grouped techniques by creator with Map-based first-appearance ordering
|
||||||
|
- Breadcrumb pattern: nav.breadcrumbs with link/text/current spans for reuse
|
||||||
|
duration: ""
|
||||||
|
verification_result: passed
|
||||||
|
completed_at: 2026-03-31T06:03:01.860Z
|
||||||
|
blocker_discovered: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# T02: Added SubTopicPage with breadcrumbs and creator-grouped technique list, wired /topics/:category/:subtopic route, updated TopicsBrowse links to dedicated pages
|
||||||
|
|
||||||
|
**Added SubTopicPage with breadcrumbs and creator-grouped technique list, wired /topics/:category/:subtopic route, updated TopicsBrowse links to dedicated pages**
|
||||||
|
|
||||||
|
## What Happened
|
||||||
|
|
||||||
|
Created SubTopicPage.tsx following CreatorDetail pattern: extracts category/subtopic from URL params, fetches via fetchSubTopicTechniques, groups by creator_name, renders breadcrumbs. Added API client function to public-client.ts. Registered route in App.tsx before /topics catch-all. Updated TopicsBrowse links from /search?q= to /topics/{cat}/{subtopic}. Added breadcrumb and sub-topic page CSS. Fixed pre-existing TS strict error in Home.tsx.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
TypeScript check (npx tsc --noEmit) passed with exit 0. Production build (npm run build) passed with exit 0, 48 modules transformed.
|
||||||
|
|
||||||
|
## Verification Evidence
|
||||||
|
|
||||||
|
| # | Command | Exit Code | Verdict | Duration |
|
||||||
|
|---|---------|-----------|---------|----------|
|
||||||
|
| 1 | `cd frontend && npx tsc --noEmit` | 0 | ✅ pass | 3400ms |
|
||||||
|
| 2 | `cd frontend && npm run build` | 0 | ✅ pass | 3300ms |
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
|
||||||
|
Fixed pre-existing TS strict error in Home.tsx (res.items[0] undefined narrowing) to pass npm run build.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
Slice-level pytest verification uses --timeout=30 but pytest-timeout not installed. Backend tests require PostgreSQL on ub01.
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `frontend/src/pages/SubTopicPage.tsx`
|
||||||
|
- `frontend/src/api/public-client.ts`
|
||||||
|
- `frontend/src/App.tsx`
|
||||||
|
- `frontend/src/pages/TopicsBrowse.tsx`
|
||||||
|
- `frontend/src/App.css`
|
||||||
|
- `frontend/src/pages/Home.tsx`
|
||||||
|
|
||||||
|
|
||||||
|
## Deviations
|
||||||
|
Fixed pre-existing TS strict error in Home.tsx (res.items[0] undefined narrowing) to pass npm run build.
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
Slice-level pytest verification uses --timeout=30 but pytest-timeout not installed. Backend tests require PostgreSQL on ub01.
|
||||||
|
|
@ -2258,6 +2258,135 @@ a.app-footer__repo:hover {
|
||||||
color: var(--color-border);
|
color: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Breadcrumbs ──────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.breadcrumbs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__link {
|
||||||
|
color: var(--color-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__link:hover {
|
||||||
|
color: var(--color-accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__sep {
|
||||||
|
color: var(--color-border);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__text {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__current {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sub-topic page ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.subtopic-page {
|
||||||
|
max-width: 56rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-page__title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-page__subtitle {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin: 0 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-groups {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-group__creator {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-group__creator-link {
|
||||||
|
color: var(--color-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-group__creator-link:hover {
|
||||||
|
color: var(--color-accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-group__count {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-group__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-technique-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: var(--color-bg-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-technique-card:hover {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-technique-card__title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-technique-card__tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtopic-technique-card__summary {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Public responsive (extended) ─────────────────────────────────────────── */
|
/* ── Public responsive (extended) ─────────────────────────────────────────── */
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import TechniquePage from "./pages/TechniquePage";
|
||||||
import CreatorsBrowse from "./pages/CreatorsBrowse";
|
import CreatorsBrowse from "./pages/CreatorsBrowse";
|
||||||
import CreatorDetail from "./pages/CreatorDetail";
|
import CreatorDetail from "./pages/CreatorDetail";
|
||||||
import TopicsBrowse from "./pages/TopicsBrowse";
|
import TopicsBrowse from "./pages/TopicsBrowse";
|
||||||
|
import SubTopicPage from "./pages/SubTopicPage";
|
||||||
import AdminReports from "./pages/AdminReports";
|
import AdminReports from "./pages/AdminReports";
|
||||||
import AdminPipeline from "./pages/AdminPipeline";
|
import AdminPipeline from "./pages/AdminPipeline";
|
||||||
import About from "./pages/About";
|
import About from "./pages/About";
|
||||||
|
|
@ -38,6 +39,7 @@ export default function App() {
|
||||||
{/* Browse routes */}
|
{/* Browse routes */}
|
||||||
<Route path="/creators" element={<CreatorsBrowse />} />
|
<Route path="/creators" element={<CreatorsBrowse />} />
|
||||||
<Route path="/creators/:slug" element={<CreatorDetail />} />
|
<Route path="/creators/:slug" element={<CreatorDetail />} />
|
||||||
|
<Route path="/topics/:category/:subtopic" element={<SubTopicPage />} />
|
||||||
<Route path="/topics" element={<TopicsBrowse />} />
|
<Route path="/topics" element={<TopicsBrowse />} />
|
||||||
|
|
||||||
{/* Admin routes */}
|
{/* Admin routes */}
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,20 @@ export async function fetchTopics(): Promise<TopicCategory[]> {
|
||||||
return request<TopicCategory[]>(`${BASE}/topics`);
|
return request<TopicCategory[]>(`${BASE}/topics`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchSubTopicTechniques(
|
||||||
|
categorySlug: string,
|
||||||
|
subtopicSlug: string,
|
||||||
|
params: { limit?: number; offset?: number } = {},
|
||||||
|
): Promise<TechniqueListResponse> {
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
if (params.limit !== undefined) qs.set("limit", String(params.limit));
|
||||||
|
if (params.offset !== undefined) qs.set("offset", String(params.offset));
|
||||||
|
const query = qs.toString();
|
||||||
|
return request<TechniqueListResponse>(
|
||||||
|
`${BASE}/topics/${encodeURIComponent(categorySlug)}/${encodeURIComponent(subtopicSlug)}${query ? `?${query}` : ""}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Creators ─────────────────────────────────────────────────────────────────
|
// ── Creators ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface CreatorListParams {
|
export interface CreatorListParams {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export default function Home() {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetchTechniques({ sort: "random", limit: 1 });
|
const res = await fetchTechniques({ sort: "random", limit: 1 });
|
||||||
if (!cancelled && res.items.length > 0) setFeatured(res.items[0]);
|
if (!cancelled && res.items.length > 0) setFeatured(res.items[0] ?? null);
|
||||||
} catch {
|
} catch {
|
||||||
// silently ignore — optional section
|
// silently ignore — optional section
|
||||||
}
|
}
|
||||||
|
|
|
||||||
162
frontend/src/pages/SubTopicPage.tsx
Normal file
162
frontend/src/pages/SubTopicPage.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
/**
|
||||||
|
* Sub-topic detail page.
|
||||||
|
*
|
||||||
|
* Shows techniques for a specific sub-topic within a category,
|
||||||
|
* grouped by creator. Breadcrumb navigation back to Topics.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Link, useParams } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
fetchSubTopicTechniques,
|
||||||
|
type TechniqueListItem,
|
||||||
|
} from "../api/public-client";
|
||||||
|
|
||||||
|
/** Convert a URL slug to a display name: replace hyphens with spaces, title-case. */
|
||||||
|
function slugToDisplayName(slug: string): string {
|
||||||
|
return slug
|
||||||
|
.replace(/-/g, " ")
|
||||||
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Group techniques by creator_name, preserving order of first appearance. */
|
||||||
|
function groupByCreator(
|
||||||
|
items: TechniqueListItem[],
|
||||||
|
): { creatorName: string; creatorSlug: string; techniques: TechniqueListItem[] }[] {
|
||||||
|
const map = new Map<string, { creatorName: string; creatorSlug: string; techniques: TechniqueListItem[] }>();
|
||||||
|
for (const item of items) {
|
||||||
|
const name = item.creator_name || "Unknown";
|
||||||
|
const existing = map.get(name);
|
||||||
|
if (existing) {
|
||||||
|
existing.techniques.push(item);
|
||||||
|
} else {
|
||||||
|
map.set(name, {
|
||||||
|
creatorName: name,
|
||||||
|
creatorSlug: item.creator_slug || "",
|
||||||
|
techniques: [item],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(map.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SubTopicPage() {
|
||||||
|
const { category, subtopic } = useParams<{ category: string; subtopic: string }>();
|
||||||
|
const [techniques, setTechniques] = useState<TechniqueListItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const categoryDisplay = category ? slugToDisplayName(category) : "";
|
||||||
|
const subtopicDisplay = subtopic ? slugToDisplayName(subtopic) : "";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!category || !subtopic) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const data = await fetchSubTopicTechniques(category, subtopic, { limit: 100 });
|
||||||
|
if (!cancelled) {
|
||||||
|
setTechniques(data.items);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setError(
|
||||||
|
err instanceof Error ? err.message : "Failed to load techniques",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [category, subtopic]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="loading">Loading techniques…</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="loading error-text">
|
||||||
|
Error: {error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = groupByCreator(techniques);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="subtopic-page">
|
||||||
|
{/* Breadcrumbs */}
|
||||||
|
<nav className="breadcrumbs" aria-label="Breadcrumb">
|
||||||
|
<Link to="/topics" className="breadcrumbs__link">Topics</Link>
|
||||||
|
<span className="breadcrumbs__sep" aria-hidden="true">›</span>
|
||||||
|
<span className="breadcrumbs__text">{categoryDisplay}</span>
|
||||||
|
<span className="breadcrumbs__sep" aria-hidden="true">›</span>
|
||||||
|
<span className="breadcrumbs__current" aria-current="page">{subtopicDisplay}</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<h2 className="subtopic-page__title">{subtopicDisplay}</h2>
|
||||||
|
<p className="subtopic-page__subtitle">
|
||||||
|
{techniques.length} technique{techniques.length !== 1 ? "s" : ""} in {categoryDisplay}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{techniques.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
No techniques found for this sub-topic.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="subtopic-groups">
|
||||||
|
{groups.map((group) => (
|
||||||
|
<section key={group.creatorName} className="subtopic-group">
|
||||||
|
<h3 className="subtopic-group__creator">
|
||||||
|
{group.creatorSlug ? (
|
||||||
|
<Link to={`/creators/${group.creatorSlug}`} className="subtopic-group__creator-link">
|
||||||
|
{group.creatorName}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
group.creatorName
|
||||||
|
)}
|
||||||
|
<span className="subtopic-group__count">
|
||||||
|
{group.techniques.length} technique{group.techniques.length !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<div className="subtopic-group__list">
|
||||||
|
{group.techniques.map((t) => (
|
||||||
|
<Link
|
||||||
|
key={t.id}
|
||||||
|
to={`/techniques/${t.slug}`}
|
||||||
|
className="subtopic-technique-card"
|
||||||
|
>
|
||||||
|
<span className="subtopic-technique-card__title">{t.title}</span>
|
||||||
|
{t.topic_tags && t.topic_tags.length > 0 && (
|
||||||
|
<span className="subtopic-technique-card__tags">
|
||||||
|
{t.topic_tags.map((tag) => (
|
||||||
|
<span key={tag} className="pill">{tag}</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{t.summary && (
|
||||||
|
<span className="subtopic-technique-card__summary">
|
||||||
|
{t.summary.length > 150
|
||||||
|
? `${t.summary.slice(0, 150)}…`
|
||||||
|
: t.summary}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -156,10 +156,12 @@ export default function TopicsBrowse() {
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="topic-subtopics">
|
<div className="topic-subtopics">
|
||||||
{cat.sub_topics.map((st) => (
|
{cat.sub_topics.map((st) => {
|
||||||
|
const stSlug = st.name.toLowerCase().replace(/\s+/g, "-");
|
||||||
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={st.name}
|
key={st.name}
|
||||||
to={`/search?q=${encodeURIComponent(st.name)}&scope=topics`}
|
to={`/topics/${slug}/${stSlug}`}
|
||||||
className="topic-subtopic"
|
className="topic-subtopic"
|
||||||
>
|
>
|
||||||
<span className="topic-subtopic__name">{st.name}</span>
|
<span className="topic-subtopic__name">{st.name}</span>
|
||||||
|
|
@ -173,7 +175,8 @@ export default function TopicsBrowse() {
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReportIssueModal.tsx","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/SearchResults.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx"],"version":"5.6.3"}
|
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/public-client.ts","./src/components/AdminDropdown.tsx","./src/components/AppFooter.tsx","./src/components/CategoryIcons.tsx","./src/components/CopyLinkButton.tsx","./src/components/CreatorAvatar.tsx","./src/components/ReportIssueModal.tsx","./src/pages/About.tsx","./src/pages/AdminPipeline.tsx","./src/pages/AdminReports.tsx","./src/pages/CreatorDetail.tsx","./src/pages/CreatorsBrowse.tsx","./src/pages/Home.tsx","./src/pages/SearchResults.tsx","./src/pages/SubTopicPage.tsx","./src/pages/TechniquePage.tsx","./src/pages/TopicsBrowse.tsx"],"version":"5.6.3"}
|
||||||
Loading…
Add table
Reference in a new issue