Files
bookhoard/templates/profile_form.templ
T
John O'Keefe c645b8e207 feat(profile): keep_screen_awake user preference (default on)
New users column surfaced as an On/Off dropdown in the profile form.
The form and API carry "true"/"false" strings with the theme-style
""-guard, so JSON clients that omit the field can't clobber it;
anything else is rejected. GET/PUT /api/auth/profile expose
keep_screen_awake alongside the other profile fields, and the reader
route threads the preference into its template metadata.

Integration tests pin the schema default, both values, omission
semantics, and rejection — dedicated test user, never testuser.
2026-10-26 13:59:32 -04:00

255 lines
9.9 KiB
Templ
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package templates
import "fmt"
// effectiveAudioSpeed renders 0 (unset) as 1× — the schema default.
func effectiveAudioSpeed(user User) float64 {
if user.DefaultAudioSpeed == 0 {
return 1
}
return user.DefaultAudioSpeed
}
templ ProfileForm(user User, actionURL string, requireCurrentPassword bool, showRoleField bool, showCancelButton bool) {
<form
hx-put={ actionURL }
hx-target="#profile-result"
hx-swap="innerHTML"
class="space-y-8"
>
<div>
<div class="flex items-center gap-2 mb-4">
@Icon("user", "h-5 w-5")
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Account Information</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Username</label>
<input
type="text"
name="username"
value={ user.Username }
class="input"
/>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Email Address</label>
<input
type="email"
name="email"
value={ user.Email }
class="input"
/>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">First Name</label>
<input
type="text"
name="first_name"
value={ user.FirstName }
placeholder="Optional"
class="input"
/>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Last Name</label>
<input
type="text"
name="last_name"
value={ user.LastName }
placeholder="Optional"
class="input"
/>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Theme</label>
<select
name="theme"
class="input"
>
<option value="tokyo-night" selected?={ user.Theme == "tokyo-night" }>Tokyo Night</option>
<option value="dracula" selected?={ user.Theme == "dracula" }>Dracula</option>
<option value="nord" selected?={ user.Theme == "nord" }>Nord</option>
<option value="solarized-dark" selected?={ user.Theme == "solarized-dark" }>Solarized Dark</option>
<option value="monokai" selected?={ user.Theme == "monokai" }>Monokai</option>
<option value="one-dark-pro" selected?={ user.Theme == "one-dark-pro" }>One Dark Pro</option>
<option value="material-dark" selected?={ user.Theme == "material-dark" }>Material Dark</option>
</select>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Default Playback Speed</label>
<select
name="default_audio_speed"
class="input"
>
for _, rate := range SpeedOptions {
<option value={ fmt.Sprintf("%g", rate) } selected?={ effectiveAudioSpeed(user) == rate }>{ fmt.Sprintf("%g", rate) }×</option>
}
</select>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Keep Screen Awake</label>
<select
name="keep_screen_awake"
class="input"
>
<option value="true" selected?={ user.KeepScreenAwake }>On</option>
<option value="false" selected?={ !user.KeepScreenAwake }>Off</option>
</select>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Timezone</label>
<select
name="timezone"
class="input"
>
<option value="UTC" selected?={ user.Timezone == "UTC" }>UTC (UTC+0)</option>
<option value="Pacific/Honolulu" selected?={ user.Timezone == "Pacific/Honolulu" }>Hawaii (UTC-10)</option>
<option value="America/Anchorage" selected?={ user.Timezone == "America/Anchorage" }>Alaska (UTC-9/-8)</option>
<option value="America/Los_Angeles" selected?={ user.Timezone == "America/Los_Angeles" }>Pacific (UTC-8/-7)</option>
<option value="America/Denver" selected?={ user.Timezone == "America/Denver" }>Mountain (UTC-7/-6)</option>
<option value="America/Phoenix" selected?={ user.Timezone == "America/Phoenix" }>Mountain - no DST (UTC-7)</option>
<option value="America/Chicago" selected?={ user.Timezone == "America/Chicago" }>Central (UTC-6/-5)</option>
<option value="America/New_York" selected?={ user.Timezone == "America/New_York" }>Eastern (UTC-5/-4)</option>
<option value="America/Sao_Paulo" selected?={ user.Timezone == "America/Sao_Paulo" }>Brasilia (UTC-3/-2)</option>
<option value="Europe/London" selected?={ user.Timezone == "Europe/London" }>British (UTC+0/+1)</option>
<option value="Europe/Paris" selected?={ user.Timezone == "Europe/Paris" }>Central European (UTC+1/+2)</option>
<option value="Europe/Helsinki" selected?={ user.Timezone == "Europe/Helsinki" }>Eastern European (UTC+2/+3)</option>
<option value="Europe/Moscow" selected?={ user.Timezone == "Europe/Moscow" }>Moscow (UTC+3)</option>
<option value="Asia/Tehran" selected?={ user.Timezone == "Asia/Tehran" }>Iran (UTC+3:30)</option>
<option value="Asia/Dubai" selected?={ user.Timezone == "Asia/Dubai" }>Gulf (UTC+4)</option>
<option value="Asia/Karachi" selected?={ user.Timezone == "Asia/Karachi" }>Pakistan (UTC+5)</option>
<option value="Asia/Kolkata" selected?={ user.Timezone == "Asia/Kolkata" }>India (UTC+5:30)</option>
<option value="Asia/Dhaka" selected?={ user.Timezone == "Asia/Dhaka" }>Bangladesh (UTC+6)</option>
<option value="Asia/Bangkok" selected?={ user.Timezone == "Asia/Bangkok" }>Indochina (UTC+7)</option>
<option value="Asia/Shanghai" selected?={ user.Timezone == "Asia/Shanghai" }>China (UTC+8)</option>
<option value="Asia/Tokyo" selected?={ user.Timezone == "Asia/Tokyo" }>Japan/Korea (UTC+9)</option>
<option value="Australia/Darwin" selected?={ user.Timezone == "Australia/Darwin" }>Australian Central (UTC+9:30)</option>
<option value="Australia/Sydney" selected?={ user.Timezone == "Australia/Sydney" }>Australian Eastern (UTC+10/+11)</option>
<option value="Pacific/Auckland" selected?={ user.Timezone == "Pacific/Auckland" }>New Zealand (UTC+12/+13)</option>
</select>
</div>
if showRoleField {
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Role</label>
<select
name="role"
class="input"
>
<option value="user" selected?={ user.Role == "user" }>User</option>
<option value="admin" selected?={ user.Role == "admin" }>Admin</option>
</select>
</div>
}
</div>
</div>
<div x-data="profilePasswordForm">
<div class="flex items-center gap-2 mb-4">
@Icon("shield", "h-5 w-5")
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Change Password</h3>
</div>
if requireCurrentPassword {
<div class="space-y-4 max-w-md">
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Current Password</label>
<input
type="password"
name="current_password"
class="input"
/>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">New Password</label>
<input
type="password"
id="new-password"
name="new_password"
class="input"
/>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Confirm New Password</label>
<input
type="password"
id="confirm-password"
name="confirm_password"
class="input"
/>
</div>
<div id="password-requirements" class="text-xs"></div>
<button
type="button"
id="password-submit-btn"
hx-put="/api/auth/password"
hx-target="#password-result"
hx-swap="innerHTML"
hx-include="closest form"
class="btn btn-primary"
>
@Icon("save", "h-4 w-4")
Update Password
</button>
<div id="password-result" class="mt-2"></div>
</div>
} else {
<p class="text-sm italic mb-4" style="color: var(--text-secondary);">
As an admin, you can change this user's password without knowing their current password.
</p>
<div class="space-y-4 max-w-md">
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">New Password</label>
<input
type="password"
id="new-password"
name="new_password"
class="input"
/>
</div>
<div>
<label class="block text-xs font-semibold uppercase tracking-wide mb-1.5" style="color: var(--text-secondary)">Confirm New Password</label>
<input
type="password"
id="confirm-password"
name="confirm_password"
class="input"
/>
</div>
<div id="password-requirements" class="text-xs"></div>
<button
type="button"
id="password-submit-btn"
hx-put={ "/api/auth/password/" + user.ID }
hx-target="#password-result"
hx-swap="innerHTML"
hx-include="closest form"
class="btn btn-primary"
>
@Icon("save", "h-4 w-4")
Reset Password
</button>
<div id="password-result" class="mt-2"></div>
</div>
}
</div>
<div class="flex justify-end gap-3 pt-6 border-t" style="border-color: var(--border);">
if showCancelButton {
<button
type="button"
@click="closeProfileModal()"
class="btn btn-secondary"
>
Cancel
</button>
}
<button
type="submit"
class="btn btn-primary"
>
@Icon("save", "h-4 w-4")
Save Changes
</button>
</div>
<div id="profile-result" class="mt-2"></div>
</form>
}