chrysopedia/frontend/src/pages/Home.tsx

346 lines
12 KiB
TypeScript

/**
* Home / landing page.
*
* Prominent search bar with 300ms debounced typeahead (top 5 results after 2+ chars),
* navigation cards for Topics and Creators, and a "Recently Added" section.
*/
import { IconTopics, IconCreators } from "../components/CategoryIcons";
import SearchAutocomplete from "../components/SearchAutocomplete";
import TagList from "../components/TagList";
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useDocumentTitle } from "../hooks/useDocumentTitle";
import {
fetchTechniques,
fetchTopics,
fetchRandomTechnique,
fetchStats,
fetchPopularSearches,
type TechniqueListItem,
type StatsResponse,
type PopularSearchItem,
} from "../api/public-client";
export default function Home() {
useDocumentTitle("Chrysopedia — Production Knowledge, Distilled");
const [featured, setFeatured] = useState<TechniqueListItem | null>(null);
const [recent, setRecent] = useState<TechniqueListItem[]>([]);
const [recentLoading, setRecentLoading] = useState(true);
const [popularTopics, setPopularTopics] = useState<{name: string; count: number}[]>([]);
const [randomLoading, setRandomLoading] = useState(false);
const [randomError, setRandomError] = useState(false);
const [stats, setStats] = useState<StatsResponse | null>(null);
const [trending, setTrending] = useState<PopularSearchItem[] | null>(null);
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;
void (async () => {
try {
const res = await fetchTechniques({ sort: "random", limit: 1 });
if (!cancelled && res.items.length > 0) setFeatured(res.items[0] ?? null);
} catch {
// silently ignore — optional section
}
})();
return () => {
cancelled = true;
};
}, []);
// Load recently added techniques
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const res = await fetchTechniques({ sort: "recent", limit: 6 });
if (!cancelled) setRecent(res.items);
} catch {
// silently ignore — not critical
} finally {
if (!cancelled) setRecentLoading(false);
}
})();
return () => {
cancelled = true;
};
}, []);
// Load popular topics
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const categories = await fetchTopics();
const all = categories.flatMap((cat) =>
cat.sub_topics.map((st) => ({ name: st.name, count: st.technique_count }))
);
all.sort((a, b) => b.count - a.count);
if (!cancelled) setPopularTopics(all.slice(0, 8));
} catch {
// optional section — silently ignore
}
})();
return () => {
cancelled = true;
};
}, []);
// Load stats
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const data = await fetchStats();
if (!cancelled) setStats(data);
} catch {
// optional section — silently ignore
}
})();
return () => {
cancelled = true;
};
}, []);
// Load trending searches
useEffect(() => {
let cancelled = false;
void (async () => {
try {
const data = await fetchPopularSearches();
if (!cancelled && data.items.length > 0) {
setTrending(data.items);
}
} catch {
// optional section — silently ignore
}
})();
return () => {
cancelled = true;
};
}, []);
return (
<div className="home">
{/* Hero search */}
<section className="home-hero">
<h1 className="home-hero__title">Production Knowledge, Distilled</h1>
<p className="home-hero__subtitle">
Search techniques, key moments, and creators
</p>
<SearchAutocomplete
variant="hero"
autoFocus
onSearch={(q) => navigate(`/search?q=${encodeURIComponent(q)}`)}
/>
<p className="home-hero__value-prop">
Real music production techniques extracted from creator tutorials.
Skip the 4-hour videos find the insight you need in seconds.
</p>
<div className="home-how-it-works">
<div className="home-how-it-works__step">
<span className="home-how-it-works__number">1</span>
<h2 className="home-how-it-works__title">Creators Share Techniques</h2>
<p className="home-how-it-works__desc">
Real producers and sound designers publish in-depth tutorials
</p>
</div>
<div className="home-how-it-works__step">
<span className="home-how-it-works__number">2</span>
<h2 className="home-how-it-works__title">AI Extracts Key Moments</h2>
<p className="home-how-it-works__desc">
We distill hours of video into structured, searchable knowledge
</p>
</div>
<div className="home-how-it-works__step">
<span className="home-how-it-works__number">3</span>
<h2 className="home-how-it-works__title">You Find Answers Fast</h2>
<p className="home-how-it-works__desc">
Search by topic, technique, or creator get straight to the insight
</p>
</div>
</div>
<Link to="/topics" className="btn home-cta">Start Exploring</Link>
{popularTopics.length > 0 && (
<section className="home-popular-topics">
<h2 className="home-popular-topics__title">Popular Topics</h2>
<div className="home-popular-topics__list">
{popularTopics.map((topic) => (
<Link
key={topic.name}
to={`/search?q=${encodeURIComponent(topic.name)}&scope=topics`}
className="pill pill--topic-quick"
>
{topic.name}
</Link>
))}
</div>
</section>
)}
</section>
{/* Navigation cards */}
<section className="nav-cards">
<Link to="/topics" className="nav-card card-stagger" style={{ '--stagger-index': 0 } as React.CSSProperties}>
<h2 className="nav-card__title"><IconTopics /> Topics</h2>
<p className="nav-card__desc">
Browse techniques organized by category and sub-topic
</p>
</Link>
<Link to="/creators" className="nav-card card-stagger" style={{ '--stagger-index': 1 } as React.CSSProperties}>
<h2 className="nav-card__title"><IconCreators /> Creators</h2>
<p className="nav-card__desc">
Discover creators and their technique libraries
</p>
</Link>
</section>
{/* Stats scorecard */}
{stats && (
<section className="home-stats card-stagger" style={{ '--stagger-index': 2 } as React.CSSProperties}>
<div className="home-stats__metric">
<span className="home-stats__number">{stats.technique_count}</span>
<span className="home-stats__label">Articles</span>
</div>
<div className="home-stats__metric">
<span className="home-stats__number">{stats.creator_count}</span>
<span className="home-stats__label">Creators</span>
</div>
</section>
)}
{/* Trending Searches */}
{trending && trending.length > 0 && (
<section className="home-trending card-stagger" style={{ '--stagger-index': 3 } as React.CSSProperties}>
<h2 className="home-trending__title">Trending Searches</h2>
<div className="home-trending__list">
{trending.map((item, i) => (
<Link
key={item.query}
to={`/search?q=${encodeURIComponent(item.query)}`}
className="pill pill--trending card-stagger"
style={{ '--stagger-index': i } as React.CSSProperties}
>
{item.query}
</Link>
))}
</div>
</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 && (
<section className="home-featured">
<h2 className="home-featured__label">Featured Technique</h2>
<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 && (
<TagList tags={featured.topic_tags} pillClass="pill--tag" />
)}
{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 */}
<section className="recent-section">
<h2 className="recent-section__title">Recently Added</h2>
{recentLoading ? (
<div className="loading">Loading</div>
) : recent.length === 0 ? (
<div className="empty-state">No techniques yet.</div>
) : (
<div className="recent-list">
{recent
.filter((t) => t.id !== featured?.id)
.slice(0, 4)
.map((t, i) => (
<Link
key={t.id}
to={`/techniques/${t.slug}`}
className="recent-card card-stagger"
style={{ '--stagger-index': i } as React.CSSProperties}
>
<span className="recent-card__title">{t.title}</span>
<span className="recent-card__meta">
<span className="badge badge--category">
{t.topic_category}
</span>
{t.topic_tags && t.topic_tags.length > 0 && (
<TagList tags={t.topic_tags} pillClass="pill--tag" />
)}
{t.summary && (
<span className="recent-card__summary">
{t.summary.length > 150
? `${t.summary.slice(0, 150)}`
: t.summary}
</span>
)}
</span>
<span className="recent-card__footer">
<span className="recent-card__creator">
{t.creator_name || ''}
</span>
{t.created_at && (
<span className="recent-card__date">
{new Date(t.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
)}
</span>
</Link>
))}
</div>
)}
</section>
</div>
);
}