Initial commit

This commit is contained in:
2026-04-17 23:26:01 +00:00
commit 2ea4ca5d52
409 changed files with 63459 additions and 0 deletions

20
app/pages/confirm.vue Normal file
View 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
View 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
View 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
View 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>