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

feat: add study session page

brookjeynes.dev bbfbd817 f4f4e6a2

verified
+127 -7
+1
internal/server/handlers/router.go
··· 129 r.Route("/{user}", func(r chi.Router) { 130 r.Get("/", h.HandleProfilePage) 131 r.Get("/feed", h.HandleProfileFeed) 132 }) 133 }) 134
··· 129 r.Route("/{user}", func(r chi.Router) { 130 r.Get("/", h.HandleProfilePage) 131 r.Get("/feed", h.HandleProfileFeed) 132 + r.Get("/session/{rkey}", h.HandleStudySessionPage) 133 }) 134 }) 135
+55
internal/server/handlers/study-session.go
··· 9 "time" 10 11 comatproto "github.com/bluesky-social/indigo/api/atproto" 12 lexutil "github.com/bluesky-social/indigo/lex/util" 13 "github.com/go-chi/chi/v5" 14 "github.com/posthog/posthog-go" ··· 630 631 return nil 632 }
··· 9 "time" 10 11 comatproto "github.com/bluesky-social/indigo/api/atproto" 12 + "github.com/bluesky-social/indigo/atproto/identity" 13 lexutil "github.com/bluesky-social/indigo/lex/util" 14 "github.com/go-chi/chi/v5" 15 "github.com/posthog/posthog-go" ··· 631 632 return nil 633 } 634 + 635 + func (h *Handler) HandleStudySessionPage(w http.ResponseWriter, r *http.Request) { 636 + user, _ := bsky.GetUserWithBskyProfile(h.Oauth, r) 637 + didOrHandle := chi.URLParam(r, "user") 638 + if didOrHandle == "" { 639 + http.Error(w, "Bad request", http.StatusBadRequest) 640 + return 641 + } 642 + 643 + ident, ok := r.Context().Value("resolvedId").(identity.Identity) 644 + if !ok { 645 + w.WriteHeader(http.StatusNotFound) 646 + views.NotFoundPage(views.NotFoundPageParams{}).Render(r.Context(), w) 647 + return 648 + } 649 + rkey := chi.URLParam(r, "rkey") 650 + 651 + studySession, err := db.GetStudySessionByRkey(h.Db, ident.DID.String(), rkey) 652 + if err != nil { 653 + log.Println("failed to retrieve study session:", err) 654 + htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve study session, try again later.") 655 + return 656 + } 657 + 658 + bskyProfile, err := bsky.GetBskyProfile(ident.DID.String()) 659 + if err != nil { 660 + log.Println("failed to retrieve bsky profile for study session:", err) 661 + htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve bsky profile, try again later.") 662 + return 663 + } 664 + 665 + profile, err := db.GetProfile(h.Db, ident.DID.String()) 666 + if err != nil { 667 + log.Println("failed to retrieve profile for study session:", err) 668 + htmx.HxError(w, http.StatusInternalServerError, "Failed to retrieve profile, try again later.") 669 + return 670 + } 671 + 672 + isSelf := false 673 + if user != nil { 674 + isSelf = user.Did == studySession.Did 675 + } 676 + 677 + views.StudySessionPage(views.StudySessionPageParams{ 678 + User: user, 679 + DoesOwn: isSelf, 680 + StudySession: db.StudySessionFeedItem{ 681 + StudySession: *studySession, 682 + ProfileDisplayName: profile.DisplayName, 683 + ProfileLevel: profile.Level, 684 + BskyProfile: bskyProfile, 685 + }, 686 + }).Render(r.Context(), w) 687 + }
+29
internal/server/views/partials/discussion.templ
···
··· 1 + package partials 2 + 3 + templ Discussion(params DiscussionProps) { 4 + <div class="card"> 5 + <div class="flex items-center gap-2"> 6 + <i class="w-5 h-5" data-lucide="message-square"></i> 7 + <h1 class="text-2xl font-bold">Discussion</h1> 8 + </div> 9 + <div class="mt-2" x-data="{ text: '' }" x-init="text = $el.querySelector('textarea').value"> 10 + <textarea 11 + x-model="text" 12 + id="comment" 13 + name="comment" 14 + placeholder="Share your thoughts about this study session..." 15 + class="input w-full" 16 + maxLength="256" 17 + rows="3" 18 + ></textarea> 19 + <div class="flex justify-between mt-2"> 20 + <div class="text-sm text-text-muted"> 21 + <span x-text="text.length"></span> / 256 22 + </div> 23 + <button class="btn btn-primary w-fit"> 24 + Post Comment 25 + </button> 26 + </div> 27 + </div> 28 + </div> 29 + }
+3
internal/server/views/partials/partials.go
··· 218 Feed []db.NotificationWithBskyHandle 219 NextPage int 220 }
··· 218 Feed []db.NotificationWithBskyHandle 219 NextPage int 220 } 221 + 222 + type DiscussionProps struct { 223 + }
+12 -6
internal/server/views/partials/study-session.templ
··· 69 70 templ StudySession(params StudySessionProps) { 71 {{ elementId := SanitiseHtmlId(fmt.Sprintf("study-session-%s-%s", params.StudySession.Did, params.StudySession.Rkey)) }} 72 <div id={ elementId } class="card relative" x-data="{ open: false }" :class="{ 'z-20': open }"> 73 <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3"> 74 <div class="flex items-center justify-between"> ··· 148 } 149 <hr class="border-gray"/> 150 <div class="flex flex-col sm:flex-row justify-between sm:items-center gap-4"> 151 - @NewReactions(NewReactionsProps{ 152 - User: params.User, 153 - SessionDid: params.StudySession.Did, 154 - SessionRkey: params.StudySession.Rkey, 155 - ReactionEvents: params.StudySession.Reactions, 156 - }) 157 <div class="flex flex-col sm:flex-row sm:items-center gap-2 text-sm text-text-muted"> 158 <div class="flex items-center gap-1"> 159 <i class="w-4 h-4" data-lucide="clock"></i>
··· 69 70 templ StudySession(params StudySessionProps) { 71 {{ elementId := SanitiseHtmlId(fmt.Sprintf("study-session-%s-%s", params.StudySession.Did, params.StudySession.Rkey)) }} 72 + {{ studySessionUrl := templ.SafeURL("/" + params.StudySession.Did + "/session/" + params.StudySession.Rkey) }} 73 <div id={ elementId } class="card relative" x-data="{ open: false }" :class="{ 'z-20': open }"> 74 <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3"> 75 <div class="flex items-center justify-between"> ··· 149 } 150 <hr class="border-gray"/> 151 <div class="flex flex-col sm:flex-row justify-between sm:items-center gap-4"> 152 + <div class="flex items-center gap-1"> 153 + @NewReactions(NewReactionsProps{ 154 + User: params.User, 155 + SessionDid: params.StudySession.Did, 156 + SessionRkey: params.StudySession.Rkey, 157 + ReactionEvents: params.StudySession.Reactions, 158 + }) 159 + <a href={ studySessionUrl }> 160 + <i class="w-5 h-5" data-lucide="message-square-share"></i> 161 + </a> 162 + </div> 163 <div class="flex flex-col sm:flex-row sm:items-center gap-2 text-sm text-text-muted"> 164 <div class="flex items-center gap-1"> 165 <i class="w-4 h-4" data-lucide="clock"></i>
+20
internal/server/views/study-session.templ
···
··· 1 + package views 2 + 3 + import ( 4 + "yoten.app/internal/server/views/layouts" 5 + "yoten.app/internal/server/views/partials" 6 + ) 7 + 8 + templ StudySessionPage(params StudySessionPageParams) { 9 + @layouts.Base(layouts.BaseParams{Title: "study session"}) { 10 + @partials.Header(partials.HeaderProps{User: params.User}) 11 + <div class="container mx-auto px-4 py-8 max-w-4xl flex flex-col gap-8"> 12 + @partials.StudySession(partials.StudySessionProps{ 13 + User: params.User, 14 + DoesOwn: params.DoesOwn, 15 + StudySession: params.StudySession, 16 + }) 17 + @partials.Discussion(partials.DiscussionProps{}) 18 + </div> 19 + } 20 + }
+7 -1
internal/server/views/views.go
··· 123 // The current logged in user. 124 User *types.User 125 Notifications []db.NotificationWithBskyHandle 126 - ActiveTab string 127 }
··· 123 // The current logged in user. 124 User *types.User 125 Notifications []db.NotificationWithBskyHandle 126 + } 127 + 128 + type StudySessionPageParams struct { 129 + // The current logged in user. 130 + User *types.User 131 + StudySession db.StudySessionFeedItem 132 + DoesOwn bool 133 }