14oqjtsnb: Play trending video answers as video with thumbnail fallback
This commit is contained in:
@@ -102,6 +102,7 @@ button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.avatar-sm { width: 28px; height: 28px; font-size: 11px; }
|
||||
.minnect-thumb { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 8px; background: var(--line); }
|
||||
.minnect-question { margin: 0; font-size: 14px; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.minnect-video { background: #000; border-radius: 0; }
|
||||
.minnect-audio { width: 100%; height: 36px; }
|
||||
@media (max-width: 959px) {
|
||||
.col-left { flex: 1 1 100%; position: static; padding-top: 8px; }
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { DiscoverMedia } from "@/lib/discover";
|
||||
|
||||
interface Props {
|
||||
media: DiscoverMedia | null;
|
||||
thumbnailUrl: string | null;
|
||||
question: string | null;
|
||||
who: string;
|
||||
}
|
||||
|
||||
/** Video answers play inline; audio keeps the audio control; no url shows the thumbnail only. */
|
||||
export function MinnectMedia({ media, thumbnailUrl, question, who }: Props) {
|
||||
const [videoFailed, setVideoFailed] = useState(false);
|
||||
|
||||
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;
|
||||
|
||||
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}`} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DiscoverMinnect } from "@/lib/discover";
|
||||
import { MinnectMedia } from "./MinnectMedia";
|
||||
import { initials } from "@/lib/feed-model";
|
||||
|
||||
export function TrendingMinnectsRail({ minnects }: { minnects: DiscoverMinnect[] }) {
|
||||
@@ -20,17 +21,11 @@ export function TrendingMinnectsRail({ minnects }: { minnects: DiscoverMinnect[]
|
||||
)}
|
||||
<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}
|
||||
<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}
|
||||
{m.audio.url ? (
|
||||
<audio className="minnect-audio" controls preload="none" src={m.audio.url} aria-label={`Play answer from ${who}`} />
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
+26
-10
@@ -17,16 +17,22 @@ export interface DiscoverExpert {
|
||||
location: { city: string | null; country: string | null };
|
||||
}
|
||||
|
||||
export type DiscoverMediaKind = "video" | "audio";
|
||||
|
||||
export interface DiscoverMedia {
|
||||
kind: DiscoverMediaKind;
|
||||
url: string;
|
||||
duration: number | null;
|
||||
waveform: number[] | string | null;
|
||||
}
|
||||
|
||||
export interface DiscoverMinnect {
|
||||
id: string | number;
|
||||
question: string | null;
|
||||
views: number | null;
|
||||
thumbnailUrl: string | null;
|
||||
audio: {
|
||||
url: string | null;
|
||||
duration: number | null;
|
||||
waveform: number[] | string | null;
|
||||
};
|
||||
/** null when the answer has no playable url. */
|
||||
media: DiscoverMedia | null;
|
||||
expert: {
|
||||
slug: string | null;
|
||||
displayName: string;
|
||||
@@ -92,6 +98,20 @@ function waveform(v: unknown): number[] | string | null {
|
||||
return str(v);
|
||||
}
|
||||
|
||||
const VIDEO_TYPES = new Set(["MOV", "MP4", "M4V", "QUICKTIME", "MO4", "WEBM"]);
|
||||
|
||||
function mapMedia(answer: Rec): DiscoverMedia | null {
|
||||
const url = str(answer.url);
|
||||
if (!url) return null;
|
||||
const type = (str(answer.file_type) ?? "").trim().toUpperCase();
|
||||
return {
|
||||
kind: VIDEO_TYPES.has(type) ? "video" : "audio",
|
||||
url,
|
||||
duration: num(answer.duration),
|
||||
waveform: waveform(answer.waveform_data),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapMinnect(raw: unknown): DiscoverMinnect | null {
|
||||
const r = rec(raw);
|
||||
const mid = id(r.id);
|
||||
@@ -103,11 +123,7 @@ export function mapMinnect(raw: unknown): DiscoverMinnect | null {
|
||||
question: str(r.question_text),
|
||||
views: num(r.views),
|
||||
thumbnailUrl: str(rec(r.thumbnail).url),
|
||||
audio: {
|
||||
url: str(answer.url),
|
||||
duration: num(answer.duration),
|
||||
waveform: waveform(answer.waveform_data),
|
||||
},
|
||||
media: mapMedia(answer),
|
||||
expert: {
|
||||
slug: str(expert.slug),
|
||||
displayName: displayName(expert),
|
||||
|
||||
Reference in New Issue
Block a user