feat: add impact-colored tags to post modals

- Update renderTagPills to accept topicImpacts and apply color classes
- Tags colored by impact: negative=red, neutral=blue, positive=green
- Add CSS for tag color classes (sw-tag-negative, sw-tag-neutral, sw-tag-positive)
- Tags display below title in post modals

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-01-22 14:45:25 +00:00
parent 91e024e156
commit 9e6f3f8029
4 changed files with 61 additions and 25 deletions

View File

@ -524,7 +524,7 @@ function getCameraStreamUrl(postData) {
return embedUrl; return embedUrl;
} }
function renderTagPills(tags) { function renderTagPills(tags, topicImpacts = {}) {
if (!Array.isArray(tags) || tags.length === 0) return ""; if (!Array.isArray(tags) || tags.length === 0) return "";
const uniq = []; const uniq = [];
const seen = new Set(); const seen = new Set();
@ -536,16 +536,25 @@ function renderTagPills(tags) {
if (uniq.length >= 6) break; if (uniq.length >= 6) break;
} }
if (uniq.length === 0) return ""; if (uniq.length === 0) return "";
// Get impact color class: negative=red, neutral=blue, positive=green
const getImpactClass = (tag) => {
const impact = topicImpacts?.[tag] ?? topicImpacts?.[tag.toLowerCase()] ?? 0;
if (impact < 0) return "sw-tag-negative";
if (impact > 0) return "sw-tag-positive";
return "sw-tag-neutral";
};
return ` return `
<div class="sw-modal-tags"> <div class="sw-modal-tags">
${uniq.map((t) => `<span class="sw-modal-tag">#${escapeHtml(t)}</span>`).join("")} ${uniq.map((t) => `<span class="sw-modal-tag ${getImpactClass(t)}">#${escapeHtml(t)}</span>`).join("")}
</div> </div>
`; `;
} }
function setModalTags(modalWrap, post) { function setModalTags(modalWrap, post) {
if (!modalWrap) return; if (!modalWrap) return;
const tagsHtml = renderTagPills(post?.event_tags || post?.tags); const tagsHtml = renderTagPills(post?.event_tags || post?.tags, post?.topic_impacts);
const existing = modalWrap.querySelector(".sw-modal-tags"); const existing = modalWrap.querySelector(".sw-modal-tags");
if (tagsHtml) { if (tagsHtml) {
if (existing) { if (existing) {
@ -1281,7 +1290,7 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
const isCluster = isClusterMainPost(postData); const isCluster = isClusterMainPost(postData);
const clusterItems = Array.isArray(postData?.cluster_items) ? postData.cluster_items : []; const clusterItems = Array.isArray(postData?.cluster_items) ? postData.cluster_items : [];
const clusterCount = Array.isArray(postData?.cluster_members) ? postData.cluster_members.length : 0; const clusterCount = Array.isArray(postData?.cluster_members) ? postData.cluster_members.length : 0;
const tagPills = renderTagPills(postData?.event_tags || postData?.tags); const tagPills = renderTagPills(postData?.event_tags || postData?.tags, postData?.topic_impacts);
const heatmapPill = HEATMAP_ENABLED const heatmapPill = HEATMAP_ENABLED
? `<div class="sw-stat-pill sw-heatmap-pill" data-action="cluster-heatmap">🔥 Heat map</div>` ? `<div class="sw-stat-pill sw-heatmap-pill" data-action="cluster-heatmap">🔥 Heat map</div>`
: ""; : "";
@ -1336,7 +1345,9 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
</div> </div>
<div class="sw-cluster-title">${escapeHtml(postData?.cluster_title || postData?.title || "Untitled")}</div> <div class="sw-cluster-title">${escapeHtml(postData?.cluster_title || postData?.title || "Untitled")}</div>
${tagPills} <div class="sw-modal-title-meta" style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
${tagPills}
</div>
<div class="sw-cluster-body"> <div class="sw-cluster-body">
<div class="sw-cluster-left"> <div class="sw-cluster-left">
@ -1575,7 +1586,9 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
</div> </div>
</div> </div>
<div class="sw-modal-title">${escapeHtml(data?.headline || postData?.title || "Untitled")}</div> <div class="sw-modal-title">${escapeHtml(data?.headline || postData?.title || "Untitled")}</div>
${tagPills} <div class="sw-modal-title-meta" style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:12px;">
${tagPills}
</div>
<div class="sw-modal-summary"> <div class="sw-modal-summary">
${escapeHtml(data?.summary || postData?.snippet || "")} ${escapeHtml(data?.summary || postData?.snippet || "")}

View File

@ -90,7 +90,7 @@ export function getCameraStreamUrl(postData) {
return embedUrl; return embedUrl;
} }
export function renderTagPills(tags) { export function renderTagPills(tags, topicImpacts = {}) {
if (!Array.isArray(tags) || tags.length === 0) return ""; if (!Array.isArray(tags) || tags.length === 0) return "";
const uniq = []; const uniq = [];
const seen = new Set(); const seen = new Set();
@ -102,16 +102,25 @@ export function renderTagPills(tags) {
if (uniq.length >= 6) break; if (uniq.length >= 6) break;
} }
if (uniq.length === 0) return ""; if (uniq.length === 0) return "";
// Get impact color class: negative=red, neutral=blue, positive=green
const getImpactClass = (tag) => {
const impact = topicImpacts?.[tag] ?? topicImpacts?.[tag.toLowerCase()] ?? 0;
if (impact < 0) return "sw-tag-negative";
if (impact > 0) return "sw-tag-positive";
return "sw-tag-neutral";
};
return ` return `
<div class="sw-modal-tags"> <div class="sw-modal-tags">
${uniq.map((t) => `<span class="sw-modal-tag">#${escapeHtml(t)}</span>`).join("")} ${uniq.map((t) => `<span class="sw-modal-tag ${getImpactClass(t)}">#${escapeHtml(t)}</span>`).join("")}
</div> </div>
`; `;
} }
export function setModalTags(modalWrap, post) { export function setModalTags(modalWrap, post) {
if (!modalWrap) return; if (!modalWrap) return;
const tagsHtml = renderTagPills(post?.event_tags || post?.tags); const tagsHtml = renderTagPills(post?.event_tags || post?.tags, post?.topic_impacts);
const existing = modalWrap.querySelector(".sw-modal-tags"); const existing = modalWrap.querySelector(".sw-modal-tags");
if (tagsHtml) { if (tagsHtml) {
if (existing) { if (existing) {

View File

@ -11,23 +11,23 @@
export const TEMPLATE_SPECS = { export const TEMPLATE_SPECS = {
polaroid: { polaroid: {
mini_spec: { mini_spec: {
size: { w: 100, h: 130 }, size: { w: 80, h: 104 },
background: { type: "solid", value: "#ffffff" }, background: { type: "solid", value: "#ffffff" },
radius: 4, radius: 3,
layers: [ layers: [
{ id: "image", type: "image", x: 5, y: 5, w: 90, h: 90, bind: "data.image", radius: 2 }, { id: "image", type: "image", x: 4, y: 4, w: 72, h: 72, bind: "data.image", radius: 2 },
{ id: "title", type: "text", x: 5, y: 98, w: 90, h: 18, bind: "data.headline", style: "text.polaroidTitle", maxLines: 2 }, { id: "title", type: "text", x: 4, y: 78, w: 72, h: 14, bind: "data.headline", style: "text.polaroidTitle", maxLines: 2 },
{ id: "author", type: "text", x: 5, y: 116, w: 90, h: 10, bind: "data.source", style: "text.polaroidCaption", maxLines: 1 }, { id: "author", type: "text", x: 4, y: 92, w: 72, h: 8, bind: "data.source", style: "text.polaroidCaption", maxLines: 1 },
], ],
}, },
full_spec: { full_spec: {
size: { w: 190, h: 230 }, size: { w: 150, h: 180 },
background: { type: "solid", value: "#ffffff" }, background: { type: "solid", value: "#ffffff" },
radius: 8, radius: 6,
layers: [ layers: [
{ id: "image", type: "image", x: 10, y: 10, w: 170, h: 170, bind: "data.image", radius: 4 }, { id: "image", type: "image", x: 8, y: 8, w: 134, h: 134, bind: "data.image", radius: 3 },
{ id: "author", type: "text", x: 10, y: 185, w: 170, h: 18, bind: "data.source", style: "text.polaroidCaption", maxLines: 1 }, { id: "author", type: "text", x: 8, y: 146, w: 134, h: 14, bind: "data.source", style: "text.polaroidCaption", maxLines: 1 },
{ id: "caption", type: "text", x: 10, y: 205, w: 170, h: 22, bind: "data.headline", style: "text.polaroidText", maxLines: 2 }, { id: "caption", type: "text", x: 8, y: 162, w: 134, h: 16, bind: "data.headline", style: "text.polaroidText", maxLines: 2 },
], ],
}, },
}, },
@ -309,15 +309,13 @@ export function getTemplateKeyForPost(post) {
return 'camera'; return 'camera';
} }
// If content_type is empty/unknown but post has images, show as polaroid // Detect videos/cameras by URL if content_type not set
// This handles posts where content_type wasn't set but images exist
const hasImage = !!(post?.image_small || post?.image_medium || post?.image_large || post?.image || post?.thumbnail_url || post?.media_url);
const hasVideo = !!(post?.video_url || post?.embed_url); const hasVideo = !!(post?.video_url || post?.embed_url);
if (!ct && hasImage && !hasVideo) { if (!ct && hasVideo) {
return 'polaroid'; return 'video';
} }
// Default: category-based template // Default: category-based template (news, breaking, etc.)
return normCat(post); return normCat(post);
} }

View File

@ -1297,6 +1297,22 @@ body[data-theme="light"] .sw-modal-x{
color: var(--sw-modal-text, #e5e7eb); color: var(--sw-modal-text, #e5e7eb);
letter-spacing: 0.2px; letter-spacing: 0.2px;
} }
/* Tag impact colors */
.sw-modal-tag.sw-tag-negative{
background: rgba(239, 68, 68, 0.25);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.4);
}
.sw-modal-tag.sw-tag-neutral{
background: rgba(59, 130, 246, 0.25);
color: #93c5fd;
border: 1px solid rgba(59, 130, 246, 0.4);
}
.sw-modal-tag.sw-tag-positive{
background: rgba(34, 197, 94, 0.25);
color: #86efac;
border: 1px solid rgba(34, 197, 94, 0.4);
}
.sw-cluster-body{ .sw-cluster-body{
display: grid; display: grid;
grid-template-columns: minmax(220px, 44%) minmax(240px, 56%); grid-template-columns: minmax(220px, 44%) minmax(240px, 56%);