index.vue 5.11 KB
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'

const { t } = useAppI18n()
const toast = useToast()
const isSubmitting = ref(false)
const settingsApi = useSettingsApi()

const fileRef = ref<HTMLInputElement>()

const buildProfileSchema = () => z.object({
  name: z.string().min(2, t('settings.profile.validation.tooShort')),
  email: z.string().email(t('settings.profile.validation.invalidEmail')),
  username: z.string().min(2, t('settings.profile.validation.tooShort')),
  avatar: z.string().optional(),
  bio: z.string().optional()
})

const profileSchema = computed(buildProfileSchema)

type ProfileSchema = z.output<ReturnType<typeof buildProfileSchema>>

const { data: initialProfile } = await settingsApi.getProfile()

const profile = reactive<ProfileSchema>({
  name: initialProfile.value.name,
  email: initialProfile.value.email,
  username: initialProfile.value.username,
  avatar: initialProfile.value.avatar,
  bio: initialProfile.value.bio
})

async function onSubmit(event: FormSubmitEvent<ProfileSchema>) {
  isSubmitting.value = true

  try {
    const result = await settingsApi.updateProfile(event.data)

    if (!result.success) {
      toast.add({
        title: t('common.error'),
        description: result.message,
        icon: 'i-lucide-circle-alert',
        color: 'error'
      })
      return
    }

    if (result.profile) {
      profile.name = result.profile.name
      profile.email = result.profile.email
      profile.username = result.profile.username
      profile.avatar = result.profile.avatar
      profile.bio = result.profile.bio
    }

    toast.add({
      title: t('settings.profile.toastTitle'),
      description: t('settings.profile.toastDesc'),
      icon: 'i-lucide-check',
      color: 'success'
    })
  } catch {
    toast.add({
      title: t('common.error'),
      description: t('common.requestFailed'),
      icon: 'i-lucide-circle-alert',
      color: 'error'
    })
  } finally {
    isSubmitting.value = false
  }
}

function onFileChange(e: Event) {
  const input = e.target as HTMLInputElement

  if (!input.files?.length) {
    return
  }

  profile.avatar = URL.createObjectURL(input.files[0]!)
}

function onFileClick() {
  fileRef.value?.click()
}
</script>

<template>
  <UForm
    id="settings"
    :schema="profileSchema"
    :state="profile"
    @submit="onSubmit"
  >
    <UPageCard
      :title="t('settings.profile.title')"
      :description="t('settings.profile.description')"
      variant="naked"
      orientation="horizontal"
      class="mb-4"
    >
      <UButton
        form="settings"
        :label="t('common.saveChanges')"
        color="neutral"
        type="submit"
        :loading="isSubmitting"
        class="w-fit lg:ms-auto"
      />
    </UPageCard>

    <UPageCard variant="subtle">
      <UFormField
        name="name"
        :label="t('settings.profile.name')"
        :description="t('settings.profile.nameDesc')"
        required
        class="flex max-sm:flex-col justify-between items-start gap-4"
      >
        <UInput
          v-model="profile.name"
          autocomplete="off"
        />
      </UFormField>
      <USeparator />
      <UFormField
        name="email"
        :label="t('settings.profile.email')"
        :description="t('settings.profile.emailDesc')"
        required
        class="flex max-sm:flex-col justify-between items-start gap-4"
      >
        <UInput
          v-model="profile.email"
          type="email"
          autocomplete="off"
        />
      </UFormField>
      <USeparator />
      <UFormField
        name="username"
        :label="t('settings.profile.username')"
        :description="t('settings.profile.usernameDesc')"
        required
        class="flex max-sm:flex-col justify-between items-start gap-4"
      >
        <UInput
          v-model="profile.username"
          type="username"
          autocomplete="off"
        />
      </UFormField>
      <USeparator />
      <UFormField
        name="avatar"
        :label="t('settings.profile.avatar')"
        :description="t('settings.profile.avatarDesc')"
        class="flex max-sm:flex-col justify-between sm:items-center gap-4"
      >
        <div class="flex flex-wrap items-center gap-3">
          <UAvatar
            :src="profile.avatar"
            :alt="profile.name"
            size="lg"
          />
          <UButton
            :label="t('common.choose')"
            color="neutral"
            type="button"
            @click="onFileClick"
          />
          <input
            ref="fileRef"
            type="file"
            class="hidden"
            accept=".jpg, .jpeg, .png, .gif"
            @change="onFileChange"
          >
        </div>
      </UFormField>
      <USeparator />
      <UFormField
        name="bio"
        :label="t('settings.profile.bio')"
        :description="t('settings.profile.bioDesc')"
        class="flex max-sm:flex-col justify-between items-start gap-4"
        :ui="{ container: 'w-full' }"
      >
        <UTextarea
          v-model="profile.bio"
          :rows="5"
          autoresize
          class="w-full"
        />
      </UFormField>
    </UPageCard>
  </UForm>
</template>