Monorepo for Tangled
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

appview/db: fix up nits with newsletter handlers

Signed-off-by: oppiliappan <me@oppi.li>

+35 -81
+9 -15
appview/db/db.go
··· 652 652 hide_others integer default 0 653 653 ); 654 654 655 + create table if not exists newsletter_preferences ( 656 + id integer primary key autoincrement, 657 + user_did text not null unique, 658 + status text not null check (status in ('subscribed', 'dismissed')), 659 + email text, 660 + updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 661 + ); 662 + 655 663 -- indexes for better performance 656 664 create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc); 657 665 create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read); ··· 660 668 create index if not exists idx_webhooks_repo_at on webhooks(repo_at); 661 669 create index if not exists idx_webhook_deliveries_webhook_id on webhook_deliveries(webhook_id); 662 670 create index if not exists idx_site_deploys_repo_at on site_deploys(repo_at); 671 + create index if not exists idx_newsletter_prefs_user_did on newsletter_preferences(user_did); 663 672 `) 664 673 if err != nil { 665 674 return nil, err ··· 1406 1415 alter table pulls drop column stack_id; 1407 1416 `) 1408 1417 1409 - return err 1410 - }) 1411 - 1412 - orm.RunMigration(conn, logger, "add-newsletter-preferences", func(tx *sql.Tx) error { 1413 - _, err := tx.Exec(` 1414 - create table if not exists newsletter_preferences ( 1415 - id integer primary key autoincrement, 1416 - user_did text not null unique, 1417 - status text not null check (status in ('subscribed', 'dismissed')), 1418 - email text, 1419 - updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 1420 - ); 1421 - create index if not exists idx_newsletter_prefs_user_did 1422 - on newsletter_preferences(user_did); 1423 - `) 1424 1418 return err 1425 1419 }) 1426 1420
+1 -4
appview/db/newsletter.go
··· 47 47 if email.Valid { 48 48 pref.Email = email.String 49 49 } 50 - // Best-effort: the column's default format is ISO-8601, but older rows 51 - // (or manual inserts) might use other layouts. A parse failure is not 52 - // fatal — the zero time is acceptable for a UI gating check. 53 - if t, perr := time.Parse("2006-01-02T15:04:05Z", updatedAt); perr == nil { 50 + if t, perr := time.Parse(time.RFC3339, updatedAt); perr == nil { 54 51 pref.UpdatedAt = t 55 52 } 56 53
+1 -1
appview/db/star.go
··· 304 304 select rsc.subject_at 305 305 from repo_star_counts rsc 306 306 order by rsc.stars_gained_last_week desc 307 - limit 8 307 + limit 5 308 308 ` 309 309 310 310 rows, err := e.Query(query)
+3 -13
appview/pages/templates/timeline/fragments/newsletterForm.html
··· 1 - {{/* 2 - Shared newsletter signup form. Variants style the form differently, but the 3 - POST target, response-target id and request plumbing stay identical. 4 - 5 - Params (dict): 6 - Id string - unique id suffix (e.g. "widget", "home"); used to build 7 - the response span's id so multiple signup forms can 8 - coexist on one page without colliding. 9 - Variant string - "card" (compact sidebar card) | "hero" (large CTA) 10 - */}} 11 1 {{ define "timeline/fragments/newsletterForm" }} 12 2 {{ $id := .Id }} 13 3 {{ $variant := .Variant }} ··· 38 28 hx-target="#newsletter-msg-{{ $id }}" 39 29 hx-swap="outerHTML" 40 30 hx-vals='{"target":"{{ $id }}"}'> 41 - <span id="newsletter-msg-{{ $id }}" class="flex items-center gap-2"> 31 + <span id="newsletter-msg-{{ $id }}" class="flex items-stretch gap-2"> 42 32 <input 43 33 type="email" 44 34 name="email" 45 35 placeholder="your@email.com" 46 36 required 47 - class="flex-1 min-w-0 text-sm" /> 48 - <button type="submit" class="btn whitespace-nowrap h-12 px-4 pb-0 gap-2"> 37 + class="flex-1 min-w-0 text-sm py-2" /> 38 + <button type="submit" class="btn whitespace-nowrap px-4 pb-0 gap-2"> 49 39 {{ i "mail-plus" "size-4 inline" }} 50 40 subscribe 51 41 </button>
+3 -3
appview/pages/templates/timeline/fragments/trendingSidebar.html
··· 1 1 {{ define "timeline/fragments/trendingSidebar" }} 2 - <div class="py-4"> 2 + <div class="pt-4"> 3 3 <h3 class="text-xl font-bold dark:text-white flex items-center gap-2 pb-4"> 4 4 Trending 5 5 {{ i "trending-up" "size-4 flex-shrink-0" }} 6 6 </h3> 7 7 {{ if .Repos }} 8 - <div class="flex flex-col gap-4"> 8 + <div class="flex gap-4 overflow-x-auto scrollbar-hide items-stretch lg:flex-col lg:overflow-x-visible"> 9 9 {{ range $index, $repo := .Repos }} 10 - <div class="border border-gray-200 dark:border-gray-700 rounded-sm"> 10 + <div class="flex-none w-96 border border-gray-200 dark:border-gray-700 rounded-sm lg:flex-none lg:w-auto"> 11 11 {{ template "user/fragments/repoCard" (list $ $repo true) }} 12 12 </div> 13 13 {{ end }}
+9 -43
appview/pages/templates/timeline/timeline.html
··· 9 9 10 10 {{ define "mainLayout" }} 11 11 <div class="flex-grow"> 12 - <div class="max-w-screen-xl mx-auto flex flex-col gap-4"> 12 + <div class="max-w-screen-lg mx-auto flex flex-col gap-4"> 13 13 <main>{{ block "content" . }}{{ end }}</main> 14 14 </div> 15 15 </div> 16 16 {{ end }} 17 17 18 18 {{ define "content" }} 19 - {{/* 20 - Single responsive grid. Every fragment is rendered once, with CSS alone 21 - handling the mobile → desktop reflow. Only trending has two variants 22 - (horizontal scroll for mobile, vertical sidebar for desktop); both are 23 - present in the DOM and toggled via utility classes. 24 - 25 - Desktop grid (lg+): 26 - row 1: [ gfi-banner (col-span-2) ] [ newsletter (col 3) ] 27 - row 2: [ timeline (col-span-2) ] [ trendingSidebar (col 3) ] 28 - 29 - When the newsletter is hidden (server-side via .ShowNewsletter, or 30 - client-side via the dismiss button + localStorage), the gfi-banner 31 - widens to col-span-3. 32 - */}} 33 - <div id="timeline-grid" 34 - class="flex flex-col gap-4 lg:grid lg:grid-cols-3 lg:gap-x-2 lg:gap-y-6"> 35 - 36 - {{ if .ShowNewsletter }} 37 - <div id="newsletter-col" 38 - class="order-1 lg:order-none lg:col-start-3 lg:row-start-1 lg:pl-8"> 39 - {{ template "timeline/fragments/newsletterWidget" . }} 40 - </div> 41 - {{ end }} 42 - 43 - <div id="gfi-banner" 44 - class="order-2 lg:order-none lg:row-start-1 {{ if .ShowNewsletter }}lg:col-span-2{{ else }}lg:col-span-3 lg:max-w-4xl lg:mx-auto{{ end }}"> 45 - {{ template "timeline/fragments/goodfirstissues" . }} 46 - </div> 19 + <div id="timeline-grid" class="flex flex-col gap-4 lg:grid lg:grid-cols-3"> 47 20 48 - <div class="order-4 lg:order-none lg:col-span-2 lg:row-start-2"> 21 + <div class="order-2 lg:order-none lg:col-span-2 lg:row-start-1"> 49 22 {{ template "timeline/fragments/timeline" . }} 50 23 </div> 51 24 52 - <div class="order-3 lg:hidden"> 53 - {{ template "timeline/fragments/trending" . }} 54 - </div> 55 - 56 - <div class="hidden lg:flex lg:flex-col gap-6 lg:pl-8 lg:col-start-3 lg:row-start-2"> 25 + <div class="order-1 lg:order-none flex flex-col gap-6 lg:col-start-3 lg:row-start-1"> 26 + {{ if .ShowNewsletter }} 27 + <div id="newsletter-col" class="order-first lg:order-last"> 28 + {{ template "timeline/fragments/newsletterWidget" . }} 29 + </div> 30 + {{ end }} 57 31 {{ template "timeline/fragments/trendingSidebar" . }} 58 32 </div> 59 33 </div> ··· 63 37 (function() { 64 38 var DISMISS_KEY = 'newsletter-dismissed'; 65 39 var newsletterCol = document.getElementById('newsletter-col'); 66 - var gfi = document.getElementById('gfi-banner'); 67 40 if (!newsletterCol) return; 68 41 69 - function widenGfi() { 70 - if (!gfi) return; 71 - gfi.classList.remove('lg:col-span-2'); 72 - gfi.classList.add('lg:col-span-3', 'lg:max-w-4xl', 'lg:mx-auto'); 73 - } 74 - 75 42 // hide removes the widget from the DOM without persisting anything. 76 43 // Used when this browser's localStorage says we already dismissed in a 77 44 // past session — the server has already told us .ShowNewsletter is true ··· 80 47 // device after this localStorage entry was set. 81 48 function hide() { 82 49 newsletterCol.remove(); 83 - widenGfi(); 84 50 } 85 51 86 52 // dismiss is the user-initiated path. Persists both locally and (for
+7
appview/state/state.go
··· 362 362 s.logger.Error("failed to add newsletter contact", "error", err) 363 363 } 364 364 }() 365 + } else { 366 + s.logger.Error( 367 + "failed to add newsletter contact, missing resend config", 368 + "isKeyPresent", s.config.Resend.ApiKey != "", 369 + "isSegmentIdPresent", s.config.Resend.NewsletterSegmentId != "", 370 + "emailAddr", emailAddr, 371 + ) 365 372 } 366 373 367 374 s.pages.NewsletterResponse(w, pages.NewsletterResponseParams{Id: target})
+2 -2
flake.lock
··· 120 120 "lastModified": 1731402384, 121 121 "narHash": "sha256-OwUmrPfEehLDz0fl2ChYLK8FQM2p0G1+EMrGsYEq+6g=", 122 122 "type": "tarball", 123 - "url": "https://github.com/IBM/plex/releases/download/@ibm/plex-mono@1.1.0/ibm-plex-mono.zip" 123 + "url": "https://github.com/IBM/plex/releases/download/@ibm%2Fplex-mono@1.1.0/ibm-plex-mono.zip" 124 124 }, 125 125 "original": { 126 126 "type": "tarball", 127 - "url": "https://github.com/IBM/plex/releases/download/@ibm/plex-mono@1.1.0/ibm-plex-mono.zip" 127 + "url": "https://github.com/IBM/plex/releases/download/@ibm%2Fplex-mono@1.1.0/ibm-plex-mono.zip" 128 128 } 129 129 }, 130 130 "indigo": {