feat: add registration functionality with form validation and Google authentication
This commit is contained in:
parent
b3eab9320c
commit
c307593a1b
215
app/components/RegisterForm.vue
Normal file
215
app/components/RegisterForm.vue
Normal file
@ -0,0 +1,215 @@
|
||||
<script setup lang="ts">
|
||||
import { toast } from 'vue-sonner'
|
||||
import * as z from 'zod'
|
||||
|
||||
useSeoMeta({
|
||||
title: 'Daftar Akun',
|
||||
ogTitle: 'Daftar Akun',
|
||||
description: 'Ayo daftar dan nikmati layanan kami!',
|
||||
ogDescription: 'Ayo daftar dan nikmati layanan kami!',
|
||||
})
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const { signInWithGoogle, loading: googleLoading } = useGoogleAuth()
|
||||
|
||||
const fullName = ref('')
|
||||
const email = ref('')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const passwordConfirmation = ref('')
|
||||
const errors = ref<Record<string, string>>({})
|
||||
|
||||
const schema = z.object({
|
||||
full_name: z.string().min(1, 'Nama lengkap wajib diisi').max(200, 'Nama lengkap maksimal 200 karakter'),
|
||||
email: z.string().min(1, 'Email wajib diisi').email('Format email tidak valid').max(100, 'Email maksimal 100 karakter'),
|
||||
username: z.string().min(1, 'Username wajib diisi').max(20, 'Username maksimal 20 karakter'),
|
||||
password: z.string().min(8, 'Kata sandi minimal 8 karakter'),
|
||||
password_confirmation: z.string().min(1, 'Konfirmasi kata sandi wajib diisi'),
|
||||
}).refine(data => data.password === data.password_confirmation, {
|
||||
message: 'Kata sandi tidak cocok',
|
||||
path: ['password_confirmation'],
|
||||
})
|
||||
|
||||
async function onSubmit() {
|
||||
errors.value = {}
|
||||
|
||||
const result = schema.safeParse({
|
||||
full_name: fullName.value,
|
||||
email: email.value,
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
password_confirmation: passwordConfirmation.value,
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
for (const issue of result.error.issues) {
|
||||
const field = issue.path[0] as string
|
||||
errors.value[field] = issue.message
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await $fetch(`${config.public.apiBase}/auth/register`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
full_name: fullName.value,
|
||||
email: email.value,
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
password_confirmation: passwordConfirmation.value,
|
||||
}
|
||||
})
|
||||
toast.success('Akun berhasil dibuat!', {
|
||||
description: `Selamat datang, ${fullName.value}!`,
|
||||
})
|
||||
router.push('/')
|
||||
} catch (error: any) {
|
||||
const detail = error?.data?.detail
|
||||
if (detail && Array.isArray(detail)) {
|
||||
const fieldErrors = detail
|
||||
.filter((e: any) => e.loc?.length >= 2)
|
||||
.map((e: any) => ({
|
||||
path: e.loc[e.loc.length - 1],
|
||||
message: e.msg
|
||||
}))
|
||||
for (const fe of fieldErrors) {
|
||||
errors.value[fe.path] = fe.message
|
||||
}
|
||||
} else {
|
||||
toast.error('Gagal mendaftar', {
|
||||
description: error?.data?.message || 'Ups, terjadi kesalahan. Silakan coba lagi beberapa saat.',
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGoogleRegister() {
|
||||
await signInWithGoogle(
|
||||
(result) => {
|
||||
toast.success('Berhasil mendaftar!', {
|
||||
description: `Selamat datang, ${result.user.email}!`,
|
||||
})
|
||||
router.push('/')
|
||||
},
|
||||
(errorMsg) => {
|
||||
toast.error('Gagal mendaftar', {
|
||||
description: errorMsg,
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Card>
|
||||
<CardHeader class="text-center">
|
||||
<CardTitle class="text-xl">
|
||||
Buat akun baru
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Kelola bisnis Anda dengan mudah dan efisien bersama platform kami.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form @submit.prevent="onSubmit">
|
||||
<FieldGroup>
|
||||
<Field :class="{ 'text-destructive': errors.full_name }">
|
||||
<FieldLabel for="full_name">
|
||||
Nama Lengkap <span class="text-destructive">*</span>
|
||||
</FieldLabel>
|
||||
<Input id="full_name" v-model="fullName" type="text" placeholder="Masukkan nama lengkap"
|
||||
:class="{ 'border-destructive': errors.full_name }" required />
|
||||
<p v-if="errors.full_name" class="text-sm text-destructive mt-1">
|
||||
{{ errors.full_name }}
|
||||
</p>
|
||||
</Field>
|
||||
<Field :class="{ 'text-destructive': errors.email }">
|
||||
<FieldLabel for="email">
|
||||
Email <span class="text-destructive">*</span>
|
||||
</FieldLabel>
|
||||
<Input id="email" v-model="email" type="email" placeholder="Masukkan email"
|
||||
:class="{ 'border-destructive': errors.email }" required />
|
||||
<p v-if="errors.email" class="text-sm text-destructive mt-1">
|
||||
{{ errors.email }}
|
||||
</p>
|
||||
</Field>
|
||||
<Field :class="{ 'text-destructive': errors.username }">
|
||||
<FieldLabel for="username">
|
||||
Username <span class="text-destructive">*</span>
|
||||
</FieldLabel>
|
||||
<Input id="username" v-model="username" type="text" placeholder="Masukkan username"
|
||||
:class="{ 'border-destructive': errors.username }" required />
|
||||
<p v-if="errors.username" class="text-sm text-destructive mt-1">
|
||||
{{ errors.username }}
|
||||
</p>
|
||||
</Field>
|
||||
<Field :class="{ 'text-destructive': errors.password }">
|
||||
<FieldLabel for="password">
|
||||
Kata Sandi <span class="text-destructive">*</span>
|
||||
</FieldLabel>
|
||||
<Input id="password" v-model="password" type="password" placeholder="Buat kata sandi"
|
||||
:class="{ 'border-destructive': errors.password }" required />
|
||||
<p v-if="errors.password" class="text-sm text-destructive mt-1">
|
||||
{{ errors.password }}
|
||||
</p>
|
||||
</Field>
|
||||
<Field :class="{ 'text-destructive': errors.password_confirmation }">
|
||||
<FieldLabel for="password_confirmation">
|
||||
Konfirmasi Kata Sandi <span class="text-destructive">*</span>
|
||||
</FieldLabel>
|
||||
<Input id="password_confirmation" v-model="passwordConfirmation" type="password"
|
||||
placeholder="Konfirmasi kata sandi"
|
||||
:class="{ 'border-destructive': errors.password_confirmation }" required />
|
||||
<p v-if="errors.password_confirmation" class="text-sm text-destructive mt-1">
|
||||
{{ errors.password_confirmation }}
|
||||
</p>
|
||||
</Field>
|
||||
<Field>
|
||||
<Button type="submit" :disabled="loading">
|
||||
<span v-if="loading" class="i-lucide-loader-2 h-4 w-4 animate-spin mr-2" />
|
||||
Daftar
|
||||
</Button>
|
||||
<FieldDescription class="text-center">
|
||||
Sudah punya akun?
|
||||
<NuxtLink to="/auth/login" class="text-primary hover:underline">
|
||||
Masuk
|
||||
</NuxtLink>
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<FieldSeparator class="*:data-[slot=field-separator-content]:bg-card">
|
||||
Atau daftar dengan
|
||||
</FieldSeparator>
|
||||
<Field>
|
||||
<Button variant="outline" type="button" :disabled="googleLoading" @click="handleGoogleRegister">
|
||||
<span v-if="googleLoading" class="i-lucide-loader-2 h-4 w-4 animate-spin mr-2" />
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="h-4 w-4 mr-2">
|
||||
<path
|
||||
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
Daftar dengan Google
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<FieldDescription class="px-6 text-center">
|
||||
Dengan mendaftar, Anda menyetujui
|
||||
<NuxtLink to="/terms" class="text-primary hover:underline">
|
||||
Ketentuan Layanan
|
||||
</NuxtLink>
|
||||
dan
|
||||
<NuxtLink to="/privacy" class="text-primary hover:underline">
|
||||
Kebijakan Privasi
|
||||
</NuxtLink>.
|
||||
</FieldDescription>
|
||||
</div>
|
||||
</template>
|
||||
14
app/pages/auth/register/index.vue
Normal file
14
app/pages/auth/register/index.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RegisterForm from '@/components/RegisterForm.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||
<div class="flex w-full max-w-sm flex-col gap-6">
|
||||
<RegisterForm />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
51
docs/history/2026-07-23-register-form-functionality.md
Normal file
51
docs/history/2026-07-23-register-form-functionality.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Register Form Functionality - Session 1
|
||||
|
||||
**Tanggal:** 2026-07-23
|
||||
**Status:** Selesai
|
||||
|
||||
## Tujuan
|
||||
|
||||
Menambahkan fungsionalitas register pada `web2/app/components/RegisterForm.vue` dengan referensi dari `web/app/pages/auth/register.vue`, menggunakan UI dari `web2/app/components/LoginForm.vue`.
|
||||
|
||||
## Yang Dikerjakan
|
||||
|
||||
### 1. Buat `app/components/RegisterForm.vue`
|
||||
- Validasi form dengan Zod (`full_name`, `email`, `username`, `password`, `password_confirmation`).
|
||||
- Validasi `password_confirmation` harus cocok dengan `password` menggunakan `.refine()`.
|
||||
- Submit handler: `POST /auth/register` via `$fetch`.
|
||||
- Google register handler: `useGoogleAuth().signInWithGoogle()`.
|
||||
- Loading state pada tombol Daftar dan tombol Google.
|
||||
- Error handling: field-level errors + toast notification (success/error).
|
||||
- Navigasi: `<NuxtLink>` untuk "Masuk", "Ketentuan Layanan", "Kebijakan Privasi".
|
||||
|
||||
### 2. Buat `app/pages/auth/register/index.vue`
|
||||
- Page wrapper yang merender `<RegisterForm />`.
|
||||
- Layout mirip dengan login page.
|
||||
|
||||
## File yang Dibuat
|
||||
|
||||
| File | Aksi |
|
||||
|------|------|
|
||||
| `app/components/RegisterForm.vue` | Dibuat |
|
||||
| `app/pages/auth/register/index.vue` | Dibuat |
|
||||
|
||||
## API yang Digunakan
|
||||
|
||||
- `POST /v1/auth/register` - Body: `{ full_name, email, username, password, password_confirmation }`
|
||||
- `POST /v1/auth/google-login` - Body: `{ credential }` (Google JWT)
|
||||
|
||||
## Fields Form
|
||||
|
||||
| Field | Type | Validasi |
|
||||
|-------|------|----------|
|
||||
| `full_name` | text | wajib, max 200 karakter |
|
||||
| `email` | email | wajib, format email, max 100 karakter |
|
||||
| `username` | text | wajib, max 20 karakter |
|
||||
| `password` | password | wajib, min 8 karakter |
|
||||
| `password_confirmation` | password | wajib, harus cocok dengan password |
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Field-level errors dari API response (`error.data.detail`)
|
||||
- Toast notification untuk error umum
|
||||
- Validasi client-side sebelum submit
|
||||
Loading…
Reference in New Issue
Block a user