From df559bbca00b7e78aa028b9c7839c5a0209ce3c9 Mon Sep 17 00:00:00 2001 From: jlightner Date: Tue, 31 Mar 2026 08:24:38 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Added=20GET=20/api/v1/techniques/random?= =?UTF-8?q?=20endpoint=20returning=20{slug},=20fe=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "backend/routers/techniques.py" - "frontend/src/api/public-client.ts" - "frontend/src/pages/Home.tsx" - "frontend/src/App.css" GSD-Task: S01/T02 --- backend/routers/techniques.py | 13 +++++++++++++ frontend/src/App.css | 27 +++++++++++++++++++++++++++ frontend/src/api/public-client.ts | 4 ++++ frontend/src/pages/Home.tsx | 28 ++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+) diff --git a/backend/routers/techniques.py b/backend/routers/techniques.py index 0c2dbea..358c86b 100644 --- a/backend/routers/techniques.py +++ b/backend/routers/techniques.py @@ -182,6 +182,19 @@ async def list_techniques( ) +@router.get("/random") +async def random_technique( + db: AsyncSession = Depends(get_session), +) -> dict: + """Return the slug of a single random technique page.""" + stmt = select(TechniquePage.slug).order_by(func.random()).limit(1) + result = await db.execute(stmt) + slug = result.scalar_one_or_none() + if slug is None: + raise HTTPException(status_code=404, detail="No techniques available") + return {"slug": slug} + + @router.get("/{slug}", response_model=TechniquePageDetail) async def get_technique( slug: str, diff --git a/frontend/src/App.css b/frontend/src/App.css index 6b1e7de..d0f58f5 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -2729,6 +2729,33 @@ a.app-footer__repo:hover { opacity: 0.85; } +.btn--random { + background: var(--color-bg-input); + color: var(--color-text-primary); + border-color: var(--color-border); + font-size: 0.95rem; + padding: 0.6rem 1.4rem; + display: inline-flex; + align-items: center; + gap: 0.4rem; + transition: background 0.15s, border-color 0.15s, transform 0.15s; +} + +.btn--random:hover:not(:disabled) { + background: var(--color-border); + transform: scale(1.04); +} + +.btn--random:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.home-random { + text-align: center; + margin: 1.5rem 0 0.5rem; +} + /* ── Admin Reports ──────────────────────────────────────────────────────── */ .admin-reports { diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts index 3ecee06..6f571b6 100644 --- a/frontend/src/api/public-client.ts +++ b/frontend/src/api/public-client.ts @@ -258,6 +258,10 @@ export async function fetchTechnique( return request(`${BASE}/techniques/${slug}`); } +export async function fetchRandomTechnique(): Promise<{ slug: string }> { + return request<{ slug: string }>(`${BASE}/techniques/random`); +} + export async function fetchTechniqueVersions( slug: string, ): Promise { diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 2abfc18..0f6e642 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -12,6 +12,7 @@ import { Link, useNavigate } from "react-router-dom"; import { fetchTechniques, fetchTopics, + fetchRandomTechnique, type TechniqueListItem, } from "../api/public-client"; @@ -20,8 +21,24 @@ export default function Home() { const [recent, setRecent] = useState([]); const [recentLoading, setRecentLoading] = useState(true); const [popularTopics, setPopularTopics] = useState<{name: string; count: number}[]>([]); + const [randomLoading, setRandomLoading] = useState(false); + const [randomError, setRandomError] = useState(false); const navigate = useNavigate(); + const handleRandomTechnique = async () => { + setRandomLoading(true); + setRandomError(false); + try { + const { slug } = await fetchRandomTechnique(); + navigate(`/techniques/${slug}`); + } catch { + setRandomError(true); + setTimeout(() => setRandomError(false), 2000); + } finally { + setRandomLoading(false); + } + }; + // Load featured technique (random) useEffect(() => { let cancelled = false; @@ -156,6 +173,17 @@ export default function Home() { + {/* Random technique discovery */} +
+ +
+ {/* Featured Technique Spotlight */} {featured && (