Initial commit from monorepo
This commit is contained in:
157
app/pages/clientarea/profile/index.vue
Normal file
157
app/pages/clientarea/profile/index.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<Section variant="plain" paddingY="md">
|
||||
<Stack gap="6">
|
||||
<PageHeader
|
||||
:title="$t('dashboard.profile')"
|
||||
:actions="[{ label: t('clientProfile.actions.debugTokens'), icon: 'lucide:bug', to: localePath('/clientarea/profile/debug-tokens') }]"
|
||||
/>
|
||||
|
||||
<Alert v-if="hasError" variant="error">
|
||||
<Stack gap="1">
|
||||
<Heading :level="4" weight="semibold">{{ $t('common.error') }}</Heading>
|
||||
<Text tone="muted">{{ error }}</Text>
|
||||
</Stack>
|
||||
</Alert>
|
||||
|
||||
<Stack v-if="isLoading" align="center" justify="center" gap="3">
|
||||
<Spinner />
|
||||
<Text tone="muted">{{ t('clientProfile.states.loading') }}</Text>
|
||||
</Stack>
|
||||
|
||||
<template v-else>
|
||||
<Card padding="lg">
|
||||
<Grid :cols="1" :lg="3" :gap="8">
|
||||
<GridItem :lg="2">
|
||||
<Stack gap="4">
|
||||
<form @submit.prevent="updateProfile">
|
||||
<Stack gap="4">
|
||||
<Input
|
||||
v-model="profileForm.firstName"
|
||||
type="text"
|
||||
:label="$t('profile.first_name')"
|
||||
:placeholder="$t('profile.first_name_placeholder')"
|
||||
/>
|
||||
<Input
|
||||
v-model="profileForm.lastName"
|
||||
type="text"
|
||||
:label="$t('profile.last_name')"
|
||||
:placeholder="$t('profile.last_name_placeholder')"
|
||||
/>
|
||||
<Input
|
||||
v-model="profileForm.phone"
|
||||
type="tel"
|
||||
:label="$t('profile.phone')"
|
||||
:placeholder="$t('profile.phone_placeholder')"
|
||||
/>
|
||||
<Button type="submit" :full-width="true" :disabled="isUpdating">
|
||||
<template v-if="isUpdating">{{ $t('profile.saving') }}...</template>
|
||||
<template v-else>{{ $t('profile.save') }}</template>
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
</GridItem>
|
||||
|
||||
<GridItem>
|
||||
<Stack gap="6" align="center">
|
||||
<Stack gap="3" align="center">
|
||||
<Heading :level="3">{{ $t('profile.avatar') }}</Heading>
|
||||
<UserAvatar
|
||||
:userId="userData?.id"
|
||||
:firstName="userData?.firstName"
|
||||
:lastName="userData?.lastName"
|
||||
:avatarId="userData?.avatarId"
|
||||
@avatar-changed="handleAvatarChange"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Card>
|
||||
</template>
|
||||
</Stack>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ['auth-oidc']
|
||||
})
|
||||
|
||||
const localePath = useLocalePath()
|
||||
const { t } = useI18n()
|
||||
const { mutate } = useGraphQL()
|
||||
|
||||
const userData = useState<{
|
||||
id?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
phone?: string | null
|
||||
avatarId?: string | null
|
||||
} | null>('me', () => null)
|
||||
const isLoading = ref(true)
|
||||
const hasError = ref(false)
|
||||
const error = ref('')
|
||||
const isUpdating = ref(false)
|
||||
const avatarDraftId = ref<string | null>(null)
|
||||
|
||||
const profileForm = reactive({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
phone: ''
|
||||
})
|
||||
|
||||
const syncProfileForm = () => {
|
||||
if (!userData.value) {
|
||||
hasError.value = true
|
||||
error.value = t('clientProfile.error.load')
|
||||
isLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
hasError.value = false
|
||||
error.value = ''
|
||||
profileForm.firstName = userData.value.firstName || ''
|
||||
profileForm.lastName = userData.value.lastName || ''
|
||||
profileForm.phone = userData.value.phone || ''
|
||||
avatarDraftId.value = userData.value.avatarId || null
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const updateProfile = async () => {
|
||||
try {
|
||||
isUpdating.value = true
|
||||
|
||||
const { UpdateUserDocument } = await import('~/composables/graphql/user/teams-generated')
|
||||
const result = await mutate(UpdateUserDocument, {
|
||||
userId: userData.value.id,
|
||||
input: {
|
||||
firstName: profileForm.firstName,
|
||||
lastName: profileForm.lastName,
|
||||
phone: profileForm.phone,
|
||||
avatarId: avatarDraftId.value || null
|
||||
},
|
||||
}, 'user', 'teams')
|
||||
|
||||
if (result?.updateUser?.user) {
|
||||
userData.value = { ...(userData.value || {}), ...result.updateUser.user }
|
||||
avatarDraftId.value = userData.value.avatarId || avatarDraftId.value
|
||||
}
|
||||
} catch (err) {
|
||||
hasError.value = true
|
||||
error.value = err?.message || t('clientProfile.error.save')
|
||||
} finally {
|
||||
isUpdating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAvatarChange = async (newAvatarId?: string) => {
|
||||
if (!newAvatarId) return
|
||||
// Only stage avatar change; will be saved on form submit
|
||||
avatarDraftId.value = newAvatarId
|
||||
}
|
||||
|
||||
watch(userData, () => {
|
||||
syncProfileForm()
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
Reference in New Issue
Block a user