Merge pull request #1194 from PR0M3TH3AN/feature/video-card-layout-update-14744375313479485048

Update Video Card layout with dedicated engagement section
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-12 16:54:36 -05:00
commit df5a493969
4 files changed
+310 -98

No files matched your search

+7 -1
View File
@@ -175,7 +175,13 @@
<div class="video-modal__meta-info flex flex-wrap items-center gap-3">
<span id="videoTimestamp" class="modal-meta">Posted just now</span>
<span id="videoEditedTimestamp" class="modal-meta hidden">Last edited just now</span>
<span id="videoViewCount" class="modal-meta modal-view-count">– views</span>
<span id="videoViewCount" class="modal-meta modal-view-count flex items-center gap-1.5" title="Views">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<span data-view-count-text>–</span>
</span>
</div>
</div>
+150 -19
View File
@@ -2,6 +2,7 @@ import { normalizeDesignSystemContext } from "../../designSystem.js";
import { formatShortNpub } from "../../utils/formatters.js";
import { sanitizeProfileMediaUrl } from "../../utils/profileMedia.js";
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
const DEFAULT_PROFILE_AVATAR = "assets/svg/default-profile.svg";
export class SimilarContentCard {
@@ -66,6 +67,7 @@ export class SimilarContentCard {
this.authorNpubEl = null;
this.timeEl = null;
this.viewCountEl = null;
this.discussionCountEl = null;
this.build();
}
@@ -82,6 +84,10 @@ export class SimilarContentCard {
return this.viewCountEl;
}
getDiscussionCountElement() {
return this.discussionCountEl;
}
closeMoreMenu() {}
closeSettingsMenu() {}
@@ -469,9 +475,9 @@ export class SimilarContentCard {
titleLink.title = this.video.title || "Untitled";
titleLink.style.minWidth = "0";
// Modified order: Title -> Author -> Meta
const authorStack = this.buildAuthorStack();
const metaRow = this.buildMetaRow();
const engagement = this.buildEngagementSection();
content.appendChild(titleLink);
if (authorStack) {
@@ -480,6 +486,9 @@ export class SimilarContentCard {
if (metaRow) {
content.appendChild(metaRow);
}
if (engagement) {
content.appendChild(engagement);
}
this.contentEl = content;
this.titleEl = titleLink;
@@ -556,24 +565,6 @@ export class SimilarContentCard {
row.classList.add("player-modal__similar-card-meta");
row.style.minWidth = "0";
// Reordered: Views -> Separator -> Time
const viewEl = this.document.createElement("span");
viewEl.classList.add("player-modal__similar-card-views", "view-count-text");
viewEl.dataset.viewCount = "";
viewEl.textContent = "– views";
if (this.pointerInfo?.key) {
viewEl.dataset.viewPointer = this.pointerInfo.key;
}
row.appendChild(viewEl);
this.viewCountEl = viewEl;
const separator = this.document.createElement("span");
separator.classList.add("player-modal__similar-card-separator");
separator.setAttribute("aria-hidden", "true");
separator.textContent = "•";
row.appendChild(separator);
const timeEl = this.document.createElement("time");
timeEl.classList.add("player-modal__similar-card-timestamp");
if (this.postedAt !== null) {
@@ -592,6 +583,146 @@ export class SimilarContentCard {
return row;
}
createEyeIcon(classNames = []) {
const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("fill", "none");
svg.setAttribute("aria-hidden", "true");
svg.setAttribute("focusable", "false");
svg.setAttribute("stroke", "currentColor");
svg.setAttribute("stroke-width", "2");
svg.setAttribute("stroke-linecap", "round");
svg.setAttribute("stroke-linejoin", "round");
classNames.forEach((className) => {
if (className) {
svg.classList.add(className);
}
});
const path = this.document.createElementNS(SVG_NAMESPACE, "path");
path.setAttribute("d", "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z");
svg.appendChild(path);
const circle = this.document.createElementNS(SVG_NAMESPACE, "circle");
circle.setAttribute("cx", "12");
circle.setAttribute("cy", "12");
circle.setAttribute("r", "3");
svg.appendChild(circle);
return svg;
}
createMessageIcon(classNames = []) {
const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("fill", "none");
svg.setAttribute("aria-hidden", "true");
svg.setAttribute("focusable", "false");
svg.setAttribute("stroke", "currentColor");
svg.setAttribute("stroke-width", "2");
svg.setAttribute("stroke-linecap", "round");
svg.setAttribute("stroke-linejoin", "round");
classNames.forEach((className) => {
if (className) {
svg.classList.add(className);
}
});
const path = this.document.createElementNS(SVG_NAMESPACE, "path");
path.setAttribute(
"d",
"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"
);
svg.appendChild(path);
return svg;
}
buildEngagementSection() {
const hasPointer = this.pointerInfo && this.pointerInfo.key;
const hasDiscussion =
this.video.enableComments !== false &&
(typeof this.video.discussionCount === "number" ||
(typeof this.video.discussionCount === "string" &&
this.video.discussionCount.trim()));
if (!hasPointer && !hasDiscussion) {
return null;
}
const container = this.document.createElement("div");
container.classList.add(
"flex",
"items-center",
"gap-4",
"text-xs",
"text-muted-strong",
"mt-1.5",
"video-card__engagement" // Reuse this class for consistent styling if global, otherwise it's just a marker
);
// Add specific class for scoping if needed, but Tailwind classes usually suffice.
// Views
if (hasPointer) {
const wrapper = this.document.createElement("div");
wrapper.classList.add("flex", "items-center", "gap-1.5");
wrapper.setAttribute("title", "Views");
const icon = this.createEyeIcon(["w-3.5", "h-3.5"]);
wrapper.appendChild(icon);
const view = this.document.createElement("span");
view.classList.add("view-count-text");
view.textContent = "–";
view.dataset.viewCount = "";
view.dataset.viewPointer = this.pointerInfo.key;
wrapper.appendChild(view);
container.appendChild(wrapper);
this.viewCountEl = view;
}
// Discussion
if (hasDiscussion) {
let initialCount = 0;
if (typeof this.video.discussionCount === "number") {
initialCount = this.video.discussionCount;
} else if (typeof this.video.discussionCount === "string") {
const parsed = Number.parseInt(this.video.discussionCount.trim(), 10);
if (Number.isFinite(parsed)) {
initialCount = parsed;
}
}
if (Number.isFinite(initialCount) && initialCount >= 0) {
const wrapper = this.document.createElement("div");
wrapper.classList.add("flex", "items-center", "gap-1.5");
wrapper.dataset.discussionCount = this.video.id;
wrapper.dataset.countState = "ready";
wrapper.setAttribute("title", "Comments");
const icon = this.createMessageIcon(["w-3.5", "h-3.5"]);
wrapper.appendChild(icon);
const displayValue = initialCount.toLocaleString();
const valueEl = this.document.createElement("span");
valueEl.textContent = displayValue;
valueEl.dataset.discussionCountValue = "";
wrapper.appendChild(valueEl);
container.appendChild(wrapper);
this.discussionCountEl = wrapper;
}
}
return container;
}
applyPointerDatasets() {
if (!this.root || !this.pointerInfo) {
return;
+139 -72
View File
@@ -476,9 +476,9 @@ export class VideoCard {
content.appendChild(badgesContainer);
}
const discussion = this.buildDiscussionCount();
if (discussion) {
content.appendChild(discussion);
const engagement = this.buildEngagementSection();
if (engagement) {
content.appendChild(engagement);
}
if (this.showUnsupportedTorrentBadge) {
@@ -926,30 +926,7 @@ export class VideoCard {
metadata.appendChild(timeEl);
this.timestampEl = timeEl;
if (this.pointerInfo && this.pointerInfo.key) {
const dotClassNames = ["text-muted-strong"];
if (!isCompact) {
dotClassNames.push("mx-1");
}
const dot = this.createElement("span", {
classNames: dotClassNames,
textContent: "•"
});
dot.setAttribute("aria-hidden", "true");
const view = this.createElement("span", {
classNames: ["view-count-text"],
textContent: "– views"
});
view.dataset.viewCount = "";
view.dataset.viewPointer = this.pointerInfo.key;
metadata.appendChild(dot);
metadata.appendChild(view);
this.viewCountEl = view;
}
// (View count moved to engagement section)
if (
this.postedAt !== null &&
@@ -2330,62 +2307,152 @@ export class VideoCard {
}
}
buildDiscussionCount() {
if (this.variant === "compact") {
createEyeIcon(classNames = []) {
const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("fill", "none");
svg.setAttribute("aria-hidden", "true");
svg.setAttribute("focusable", "false");
svg.setAttribute("stroke", "currentColor");
svg.setAttribute("stroke-width", "2");
svg.setAttribute("stroke-linecap", "round");
svg.setAttribute("stroke-linejoin", "round");
classNames.forEach((className) => {
if (className) {
svg.classList.add(className);
}
});
const path = this.document.createElementNS(SVG_NAMESPACE, "path");
path.setAttribute("d", "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z");
svg.appendChild(path);
const circle = this.document.createElementNS(SVG_NAMESPACE, "circle");
circle.setAttribute("cx", "12");
circle.setAttribute("cy", "12");
circle.setAttribute("r", "3");
svg.appendChild(circle);
return svg;
}
createMessageIcon(classNames = []) {
const svg = this.document.createElementNS(SVG_NAMESPACE, "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("fill", "none");
svg.setAttribute("aria-hidden", "true");
svg.setAttribute("focusable", "false");
svg.setAttribute("stroke", "currentColor");
svg.setAttribute("stroke-width", "2");
svg.setAttribute("stroke-linecap", "round");
svg.setAttribute("stroke-linejoin", "round");
classNames.forEach((className) => {
if (className) {
svg.classList.add(className);
}
});
const path = this.document.createElementNS(SVG_NAMESPACE, "path");
path.setAttribute(
"d",
"M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"
);
svg.appendChild(path);
return svg;
}
buildEngagementSection() {
// Check availability
const hasPointer = this.pointerInfo && this.pointerInfo.key;
const hasDiscussion =
this.variant !== "compact" &&
this.video.enableComments !== false &&
(typeof this.video.discussionCount === "number" ||
(typeof this.video.discussionCount === "string" &&
this.video.discussionCount.trim()));
if (!hasPointer && !hasDiscussion) {
return null;
}
if (this.video.enableComments === false) {
return null;
const container = this.createElement("div", {
classNames: [
"flex",
"items-center",
"gap-4",
"text-xs",
"text-muted-strong",
"mt-3",
"video-card__engagement",
],
});
// Views
if (hasPointer) {
const wrapper = this.createElement("div", {
classNames: ["flex", "items-center", "gap-1.5"],
});
wrapper.setAttribute("title", "Views");
const icon = this.createEyeIcon(["w-4", "h-4"]);
wrapper.appendChild(icon);
const view = this.createElement("span", {
classNames: ["view-count-text"],
textContent: "–",
});
view.dataset.viewCount = "";
view.dataset.viewPointer = this.pointerInfo.key;
wrapper.appendChild(view);
container.appendChild(wrapper);
this.viewCountEl = view;
}
let initialCount = null;
if (typeof this.video.discussionCount === "number") {
initialCount = this.video.discussionCount;
} else if (typeof this.video.discussionCount === "string") {
const trimmed = this.video.discussionCount.trim();
if (trimmed) {
const parsed = Number.parseInt(trimmed, 10);
// Discussion
if (hasDiscussion) {
let initialCount = 0;
if (typeof this.video.discussionCount === "number") {
initialCount = this.video.discussionCount;
} else if (typeof this.video.discussionCount === "string") {
const parsed = Number.parseInt(this.video.discussionCount.trim(), 10);
if (Number.isFinite(parsed)) {
initialCount = parsed;
}
}
if (Number.isFinite(initialCount) && initialCount >= 0) {
const wrapper = this.createElement("div", {
classNames: ["flex", "items-center", "gap-1.5"],
});
wrapper.dataset.discussionCount = this.video.id;
wrapper.dataset.countState = "ready";
wrapper.setAttribute("title", "Comments");
const icon = this.createMessageIcon(["w-4", "h-4"]);
wrapper.appendChild(icon);
const safeCount = Math.floor(initialCount);
const displayValue = this.helpers.toLocaleString
? this.helpers.toLocaleString(safeCount)
: safeCount.toLocaleString();
const valueEl = this.createElement("span", {
textContent: displayValue,
});
valueEl.dataset.discussionCountValue = "";
wrapper.appendChild(valueEl);
container.appendChild(wrapper);
this.discussionCountEl = wrapper;
}
}
if (
initialCount === null ||
!Number.isFinite(initialCount) ||
initialCount < 0
) {
return null;
}
const safeCount = Math.floor(initialCount);
const displayValue = this.helpers.toLocaleString
? this.helpers.toLocaleString(safeCount)
: safeCount.toLocaleString();
const container = this.createElement("div", {
classNames: ["flex", "items-center", "text-xs", "text-muted-strong"]
});
container.dataset.discussionCount = this.video.id;
container.dataset.countState = "ready";
const valueEl = this.createElement("span", {
textContent: displayValue
});
valueEl.dataset.discussionCountValue = "";
const labelEl = this.createElement("span", {
classNames: ["ml-1"],
textContent: "notes"
});
container.appendChild(valueEl);
container.appendChild(labelEl);
this.discussionCountEl = container;
return container;
}
+14 -6
View File
@@ -3959,8 +3959,17 @@ export class VideoModal {
}
updateViewCountLabel(text) {
if (this.videoViewCountEl) {
this.videoViewCountEl.textContent = text || "";
if (!this.videoViewCountEl) {
return;
}
const textEl = this.videoViewCountEl.querySelector(
"[data-view-count-text]"
);
if (textEl) {
textEl.textContent = text || "–";
} else {
// Fallback if span is missing
this.videoViewCountEl.textContent = text || "–";
}
}
@@ -5438,15 +5447,14 @@ export class VideoModal {
return this.formatViewCountLabel(Number(total));
}
if (status === "hydrating") {
return "Loading views…";
return "Loading…";
}
return "– views";
return "–";
}
formatViewCountLabel(total) {
const numeric = Number.isFinite(total) ? Math.max(0, Number(total)) : 0;
const label = numeric === 1 ? "view" : "views";
return `${formatViewCount(numeric)} ${label}`;
return formatViewCount(numeric);
}
syncMoreMenuData({ currentVideo, canManageBlacklist }) {