Initial commit
This commit is contained in:
20
app/pages/confirm.vue
Normal file
20
app/pages/confirm.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<UEmpty
|
||||
:title="$t('confirm.signingIn')"
|
||||
:description="$t('confirm.redirectedSoon')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'empty'
|
||||
})
|
||||
|
||||
const { isAuthenticated } = storeToRefs(useUser())
|
||||
|
||||
watch(isAuthenticated, () => {
|
||||
if (isAuthenticated.value) {
|
||||
return navigateTo('/profile')
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
41
app/pages/index.vue
Normal file
41
app/pages/index.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<UPageHero
|
||||
:title="$t('hero.title')"
|
||||
:description="$t('hero.description')"
|
||||
:links="links"
|
||||
/>
|
||||
<div class="flex justify-center mt-8">
|
||||
<CounterWidget v-if="isAuthenticated" />
|
||||
<UEmpty
|
||||
v-else
|
||||
icon="i-lucide-user-x"
|
||||
:title="$t('counter.notAuthenticated')"
|
||||
:description="$t('counter.signInToUse')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ButtonProps } from '@nuxt/ui'
|
||||
|
||||
const { isAuthenticated } = storeToRefs(useUser())
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const links = computed(() => [
|
||||
{
|
||||
label: t('hero.signUp'),
|
||||
to: '/login',
|
||||
icon: 'i-lucide-log-in'
|
||||
},
|
||||
{
|
||||
label: t('hero.profile'),
|
||||
to: '/profile',
|
||||
color: 'neutral',
|
||||
variant: 'subtle',
|
||||
trailingIcon: 'i-lucide-arrow-right'
|
||||
}
|
||||
] as ButtonProps[])
|
||||
</script>
|
||||
110
app/pages/login.vue
Normal file
110
app/pages/login.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<UAuthForm
|
||||
:fields="fields"
|
||||
:schema="schema"
|
||||
:providers="providers"
|
||||
:title="$t('login.title')"
|
||||
icon="i-lucide-lock"
|
||||
@submit="onSubmit($event)"
|
||||
>
|
||||
<template #footer>
|
||||
{{ $t('login.agree') }}
|
||||
<ULink
|
||||
to="/"
|
||||
class="text-primary font-medium"
|
||||
>
|
||||
{{ $t('login.terms') }}
|
||||
</ULink>.
|
||||
</template>
|
||||
</UAuthForm>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
import type { AuthFormField } from '@nuxt/ui/runtime/components/AuthForm.vue.js'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'empty'
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
// The otpId is used to track the OTP request and verify the user
|
||||
// null if no request was sent yet
|
||||
const otpId = ref<string | null>(null)
|
||||
|
||||
const fields = computed(() => {
|
||||
const fields: AuthFormField[] = [{
|
||||
name: 'email',
|
||||
type: 'text',
|
||||
label: t('login.email'),
|
||||
placeholder: t('login.emailPlaceholder'),
|
||||
required: true
|
||||
}]
|
||||
if (otpId.value !== null) {
|
||||
fields.push({
|
||||
name: 'otp',
|
||||
type: 'otp',
|
||||
label: t('login.otp'),
|
||||
length: 6,
|
||||
size: 'xl'
|
||||
})
|
||||
}
|
||||
return fields
|
||||
})
|
||||
|
||||
const { signInWithEmail, signInWithOAuth, signInWithOtp } = useUser()
|
||||
|
||||
const oAuthAndRedirect = async (provider: 'apple' | 'google') => {
|
||||
try {
|
||||
await signInWithOAuth(provider)
|
||||
navigateTo('/confirm')
|
||||
} catch (error) {
|
||||
toast.add({ color: 'error', description: (error as Error).message })
|
||||
}
|
||||
}
|
||||
|
||||
const providers = [
|
||||
{
|
||||
label: 'Google',
|
||||
icon: 'i-simple-icons-google',
|
||||
onClick: () => oAuthAndRedirect('google')
|
||||
},
|
||||
{
|
||||
label: 'Apple',
|
||||
icon: 'i-simple-icons-apple',
|
||||
onClick: () => oAuthAndRedirect('apple')
|
||||
}
|
||||
]
|
||||
|
||||
const schema = z.object({
|
||||
email: z.email(t('login.invalidEmail')),
|
||||
otp: z.array(z.string()).optional().transform(val => val?.join(''))
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
const { locale } = useI18n()
|
||||
async function onSubmit(payload: FormSubmitEvent<Schema>) {
|
||||
try {
|
||||
if (otpId.value !== null && payload.data.otp?.length) {
|
||||
// If OTP is provided, sign in with OTP
|
||||
await signInWithOtp(otpId.value, payload.data.otp)
|
||||
navigateTo('/confirm')
|
||||
} else {
|
||||
// If OTP is not provided, sign in with email (send OTP)
|
||||
otpId.value = await signInWithEmail(payload.data.email, locale.value)
|
||||
toast.add({
|
||||
title: t('login.emailSent'),
|
||||
description: t('login.emailSentDescription'),
|
||||
icon: 'i-lucide-check',
|
||||
color: 'success'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
toast.add({ color: 'error', description: (error as Error).message })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
142
app/pages/profile.vue
Normal file
142
app/pages/profile.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<UForm
|
||||
id="settings"
|
||||
:schema="profileSchema"
|
||||
:state="profile"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<UPageCard
|
||||
:title="$t('profile.title')"
|
||||
:description="$t('profile.description')"
|
||||
variant="naked"
|
||||
orientation="horizontal"
|
||||
class="mb-4"
|
||||
>
|
||||
<UButton
|
||||
form="settings"
|
||||
:label="$t('profile.save')"
|
||||
color="neutral"
|
||||
type="submit"
|
||||
class="w-fit lg:ms-auto"
|
||||
/>
|
||||
</UPageCard>
|
||||
|
||||
<UPageCard variant="subtle">
|
||||
<UFormField
|
||||
name="name"
|
||||
:label="$t('profile.name')"
|
||||
:description="$t('profile.nameDescription')"
|
||||
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('profile.email')"
|
||||
:description="$t('profile.emailDescription')"
|
||||
required
|
||||
class="flex max-sm:flex-col justify-between items-start gap-4"
|
||||
>
|
||||
<UInput
|
||||
:model-value="user?.email"
|
||||
type="email"
|
||||
autocomplete="off"
|
||||
disabled
|
||||
/>
|
||||
</UFormField>
|
||||
<USeparator />
|
||||
<UFormField
|
||||
name="avatar"
|
||||
:label="$t('profile.avatar')"
|
||||
:description="$t('profile.avatarDescription')"
|
||||
class="flex max-sm:flex-col justify-between sm:items-center gap-4"
|
||||
>
|
||||
<ProfileAvatarUpload />
|
||||
</UFormField>
|
||||
<USeparator />
|
||||
<UFormField
|
||||
name="delete"
|
||||
:label="$t('profile.delete')"
|
||||
:description="$t('profile.deleteDescription')"
|
||||
class="flex max-sm:flex-col justify-between sm:items-center gap-4"
|
||||
>
|
||||
<UModal
|
||||
:title="$t('profile.delete')"
|
||||
:description="$t('profile.deleteWarning')"
|
||||
:ui="{ footer: 'justify-end' }"
|
||||
:close="false"
|
||||
>
|
||||
<UButton
|
||||
:label="$t('profile.delete')"
|
||||
color="error"
|
||||
/>
|
||||
<template #footer="{ close }">
|
||||
<UButton
|
||||
:label="$t('cancel')"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
@click="close"
|
||||
/>
|
||||
<UButton
|
||||
:label="$t('profile.delete')"
|
||||
color="error"
|
||||
@click="onDelete"
|
||||
/>
|
||||
</template>
|
||||
</UModal>
|
||||
</UFormField>
|
||||
</UPageCard>
|
||||
</UForm>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['auth']
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const { user, updateUser, deleteUser } = useUser()
|
||||
|
||||
const profileSchema = z.object({
|
||||
name: z.string().min(2, t('profile.tooShort'))
|
||||
})
|
||||
|
||||
type ProfileSchema = z.output<typeof profileSchema>
|
||||
const profile = reactive<Partial<ProfileSchema>>({
|
||||
name: user?.name ?? ''
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
async function onSubmit() {
|
||||
try {
|
||||
if (profile.name) {
|
||||
await updateUser({ name: profile.name })
|
||||
}
|
||||
toast.add({
|
||||
title: t('profile.success'),
|
||||
description: t('profile.settingsUpdated'),
|
||||
icon: 'i-lucide-check',
|
||||
color: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({ color: 'error', description: (error as Error).message })
|
||||
console.warn(error)
|
||||
}
|
||||
}
|
||||
|
||||
const onDelete = async () => {
|
||||
try {
|
||||
await deleteUser()
|
||||
navigateTo('/')
|
||||
} catch (error) {
|
||||
toast.add({ color: 'error', description: (error as Error).message })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user