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;
}
function renderTagPills(tags) {
function renderTagPills(tags, topicImpacts = {}) {
if (!Array.isArray(tags) || tags.length === 0) return "";
const uniq = [];
const seen = new Set();
@ -536,16 +536,25 @@ function renderTagPills(tags) {
if (uniq.length >= 6) break;
}
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 `
<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>
`;
}
function setModalTags(modalWrap, post) {
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");
if (tagsHtml) {
if (existing) {
@ -1281,7 +1290,7 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
const isCluster = isClusterMainPost(postData);
const clusterItems = Array.isArray(postData?.cluster_items) ? postData.cluster_items : [];
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
? `<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 class="sw-cluster-title">${escapeHtml(postData?.cluster_title || postData?.title || "Untitled")}</div>
<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-left">
@ -1575,7 +1586,9 @@ export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
</div>
</div>
<div class="sw-modal-title">${escapeHtml(data?.headline || postData?.title || "Untitled")}</div>
<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">
${escapeHtml(data?.summary || postData?.snippet || "")}

View File

@ -90,7 +90,7 @@ export function getCameraStreamUrl(postData) {
return embedUrl;
}
export function renderTagPills(tags) {
export function renderTagPills(tags, topicImpacts = {}) {
if (!Array.isArray(tags) || tags.length === 0) return "";
const uniq = [];
const seen = new Set();
@ -102,16 +102,25 @@ export function renderTagPills(tags) {
if (uniq.length >= 6) break;
}
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 `
<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>
`;
}
export function setModalTags(modalWrap, post) {
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");
if (tagsHtml) {
if (existing) {

View File

@ -11,23 +11,23 @@
export const TEMPLATE_SPECS = {
polaroid: {
mini_spec: {
size: { w: 100, h: 130 },
size: { w: 80, h: 104 },
background: { type: "solid", value: "#ffffff" },
radius: 4,
radius: 3,
layers: [
{ id: "image", type: "image", x: 5, y: 5, w: 90, h: 90, 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: "author", type: "text", x: 5, y: 116, w: 90, h: 10, bind: "data.source", style: "text.polaroidCaption", maxLines: 1 },
{ id: "image", type: "image", x: 4, y: 4, w: 72, h: 72, bind: "data.image", radius: 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: 4, y: 92, w: 72, h: 8, bind: "data.source", style: "text.polaroidCaption", maxLines: 1 },
],
},
full_spec: {
size: { w: 190, h: 230 },
size: { w: 150, h: 180 },
background: { type: "solid", value: "#ffffff" },
radius: 8,
radius: 6,
layers: [
{ id: "image", type: "image", x: 10, y: 10, w: 170, h: 170, bind: "data.image", radius: 4 },
{ id: "author", type: "text", x: 10, y: 185, w: 170, h: 18, 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: "image", type: "image", x: 8, y: 8, w: 134, h: 134, bind: "data.image", radius: 3 },
{ 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: 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';
}
// If content_type is empty/unknown but post has images, show as polaroid
// 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);
// Detect videos/cameras by URL if content_type not set
const hasVideo = !!(post?.video_url || post?.embed_url);
if (!ct && hasImage && !hasVideo) {
return 'polaroid';
if (!ct && hasVideo) {
return 'video';
}
// Default: category-based template
// Default: category-based template (news, breaking, etc.)
return normCat(post);
}

View File

@ -1297,6 +1297,22 @@ body[data-theme="light"] .sw-modal-x{
color: var(--sw-modal-text, #e5e7eb);
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{
display: grid;
grid-template-columns: minmax(220px, 44%) minmax(240px, 56%);