Yōten: A social tracker for your language learning journey built on the atproto.
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>@{ 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}