Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee
17
fork

Configure Feed

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

feat: client-side time zone handling for records

authored by

Patrick Dewey and committed by tangled.org 988609e3 3d6ef573

+44 -9
+5
internal/web/bff/helpers.go
··· 184 184 return s 185 185 } 186 186 187 + // FormatISO returns the time formatted as RFC3339 UTC, suitable for HTML datetime attributes. 188 + func FormatISO(t time.Time) string { 189 + return t.UTC().Format(time.RFC3339) 190 + } 191 + 187 192 // FormatTimeAgo returns a human-readable relative time string 188 193 func FormatTimeAgo(t time.Time) string { 189 194 now := time.Now()
+4 -2
internal/web/components/brew_list_table.templ
··· 56 56 <tr class="table-row"> 57 57 <!-- Date --> 58 58 <td class="px-4 py-4 whitespace-nowrap text-sm text-brown-900 font-medium align-top"> 59 - <div>{ brew.CreatedAt.Format("Jan 2") }</div> 60 - <div class="text-xs text-brown-600">{ brew.CreatedAt.Format("2006") }</div> 59 + <time datetime={ bff.FormatISO(brew.CreatedAt) } data-local="date">{ brew.CreatedAt.Format("Jan 2") }</time> 60 + <div class="text-xs text-brown-600"> 61 + <time datetime={ bff.FormatISO(brew.CreatedAt) } data-local="year">{ brew.CreatedAt.Format("2006") }</time> 62 + </div> 61 63 </td> 62 64 <!-- Bean (with all details) --> 63 65 <td class="px-4 py-4 text-sm text-brown-900 align-top">
+24
internal/web/components/layout.templ
··· 177 177 } 178 178 }); 179 179 180 + // Format <time data-local="..."> elements in the user's local timezone 181 + document.addEventListener('DOMContentLoaded', function() { 182 + document.querySelectorAll('time[data-local]').forEach(function(el) { 183 + var dt = new Date(el.getAttribute('datetime')); 184 + if (isNaN(dt.getTime())) return; 185 + var fmt = el.getAttribute('data-local'); 186 + var text; 187 + try { 188 + if (fmt === 'date') { 189 + text = dt.toLocaleDateString(undefined, {month: 'short', day: 'numeric'}); 190 + } else if (fmt === 'year') { 191 + text = String(dt.getFullYear()); 192 + } else if (fmt === 'long') { 193 + text = dt.toLocaleDateString(undefined, {month: 'long', day: 'numeric', year: 'numeric'}) + 194 + ' at ' + dt.toLocaleTimeString(undefined, {hour: 'numeric', minute: '2-digit'}); 195 + } else if (fmt === 'short') { 196 + text = dt.toLocaleDateString(undefined, {month: 'short', day: 'numeric', year: 'numeric'}) + 197 + ' ' + dt.toLocaleTimeString(undefined, {hour: '2-digit', minute: '2-digit'}); 198 + } 199 + if (text) el.textContent = text; 200 + } catch(e) {} 201 + }); 202 + }); 203 + 180 204 // Configure HTMX before it loads 181 205 document.addEventListener('DOMContentLoaded', function() { 182 206 if (typeof htmx !== 'undefined') {
+3 -2
internal/web/pages/admin.templ
··· 3 3 import ( 4 4 "arabica/internal/database/boltstore" 5 5 "arabica/internal/moderation" 6 + "arabica/internal/web/bff" 6 7 "arabica/internal/web/components" 7 8 "fmt" 8 9 ) ··· 397 398 <!-- Header with status badge and time --> 398 399 <div class="flex items-center justify-between"> 399 400 @ReportStatusBadge(report.Report.Status) 400 - <span class="text-sm text-brown-500">{ report.Report.CreatedAt.Format("Jan 2, 2006 15:04") }</span> 401 + <time class="text-sm text-brown-500" datetime={ bff.FormatISO(report.Report.CreatedAt) } data-local="short">{ report.Report.CreatedAt.Format("Jan 2, 2006 15:04") }</time> 401 402 </div> 402 403 <!-- AT-URI with copy button --> 403 404 <div> ··· 692 693 <div class="flex flex-col gap-3"> 693 694 <div class="flex items-center justify-between"> 694 695 <span class="font-medium text-brown-900">{ req.Email }</span> 695 - <span class="text-sm text-brown-500">{ req.CreatedAt.Format("Jan 2, 2006 15:04") }</span> 696 + <time class="text-sm text-brown-500" datetime={ bff.FormatISO(req.CreatedAt) } data-local="short">{ req.CreatedAt.Format("Jan 2, 2006 15:04") }</time> 696 697 </div> 697 698 <div class="flex flex-wrap gap-x-6 gap-y-2 text-sm"> 698 699 <div>
+2 -1
internal/web/pages/bean_view.templ
··· 3 3 import ( 4 4 "arabica/internal/firehose" 5 5 "arabica/internal/models" 6 + "arabica/internal/web/bff" 6 7 "arabica/internal/web/components" 7 8 "fmt" 8 9 "strings" ··· 125 126 { props.Bean.Origin } 126 127 } 127 128 </h2> 128 - <p class="text-sm text-brown-600 mt-1">{ props.Bean.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</p> 129 + <p class="text-sm text-brown-600 mt-1"><time datetime={ bff.FormatISO(props.Bean.CreatedAt) } data-local="long">{ props.Bean.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</time></p> 129 130 </div> 130 131 } 131 132
+1 -1
internal/web/pages/brew_view.templ
··· 102 102 templ BrewViewHeader(props BrewViewProps) { 103 103 <div class="mb-6"> 104 104 <h2 class="text-3xl font-bold text-brown-900">Brew Details</h2> 105 - <p class="text-sm text-brown-600 mt-1">{ props.Brew.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</p> 105 + <p class="text-sm text-brown-600 mt-1"><time datetime={ bff.FormatISO(props.Brew.CreatedAt) } data-local="long">{ props.Brew.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</time></p> 106 106 </div> 107 107 } 108 108
+2 -1
internal/web/pages/brewer_view.templ
··· 3 3 import ( 4 4 "arabica/internal/firehose" 5 5 "arabica/internal/models" 6 + "arabica/internal/web/bff" 6 7 "arabica/internal/web/components" 7 8 ) 8 9 ··· 90 91 templ BrewerViewHeader(props BrewerViewProps) { 91 92 <div class="mb-6"> 92 93 <h2 class="text-3xl font-bold text-brown-900">{ props.Brewer.Name }</h2> 93 - <p class="text-sm text-brown-600 mt-1">{ props.Brewer.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</p> 94 + <p class="text-sm text-brown-600 mt-1"><time datetime={ bff.FormatISO(props.Brewer.CreatedAt) } data-local="long">{ props.Brewer.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</time></p> 94 95 </div> 95 96 }
+2 -1
internal/web/pages/grinder_view.templ
··· 3 3 import ( 4 4 "arabica/internal/firehose" 5 5 "arabica/internal/models" 6 + "arabica/internal/web/bff" 6 7 "arabica/internal/web/components" 7 8 ) 8 9 ··· 93 94 templ GrinderViewHeader(props GrinderViewProps) { 94 95 <div class="mb-6"> 95 96 <h2 class="text-3xl font-bold text-brown-900">{ props.Grinder.Name }</h2> 96 - <p class="text-sm text-brown-600 mt-1">{ props.Grinder.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</p> 97 + <p class="text-sm text-brown-600 mt-1"><time datetime={ bff.FormatISO(props.Grinder.CreatedAt) } data-local="long">{ props.Grinder.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</time></p> 97 98 </div> 98 99 } 99 100
+1 -1
internal/web/pages/roaster_view.templ
··· 99 99 templ RoasterViewHeader(props RoasterViewProps) { 100 100 <div class="mb-6"> 101 101 <h2 class="text-3xl font-bold text-brown-900">{ props.Roaster.Name }</h2> 102 - <p class="text-sm text-brown-600 mt-1">{ props.Roaster.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</p> 102 + <p class="text-sm text-brown-600 mt-1"><time datetime={ bff.FormatISO(props.Roaster.CreatedAt) } data-local="long">{ props.Roaster.CreatedAt.Format("January 2, 2006 at 3:04 PM") }</time></p> 103 103 </div> 104 104 } 105 105