Files
Pete/internal/web/templates/_card.html
prosolis 616055a704 Add per-story views, trending, and reader reading-experience upgrades
Surface read counts and sharpen the reader:
- story_views table + RecordStoryView on /api/article (background,
  filter-guarded); "Popular this week" home rail via TrendingStories;
  read-count badge and reading-time chip decorated onto every listing
- reader: signed-in-only read-aloud (TTS), native share/copy, and an
  Aa typography popover (size/serif/sepia) persisted per device
- real alt text on card/reader/related/search images; time-of-day Pete
  greeting on the home hero
- harden exec() to skip (not panic) on a nil DB so background writes
  can't crash on a closed handle

Tests: story_views_test.go, trending_test.go. Suite green, CSS rebuilt.
2026-07-07 22:17:23 -07:00

57 lines
3.6 KiB
HTML

{{define "card"}}{{$ch := channel .Story.Channel}}
<a href="{{.Story.ArticleURL}}" target="_blank" rel="noopener noreferrer"
data-story-card data-source="{{.Story.Source}}" data-channel="{{.Story.Channel}}"
data-id="{{.Story.ID}}"
data-headline="{{.Story.Headline}}"
data-lede="{{.Story.Lede}}"
data-url="{{.Story.ArticleURL}}"
data-image="{{if .Story.ImageURL}}{{thumb .Story.ImageURL}}{{end}}"
data-time="{{timeAgo .Story.SeenAt}}"
data-ch-title="{{$ch.Title}}" data-ch-emoji="{{$ch.Emoji}}" data-ch-theme="{{$ch.Theme}}"
data-posted="{{if .Story.Posted}}1{{end}}"
data-paywalled="{{if .Story.Paywalled}}1{{end}}"
class="group relative block rounded-3xl bg-[color:var(--card)] border-2 {{if .Story.Posted}}border-theme-{{.Theme}} glow-theme-{{.Theme}}{{else}}border-[color:var(--ink)]/10{{end}} shadow-pete overflow-hidden hover:-translate-y-1 hover:shadow-pete-lg transition">
<span role="button" tabindex="0" data-bookmark-btn data-story-id="{{.Story.ID}}"
aria-label="Bookmark this story" aria-pressed="false"
style="display:none;position:absolute;top:.6rem;left:.6rem;z-index:6;height:1.9rem;width:1.9rem;align-items:center;justify-content:center;border-radius:9999px;background:rgba(20,14,6,.62);color:#fff;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M6 2h12a1 1 0 0 1 1 1v18l-7-4-7 4V3a1 1 0 0 1 1-1z"></path>
</svg>
</span>
{{if .Story.ImageURL}}
<div class="aspect-[16/10] w-full overflow-hidden bg-[color:var(--ink)]/5">
<img src="{{thumb .Story.ImageURL}}" alt="{{.Story.Headline}}" loading="lazy" decoding="async"
class="h-full w-full object-cover group-hover:scale-105 transition duration-500">
</div>
{{end}}
<div class="p-4 space-y-2">
<div class="flex items-center gap-2 text-xs flex-wrap">
{{if .ShowChannel}}{{$ch := channel .Story.Channel}}
<span class="inline-flex items-center rounded-full bg-theme-{{$ch.Theme}} text-white px-2.5 py-0.5 font-semibold uppercase tracking-wider">
<span aria-hidden="true" class="mr-1">{{$ch.Emoji}}</span>{{$ch.Title}}
</span>
<span class="text-[color:var(--ink)]/60 font-semibold">{{.Story.Source}}</span>
{{else}}
<span class="inline-flex items-center rounded-full bg-theme-{{.Theme}} text-white px-2.5 py-0.5 font-semibold uppercase tracking-wider">
{{.Story.Source}}
</span>
{{end}}
<span class="text-[color:var(--ink)]/50">{{timeAgo .Story.SeenAt}}</span>
{{if .Story.ReadMins}}<span class="text-[color:var(--ink)]/45">· {{.Story.ReadMins}} min read</span>{{end}}
{{if .Story.Views}}<span class="ml-auto inline-flex items-center gap-1 text-[color:var(--ink)]/45 tabular-nums" title="{{.Story.Views}} reads">
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z"></path><circle cx="12" cy="12" r="3"></circle></svg>
{{.Story.Views}}</span>{{end}}
</div>
<h3 class="font-display text-lg font-semibold leading-snug group-hover:underline underline-offset-2 decoration-theme-{{.Theme}}">
{{.Story.Headline}}
</h3>
{{if .Story.Lede}}
<p class="text-sm text-[color:var(--ink)]/70 line-clamp-3">{{.Story.Lede}}</p>
{{end}}
</div>
{{if .Story.Paywalled}}
<div class="paywall-stamp" aria-label="Paywalled article"></div>
{{end}}
</a>
{{end}}