This commit is contained in:
parent
62903bf565
commit
e60e47d28e
|
@ -8,6 +8,9 @@ export default defineAppConfig({
|
|||
}
|
||||
},
|
||||
width: 'w-screen max-w-sm',
|
||||
},
|
||||
modal: {
|
||||
container: 'flex min-h-full items-center justify-center text-center',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<div class="relative rounded-lg overflow-hidden p-4 ">
|
||||
<slot></slot>
|
||||
<div class="p-4">
|
||||
<div class="flex flex-col items-center space-y-3 mb-4">
|
||||
<NuxtImg src="/assets/icons/logo.png" height="44" width="44" type="webp" />
|
||||
<h2 class="text-2xl font-bold mt-[0_!important]">
|
||||
<span class="text-blue-700">Stok</span>
|
||||
<span class="text-yellow-300">In</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div v-if="authSectionIsLoading"
|
||||
class="absolute top-0 left-0 right-0 bottom-0 z-30 flex backdrop-brightness-75">
|
||||
<div class="flex flex-col w-1/2 m-auto items-center justify-center">
|
||||
<OverlayLoadingPadlock class="scale-50" />
|
||||
<p>Please Wait...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="authSection === 'login'">
|
||||
<AuthLogin @is-loading="e => authSectionIsLoading = e"
|
||||
@section:forgot-password="authSection = 'forgot-password'" />
|
||||
<div class="mt-4 flex justify-center items-center">
|
||||
<span>
|
||||
Tidak punya akun?
|
||||
</span>
|
||||
<NuxtUiButton label="Daftar" variant="link" @click="authSection = 'register'" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="authSection === 'register'">
|
||||
<AuthRegister @is-loading="e => authSectionIsLoading = e" />
|
||||
<div class="mt-4 flex justify-center items-center">
|
||||
<span>
|
||||
Sudah punya akun?
|
||||
</span>
|
||||
<NuxtUiButton label="Masuk" variant="link" @click="authSection = 'login'" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="authSection === 'forgot-password'">
|
||||
<AuthForgotPassword @is-loading="e => authSectionIsLoading = e" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { AuthForgotPassword } from '#components';
|
||||
|
||||
const authSection = useState<'login' | 'register' | 'forgot-password'>('auth-section', () => 'login')
|
||||
const authSectionIsLoading = ref<boolean>(false)
|
||||
</script>
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 text-center mb-4">
|
||||
Enter your email to receive password reset instructions
|
||||
</p>
|
||||
<NuxtUiForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit" v-if="!isEmailSent">
|
||||
<div class="flex gap-y-1 mt-4 divide-x-2 justify-end">
|
||||
<NuxtUiButton label="Login" variant="link" @click="authSection = 'login'" />
|
||||
<NuxtUiButton label="Register" variant="link" @click="authSection = 'register'" />
|
||||
</div>
|
||||
<NuxtUiFormGroup label="Email" name="email" :error="invalidCredentials ? 'Email not registered' : ''">
|
||||
<NuxtUiInput v-model="state.email" icon="i-heroicons-envelope" />
|
||||
</NuxtUiFormGroup>
|
||||
<NuxtUiButton :loading="isSubmitting" type="submit">
|
||||
Submit
|
||||
</NuxtUiButton>
|
||||
</NuxtUiForm>
|
||||
<div v-else>
|
||||
<NuxtUiCard class="p-4 text-center space-y-2">
|
||||
<NuxtUiIcon name="lucide:check-circle" class="text-green-600 w-6 h-6 mx-auto" />
|
||||
<h3 class="text-lg font-semibold">Email Sent</h3>
|
||||
<p class="text-sm text-gray-500">
|
||||
We've sent you an email with a link to reset your password.
|
||||
Please check your inbox and follow the instructions provided.
|
||||
</p>
|
||||
</NuxtUiCard>
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<NuxtUiButton to="/auth/forgot-password/success">true</NuxtUiButton>
|
||||
<NuxtUiButton to="/auth/forgot-password/invalid">false</NuxtUiButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { z } from 'zod';
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
|
||||
const authSection = useState<'login' | 'register' | 'forgot-password'>('auth-section', () => 'login')
|
||||
|
||||
const isSubmitting = ref<boolean>(false)
|
||||
const invalidCredentials = ref<boolean>(false)
|
||||
const isEmailSent = ref<boolean>(false)
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('Invalid email')
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
const state = reactive({
|
||||
email: undefined,
|
||||
})
|
||||
|
||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
isSubmitting.value = true
|
||||
isEmailSent.value = false
|
||||
setTimeout(() => {
|
||||
const { email } = event.data
|
||||
if (email === 'fahim@gmail.com') {
|
||||
isSubmitting.value = false
|
||||
invalidCredentials.value = false
|
||||
isEmailSent.value = true
|
||||
return
|
||||
}
|
||||
|
||||
invalidCredentials.value = true
|
||||
isSubmitting.value = false
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,68 @@
|
|||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
|
||||
const route = useRoute();
|
||||
const emit = defineEmits(['is-loading', 'section:forgot-password'])
|
||||
|
||||
const invalidCredentials = ref<boolean>(false)
|
||||
|
||||
const isSubmitting = ref<boolean>(false)
|
||||
watch(isSubmitting, newVal => emit('is-loading', newVal))
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('Invalid email'),
|
||||
password: z.string().min(8, 'Must be at least 8 characters')
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
const state = reactive({
|
||||
email: undefined,
|
||||
password: undefined
|
||||
})
|
||||
|
||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
isSubmitting.value = true
|
||||
setTimeout(() => {
|
||||
const { email, password } = event.data
|
||||
if (email === 'fahim@gmail.com' && password === '99999999') {
|
||||
isSubmitting.value = false
|
||||
invalidCredentials.value = false
|
||||
console.log('✅ Data User:', event.data)
|
||||
return
|
||||
}
|
||||
|
||||
invalidCredentials.value = true
|
||||
isSubmitting.value = false
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p class="text-sm text-gray-500 text-center mb-4">
|
||||
Login to access your stock predictions dashboard
|
||||
</p>
|
||||
|
||||
<NuxtUiForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<NuxtUiFormGroup label="Email" name="email" :error="invalidCredentials ? 'Invalid Email or Password' : ''">
|
||||
<NuxtUiInput v-model="state.email" icon="i-heroicons-envelope" />
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<NuxtUiFormGroup label="Password" name="password"
|
||||
:error="invalidCredentials ? 'Invalid Email or Password' : ''">
|
||||
<template #hint>
|
||||
<NuxtUiButton type="button" @click="emit('section:forgot-password')" color="blue" variant="link"
|
||||
class="pe-0">
|
||||
Forgot Password?
|
||||
</NuxtUiButton>
|
||||
</template>
|
||||
<NuxtUiInput v-model="state.password" type="password" icon="i-heroicons-lock-closed" id="auth-pw" />
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
|
||||
<NuxtUiButton type="submit" :loading="isSubmitting">
|
||||
Submit
|
||||
</NuxtUiButton>
|
||||
</NuxtUiForm>
|
||||
</template>
|
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<p class="text-sm text-gray-500 text-center mb-4">
|
||||
Create your account to get started with predictive financial insights
|
||||
</p>
|
||||
|
||||
<NuxtUiForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<NuxtUiFormGroup label="Email" name="email" :error="!!usedEmail ? 'Email telah digunakan.' : ''">
|
||||
<NuxtUiInput v-model="state.email" icon="i-heroicons-envelope" />
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<NuxtUiFormGroup label="Password" name="password">
|
||||
<NuxtUiInput v-model="state.password" type="password" icon="i-heroicons-lock-closed" />
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<NuxtUiFormGroup label="Re-Enter Password" name="password_confirmation">
|
||||
<NuxtUiInput v-model="state.password_confirmation" type="password" icon="i-heroicons-lock-closed" />
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<NuxtUiButton type="submit" :loading="isSubmitting">
|
||||
Submit
|
||||
</NuxtUiButton>
|
||||
</NuxtUiForm>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
|
||||
const emit = defineEmits(['is-loading'])
|
||||
|
||||
const usedEmail = ref<string | null>(null)
|
||||
|
||||
const isSubmitting = ref<boolean>(false)
|
||||
watch(isSubmitting, newVal => emit('is-loading', newVal))
|
||||
|
||||
const baseSchema = z.object({
|
||||
email: z.string().email('Invalid email'),
|
||||
password: z.string().min(8, 'Must be at least 8 characters'),
|
||||
password_confirmation: z.string().min(8, 'Must be at least 8 characters'),
|
||||
})
|
||||
|
||||
const schema = baseSchema
|
||||
.refine((data) => data.password === data.password_confirmation, {
|
||||
message: "Passwords don't match",
|
||||
path: ['password_confirmation'],
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
const state = reactive<Schema>({
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: ''
|
||||
})
|
||||
|
||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
isSubmitting.value = true
|
||||
setTimeout(() => {
|
||||
if (event.data.email === 'fahim@gmail.com') {
|
||||
usedEmail.value = 'fahim@gmail.com'
|
||||
isSubmitting.value = false
|
||||
return
|
||||
}
|
||||
|
||||
usedEmail.value = null
|
||||
console.log('✅ Data User:', event.data)
|
||||
isSubmitting.value = false
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
|
@ -4,16 +4,9 @@
|
|||
<footer class="border-t bg-background">
|
||||
<div class=" flex flex-col items-center justify-between gap-4 py-10 md:h-24 md:flex-row md:py-0">
|
||||
<div class="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="text-primary-foreground">
|
||||
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
|
||||
<path d="M21 3v5h-5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<NuxtImg src="assets/icons/logo.png" format="webp" height="24" width="auto" />
|
||||
<p class="text-center text-sm leading-loose text-muted-foreground md:text-left">
|
||||
© 2025 SmartLog. All rights reserved.
|
||||
© 2025 StokIn. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
|
|
|
@ -4,17 +4,9 @@
|
|||
class="fixed left-0 right-0 top-0 z-40 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<NuxtUiContainer>
|
||||
<div class="flex h-16 items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="text-primary-foreground">
|
||||
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
|
||||
<path d="M21 3v5h-5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-xl font-bold">SmartLog</span>
|
||||
</div>
|
||||
<NuxtLink href="/" class="flex items-center gap-2">
|
||||
<NuxtImg src="/assets/icons/logo-text.png" width="auto" height="44" format="webp" />
|
||||
</NuxtLink>
|
||||
|
||||
<nav class="ml-auto flex items-center gap-4 sm:gap-6">
|
||||
<div v-if="window.width.value <= 640">
|
||||
|
@ -29,17 +21,13 @@
|
|||
|
||||
<!-- Navigasi -->
|
||||
<div class="mt-10 space-y-6">
|
||||
<NuxtLink to="/#features" class="text-lg block hover:text-green-500"
|
||||
@click="isOpen = false">
|
||||
Features
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/#how-it-works" class="text-lg block hover:text-green-500"
|
||||
@click="isOpen = false">
|
||||
How It Works
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/#pricing" class="text-lg block hover:text-green-500"
|
||||
<NuxtLink to="/#features" class="text-lg block hover:text-green-500"
|
||||
@click="isOpen = false">
|
||||
Pricing
|
||||
Features
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/demo" class="text-lg block text-green-500 font-semibold"
|
||||
@click="isOpen = false">
|
||||
|
@ -49,37 +37,60 @@
|
|||
|
||||
<!-- Tombol "Get Started" -->
|
||||
<div class="mt-auto flex justify-start">
|
||||
<NuxtUiButton label="Log In" color="green" />
|
||||
<NuxtUiButton color="green" @click="() => {
|
||||
if (route.path.startsWith('/auth/forgot-password')) {
|
||||
navigateTo('/auth')
|
||||
} else {
|
||||
authModalIsOpen = true
|
||||
isOpen = false
|
||||
}
|
||||
}">
|
||||
Log In
|
||||
</NuxtUiButton>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtUiSlideover>
|
||||
</div>
|
||||
<div v-else class="ml-auto flex items-center gap-4 sm:gap-6">
|
||||
<NuxtLink href="/#features" class="text-sm font-medium transition-colors hover:text-primary">
|
||||
Features
|
||||
</NuxtLink>
|
||||
<NuxtLink href="/#how-it-works"
|
||||
class="text-sm font-medium transition-colors hover:text-primary">How
|
||||
It
|
||||
Works</NuxtLink>
|
||||
<NuxtLink href="/#pricing" class="text-sm font-medium transition-colors hover:text-primary">
|
||||
Pricing
|
||||
<NuxtLink href="/#features" class="text-sm font-medium transition-colors hover:text-primary">
|
||||
Features
|
||||
</NuxtLink>
|
||||
<NuxtLink href="/demo"
|
||||
class="text-sm font-medium text-primary transition-colors hover:text-primary/80">
|
||||
Demo</NuxtLink>
|
||||
<NuxtUiButton color="green">
|
||||
Get Started
|
||||
<NuxtUiButton color="green" @click="() => {
|
||||
if (route.path.startsWith('/auth/forgot-password')) {
|
||||
navigateTo('/auth')
|
||||
} else {
|
||||
authModalIsOpen = true
|
||||
}
|
||||
}">
|
||||
Log In
|
||||
</NuxtUiButton>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<NuxtUiModal v-model="authModalIsOpen" prevent-close :ui="{
|
||||
width: 'w-full max-w-[400px]',
|
||||
}">
|
||||
<Auth @vue:mounted="authSection = 'login'">
|
||||
<div class="flex justify-end">
|
||||
<NuxtUiButton icon="i-heroicons-x-mark-16-solid" color="red" @click="authModalIsOpen = false" />
|
||||
</div>
|
||||
</Auth>
|
||||
</NuxtUiModal>
|
||||
</NuxtUiContainer>
|
||||
</header>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
|
||||
const window = useWindowSize()
|
||||
const isOpen = ref(false)
|
||||
const authModalIsOpen = ref<boolean>(false)
|
||||
const route = useRoute();
|
||||
const authSection = useState<'login' | 'register' | 'forgot-password'>('auth-section', () => 'login')
|
||||
</script>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<!-- CTA Section -->
|
||||
<section class="container py-20">
|
||||
<section class=" py-20">
|
||||
<div class="mx-auto max-w-4xl rounded-lg p-8 md:p-12">
|
||||
<div class="flex flex-col items-center justify-between gap-4 text-center md:flex-row md:text-left">
|
||||
<div class="space-y-2">
|
||||
|
@ -11,7 +11,7 @@
|
|||
</p>
|
||||
</div>
|
||||
<NuxtUiButton color="green"
|
||||
class="h-10 items-center justify-center rounded-md px-8 text-sm font-medium shadow ">
|
||||
class="h-10 items-center justify-center rounded-md px-8 text-sm font-medium shadow " to="/demo">
|
||||
<span>
|
||||
Try Demo
|
||||
</span>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="container py-20">
|
||||
<section id="features" class=" py-20">
|
||||
<div class="mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center">
|
||||
<h2 class="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">Powerful Prediction Features
|
||||
</h2>
|
||||
|
|
|
@ -1,54 +1,32 @@
|
|||
<template>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="container py-20 md:py-32">
|
||||
<div class="grid gap-10 md:grid-cols-2 md:gap-16">
|
||||
<div class="flex flex-col justify-center space-y-6 sm:pb-32">
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
|
||||
Predict the Future with ARIMA
|
||||
</h1>
|
||||
<p class="text-lg text-muted-foreground md:text-xl">
|
||||
Harness the power of ARIMA to make accurate predictions and informed decisions.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 min-[400px]:flex-row">
|
||||
<NuxtUiButton
|
||||
class="inline-flex h-10 items-center justify-center rounded-md px-8 text-sm font-medium shadow"
|
||||
to="/demo">
|
||||
Try Demo
|
||||
</NuxtUiButton>
|
||||
<NuxtUiButton variant="outline" color="white"
|
||||
class="inline-flex h-10 items-center justify-center rounded-md px-8 text-sm font-medium shadow">
|
||||
Learn More
|
||||
</NuxtUiButton>
|
||||
</div>
|
||||
<section class="mt-20 pb-20 md:mt-32 md:pb-32 mx-auto min-h-screen flex gap-3 flex-col sm:flex-row">
|
||||
<div class="flex flex-col justify-center space-y-6 sm:w-[60%] py-10">
|
||||
<div class="space-y-2">
|
||||
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
|
||||
Predict the Future with ARIMA
|
||||
</h1>
|
||||
<p class="text-lg text-muted-foreground md:text-xl">
|
||||
Harness the power of ARIMA to make accurate predictions and informed decisions.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<div
|
||||
class="relative h-[350px] w-[350px] rounded-full bg-gradient-to-r from-primary/20 to-primary/40 p-4">
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="h-[250px] w-[250px] rounded-full bg-background p-4 shadow-lg">
|
||||
<div
|
||||
class="flex h-full w-full items-center justify-center rounded-full bg-gradient-to-br from-primary/10 to-primary/30">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round"
|
||||
stroke-linejoin="round" class="text-primary">
|
||||
<path d="M12 2v4"></path>
|
||||
<path d="M12 18v4"></path>
|
||||
<path d="m4.93 4.93 2.83 2.83"></path>
|
||||
<path d="m16.24 16.24 2.83 2.83"></path>
|
||||
<path d="M2 12h4"></path>
|
||||
<path d="M18 12h4"></path>
|
||||
<path d="m4.93 19.07 2.83-2.83"></path>
|
||||
<path d="m16.24 7.76 2.83-2.83"></path>
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 min-[400px]:flex-row">
|
||||
<NuxtUiButton
|
||||
class="inline-flex h-10 items-center justify-center rounded-md px-8 text-sm font-medium shadow"
|
||||
to="/demo">
|
||||
Try Demo
|
||||
</NuxtUiButton>
|
||||
<NuxtUiButton variant="outline" color="white"
|
||||
class="inline-flex h-10 items-center justify-center rounded-md px-8 text-sm font-medium shadow"
|
||||
to="/#how-it-works">
|
||||
Learn More
|
||||
</NuxtUiButton>
|
||||
</div>
|
||||
</div>
|
||||
<Placeholder class="aspect-video flex-grow bg-gray-500" />
|
||||
</section>
|
||||
</template>
|
||||
<style>
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
|
||||
<!-- How It Works Section -->
|
||||
<section id="how-it-works" class="container py-20 bg-muted/50 rounded-lg my-10">
|
||||
<section id="how-it-works" class=" py-20 bg-muted/50 rounded-lg my-10">
|
||||
<div class="mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center">
|
||||
<h2 class="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">How It Works</h2>
|
||||
<p class="max-w-[85%] text-muted-foreground md:text-xl/relaxed lg:text-base/relaxed xl:text-xl/relaxed">
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<span class="loader"></span>
|
||||
</template>
|
||||
<style scoped>
|
||||
.loader {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 130px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.loader:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 54px;
|
||||
height: 25px;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
background-image:
|
||||
radial-gradient(ellipse at center, #0000 24%, #de3500 25%, #de3500 64%, #0000 65%),
|
||||
linear-gradient(to bottom, #0000 34%, #de3500 35%);
|
||||
background-size: 12px 12px, 100% auto;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center top;
|
||||
transform: translate(-50%, -65%);
|
||||
box-shadow: 0 -3px rgba(0, 0, 0, 0.25) inset;
|
||||
}
|
||||
|
||||
.loader:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 20%;
|
||||
transform: translateX(-50%);
|
||||
width: 66%;
|
||||
height: 60%;
|
||||
background: linear-gradient(to bottom, #f79577 30%, #0000 31%);
|
||||
background-size: 100% 16px;
|
||||
animation: writeDown 2s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes writeDown {
|
||||
0% {
|
||||
height: 0%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
20% {
|
||||
height: 0%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
80% {
|
||||
height: 65%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
height: 65%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<span class="loader"></span>
|
||||
</template>
|
||||
<style scoped>
|
||||
.loader {
|
||||
width: 200px;
|
||||
height: 140px;
|
||||
background: #979794;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.loader:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
border-radius: 8px;
|
||||
background: #f5f5f5 no-repeat;
|
||||
background-size: 60px 10px;
|
||||
background-image: linear-gradient(#ddd 100px, transparent 0),
|
||||
linear-gradient(#ddd 100px, transparent 0),
|
||||
linear-gradient(#ddd 100px, transparent 0),
|
||||
linear-gradient(#ddd 100px, transparent 0),
|
||||
linear-gradient(#ddd 100px, transparent 0),
|
||||
linear-gradient(#ddd 100px, transparent 0);
|
||||
|
||||
background-position: 15px 30px, 15px 60px, 15px 90px,
|
||||
105px 30px, 105px 60px, 105px 90px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.loader:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: calc(50% - 10px);
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
border-radius: 8px;
|
||||
background: #fff no-repeat;
|
||||
background-size: 60px 10px;
|
||||
background-image: linear-gradient(#ddd 100px, transparent 0),
|
||||
linear-gradient(#ddd 100px, transparent 0),
|
||||
linear-gradient(#ddd 100px, transparent 0);
|
||||
background-position: 50% 30px, 50% 60px, 50% 90px;
|
||||
transform: rotateY(0deg);
|
||||
transform-origin: left center;
|
||||
animation: paging 1s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
@keyframes paging {
|
||||
to {
|
||||
transform: rotateY(-180deg);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<span class="loader"></span>
|
||||
</template>
|
||||
<style scoped>
|
||||
.loader {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #FFF;
|
||||
box-sizing: border-box;
|
||||
animation: flipX 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes flipX {
|
||||
0% {
|
||||
transform: perspective(200px) rotateX(0deg) rotateY(0deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: perspective(200px) rotateX(-180deg) rotateY(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: perspective(200px) rotateX(-180deg) rotateY(-180deg);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<span class="loader"></span>
|
||||
</template>
|
||||
<style scoped>
|
||||
.loader {
|
||||
width: 64px;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
border: 5px solid #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.loader::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border: 5px solid #fff;
|
||||
width: 32px;
|
||||
height: 28px;
|
||||
border-radius: 50% 50% 0 0;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translate(-50%, -100%)
|
||||
}
|
||||
|
||||
.loader::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
box-shadow: 16px 0 #fff, -16px 0 #fff;
|
||||
animation: flash 0.5s ease-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0% {
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
box-shadow: 16px 0 rgba(255, 255, 255, 0.25), -16px 0 rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
box-shadow: 16px 0 rgba(255, 255, 255, 0.25), -16px 0 rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
box-shadow: 16px 0 rgba(255, 255, 255, 1), -16px 0 rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<span class="loader"></span>
|
||||
</template>
|
||||
<style scoped>
|
||||
.loader {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loader::after,
|
||||
.loader::before {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #FFF;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
animation: animloader 2s linear infinite;
|
||||
}
|
||||
|
||||
.loader::after {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes animloader {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<span class="loader">{{ props?.loaderText || 'Loading' }}</span>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps<{
|
||||
loaderText?: string | number
|
||||
}>()
|
||||
</script>
|
||||
<style scoped>
|
||||
.loader {
|
||||
color: #FFF;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
font-size: 48px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.loader::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background: currentColor;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: -5px;
|
||||
box-sizing: border-box;
|
||||
animation: animloader 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes animloader {
|
||||
0% {
|
||||
box-shadow: 10px 0 rgba(255, 255, 255, 0), 20px 0 rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 10px 0 white, 20px 0 rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 10px 0 white, 20px 0 white;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<span class="loader"></span>
|
||||
</template>
|
||||
<style scoped>
|
||||
.loader {
|
||||
position: relative;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
border-width: 40px 60px 30px 60px;
|
||||
border-color: #3760C9 #96DDFC #96DDFC #36BBF7;
|
||||
animation: envFloating 1s ease-in infinite alternate;
|
||||
}
|
||||
|
||||
.loader:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 62px;
|
||||
top: -40px;
|
||||
height: 70px;
|
||||
width: 50px;
|
||||
background-image:
|
||||
linear-gradient(#fff 45px, transparent 0),
|
||||
linear-gradient(#fff 45px, transparent 0),
|
||||
linear-gradient(#fff 45px, transparent 0);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 30px 4px;
|
||||
background-position: 0px 11px, 8px 35px, 0px 60px;
|
||||
animation: envDropping 0.75s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes envFloating {
|
||||
0% {
|
||||
transform: translate(-2px, -5px)
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(0, 5px)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes envDropping {
|
||||
0% {
|
||||
background-position: 100px 11px, 115px 35px, 105px 60px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 0px 11px, 20px 35px, 5px 60px;
|
||||
}
|
||||
|
||||
60% {
|
||||
background-position: -30px 11px, 0px 35px, -10px 60px;
|
||||
}
|
||||
|
||||
75%,
|
||||
100% {
|
||||
background-position: -30px 11px, -30px 35px, -30px 60px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'rounded-xl border bg-card text-card-foreground shadow',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('p-6 pt-0', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p :class="cn('text-sm text-muted-foreground', props.class)">
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex items-center p-6 pt-0', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3
|
||||
:class="
|
||||
cn('font-semibold leading-none tracking-tight', props.class)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
|
@ -0,0 +1,6 @@
|
|||
export { default as Card } from './Card.vue'
|
||||
export { default as CardContent } from './CardContent.vue'
|
||||
export { default as CardDescription } from './CardDescription.vue'
|
||||
export { default as CardFooter } from './CardFooter.vue'
|
||||
export { default as CardHeader } from './CardHeader.vue'
|
||||
export { default as CardTitle } from './CardTitle.vue'
|
|
@ -0,0 +1,27 @@
|
|||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Label, type LabelProps } from 'reka-ui'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Label
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</Label>
|
||||
</template>
|
|
@ -0,0 +1 @@
|
|||
export { default as Label } from './Label.vue'
|
|
@ -1,3 +1,3 @@
|
|||
<template>
|
||||
<slot />
|
||||
<slot></slot>
|
||||
</template>
|
|
@ -2,9 +2,17 @@
|
|||
<NuxtUiContainer
|
||||
class="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 flex flex-col">
|
||||
<LandingHeader />
|
||||
<div class="grow">
|
||||
<div class="grow mt-[64px] flex flex-col">
|
||||
<slot />
|
||||
</div>
|
||||
<LandingFooter />
|
||||
</NuxtUiContainer>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const colorMode = useColorMode()
|
||||
onMounted(() => {
|
||||
if (colorMode.preference !== 'light') {
|
||||
colorMode.preference = 'light'
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,42 @@
|
|||
export default defineNuxtRouteMiddleware(async (to, from) => {
|
||||
const token = to.params.token;
|
||||
const forgotPasswordTokenStatus = useState<'unset' | 'invalid' | 'valid'>('forgot-password-token-status', () => 'unset')
|
||||
|
||||
if (!token) {
|
||||
return navigateTo('/login');
|
||||
}
|
||||
|
||||
try {
|
||||
const isValid = await verifyToken(token as string);
|
||||
|
||||
if (!isValid) {
|
||||
forgotPasswordTokenStatus.value = 'invalid'
|
||||
} else {
|
||||
forgotPasswordTokenStatus.value = 'valid'
|
||||
}
|
||||
} catch (error) {
|
||||
forgotPasswordTokenStatus.value = 'invalid'
|
||||
}
|
||||
|
||||
async function verifyToken(token: string) {
|
||||
return new Promise((resolve) => setTimeout(() => {
|
||||
if (token === 'success') {
|
||||
return resolve(true)
|
||||
} else {
|
||||
return resolve(false)
|
||||
}
|
||||
}, 2000))
|
||||
// const response = await fetch('https://your-backend.com/api/verify-token', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// },
|
||||
// body: JSON.stringify({ token }),
|
||||
// });
|
||||
|
||||
// const data = await response.json();
|
||||
|
||||
// return data.isValid;
|
||||
}
|
||||
|
||||
});
|
|
@ -1,5 +1,15 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
nitro: {
|
||||
routeRules: {
|
||||
'/_nuxt/**': {
|
||||
headers: {
|
||||
'cache-control': 'public, max-age=31536000, immutable',
|
||||
'content-encoding': 'br'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
compatibilityDate: '2024-11-01',
|
||||
devtools: { enabled: true },
|
||||
modules: ['@nuxt/image', '@nuxt/ui', 'shadcn-nuxt'],
|
||||
|
@ -16,5 +26,9 @@ export default defineNuxtConfig({
|
|||
* @default "./components/ui"
|
||||
*/
|
||||
componentDir: './components/ui'
|
||||
},
|
||||
image: {
|
||||
format: ['webp'],
|
||||
quality: 80,
|
||||
}
|
||||
})
|
|
@ -14,12 +14,13 @@
|
|||
"clsx": "^2.1.1",
|
||||
"lucide-vue-next": "^0.485.0",
|
||||
"nuxt": "^3.16.1",
|
||||
"reka-ui": "^2.1.1",
|
||||
"reka-ui": "^2.2.0",
|
||||
"shadcn-nuxt": "^1.0.3",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
"vue-router": "^4.5.0",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
|
@ -8904,9 +8905,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/reka-ui": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.1.1.tgz",
|
||||
"integrity": "sha512-awvpQ041LPXAvf2uRVFwedsyz9SwsuoWlRql1fg4XimUCxEI2GOfHo6FIdL44dSPb/eG/gWbdGhoGHLlbX5gPA==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.2.0.tgz",
|
||||
"integrity": "sha512-eeRrLI4LwJ6dkdwks6KFNKGs0+beqZlHO3JMHen7THDTh+yJ5Z0KNwONmOhhV/0hZC2uJCEExgG60QPzGstkQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
|
@ -11544,6 +11545,15 @@
|
|||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.24.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,11 +17,12 @@
|
|||
"clsx": "^2.1.1",
|
||||
"lucide-vue-next": "^0.485.0",
|
||||
"nuxt": "^3.16.1",
|
||||
"reka-ui": "^2.1.1",
|
||||
"reka-ui": "^2.2.0",
|
||||
"shadcn-nuxt": "^1.0.3",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
"vue-router": "^4.5.0",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<NuxtLayout name="auth">
|
||||
<NuxtLayout name="default">
|
||||
<div></div>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<NuxtLayout name="auth">
|
||||
<div></div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<NuxtLayout name="auth">
|
||||
<div></div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -0,0 +1,89 @@
|
|||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
definePageMeta({
|
||||
middleware: ['forgot-password-confirmation'],
|
||||
});
|
||||
const forgotPasswordTokenStatus = useState<'unset' | 'invalid' | 'valid'>('forgot-password-token-status', () => 'unset')
|
||||
const authSection = useState<'login' | 'register' | 'forgot-password'>('auth-section', () => 'login')
|
||||
|
||||
const isSubmitting = ref<boolean>(false)
|
||||
|
||||
const baseSchema = z.object({
|
||||
newPassword: z.string().min(8, 'Must be at least 8 characters'),
|
||||
newPassword2: z.string().min(8, 'Must be at least 8 characters')
|
||||
})
|
||||
|
||||
const schema = baseSchema
|
||||
.refine((data) => data.newPassword === data.newPassword2, {
|
||||
message: "Passwords don't match",
|
||||
path: ['newPassword2'],
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
const state = reactive({
|
||||
newPassword: undefined,
|
||||
newPassword2: undefined
|
||||
})
|
||||
|
||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
isSubmitting.value = true
|
||||
setTimeout(() => {
|
||||
isSubmitting.value = false
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLayout name="default">
|
||||
<div class="my-auto p-5">
|
||||
<div class="grid grid-cols-1 max-w-sm mx-auto">
|
||||
<div class="pb-3 flex gap-3">
|
||||
<NuxtUiButton to="/auth/forgot-password/success">true</NuxtUiButton>
|
||||
<NuxtUiButton to="/auth/forgot-password/invalid">false</NuxtUiButton>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div v-if="forgotPasswordTokenStatus === 'valid'">
|
||||
<NuxtUiForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<NuxtUiFormGroup label="New Password" name="password">
|
||||
<NuxtUiInput v-model="state.newPassword" />
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<NuxtUiFormGroup label="New Password Confirmation" name="password_confirmation">
|
||||
<NuxtUiInput v-model="state.newPassword2" type="password" />
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<NuxtUiButton type="submit" :loading="isSubmitting">
|
||||
Submit
|
||||
</NuxtUiButton>
|
||||
</NuxtUiForm>
|
||||
</div>
|
||||
<div v-else>
|
||||
<NuxtUiCard class="max-w-md mx-auto text-center">
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold text-red-600">Invalid or Expired Token</h2>
|
||||
</template>
|
||||
|
||||
<p class="text-gray-700">
|
||||
The password reset link you used is either invalid, expired, or has already been used.
|
||||
</p>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Please request a new reset link to continue.
|
||||
</p>
|
||||
|
||||
<template #footer>
|
||||
<NuxtUiButton color="primary" @click="() => {
|
||||
authSection = 'forgot-password',
|
||||
navigateTo('/auth')
|
||||
}">
|
||||
Request New Link
|
||||
</NuxtUiButton>
|
||||
</template>
|
||||
</NuxtUiCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<NuxtLayout name="default">
|
||||
<div class="w-screen h-screen grid">
|
||||
<NuxtUiCard class="w-full max-w-sm m-auto shadow-xl rounded-2xl">
|
||||
<Auth />
|
||||
</NuxtUiCard>
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<NuxtLayout name="auth">
|
||||
<div></div>
|
||||
</NuxtLayout>
|
||||
</template>
|
|
@ -1,5 +0,0 @@
|
|||
<template>
|
||||
<NuxtLayout name="auth">
|
||||
<div></div>
|
||||
</NuxtLayout>
|
||||
</template>
|
Binary file not shown.
After Width: | Height: | Size: 108 KiB |
Binary file not shown.
After Width: | Height: | Size: 426 KiB |
Loading…
Reference in New Issue