feat: Added GET /api/v1/techniques/random endpoint returning {slug}, fe…

- "backend/routers/techniques.py"
- "frontend/src/api/public-client.ts"
- "frontend/src/pages/Home.tsx"
- "frontend/src/App.css"

GSD-Task: S01/T02
This commit is contained in:
jlightner 2026-03-31 08:24:38 +00:00
parent 9e4f10b0af
commit df559bbca0
4 changed files with 72 additions and 0 deletions

View file

@ -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) @router.get("/{slug}", response_model=TechniquePageDetail)
async def get_technique( async def get_technique(
slug: str, slug: str,

View file

@ -2729,6 +2729,33 @@ a.app-footer__repo:hover {
opacity: 0.85; 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 ──────────────────────────────────────────────────────── */
.admin-reports { .admin-reports {

View file

@ -258,6 +258,10 @@ export async function fetchTechnique(
return request<TechniquePageDetail>(`${BASE}/techniques/${slug}`); return request<TechniquePageDetail>(`${BASE}/techniques/${slug}`);
} }
export async function fetchRandomTechnique(): Promise<{ slug: string }> {
return request<{ slug: string }>(`${BASE}/techniques/random`);
}
export async function fetchTechniqueVersions( export async function fetchTechniqueVersions(
slug: string, slug: string,
): Promise<TechniquePageVersionListResponse> { ): Promise<TechniquePageVersionListResponse> {

View file

@ -12,6 +12,7 @@ import { Link, useNavigate } from "react-router-dom";
import { import {
fetchTechniques, fetchTechniques,
fetchTopics, fetchTopics,
fetchRandomTechnique,
type TechniqueListItem, type TechniqueListItem,
} from "../api/public-client"; } from "../api/public-client";
@ -20,8 +21,24 @@ export default function Home() {
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}[]>([]);
const [randomLoading, setRandomLoading] = useState(false);
const [randomError, setRandomError] = useState(false);
const navigate = useNavigate(); 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) // Load featured technique (random)
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@ -156,6 +173,17 @@ export default function Home() {
</Link> </Link>
</section> </section>
{/* Random technique discovery */}
<div className="home-random">
<button
className="btn btn--random"
onClick={handleRandomTechnique}
disabled={randomLoading}
>
{randomLoading ? "Loading…" : randomError ? "Try again" : "🎲 Random Technique"}
</button>
</div>
{/* Featured Technique Spotlight */} {/* Featured Technique Spotlight */}
{featured && ( {featured && (
<section className="home-featured"> <section className="home-featured">