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.
57 lines
3.6 KiB
HTML
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}}
|