Yōten: A social tracker for your language learning journey built on the atproto.
at master 209 lines 7.8 kB view raw
1package views 2 3import ( 4 "fmt" 5 "math" 6 7 "yoten.app/internal/db" 8 "yoten.app/internal/server/views/layouts" 9 "yoten.app/internal/server/views/partials" 10) 11 12templ ProfilePage(params ProfilePageParams) { 13 {{ isSelf := params.User != nil && params.User.Did == params.Profile.Did }} 14 {{ streakClass := "pill flex items-center gap-1 px-2 " }} 15 @layouts.Base(layouts.BaseParams{Title: params.Profile.DisplayName}) { 16 @partials.Header(partials.HeaderProps{User: params.User}) 17 <div class="container mx-auto max-w-6xl px-4 py-8"> 18 <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8"> 19 <div class="card lg:col-span-1"> 20 <div class="flex flex-col items-center text-center pt-6 gap-4"> 21 if params.Profile.Avatar == "" { 22 <div class="flex items-center justify-center w-20 h-20 rounded-full bg-primary"> 23 <i class="w-15 h-15" data-lucide="user"></i> 24 </div> 25 } else { 26 <img src={ params.Profile.Avatar } class="w-20 h-20 rounded-full"/> 27 } 28 <div class="flex flex-col items-center gap-2 mb-2"> 29 <h1 class="text-2xl font-bold">{ params.Profile.DisplayName }</h1> 30 <div class="flex flex-col gap-1 text-sm text-text-muted"> 31 <span>&commat;{ params.BskyProfile.Handle }</span> 32 if params.Profile.Location != "" { 33 <div class="flex items-center justify-center gap-1"> 34 <i class="w-4 h-4" data-lucide="map-pin"></i> 35 <p>{ params.Profile.Location }</p> 36 </div> 37 } 38 </div> 39 <div class="flex items-center gap-2"> 40 if !isSelf { 41 <p class="pill pill-secondary h-fit flex items-center justify-center gap-1 w-fit"> 42 <i class="w-4 h-4" data-lucide="star"></i> 43 <span>Level { params.Profile.Level }</span> 44 </p> 45 } else { 46 <div 47 title={ fmt.Sprintf("%d day study streak", params.Streak) } 48 if params.Streak > 0 { 49 class={ streakClass + "pill-streak" } 50 } else { 51 class={ streakClass + "pill-muted" } 52 } 53 > 54 <i class="w-4 h-4" data-lucide="flame"></i> 55 <span>{ params.Streak }</span> 56 </div> 57 } 58 </div> 59 </div> 60 if params.FollowStatus != db.IsSelf { 61 @partials.FollowButton(partials.FollowButtonProps{ 62 SubjectDid: params.Profile.Did, 63 FollowStatus: params.FollowStatus, 64 }) 65 } 66 if isSelf { 67 <div class="flex flex-col sm:flex-row lg:flex-col gap-2 sm:w-1/3 lg:w-full"> 68 <a href="/profile/edit" class="btn btn-secondary"> 69 <i class="w-4 h-4" data-lucide="square-pen"></i> 70 Edit Profile 71 </a> 72 <a href="/session/new" class="btn btn-primary w-content whitespace-nowrap"> 73 <i class="w-4 h-4" data-lucide="plus"></i> 74 Log Session 75 </a> 76 </div> 77 } 78 </div> 79 </div> 80 <div class="lg:col-span-2 space-y-6"> 81 <div class="card"> 82 <div> 83 if params.Profile.Description != "" { 84 <h3 class="font-semibold">About</h3> 85 <p class="whitespace-pre-wrap leading-relaxed wrap-break-word">{ params.Profile.Description }</p> 86 } 87 </div> 88 <div class="flex flex-col gap-3 mt-4"> 89 <div> 90 <p class="text-sm font-medium">Currently Learning:</p> 91 <div class="flex items-center gap-1"> 92 for _, language := range params.Profile.Languages { 93 <span>{ language.Flag }</span> 94 } 95 </div> 96 </div> 97 <div class="flex flex-wrap gap-2"> 98 for _, language := range params.Profile.Languages[:min(4, len(params.Profile.Languages))] { 99 <div class="pill pill-primary"> 100 { language.Name } 101 if language.NativeName != nil { 102 ({ *language.NativeName }) 103 } 104 </div> 105 } 106 if len(params.Profile.Languages) > 4 { 107 <div class="pill pill-muted"> 108 +{ len(params.Profile.Languages) - 4 } more 109 </div> 110 } 111 </div> 112 <div class="pt-2 border-t border-bg-dark gap-2 w-full"> 113 <p class="text-text-muted text-sm"> 114 Learning since { params.Profile.CreatedAt.Format("January 2006") } 115 </p> 116 </div> 117 </div> 118 </div> 119 if isSelf { 120 <div class="card"> 121 <div class="flex justify-between w-full"> 122 <p class="pill pill-secondary h-fit flex items-center justify-center gap-1 w-fit"> 123 <i class="w-4 h-4" data-lucide="star"></i> 124 <span>Level { params.Profile.Level }</span> 125 </p> 126 <div class="flex flex-col items-center"> 127 {{ 128 xpForNextLevel := db.XpForLevel(params.Profile.Level + 1) 129 xpForCurrentLevel := db.XpForLevel(params.Profile.Level) 130 xpNeededForThisLevel := xpForNextLevel - xpForCurrentLevel 131 userProgressInThisLevel := params.Profile.Xp - xpForCurrentLevel 132 progressPercentage := 0.0 133 if params.Profile.Level > 0 { 134 progressPercentage = (float64(userProgressInThisLevel) / float64(xpNeededForThisLevel)) * 100 135 } else { 136 progressPercentage = (float64(params.Profile.Xp) / float64(db.XpForLevel(0))) * 100 137 } 138 }} 139 <p class="text-sm">{ params.Profile.Xp } Total XP</p> 140 <p class="text-xs text-text-muted"> 141 if params.Profile.Level > 0 { 142 { xpNeededForThisLevel - userProgressInThisLevel } to Level { params.Profile.Level + 1 } 143 } else { 144 { db.XpForLevel(0) - params.Profile.Xp } to Level { params.Profile.Level + 1 } 145 } 146 </p> 147 </div> 148 </div> 149 <div class="shadow-sm bg-gray-light rounded-full w-full h-2"> 150 <div 151 class="rounded-full bg-secondary h-2" 152 style={ fmt.Sprintf("width: %f%%", math.Abs(progressPercentage)) } 153 ></div> 154 </div> 155 </div> 156 } 157 <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4"> 158 <div class="card text-center"> 159 <p class="text-2xl font-bold">{ params.TotalStudySessions }</p> 160 <div class="text-xs text-text-muted flex items-center justify-center gap-1 mt-1"> 161 <i class="w-4 h-4" data-lucide="target"></i> 162 <p class="text-xs">Sessions</p> 163 </div> 164 </div> 165 <div class="card text-center"> 166 <p class="text-2xl font-bold">{ fmt.Sprintf("%.2f", params.TotalStudyTime.Hours()) }</p> 167 <div class="text-xs text-text-muted flex items-center justify-center gap-1 mt-1"> 168 <i class="w-4 h-4" data-lucide="clock"></i> 169 <p class="text-xs">Hours</p> 170 </div> 171 </div> 172 <div class="card text-center"> 173 <p class="text-2xl font-bold">{ params.Followers }</p> 174 <div class="text-xs text-text-muted flex items-center justify-center gap-1 mt-1"> 175 <i class="w-4 h-4" data-lucide="users"></i> 176 <p class="text-xs">Followers</p> 177 </div> 178 </div> 179 <div class="card text-center"> 180 <p class="text-2xl font-bold">{ params.Following }</p> 181 <div class="text-xs text-text-muted flex items-center justify-center gap-1 mt-1"> 182 <i class="w-4 h-4" data-lucide="user-plus"></i> 183 <p class="text-xs">Following</p> 184 </div> 185 </div> 186 </div> 187 </div> 188 </div> 189 <div class="flex flex-col gap-6"> 190 <div class="flex flex-col md:flex-row gap-2 md:items-center md:justify-between"> 191 <h2 class="text-xl font-semibold">Your Study Sessions</h2> 192 <p class="text-text-muted">{ params.TotalStudySessions } sessions</p> 193 </div> 194 <div> 195 <div 196 id="study-feed" 197 hx-trigger="load" 198 hx-get={ templ.SafeURL(fmt.Sprintf("/%s/feed", params.Profile.Did)) } 199 class="flex flex-col gap-6" 200 > 201 <div class="flex justify-center py-4"> 202 <i data-lucide="loader-circle" class="w-6 h-6 animate-spin text-text-muted"></i> 203 </div> 204 </div> 205 </div> 206 </div> 207 </div> 208 } 209}