minor: new nav system (#5)
All checks were successful
ci/woodpecker/push/push Pipeline was successful
All checks were successful
ci/woodpecker/push/push Pipeline was successful
Co-authored-by: Madison Grubb <madison@elastiflow.com> Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -4,6 +4,51 @@
|
||||
Account
|
||||
</h2>
|
||||
|
||||
<section
|
||||
v-if="user"
|
||||
class="mb-8"
|
||||
>
|
||||
<h3 class="kestrel-section-label">
|
||||
Profile photo
|
||||
</h3>
|
||||
<div class="kestrel-card flex items-center gap-4 p-4">
|
||||
<div class="flex h-16 w-16 shrink-0 overflow-hidden rounded-full border border-kestrel-border bg-kestrel-border">
|
||||
<img
|
||||
v-if="user.avatar_url"
|
||||
:src="`${user.avatar_url}${avatarBust ? `?t=${avatarBust}` : ''}`"
|
||||
alt=""
|
||||
class="h-full w-full object-cover"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="flex h-full w-full items-center justify-center text-lg font-medium text-kestrel-text"
|
||||
>
|
||||
{{ accountInitials }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label class="kestrel-btn-secondary cursor-pointer">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/jpeg,image/png"
|
||||
class="sr-only"
|
||||
:disabled="avatarLoading"
|
||||
@change="onAvatarFileChange"
|
||||
>
|
||||
{{ avatarLoading ? 'Uploading…' : 'Upload' }}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="kestrel-btn-secondary disabled:opacity-50"
|
||||
:disabled="avatarLoading || !user.avatar_url"
|
||||
@click="onRemoveAvatar"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h3 class="kestrel-section-label">
|
||||
Profile
|
||||
@@ -126,8 +171,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { user } = useUser()
|
||||
const { user, refresh } = useUser()
|
||||
|
||||
const avatarBust = ref(0)
|
||||
const avatarLoading = ref(false)
|
||||
const currentPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
@@ -135,6 +182,45 @@ const passwordLoading = ref(false)
|
||||
const passwordSuccess = ref(false)
|
||||
const passwordError = ref('')
|
||||
|
||||
const accountInitials = computed(() => {
|
||||
const id = user.value?.identifier ?? ''
|
||||
const parts = id.trim().split(/\s+/)
|
||||
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase()
|
||||
return id.slice(0, 2).toUpperCase() || '?'
|
||||
})
|
||||
|
||||
async function onAvatarFileChange(e) {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
avatarLoading.value = true
|
||||
try {
|
||||
const form = new FormData()
|
||||
form.append('avatar', file, file.name)
|
||||
await $fetch('/api/me/avatar', { method: 'PUT', body: form, credentials: 'include' })
|
||||
avatarBust.value = Date.now()
|
||||
await refresh()
|
||||
}
|
||||
catch {
|
||||
// Error surfaced by refresh or network
|
||||
}
|
||||
finally {
|
||||
avatarLoading.value = false
|
||||
e.target.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function onRemoveAvatar() {
|
||||
avatarLoading.value = true
|
||||
try {
|
||||
await $fetch('/api/me/avatar', { method: 'DELETE', credentials: 'include' })
|
||||
avatarBust.value = Date.now()
|
||||
await refresh()
|
||||
}
|
||||
finally {
|
||||
avatarLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onChangePassword() {
|
||||
passwordError.value = ''
|
||||
passwordSuccess.value = false
|
||||
|
||||
Reference in New Issue
Block a user