feat: Added technique_section result rendering with Section badge, deep…
- "frontend/src/api/public-client.ts" - "frontend/src/pages/TechniquePage.tsx" - "frontend/src/pages/SearchResults.tsx" - "frontend/src/components/SearchAutocomplete.tsx" GSD-Task: S07/T02
This commit is contained in:
parent
fd683e8266
commit
7bdba76d50
4 changed files with 87 additions and 24 deletions
|
|
@ -19,6 +19,8 @@ export interface SearchResultItem {
|
|||
topic_tags: string[];
|
||||
technique_page_slug?: string;
|
||||
match_context?: string;
|
||||
section_anchor?: string;
|
||||
section_heading?: string;
|
||||
}
|
||||
|
||||
export interface SearchResponse {
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ export default function SearchAutocomplete({
|
|||
creator: "Creator",
|
||||
technique_page: "Technique",
|
||||
key_moment: "Key Moment",
|
||||
technique_section: "Section",
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -215,26 +216,37 @@ export default function SearchAutocomplete({
|
|||
|
||||
{showSearch && (
|
||||
<>
|
||||
{searchResults.map((item) => (
|
||||
<Link
|
||||
key={`${item.type}-${item.slug}`}
|
||||
to={`/techniques/${item.slug}`}
|
||||
className="typeahead-item"
|
||||
onClick={() => setShowDropdown(false)}
|
||||
>
|
||||
<span className="typeahead-item__title">{item.title}</span>
|
||||
<span className="typeahead-item__meta">
|
||||
<span className={`typeahead-item__type typeahead-item__type--${item.type}`}>
|
||||
{typeLabel[item.type] ?? item.type}
|
||||
</span>
|
||||
{item.creator_name && (
|
||||
<span className="typeahead-item__creator">
|
||||
{item.creator_name}
|
||||
</span>
|
||||
{searchResults.map((item) => {
|
||||
let linkTo = `/techniques/${item.slug}`;
|
||||
if (item.type === "technique_section" && item.technique_page_slug) {
|
||||
linkTo = `/techniques/${item.technique_page_slug}${item.section_anchor ? `#${item.section_anchor}` : ""}`;
|
||||
} else if (item.type === "key_moment" && item.technique_page_slug) {
|
||||
linkTo = `/techniques/${item.technique_page_slug}#km-${item.slug || item.title}`;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
key={`${item.type}-${item.slug}-${item.section_anchor ?? ""}`}
|
||||
to={linkTo}
|
||||
className="typeahead-item"
|
||||
onClick={() => setShowDropdown(false)}
|
||||
>
|
||||
<span className="typeahead-item__title">{item.title}</span>
|
||||
{item.type === "technique_section" && item.section_heading && (
|
||||
<span className="typeahead-item__section">§ {item.section_heading}</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
<span className="typeahead-item__meta">
|
||||
<span className={`typeahead-item__type typeahead-item__type--${item.type}`}>
|
||||
{typeLabel[item.type] ?? item.type}
|
||||
</span>
|
||||
{item.creator_name && (
|
||||
<span className="typeahead-item__creator">
|
||||
{item.creator_name}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<Link
|
||||
to={`/search?q=${encodeURIComponent(query)}`}
|
||||
className="typeahead-see-all"
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export default function SearchResults() {
|
|||
|
||||
// Group results by type
|
||||
const techniqueResults = results.filter((r) => r.type === "technique_page");
|
||||
const sectionResults = results.filter((r) => r.type === "technique_section");
|
||||
const momentResults = results.filter((r) => r.type === "key_moment");
|
||||
|
||||
return (
|
||||
|
|
@ -124,6 +125,20 @@ export default function SearchResults() {
|
|||
</section>
|
||||
)}
|
||||
|
||||
{/* Technique sections */}
|
||||
{sectionResults.length > 0 && (
|
||||
<section className="search-group">
|
||||
<h3 className="search-group__title">
|
||||
Sections ({sectionResults.length})
|
||||
</h3>
|
||||
<div className="search-group__list">
|
||||
{sectionResults.map((item, i) => (
|
||||
<SearchResultCard key={`ts-${item.slug}-${item.section_anchor}-${i}`} item={item} staggerIndex={i} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Key moments */}
|
||||
{momentResults.length > 0 && (
|
||||
<section className="search-group">
|
||||
|
|
@ -142,6 +157,15 @@ export default function SearchResults() {
|
|||
}
|
||||
|
||||
function getSearchResultLink(item: SearchResultItem): string {
|
||||
if (item.type === "technique_section") {
|
||||
if (item.technique_page_slug && item.section_anchor) {
|
||||
return `/techniques/${item.technique_page_slug}#${item.section_anchor}`;
|
||||
}
|
||||
if (item.technique_page_slug) {
|
||||
return `/techniques/${item.technique_page_slug}`;
|
||||
}
|
||||
return `/search?q=${encodeURIComponent(item.title)}`;
|
||||
}
|
||||
if (item.type === "key_moment") {
|
||||
if (item.technique_page_slug) {
|
||||
return `/techniques/${item.technique_page_slug}#km-${item.slug || item.title}`;
|
||||
|
|
@ -153,6 +177,12 @@ function getSearchResultLink(item: SearchResultItem): string {
|
|||
}
|
||||
|
||||
function SearchResultCard({ item, staggerIndex }: { item: SearchResultItem; staggerIndex: number }) {
|
||||
const typeLabels: Record<string, string> = {
|
||||
technique_page: "Technique",
|
||||
key_moment: "Key Moment",
|
||||
technique_section: "Section",
|
||||
};
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={getSearchResultLink(item)}
|
||||
|
|
@ -162,9 +192,14 @@ function SearchResultCard({ item, staggerIndex }: { item: SearchResultItem; stag
|
|||
<div className="search-result-card__header">
|
||||
<span className="search-result-card__title">{item.title}</span>
|
||||
<span className={`badge badge--type badge--type-${item.type}`}>
|
||||
{item.type === "technique_page" ? "Technique" : "Key Moment"}
|
||||
{typeLabels[item.type] ?? item.type}
|
||||
</span>
|
||||
</div>
|
||||
{item.type === "technique_section" && item.section_heading && (
|
||||
<div className="search-result-card__section-context">
|
||||
§ {item.section_heading}
|
||||
</div>
|
||||
)}
|
||||
{item.match_context && (
|
||||
<div className="search-result-card__match-context">
|
||||
<span className="match-context__icon">⚡</span>
|
||||
|
|
@ -198,6 +233,7 @@ function SearchResultCard({ item, staggerIndex }: { item: SearchResultItem; stag
|
|||
|
||||
function PartialMatchResults({ items }: { items: SearchResultItem[] }) {
|
||||
const techniqueResults = items.filter((r) => r.type === "technique_page");
|
||||
const sectionResults = items.filter((r) => r.type === "technique_section");
|
||||
const momentResults = items.filter((r) => r.type === "key_moment");
|
||||
|
||||
return (
|
||||
|
|
@ -219,6 +255,19 @@ function PartialMatchResults({ items }: { items: SearchResultItem[] }) {
|
|||
</section>
|
||||
)}
|
||||
|
||||
{sectionResults.length > 0 && (
|
||||
<section className="search-group">
|
||||
<h4 className="search-group__title">
|
||||
Sections ({sectionResults.length})
|
||||
</h4>
|
||||
<div className="search-group__list">
|
||||
{sectionResults.map((item, i) => (
|
||||
<SearchResultCard key={`partial-ts-${item.slug}-${item.section_anchor}-${i}`} item={item} staggerIndex={i} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{momentResults.length > 0 && (
|
||||
<section className="search-group">
|
||||
<h4 className="search-group__title">
|
||||
|
|
|
|||
|
|
@ -171,12 +171,12 @@ export default function TechniquePage() {
|
|||
};
|
||||
}, [slug, selectedVersion]);
|
||||
|
||||
// Scroll to key moment if URL has a #km- hash fragment
|
||||
// Scroll to hash fragment after technique loads (key moments, section anchors, etc.)
|
||||
useEffect(() => {
|
||||
if (!technique) return;
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith("#km-")) {
|
||||
const el = document.getElementById(hash.slice(1));
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash) {
|
||||
const el = document.getElementById(hash);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue