8iz120vqu: Restyle the Trending minnects and Find experts panels
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user