gm4ebnr3j: Add three-column layout with featured experts and trending minnects rails
This commit is contained in:
+15
-1
@@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { loadSession, type Person } from "@/lib/auth-client";
|
||||
import { SignInForm } from "./SignInForm";
|
||||
import { Logo } from "./Logo";
|
||||
import { Feed } from "./Feed";
|
||||
import { DiscoverRails } from "./DiscoverRails";
|
||||
|
||||
type State = { kind: "loading" } | { kind: "out" } | { kind: "in"; person: Person };
|
||||
|
||||
@@ -46,7 +48,19 @@ export function App() {
|
||||
<span>{state.person.email}</span>
|
||||
</div>
|
||||
</header>
|
||||
<Feed feedUserId={state.person.feedUserId} onSignedOut={() => setState({ kind: "out" })} />
|
||||
<div className="columns">
|
||||
<nav className="col-left" aria-label="Main">
|
||||
<ul>
|
||||
<li><Link href="/" aria-current="page">Feed</Link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="col-center">
|
||||
<Feed feedUserId={state.person.feedUserId} onSignedOut={() => setState({ kind: "out" })} />
|
||||
</div>
|
||||
<aside className="col-right" aria-label="Discover">
|
||||
<DiscoverRails />
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { DiscoverData } from "@/lib/discover";
|
||||
import { fetchDiscover } from "@/lib/discover-client";
|
||||
import { FeaturedExpertsRail } from "./FeaturedExpertsRail";
|
||||
import { TrendingMinnectsRail } from "./TrendingMinnectsRail";
|
||||
|
||||
export function DiscoverRailsView({ data }: { data: DiscoverData | null }) {
|
||||
if (!data) return null;
|
||||
return (
|
||||
<>
|
||||
<FeaturedExpertsRail experts={data.featuredExperts} />
|
||||
<TrendingMinnectsRail minnects={data.trendingMinnects} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Loads /api/discover once. Failure hides the rails; the feed is unaffected. */
|
||||
export function DiscoverRails() {
|
||||
const [data, setData] = useState<DiscoverData | null>(null);
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
fetchDiscover().then((d) => { if (live) setData(d); }, () => {});
|
||||
return () => { live = false; };
|
||||
}, []);
|
||||
return <DiscoverRailsView data={data} />;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { DiscoverExpert } from "@/lib/discover";
|
||||
import { initials } from "@/lib/feed-model";
|
||||
|
||||
function Rating({ rating, reviews }: { rating: number | null; reviews: number | null }) {
|
||||
if (rating === null && reviews === null) return null;
|
||||
return (
|
||||
<span className="rail-meta">
|
||||
{rating !== null ? <span aria-label={`Rated ${rating.toFixed(1)} out of 5`}>★ {rating.toFixed(1)}</span> : null}
|
||||
{reviews !== null ? <span> ({reviews} {reviews === 1 ? "review" : "reviews"})</span> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function FeaturedExpertsRail({ experts }: { experts: DiscoverExpert[] }) {
|
||||
if (experts.length === 0) return null;
|
||||
return (
|
||||
<section className="rail" aria-labelledby="rail-experts">
|
||||
<h2 id="rail-experts">Featured experts</h2>
|
||||
<ul className="rail-list">
|
||||
{experts.map((e) => (
|
||||
<li key={String(e.id)} className="rail-card expert-card" data-testid="expert-card">
|
||||
{e.pictureUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className="avatar" src={e.pictureUrl} alt="" width={40} height={40} />
|
||||
) : (
|
||||
<span className="avatar avatar-initials" aria-hidden="true">{initials(e.displayName)}</span>
|
||||
)}
|
||||
<div className="rail-text">
|
||||
<strong className="rail-name">{e.displayName || "Expert"}</strong>
|
||||
{e.byline ? <span className="rail-byline">{e.byline}</span> : null}
|
||||
<Rating rating={e.rating} reviews={e.reviewsCount} />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { DiscoverMinnect } from "@/lib/discover";
|
||||
import { initials } from "@/lib/feed-model";
|
||||
|
||||
export function TrendingMinnectsRail({ minnects }: { minnects: DiscoverMinnect[] }) {
|
||||
if (minnects.length === 0) return null;
|
||||
return (
|
||||
<section className="rail" aria-labelledby="rail-trending">
|
||||
<h2 id="rail-trending">Trending minnects</h2>
|
||||
<ul className="rail-list">
|
||||
{minnects.map((m) => {
|
||||
const who = m.expert.displayName || "Expert";
|
||||
return (
|
||||
<li key={String(m.id)} className="rail-card minnect-card" data-testid="minnect-card">
|
||||
<div className="minnect-who">
|
||||
{m.expert.pictureUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className="avatar avatar-sm" src={m.expert.pictureUrl} alt="" width={28} height={28} />
|
||||
) : (
|
||||
<span className="avatar avatar-sm avatar-initials" aria-hidden="true">{initials(who)}</span>
|
||||
)}
|
||||
<strong className="rail-name">{who}</strong>
|
||||
</div>
|
||||
{m.thumbnailUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img className="minnect-thumb" src={m.thumbnailUrl} alt={m.question ? `Thumbnail for: ${m.question}` : "Minnect thumbnail"} />
|
||||
) : null}
|
||||
{m.question ? <p className="minnect-question">{m.question}</p> : null}
|
||||
{m.views !== null ? (
|
||||
<span className="rail-meta">{m.views.toLocaleString("en-US")} {m.views === 1 ? "view" : "views"}</span>
|
||||
) : null}
|
||||
{m.audio.url ? (
|
||||
<audio className="minnect-audio" controls preload="none" src={m.audio.url} aria-label={`Play answer from ${who}`} />
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user