Yōten: A social tracker for your language learning journey built on the atproto.

fix: store UTC dates and attempt to use users timezone when possible

brookjeynes.dev 49c8f3cb 44840518

verified
+44 -30
+1 -1
internal/consumer/ingester.go
··· 448 reactionType := record.ReactionType 449 createdAt, err := time.Parse(time.RFC3339, record.CreatedAt) 450 if err != nil { 451 - createdAt = time.Now() 452 } 453 454 reaction, err := db.ReactionFromString(reactionType)
··· 448 reactionType := record.ReactionType 449 createdAt, err := time.Parse(time.RFC3339, record.CreatedAt) 450 if err != nil { 451 + createdAt = time.Now().UTC() 452 } 453 454 reaction, err := db.ReactionFromString(reactionType)
+1 -1
internal/server/handlers/activity.go
··· 84 } 85 newActivity.Did = user.Did 86 newActivity.Rkey = atproto.TID() 87 - newActivity.CreatedAt = time.Now() 88 89 if err := db.ValidateActivity(newActivity); err != nil { 90 log.Println("invalid activity def:", err)
··· 84 } 85 newActivity.Did = user.Did 86 newActivity.Rkey = atproto.TID() 87 + newActivity.CreatedAt = time.Now().UTC() 88 89 if err := db.ValidateActivity(newActivity); err != nil { 90 log.Println("invalid activity def:", err)
+1 -1
internal/server/handlers/comment.go
··· 82 ParentCommentUri: (*syntax.ATURI)(parentCommentUri), 83 StudySessionUri: syntax.ATURI(studySessionUri), 84 Body: commentBody, 85 - CreatedAt: time.Now(), 86 } 87 88 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
··· 82 ParentCommentUri: (*syntax.ATURI)(parentCommentUri), 83 StudySessionUri: syntax.ATURI(studySessionUri), 84 Body: commentBody, 85 + CreatedAt: time.Now().UTC(), 86 } 87 88 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+1 -1
internal/server/handlers/follow.go
··· 55 56 switch r.Method { 57 case http.MethodPost: 58 - createdAt := time.Now().Format(time.RFC3339) 59 rkey := atproto.TID() 60 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 61 Collection: yoten.GraphFollowNSID,
··· 55 56 switch r.Method { 57 case http.MethodPost: 58 + createdAt := time.Now().UTC().Format(time.RFC3339) 59 rkey := atproto.TID() 60 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 61 Collection: yoten.GraphFollowNSID,
+1 -1
internal/server/handlers/reaction.go
··· 99 return 100 } 101 102 - createdAt := time.Now().Format(time.RFC3339) 103 rkey := atproto.TID() 104 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 105 Collection: yoten.FeedReactionNSID,
··· 99 return 100 } 101 102 + createdAt := time.Now().UTC().Format(time.RFC3339) 103 rkey := atproto.TID() 104 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 105 Collection: yoten.FeedReactionNSID,
+1 -1
internal/server/handlers/resource.go
··· 92 } 93 newResource.Did = user.Did 94 newResource.Rkey = atproto.TID() 95 - newResource.CreatedAt = time.Now() 96 97 if err := db.ValidateResource(newResource); err != nil { 98 log.Println("invalid resource definition:", err)
··· 92 } 93 newResource.Did = user.Did 94 newResource.Rkey = atproto.TID() 95 + newResource.CreatedAt = time.Now().UTC() 96 97 if err := db.ValidateResource(newResource); err != nil { 98 log.Println("invalid resource definition:", err)
+12 -2
internal/server/handlers/study-session.go
··· 446 } 447 newStudySession.Did = user.Did 448 newStudySession.Rkey = atproto.TID() 449 - newStudySession.CreatedAt = time.Now() 450 451 if err := db.ValidateStudySession(newStudySession); err != nil { 452 log.Println("invalid study session:", err) ··· 532 Set("activity_id", newStudySession.Activity.ID). 533 Set("is_custom_activity", len(newStudySession.Activity.Did) > 0). 534 Set("description_provided", len(newStudySession.Description) > 0). 535 - Set("date_is_today", newStudySession.Date.Truncate(24*time.Hour).Equal(time.Now().Truncate(24*time.Hour))), 536 }) 537 if err != nil { 538 log.Println("failed to enqueue posthog event:", err)
··· 446 } 447 newStudySession.Did = user.Did 448 newStudySession.Rkey = atproto.TID() 449 + newStudySession.CreatedAt = time.Now().UTC() 450 + 451 + timezone := r.FormValue("timezone") 452 + if timezone == "" { 453 + timezone = "UTC" 454 + } 455 + 456 + loc, err := time.LoadLocation(timezone) 457 + if err != nil { 458 + loc = time.UTC 459 + } 460 461 if err := db.ValidateStudySession(newStudySession); err != nil { 462 log.Println("invalid study session:", err) ··· 542 Set("activity_id", newStudySession.Activity.ID). 543 Set("is_custom_activity", len(newStudySession.Activity.Did) > 0). 544 Set("description_provided", len(newStudySession.Description) > 0). 545 + Set("date_is_today", newStudySession.Date.Truncate(24*time.Hour).Equal(time.Now().UTC().In(loc).Truncate(24*time.Hour))), 546 }) 547 if err != nil { 548 log.Println("failed to enqueue posthog event:", err)
+9 -10
internal/server/views/edit-study-session.templ
··· 2 3 import ( 4 "fmt" 5 - "time" 6 7 "yoten.app/internal/db" 8 "yoten.app/internal/server/views/layouts" ··· 278 </div> 279 </div> 280 </div> 281 - {{ 282 - var ( 283 - today = time.Now().Format("2006-01-02") 284 - oneYearAgo = time.Now().AddDate(-1, 0, 0).Format("2006-01-02") 285 - ) 286 - }} 287 - <div class="flex flex-col gap-2"> 288 <label for="date" class="font-medium text-sm">Date</label> 289 <input 290 type="date" ··· 292 id="date" 293 value={ params.StudySession.Date.Format("2006-01-02") } 294 class="input w-full" 295 - max={ today } 296 - min={ oneYearAgo } 297 /> 298 </div> 299 </div>
··· 2 3 import ( 4 "fmt" 5 6 "yoten.app/internal/db" 7 "yoten.app/internal/server/views/layouts" ··· 277 </div> 278 </div> 279 </div> 280 + <div 281 + x-data="{ 282 + today: (d => d.toISOString().slice(0,10))(new Date()), 283 + oneYearAgo: (d => d.toISOString().slice(0,10))(new Date(new Date().setFullYear(new Date().getFullYear() - 1))) 284 + }" 285 + class="flex flex-col gap-2" 286 + > 287 <label for="date" class="font-medium text-sm">Date</label> 288 <input 289 type="date" ··· 291 id="date" 292 value={ params.StudySession.Date.Format("2006-01-02") } 293 class="input w-full" 294 + :min="oneYearAgo" 295 + :max="today" 296 /> 297 </div> 298 </div>
+9 -11
internal/server/views/new-study-session.templ
··· 1 package views 2 3 import ( 4 - "time" 5 - 6 "yoten.app/internal/server/views/layouts" 7 "yoten.app/internal/server/views/partials" 8 ) 9 10 templ NewStudySessionPage(params NewStudySessionPageParams) { 11 {{ 12 - var ( 13 - tomorrow = time.Now().AddDate(0, 0, 1).Format("2006-01-02") 14 - today = time.Now().Format("2006-01-02") 15 - oneYearAgo = time.Now().AddDate(-1, 0, 0).Format("2006-01-02") 16 - initialLangCode = "" 17 - ) 18 19 if len(params.Profile.Languages) == 1 { 20 initialLangCode = string(params.Profile.Languages[0].Code) ··· 29 class="card group" 30 hx-post="/session/new" 31 hx-swap="none" 32 hx-disabled-elt="#save-button,#cancel-button,#start-timer-button,#pause-timer-button,#reset-timer-button,#stop-timer-button" 33 > 34 <h1 class="text-3xl font-bold">Log New Study Session</h1> ··· 339 </div> 340 </div> 341 <div 342 x-show="mode === 'manual'" 343 x-transition 344 class="flex flex-col gap-2" ··· 349 name="date" 350 id="date" 351 class="input w-full" 352 - value={ today } 353 - max={ tomorrow } 354 - min={ oneYearAgo } 355 /> 356 </div> 357 </div>
··· 1 package views 2 3 import ( 4 "yoten.app/internal/server/views/layouts" 5 "yoten.app/internal/server/views/partials" 6 ) 7 8 templ NewStudySessionPage(params NewStudySessionPageParams) { 9 {{ 10 + var initialLangCode = "" 11 12 if len(params.Profile.Languages) == 1 { 13 initialLangCode = string(params.Profile.Languages[0].Code) ··· 22 class="card group" 23 hx-post="/session/new" 24 hx-swap="none" 25 + hx-vals="js:{timezone: Intl.DateTimeFormat().resolvedOptions().timeZone}" 26 hx-disabled-elt="#save-button,#cancel-button,#start-timer-button,#pause-timer-button,#reset-timer-button,#stop-timer-button" 27 > 28 <h1 class="text-3xl font-bold">Log New Study Session</h1> ··· 333 </div> 334 </div> 335 <div 336 + x-data="{ 337 + today: (d => d.toISOString().slice(0,10))(new Date()), 338 + oneYearAgo: (d => d.toISOString().slice(0,10))(new Date(new Date().setFullYear(new Date().getFullYear() - 1))) 339 + }" 340 x-show="mode === 'manual'" 341 x-transition 342 class="flex flex-col gap-2" ··· 347 name="date" 348 id="date" 349 class="input w-full" 350 + x-model="today" 351 + :min="oneYearAgo" 352 + :max="today" 353 /> 354 </div> 355 </div>
+7
internal/server/views/partials/partials.go
··· 164 } 165 166 func NewHeatmap(data db.HeatmapData) templ.Component { 167 today := time.Now() 168 oneYearAgo := today.AddDate(-1, 0, 0) 169 startOffset := int(oneYearAgo.Weekday())
··· 164 } 165 166 func NewHeatmap(data db.HeatmapData) templ.Component { 167 + // TODO: Do this calculation in users local time. 168 + // loc, err := time.LoadLocation(userTimezone) 169 + // if err != nil { 170 + // loc = time.UTC // Fallback to UTC if unable to parse users timezone. 171 + // } 172 + // today := time.Now().In(loc) 173 + 174 today := time.Now() 175 oneYearAgo := today.AddDate(-1, 0, 0) 176 startOffset := int(oneYearAgo.Weekday())
+1 -1
internal/server/views/stats.templ
··· 46 </div> 47 </div> 48 </div> 49 - <div class=""> 50 @partials.NewHeatmap(params.HeatmapData) 51 </div> 52 <div x-data="{ tab: 'week' }" class="card">
··· 46 </div> 47 </div> 48 </div> 49 + <div> 50 @partials.NewHeatmap(params.HeatmapData) 51 </div> 52 <div x-data="{ tab: 'week' }" class="card">