feat(managers): About column with hover overlay on both channel tables (E4 S2)
- Backend: add channel description to the shared _channel_summary projection, so both the subscribed list and the discovery list carry it (no migration; description is already stored from the channels.list call) - New AboutCell: one truncated line in the table + full text in a portalled, always-on glass hover/focus overlay (wider than Tooltip, flips above the anchor near the bottom, closes on scroll/resize) - About column (after Channel) on both the subscribed and discovery tables; hidden in the mobile card fallback
This commit is contained in:
@@ -214,6 +214,7 @@ def _channel_summary(ch: Channel) -> dict:
|
||||
"thumbnail_url": ch.thumbnail_url,
|
||||
"subscriber_count": ch.subscriber_count,
|
||||
"video_count": ch.video_count,
|
||||
"description": ch.description,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
const MARGIN = 8;
|
||||
const POP_W = 448; // the overlay's fixed width (px)
|
||||
|
||||
/**
|
||||
* A channel-description cell: one truncated line in the table, with the full text revealed in a
|
||||
* glass hover/focus overlay. The overlay is portalled + fixed-positioned so a table's overflow or
|
||||
* stacking context can't clip it. Unlike Tooltip this is always on (not gated by the hints toggle)
|
||||
* and much wider, since a channel "About" is a paragraph, not a one-liner. Shared by the
|
||||
* subscribed and discovery channel tables.
|
||||
*/
|
||||
export default function AboutCell({ text }: { text: string | null }) {
|
||||
const desc = (text ?? "").trim();
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const popRef = useRef<HTMLDivElement>(null);
|
||||
const [coords, setCoords] = useState<{ left: number; top: number } | null>(null);
|
||||
|
||||
function show() {
|
||||
const el = ref.current;
|
||||
if (!el || !desc) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
setCoords({
|
||||
left: Math.min(Math.max(r.left, MARGIN), window.innerWidth - POP_W - MARGIN),
|
||||
top: r.bottom + 6,
|
||||
});
|
||||
}
|
||||
function hide() {
|
||||
setCoords(null);
|
||||
}
|
||||
|
||||
// While open, any scroll or resize would strand the fixed overlay at a stale anchor — close it.
|
||||
useEffect(() => {
|
||||
if (!coords) return;
|
||||
const close = () => setCoords(null);
|
||||
window.addEventListener("scroll", close, true);
|
||||
window.addEventListener("resize", close);
|
||||
return () => {
|
||||
window.removeEventListener("scroll", close, true);
|
||||
window.removeEventListener("resize", close);
|
||||
};
|
||||
}, [coords]);
|
||||
|
||||
// Flip above the anchor if the overlay would run off the bottom of the viewport.
|
||||
useLayoutEffect(() => {
|
||||
const pop = popRef.current;
|
||||
const el = ref.current;
|
||||
if (!coords || !pop || !el) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
const h = pop.offsetHeight;
|
||||
if (r.bottom + 6 + h > window.innerHeight - MARGIN && r.top - 6 - h > MARGIN) {
|
||||
const top = r.top - 6 - h;
|
||||
if (Math.abs(top - coords.top) > 0.5) setCoords((c) => (c ? { ...c, top } : c));
|
||||
}
|
||||
}, [coords]);
|
||||
|
||||
if (!desc) return <span className="text-muted">—</span>;
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
onMouseEnter={show}
|
||||
onMouseLeave={hide}
|
||||
onFocus={show}
|
||||
onBlur={hide}
|
||||
tabIndex={0}
|
||||
className="block max-w-[22rem] truncate text-muted cursor-help outline-none"
|
||||
>
|
||||
{desc}
|
||||
{coords &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={popRef}
|
||||
role="tooltip"
|
||||
style={{ position: "fixed", left: coords.left, top: coords.top, width: POP_W }}
|
||||
className="glass pointer-events-none z-tooltip max-h-[24rem] overflow-hidden px-3 py-2 rounded-lg text-xs leading-relaxed text-fg whitespace-pre-wrap break-words animate-[fadeIn_0.12s_ease]"
|
||||
>
|
||||
{desc}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { notify } from "../lib/notifications";
|
||||
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
||||
import Tooltip from "./Tooltip";
|
||||
import ChannelLink from "./ChannelLink";
|
||||
import AboutCell from "./AboutCell";
|
||||
import DataTable, { type Column } from "./DataTable";
|
||||
import { PageToolbar } from "./PageShell";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
@@ -94,6 +95,12 @@ export default function ChannelDiscovery({
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "about",
|
||||
header: t("channels.cols.about"),
|
||||
hideInCard: true,
|
||||
render: (c) => <AboutCell text={c.description} />,
|
||||
},
|
||||
subsColumn<DiscoveredChannel>(t),
|
||||
{
|
||||
key: "videos",
|
||||
|
||||
@@ -27,6 +27,7 @@ import Tooltip from "./Tooltip";
|
||||
import DataTable, { type Column } from "./DataTable";
|
||||
import { PageToolbar } from "./PageShell";
|
||||
import ChannelLink from "./ChannelLink";
|
||||
import AboutCell from "./AboutCell";
|
||||
import ChannelDiscovery from "./ChannelDiscovery";
|
||||
import TagManager from "./TagManager";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
@@ -301,6 +302,12 @@ export default function Channels() {
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "about",
|
||||
header: t("channels.cols.about"),
|
||||
hideInCard: true,
|
||||
render: (c) => <AboutCell text={c.description} />,
|
||||
},
|
||||
{
|
||||
key: "stored",
|
||||
header: t("channels.cols.stored"),
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"cols": {
|
||||
"priority": "Prio",
|
||||
"channel": "Channel",
|
||||
"about": "About",
|
||||
"stored": "Stored",
|
||||
"subs": "Subscribers",
|
||||
"lastUpload": "Last upload",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"cols": {
|
||||
"priority": "Prio",
|
||||
"channel": "Csatorna",
|
||||
"about": "Leírás",
|
||||
"stored": "Tárolt",
|
||||
"subs": "Feliratkozók",
|
||||
"lastUpload": "Utolsó feltöltés",
|
||||
|
||||
@@ -510,6 +510,7 @@ export interface ManagedChannel {
|
||||
thumbnail_url: string | null;
|
||||
subscriber_count: number | null;
|
||||
video_count: number | null;
|
||||
description: string | null;
|
||||
stored_videos: number;
|
||||
last_video_at: string | null;
|
||||
total_duration_seconds: number;
|
||||
@@ -535,6 +536,7 @@ export interface DiscoveredChannel {
|
||||
thumbnail_url: string | null;
|
||||
subscriber_count: number | null;
|
||||
video_count: number | null;
|
||||
description: string | null;
|
||||
playlist_video_count: number; // how many of the user's playlist videos are from here
|
||||
playlist_count: number; // across how many of their playlists
|
||||
details_synced: boolean;
|
||||
|
||||
Reference in New Issue
Block a user