This commit is contained in:
parent
62903bf565
commit
e60e47d28e
|
@ -8,6 +8,9 @@ export default defineAppConfig({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
width: 'w-screen max-w-sm',
|
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">
|
<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 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 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">
|
<NuxtImg src="assets/icons/logo.png" format="webp" height="24" width="auto" />
|
||||||
<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>
|
|
||||||
<p class="text-center text-sm leading-loose text-muted-foreground md:text-left">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4">
|
<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">
|
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>
|
<NuxtUiContainer>
|
||||||
<div class="flex h-16 items-center">
|
<div class="flex h-16 items-center">
|
||||||
<div class="flex items-center gap-2">
|
<NuxtLink href="/" class="flex items-center gap-2">
|
||||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-primary">
|
<NuxtImg src="/assets/icons/logo-text.png" width="auto" height="44" format="webp" />
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
|
</NuxtLink>
|
||||||
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>
|
|
||||||
|
|
||||||
<nav class="ml-auto flex items-center gap-4 sm:gap-6">
|
<nav class="ml-auto flex items-center gap-4 sm:gap-6">
|
||||||
<div v-if="window.width.value <= 640">
|
<div v-if="window.width.value <= 640">
|
||||||
|
@ -29,17 +21,13 @@
|
||||||
|
|
||||||
<!-- Navigasi -->
|
<!-- Navigasi -->
|
||||||
<div class="mt-10 space-y-6">
|
<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"
|
<NuxtLink to="/#how-it-works" class="text-lg block hover:text-green-500"
|
||||||
@click="isOpen = false">
|
@click="isOpen = false">
|
||||||
How It Works
|
How It Works
|
||||||
</NuxtLink>
|
</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">
|
@click="isOpen = false">
|
||||||
Pricing
|
Features
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink to="/demo" class="text-lg block text-green-500 font-semibold"
|
<NuxtLink to="/demo" class="text-lg block text-green-500 font-semibold"
|
||||||
@click="isOpen = false">
|
@click="isOpen = false">
|
||||||
|
@ -49,37 +37,60 @@
|
||||||
|
|
||||||
<!-- Tombol "Get Started" -->
|
<!-- Tombol "Get Started" -->
|
||||||
<div class="mt-auto flex justify-start">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</NuxtUiSlideover>
|
</NuxtUiSlideover>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="ml-auto flex items-center gap-4 sm:gap-6">
|
<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"
|
<NuxtLink href="/#how-it-works"
|
||||||
class="text-sm font-medium transition-colors hover:text-primary">How
|
class="text-sm font-medium transition-colors hover:text-primary">How
|
||||||
It
|
It
|
||||||
Works</NuxtLink>
|
Works</NuxtLink>
|
||||||
<NuxtLink href="/#pricing" class="text-sm font-medium transition-colors hover:text-primary">
|
<NuxtLink href="/#features" class="text-sm font-medium transition-colors hover:text-primary">
|
||||||
Pricing
|
Features
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<NuxtLink href="/demo"
|
<NuxtLink href="/demo"
|
||||||
class="text-sm font-medium text-primary transition-colors hover:text-primary/80">
|
class="text-sm font-medium text-primary transition-colors hover:text-primary/80">
|
||||||
Demo</NuxtLink>
|
Demo</NuxtLink>
|
||||||
<NuxtUiButton color="green">
|
<NuxtUiButton color="green" @click="() => {
|
||||||
Get Started
|
if (route.path.startsWith('/auth/forgot-password')) {
|
||||||
|
navigateTo('/auth')
|
||||||
|
} else {
|
||||||
|
authModalIsOpen = true
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
Log In
|
||||||
</NuxtUiButton>
|
</NuxtUiButton>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</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>
|
</NuxtUiContainer>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useWindowSize } from '@vueuse/core';
|
import { useWindowSize } from '@vueuse/core';
|
||||||
|
|
||||||
const window = useWindowSize()
|
const window = useWindowSize()
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
|
const authModalIsOpen = ref<boolean>(false)
|
||||||
|
const route = useRoute();
|
||||||
|
const authSection = useState<'login' | 'register' | 'forgot-password'>('auth-section', () => 'login')
|
||||||
</script>
|
</script>
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- CTA Section -->
|
<!-- 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="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="flex flex-col items-center justify-between gap-4 text-center md:flex-row md:text-left">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<NuxtUiButton color="green"
|
<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>
|
<span>
|
||||||
Try Demo
|
Try Demo
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<!-- Features Section -->
|
<!-- 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">
|
<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 class="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">Powerful Prediction Features
|
||||||
</h2>
|
</h2>
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
|
<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">
|
||||||
<!-- Hero Section -->
|
<div class="flex flex-col justify-center space-y-6 sm:w-[60%] py-10">
|
||||||
<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">
|
<div class="space-y-2">
|
||||||
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
|
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
|
||||||
Predict the Future with ARIMA
|
Predict the Future with ARIMA
|
||||||
|
@ -19,36 +16,17 @@
|
||||||
Try Demo
|
Try Demo
|
||||||
</NuxtUiButton>
|
</NuxtUiButton>
|
||||||
<NuxtUiButton variant="outline" color="white"
|
<NuxtUiButton variant="outline" color="white"
|
||||||
class="inline-flex h-10 items-center justify-center rounded-md px-8 text-sm font-medium shadow">
|
class="inline-flex h-10 items-center justify-center rounded-md px-8 text-sm font-medium shadow"
|
||||||
|
to="/#how-it-works">
|
||||||
Learn More
|
Learn More
|
||||||
</NuxtUiButton>
|
</NuxtUiButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center">
|
<Placeholder class="aspect-video flex-grow bg-gray-500" />
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<!-- How It Works Section -->
|
<!-- 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">
|
<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>
|
<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">
|
<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>
|
<template>
|
||||||
<slot />
|
<slot></slot>
|
||||||
</template>
|
</template>
|
|
@ -2,9 +2,17 @@
|
||||||
<NuxtUiContainer
|
<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">
|
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 />
|
<LandingHeader />
|
||||||
<div class="grow">
|
<div class="grow mt-[64px] flex flex-col">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<LandingFooter />
|
<LandingFooter />
|
||||||
</NuxtUiContainer>
|
</NuxtUiContainer>
|
||||||
</template>
|
</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
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
|
nitro: {
|
||||||
|
routeRules: {
|
||||||
|
'/_nuxt/**': {
|
||||||
|
headers: {
|
||||||
|
'cache-control': 'public, max-age=31536000, immutable',
|
||||||
|
'content-encoding': 'br'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
compatibilityDate: '2024-11-01',
|
compatibilityDate: '2024-11-01',
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
modules: ['@nuxt/image', '@nuxt/ui', 'shadcn-nuxt'],
|
modules: ['@nuxt/image', '@nuxt/ui', 'shadcn-nuxt'],
|
||||||
|
@ -16,5 +26,9 @@ export default defineNuxtConfig({
|
||||||
* @default "./components/ui"
|
* @default "./components/ui"
|
||||||
*/
|
*/
|
||||||
componentDir: './components/ui'
|
componentDir: './components/ui'
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
format: ['webp'],
|
||||||
|
quality: 80,
|
||||||
}
|
}
|
||||||
})
|
})
|
|
@ -14,12 +14,13 @@
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-vue-next": "^0.485.0",
|
"lucide-vue-next": "^0.485.0",
|
||||||
"nuxt": "^3.16.1",
|
"nuxt": "^3.16.1",
|
||||||
"reka-ui": "^2.1.1",
|
"reka-ui": "^2.2.0",
|
||||||
"shadcn-nuxt": "^1.0.3",
|
"shadcn-nuxt": "^1.0.3",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0",
|
||||||
|
"zod": "^3.24.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
|
@ -8904,9 +8905,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/reka-ui": {
|
"node_modules/reka-ui": {
|
||||||
"version": "2.1.1",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.2.0.tgz",
|
||||||
"integrity": "sha512-awvpQ041LPXAvf2uRVFwedsyz9SwsuoWlRql1fg4XimUCxEI2GOfHo6FIdL44dSPb/eG/gWbdGhoGHLlbX5gPA==",
|
"integrity": "sha512-eeRrLI4LwJ6dkdwks6KFNKGs0+beqZlHO3JMHen7THDTh+yJ5Z0KNwONmOhhV/0hZC2uJCEExgG60QPzGstkQg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "^1.6.13",
|
"@floating-ui/dom": "^1.6.13",
|
||||||
|
@ -11544,6 +11545,15 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"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",
|
"clsx": "^2.1.1",
|
||||||
"lucide-vue-next": "^0.485.0",
|
"lucide-vue-next": "^0.485.0",
|
||||||
"nuxt": "^3.16.1",
|
"nuxt": "^3.16.1",
|
||||||
"reka-ui": "^2.1.1",
|
"reka-ui": "^2.2.0",
|
||||||
"shadcn-nuxt": "^1.0.3",
|
"shadcn-nuxt": "^1.0.3",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0",
|
||||||
|
"zod": "^3.24.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<NuxtLayout name="auth">
|
<NuxtLayout name="default">
|
||||||
<div></div>
|
<div></div>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</template>
|
</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