Files
pichunt/app/pages/profile.vue
T
2025-10-16 20:44:52 +02:00

273 lines
8.0 KiB
Vue

<script setup lang="ts">
import { useI18nKey } from '~/composables/useI18nKey';
definePageMeta({
middleware: ['auth']
});
const router = useRouter();
const { t } = useI18n();
const { loggedIn, fetch } = useUserSession();
const localePath = useLocalePath();
const validation = useValidation();
const { tKey, tSluggy } = useI18nKey();
watch(loggedIn, (isLoggedIn) => {
if (!isLoggedIn) {
router.push(localePath('/login'));
}
});
const { data, refresh, pending } = await useFetch('/api/auth/profile');
const form = reactive({
email: data.value!.email,
name: data.value!.name
});
const uploading = ref(false);
const changed = computed(
() => form.name !== data.value?.name || form.email !== data.value?.email
);
const profileError = ref('');
async function submitProfile() {
uploading.value = true;
profileError.value = '';
try {
await $fetch(`/api/auth/profile`, {
method: 'POST',
body: form
});
await refresh();
form.email = data.value!.email;
form.name = data.value!.name;
await fetch();
} catch (e) {
if (e instanceof Error && 'statusCode' in e && e.statusCode === 409) {
profileError.value = t('auth.conflict');
} else {
profileError.value = parseError(e);
}
}
uploading.value = false;
}
const password = reactive({
password: '',
old: ''
});
const passwordError = ref('');
async function submitPassword() {
uploading.value = true;
passwordError.value = '';
try {
await $fetch(`/api/auth/password`, {
method: 'POST',
body: password
});
password.password = '';
password.old = '';
} catch (e) {
if (e instanceof Error && 'statusCode' in e && e.statusCode === 404) {
passwordError.value = t('auth.old_invalid');
} else {
passwordError.value = parseError(e);
}
}
uploading.value = false;
}
</script>
<template>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<VaCard stripe stripe-color="success">
<VaCardTitle>Public profile</VaCardTitle>
<VaCardContent v-if="data">
<div class="flex flex-col gap-4">
<VaInput
v-model="form.name"
:label="t('auth.name')"
clearable
:clear-value="data.name"
clearable-icon="replay"
:disabled="uploading"
counter
max-length="265"
min-length="3"
:rules="[
validation.required(t('auth.name')),
validation.min(t('auth.name'), 3),
validation.max(t('auth.name'), 256)
]"
/>
<VaInput
v-model="form.email"
:label="t('auth.email')"
type="email"
clearable
:clear-value="data.email"
clearable-icon="replay"
:disabled="uploading"
:rules="[
validation.required(t('auth.email')),
validation.min(t('auth.email'), 3)
]"
/>
<VaAlert
v-if="profileError.length > 0"
color="danger"
class="w-full text-center"
>{{ t(profileError) }}</VaAlert
>
<VaButton
:loading="pending || uploading"
color="success"
:disabled="!changed || pending || uploading"
@click="submitProfile"
>{{ t('page.common.update_btn') }}</VaButton
>
</div>
</VaCardContent>
</VaCard>
<VaCard stripe stripe-color="danger">
<VaCardTitle>Change password</VaCardTitle>
<VaCardContent>
<div class="flex flex-col gap-4">
<VaValue v-slot="isPasswordVisible" :default-value="false">
<VaInput
v-model="password.old"
class="w-full"
:type="isPasswordVisible.value ? 'text' : 'password'"
:label="t('page.profile.old_password')"
:placeholder="t('page.profile.old_password')"
autocomplete="current-password"
:minlength="8"
required
>
<template #prependInner>
<VaIcon name="key" color="secondary" />
</template>
<template #appendInner>
<VaButton
v-if="password.old"
preset="plain"
:title="
t(
isPasswordVisible.value
? 'auth.hidePassword'
: 'auth.showPassword'
)
"
@click="isPasswordVisible.value = !isPasswordVisible.value"
>
<VaIcon
:name="
isPasswordVisible.value ? 'visibility_off' : 'visibility'
"
color="primary"
/>
</VaButton>
</template>
</VaInput>
</VaValue>
<VaValue v-slot="isPasswordVisible" :default-value="false">
<VaInput
v-model="password.password"
class="w-full"
:type="isPasswordVisible.value ? 'text' : 'password'"
:label="t('page.profile.new_password')"
:placeholder="t('auth.password')"
autocomplete="current-password"
:minlength="8"
required
>
<template #prependInner>
<VaIcon name="key" color="secondary" />
</template>
<template #appendInner>
<VaButton
v-if="password.password"
preset="plain"
:title="
t(
isPasswordVisible.value
? 'auth.hidePassword'
: 'auth.showPassword'
)
"
@click="isPasswordVisible.value = !isPasswordVisible.value"
>
<VaIcon
:name="
isPasswordVisible.value ? 'visibility_off' : 'visibility'
"
color="primary"
/>
</VaButton>
</template>
</VaInput>
</VaValue>
<VaAlert
v-if="passwordError.length > 0"
color="danger"
class="w-full text-center"
>{{ t(profileError) }}</VaAlert
>
<VaButton
:loading="pending || uploading"
color="success"
:disabled="
password.password.length < 8 ||
password.old.length < 8 ||
pending ||
uploading
"
@click="submitPassword"
>Update</VaButton
>
</div>
</VaCardContent>
</VaCard>
<VaCard stripe stripe-color="primary">
<VaCardTitle>{{ t('page.profile.uploaded_pictures') }}</VaCardTitle>
<VaCardContent v-if="data && data.uploadedFiles.length > 0">
<VaCarousel
stateful
:items="data.uploadedFiles"
arrows
indicators
lazy
height="50vh"
indicator-trigger="hover"
>
<template #default="{ item }"
><ClickableImage
:src="item.url"
:title="`${tKey(item.answer.quest, 'title').value} — ${tKey(item.answer.quest.hunt, 'name').value}`"
/>
<NuxtLink
:to="
localePath(
`/hunt/${tSluggy(item.answer.quest.hunt).value}/${tSluggy(item.answer.quest).value}`
)
"
class="absolute top-2 mx-auto rounded bg-gray-200/75 px-2 py-1 text-center text-xl hover:underline"
>
{{ tKey(item.answer.quest, 'title') }}
{{ tKey(item.answer.quest.hunt, 'name') }}
</NuxtLink></template
>
</VaCarousel>
</VaCardContent>
<VaCardContent v-else>{{ t('page.profile.no_pictures') }}</VaCardContent>
</VaCard>
</div>
</template>
<style scoped></style>