8iz120vqu: Restyle the Trending minnects and Find experts panels

This commit is contained in:
2026-10-19 22:35:49 -06:00
parent cf45072f27
commit baecf8d865
9 changed files with 315 additions and 78 deletions
+30 -22
View File
@@ -1,5 +1,6 @@
"use client";
import { SweepButton } from "@/components/Buttons";
import { HexAvatar } from "@/components/HexAvatar";
import { useEffect, useRef, useState } from "react";
import type { DiscoverCategory } from "@/lib/discover";
@@ -27,28 +28,30 @@ export function ExpertSearchView(p: ExpertSearchViewProps) {
</div>
<form role="search" className="search-form" onSubmit={(e) => e.preventDefault()}>
<label htmlFor="expert-q">Search experts</label>
<input
id="expert-q"
type="search"
value={p.query}
placeholder="Name or topic"
autoComplete="off"
maxLength={80}
onChange={(e) => p.onQuery(e.target.value)}
/>
<div className="search-field">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
<circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" />
</svg>
<input
id="expert-q"
type="search"
value={p.query}
placeholder="Name or topic"
autoComplete="off"
maxLength={80}
onChange={(e) => p.onQuery(e.target.value)}
/>
</div>
</form>
{p.categories.length > 0 ? (
<div role="group" aria-label="Browse experts by category" className="chips">
{p.categories.map((c) => (
<button
<SweepButton
key={String(c.id)}
type="button"
className="chip"
aria-pressed={p.activeCategory === c.name}
label={c.name}
selected={p.activeCategory === c.name}
onClick={() => p.onCategory(p.activeCategory === c.name ? null : c.name)}
>
{c.name}
</button>
/>
))}
</div>
) : null}
@@ -59,15 +62,20 @@ export function ExpertSearchView(p: ExpertSearchViewProps) {
<p className="search-note">No experts found.</p>
) : null}
{p.status === "ready" && p.results.length > 0 ? (
<ul className="rail-list">
<ul className="rail-list search-rows">
{p.results.map((e) => (
<li key={String(e.id)} className="rail-card expert-card" data-testid="search-result">
<li key={String(e.id)} className="expert-row" data-testid="search-result">
<HexAvatar name={e.displayName} pictureUrl={e.pictureUrl} size="md" />
<div className="rail-text">
<strong className="rail-name">{e.displayName || "Expert"}</strong>
{e.byline ? <span className="rail-byline">{e.byline}</span> : null}
{e.rating !== null ? <span className="rail-meta">★ {e.rating.toFixed(1)}</span> : null}
<div className="expert-text">
<strong>{e.displayName || "Expert"}</strong>
{e.byline ? <span>{e.byline}</span> : null}
</div>
{e.rating !== null ? (
<span className="expert-rate" aria-label={`Rating ${e.rating.toFixed(1)}`}>
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="m12 2.8 2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 17.6l-5.8 3.1 1.1-6.5L2.6 9.6l6.5-.9L12 2.8Z" /></svg>
{e.rating.toFixed(1)}
</span>
) : null}
</li>
))}
</ul>
+64 -24
View File
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useRef, useState } from "react";
import { HexAvatar } from "@/components/HexAvatar";
import type { DiscoverMedia } from "@/lib/discover";
interface Props {
@@ -8,36 +9,75 @@ interface Props {
thumbnailUrl: string | null;
question: string | null;
who: string;
pictureUrl?: string | null;
views?: number | null;
}
/** Video answers play inline; audio keeps the audio control; no url shows the thumbnail only. */
export function MinnectMedia({ media, thumbnailUrl, question, who }: Props) {
/**
* The trending card's 16:9 frame. Video answers play inline as a <video> with a
* poster; audio keeps its <audio> control under the frame; no url shows the
* thumbnail only. The glass chip, play button, views and kind label sit over
* the thumbnail and step aside while a video plays.
*/
export function MinnectMedia({ media, thumbnailUrl, question, who, pictureUrl, views = null }: Props) {
const [videoFailed, setVideoFailed] = useState(false);
const [playing, setPlaying] = useState(false);
const video = useRef<HTMLVideoElement | null>(null);
const audio = useRef<HTMLAudioElement | null>(null);
const thumb = thumbnailUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img className="minnect-thumb" src={thumbnailUrl} alt={question ? `Thumbnail for: ${question}` : "Minnect thumbnail"} />
) : null;
const isVideo = media?.kind === "video" && !videoFailed;
const isAudio = media?.kind === "audio";
const playable = isVideo || isAudio;
const play = () => {
const el = isVideo ? video.current : audio.current;
void el?.play().catch(() => {});
};
if (media?.kind === "video" && !videoFailed) {
return (
<video
className="minnect-thumb minnect-video"
controls
playsInline
preload="none"
src={media.url}
poster={thumbnailUrl ?? undefined}
aria-label={`Play answer from ${who}`}
onError={() => setVideoFailed(true)}
/>
);
}
return (
<>
{thumb}
{media?.kind === "audio" ? (
<audio className="minnect-audio" controls preload="none" src={media.url} aria-label={`Play answer from ${who}`} />
<div className={`trend-media${playing ? " trend-media-playing" : ""}`}>
{isVideo ? (
<video
ref={video}
className="minnect-thumb minnect-video"
controls
playsInline
preload="none"
src={media.url}
poster={thumbnailUrl ?? undefined}
aria-label={`Play answer from ${who}`}
onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)}
onEnded={() => setPlaying(false)}
onError={() => setVideoFailed(true)}
/>
) : thumbnailUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img className="minnect-thumb" src={thumbnailUrl} alt={question ? `Thumbnail for: ${question}` : "Minnect thumbnail"} />
) : null}
<span className="trend-scrim" aria-hidden="true" />
<span className="trend-who">
<HexAvatar name={who} pictureUrl={pictureUrl} size="sm" />
<strong>{who}</strong>
</span>
{playable ? (
<button type="button" className="trend-play" aria-label={`Play answer from ${who}`} onClick={play}>
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5Z" /></svg>
</button>
) : null}
{views !== null ? (
<span className="trend-views">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12Z" /><circle cx="12" cy="12" r="3" />
</svg>
{views.toLocaleString("en-US")} {views === 1 ? "view" : "views"}
</span>
) : null}
{playable ? <span className="trend-kind">{isVideo ? "Video" : "Audio"}</span> : null}
</div>
{isAudio ? (
<audio ref={audio} className="minnect-audio" controls preload="none" src={media.url} aria-label={`Play answer from ${who}`} />
) : null}
</>
);
+11 -12
View File
@@ -1,4 +1,3 @@
import { HexAvatar } from "@/components/HexAvatar";
import type { DiscoverMinnect } from "@/lib/discover";
import { MinnectMedia } from "./MinnectMedia";
@@ -9,20 +8,20 @@ export function TrendingMinnectsRail({ minnects }: { minnects: DiscoverMinnect[]
<div className="panel-head">
<h2 id="rail-trending" className="type-title type-title-strong">Trending minnects</h2>
</div>
<ul className="rail-list panel-scroll fade-edges">
<ul className="rail-list trend-list panel-scroll fade-edges">
{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">
<HexAvatar name={who} pictureUrl={m.expert.pictureUrl} size="sm" />
<strong className="rail-name">{who}</strong>
</div>
<MinnectMedia media={m.media} thumbnailUrl={m.thumbnailUrl} question={m.question} who={who} />
{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}
<li key={String(m.id)} className="minnect-card" data-testid="minnect-card">
<MinnectMedia
media={m.media}
thumbnailUrl={m.thumbnailUrl}
question={m.question}
who={who}
pictureUrl={m.expert.pictureUrl}
views={m.views}
/>
{m.question ? <p className="trend-q">{m.question}</p> : null}
</li>
);
})}