Yōten: A social tracker for your language learning journey built on the atproto.
at master 190 lines 6.4 kB view raw
1package views 2 3import ( 4 "fmt" 5 6 "yoten.app/internal/db" 7 "yoten.app/internal/server/views/layouts" 8 "yoten.app/internal/server/views/partials" 9) 10 11templ EditProfilePage(params EditProfilePageParams) { 12 @layouts.Base(layouts.BaseParams{Title: "edit profile"}) { 13 @partials.Header(partials.HeaderProps{User: params.User}) 14 <div class="container mx-auto px-4 py-8 max-w-2xl"> 15 <form 16 hx-post="/profile/edit" 17 hx-swap="none" 18 hx-disabled-elt="#save-button,#cancel-button" 19 class="flex flex-col gap-8 group" 20 > 21 <div> 22 <h1 class="text-3xl font-bold">Edit Profile</h1> 23 <p class="text-text-muted mt-1">Update your display name and learning languages</p> 24 </div> 25 <div class="card"> 26 <h1 class="text-2xl font-bold">Profile Information</h1> 27 <div class="flex flex-col gap-1"> 28 <label for="display-name" class="font-medium text-sm">Display Name</label> 29 <div x-data={ templ.JSONString(JsonText{Text: params.Profile.DisplayName}) }> 30 <input 31 x-model="text" 32 id="display-name" 33 name="display_name" 34 value={ params.Profile.DisplayName } 35 placeholder="Enter your display name" 36 class="input w-full" 37 maxLength="64" 38 /> 39 <div class="text-right text-sm text-text-muted mt-1"> 40 <span x-text="text.length"></span> / 64 41 </div> 42 </div> 43 </div> 44 <div class="flex flex-col gap-1"> 45 <label for="location" class="font-medium text-sm">Location</label> 46 <div x-data={ templ.JSONString(JsonText{Text: params.Profile.Location}) }> 47 <input 48 x-model="text" 49 id="location" 50 name="location" 51 value={ params.Profile.Location } 52 placeholder="Where are you based?" 53 class="input w-full" 54 maxLength="40" 55 /> 56 <div class="text-right text-sm text-text-muted mt-1"> 57 <span x-text="text.length"></span> / 40 58 </div> 59 </div> 60 </div> 61 <div class="flex flex-col gap-1"> 62 <label for="description" class="font-medium text-sm">About You</label> 63 <div 64 x-data="{ text: '' }" 65 x-init="text = $el.querySelector('textarea').value" 66 > 67 <textarea 68 x-model="text" 69 id="description" 70 name="description" 71 placeholder="Tell others about your language learning journey, interests, or goals..." 72 class="input w-full" 73 maxLength="256" 74 rows="3" 75 > 76 { params.Profile.Description } 77 </textarea> 78 <div class="text-right text-sm text-text-muted mt-1"> 79 <span x-text="text.length"></span> / 256 80 </div> 81 </div> 82 </div> 83 </div> 84 <div 85 class="card" 86 x-data="languagePicker()" 87 > 88 <div class="flex flex-col sm:flex-row gap-2 justify-between sm:items-center"> 89 <h1 class="text-2xl font-bold">Languages You're Learning</h1> 90 <div class="pill pill-muted"> 91 <span x-text="selected.length"></span>/10 selected 92 </div> 93 </div> 94 <p class="text-sm text-text-muted mt-1"> 95 Select up to 10 languages you're currently learning. 96 </p> 97 <div class="relative mt-4"> 98 <i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted"></i> 99 <input type="text" x-model.debounce.300ms="search" placeholder="Search languages..." class="input w-full pl-9"/> 100 </div> 101 <div x-show="selected.length > 0" class="mt-4"> 102 <h3 class="text-sm font-medium mb-2">Selected Languages:</h3> 103 <div class="flex flex-wrap gap-2"> 104 <template x-for="lang in selectedObjects" :key="lang.Code"> 105 <div class="pill pill-primary flex items-center justify-center gap-2" x-init="lucide.createIcons()"> 106 <span x-text="lang.Flag + ' ' + lang.Name"></span> 107 <button type="button" @click="toggle(lang.Code)" class="hover:text-text hover:cursor-pointer"> 108 <i class="w-4 h-4" data-lucide="x"></i> 109 </button> 110 </div> 111 </template> 112 </div> 113 </div> 114 <div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4 border-t pt-4 max-h-72 overflow-y-auto"> 115 for _, l := range params.AllLanguages { 116 <div 117 key={ l.Code } 118 x-show={ fmt.Sprintf("search === '' || '%s'.toLowerCase().includes(search.toLowerCase())", db.Languages[l.Code].Name) } 119 @click={ fmt.Sprintf("toggle('%s')", l.Code) } 120 :class={ fmt.Sprintf("isSelected('%s') ? 'btn-primary' : 'btn-secondary'", l.Code) } 121 class="input flex items-center gap-3 cursor-pointer" 122 > 123 <input 124 type="checkbox" 125 name="languages[]" 126 value={ l.Code } 127 :checked={ fmt.Sprintf("isSelected('%s')", l.Code) } 128 :disabled={ fmt.Sprintf("!isSelected('%s') && selected.length >= 10", l.Code) } 129 class="pointer-events-none" 130 /> 131 <label class="pointer-events-none"> 132 <span>{ l.Flag } { l.Name }</span> 133 if l.NativeName != nil { 134 <span class="text-text-muted">({ *l.NativeName })</span> 135 } 136 </label> 137 </div> 138 } 139 </div> 140 </div> 141 <div class="flex flex-col sm:flex-row gap-4 w-full"> 142 <button id="save-button" type="submit" class="btn btn-primary"> 143 <i class="w-4 h-4" data-lucide="save"></i> 144 Save 145 <i class="w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" data-lucide="loader-circle"></i> 146 </button> 147 <button id="cancel-button" type="button" class="w-full"> 148 <a href={ templ.SafeURL(fmt.Sprintf("/%s", params.User.Did)) } class="btn btn-secondary"> 149 Cancel 150 </a> 151 </button> 152 </div> 153 </form> 154 </div> 155 <script> 156 function languagePicker() { 157 return { 158 search: '', 159 all: {{ params.AllLanguages }} || [], 160 selected: {{ params.InitialSelectedLanguages }} || [], 161 162 get selectedObjects() { 163 return this.all.filter(lang => this.selected.includes(lang.Code)); 164 }, 165 166 get filteredLanguages() { 167 if (!this.search) return this.all; 168 return this.all.filter(lang => 169 lang.Name.toLowerCase().includes(this.search.toLowerCase()) 170 ); 171 }, 172 173 isSelected(code) { 174 return this.selected.includes(code); 175 }, 176 177 toggle(code) { 178 if (this.isSelected(code)) { 179 this.selected = this.selected.filter(c => c !== code); 180 } else { 181 if (this.selected.length < 10) { 182 this.selected.push(code); 183 } 184 } 185 } 186 } 187 } 188 </script> 189 } 190}