Added login design
This commit is contained in:
21
frontend/components.json
Normal file
21
frontend/components.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "new-york",
|
||||
"typescript": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "assets/css/main.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"composables": "@/composables"
|
||||
},
|
||||
"registries": {}
|
||||
}
|
||||
122
frontend/components/LoginForm.vue
Normal file
122
frontend/components/LoginForm.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const router = useRouter()
|
||||
|
||||
const tenantId = ref('123')
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
const response = await fetch(`${config.public.apiBaseUrl}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-tenant-id': tenantId.value,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(data.message || 'Login failed')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Store credentials
|
||||
localStorage.setItem('tenantId', tenantId.value)
|
||||
localStorage.setItem('token', data.access_token)
|
||||
localStorage.setItem('user', JSON.stringify(data.user))
|
||||
|
||||
// Redirect to home
|
||||
router.push('/')
|
||||
} catch (e: any) {
|
||||
error.value = e.message || 'Login failed'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="handleLogin" class="flex flex-col gap-6">
|
||||
<div class="flex flex-col items-center gap-2 text-center">
|
||||
<h1 class="text-2xl font-bold">
|
||||
Login to your account
|
||||
</h1>
|
||||
<p class="text-balance text-sm text-muted-foreground">
|
||||
Enter your credentials below to login
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="p-3 bg-destructive/10 text-destructive rounded text-sm">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6">
|
||||
<div class="grid gap-2">
|
||||
<Label for="tenantId">Tenant ID</Label>
|
||||
<Input
|
||||
id="tenantId"
|
||||
v-model="tenantId"
|
||||
type="text"
|
||||
placeholder="123"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<div class="flex items-center">
|
||||
<Label for="password">Password</Label>
|
||||
<a
|
||||
href="#"
|
||||
class="ml-auto text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
class="w-full"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ loading ? 'Logging in...' : 'Login' }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="text-center text-sm">
|
||||
Don't have an account?
|
||||
<NuxtLink to="/register" class="underline underline-offset-4">
|
||||
Sign up
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
30
frontend/components/ui/button/Button.vue
Normal file
30
frontend/components/ui/button/Button.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import type { ButtonVariants } from "."
|
||||
import { Primitive } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "."
|
||||
|
||||
interface Props {
|
||||
variant?: ButtonVariants["variant"]
|
||||
size?: ButtonVariants["size"]
|
||||
class?: HTMLAttributes["class"]
|
||||
as?: any
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: "button",
|
||||
asChild: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
38
frontend/components/ui/button/index.ts
Normal file
38
frontend/components/ui/button/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
export { default as Button } from "./Button.vue"
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
"default": "h-9 px-4 py-2",
|
||||
"xs": "h-7 rounded px-2",
|
||||
"sm": "h-8 rounded-md px-3 text-xs",
|
||||
"lg": "h-10 rounded-md px-8",
|
||||
"icon": "h-9 w-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||
21
frontend/components/ui/card/Card.vue
Normal file
21
frontend/components/ui/card/Card.vue
Normal file
@@ -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>
|
||||
14
frontend/components/ui/card/CardContent.vue
Normal file
14
frontend/components/ui/card/CardContent.vue
Normal file
@@ -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>
|
||||
14
frontend/components/ui/card/CardDescription.vue
Normal file
14
frontend/components/ui/card/CardDescription.vue
Normal file
@@ -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>
|
||||
14
frontend/components/ui/card/CardFooter.vue
Normal file
14
frontend/components/ui/card/CardFooter.vue
Normal file
@@ -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>
|
||||
14
frontend/components/ui/card/CardHeader.vue
Normal file
14
frontend/components/ui/card/CardHeader.vue
Normal file
@@ -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>
|
||||
18
frontend/components/ui/card/CardTitle.vue
Normal file
18
frontend/components/ui/card/CardTitle.vue
Normal file
@@ -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>
|
||||
6
frontend/components/ui/card/index.ts
Normal file
6
frontend/components/ui/card/index.ts
Normal file
@@ -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"
|
||||
24
frontend/components/ui/input/Input.vue
Normal file
24
frontend/components/ui/input/Input.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
defaultValue?: string | number
|
||||
modelValue?: string | number
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: "update:modelValue", payload: string | number): void
|
||||
}>()
|
||||
|
||||
const modelValue = useVModel(props, "modelValue", emits, {
|
||||
passive: true,
|
||||
defaultValue: props.defaultValue,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-model="modelValue" :class="cn('flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', props.class)">
|
||||
</template>
|
||||
1
frontend/components/ui/input/index.ts
Normal file
1
frontend/components/ui/input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Input } from "./Input.vue"
|
||||
29
frontend/components/ui/label/Label.vue
Normal file
29
frontend/components/ui/label/Label.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Label } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface Props {
|
||||
for?: string
|
||||
class?: HTMLAttributes["class"]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</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>
|
||||
1
frontend/components/ui/label/index.ts
Normal file
1
frontend/components/ui/label/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Label } from "./Label.vue"
|
||||
6
frontend/lib/utils.ts
Normal file
6
frontend/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -2,7 +2,22 @@
|
||||
export default defineNuxtConfig({
|
||||
devtools: { enabled: true },
|
||||
|
||||
modules: ['@nuxtjs/tailwindcss', '@nuxtjs/color-mode'],
|
||||
modules: ['@nuxtjs/tailwindcss', '@nuxtjs/color-mode', 'shadcn-nuxt'],
|
||||
|
||||
shadcn: {
|
||||
/**
|
||||
* Prefix for all the imported component.
|
||||
* @default "Ui"
|
||||
*/
|
||||
prefix: '',
|
||||
/**
|
||||
* Directory that the component lives in.
|
||||
* Will respect the Nuxt aliases.
|
||||
* @link https://nuxt.com/docs/api/nuxt-config#alias
|
||||
* @default "@/components/ui"
|
||||
*/
|
||||
componentDir: '@/components/ui'
|
||||
},
|
||||
|
||||
colorMode: {
|
||||
classSuffix: '',
|
||||
@@ -28,5 +43,18 @@ export default defineNuxtConfig({
|
||||
strict: true,
|
||||
},
|
||||
|
||||
vite: {
|
||||
server: {
|
||||
hmr: {
|
||||
clientPort: 3001,
|
||||
},
|
||||
allowedHosts: [
|
||||
'jupiter.routebox.co',
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
compatibilityDate: '2024-01-01',
|
||||
})
|
||||
})
|
||||
9194
frontend/package-lock.json
generated
Normal file
9194
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,15 +13,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.11.4",
|
||||
"@vueuse/core": "^10.7.2",
|
||||
"nuxt": "^3.10.0",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5",
|
||||
"radix-vue": "^1.4.1",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"lucide-vue-next": "^0.309.0",
|
||||
"nuxt": "^3.10.0",
|
||||
"radix-vue": "^1.4.1",
|
||||
"reka-ui": "^2.6.0",
|
||||
"shadcn-nuxt": "^2.3.3",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"lucide-vue-next": "^0.309.0"
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/color-mode": "^3.3.2",
|
||||
|
||||
@@ -1,110 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { LayoutGrid } from 'lucide-vue-next'
|
||||
import LoginForm from '@/components/LoginForm.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-background flex items-center justify-center">
|
||||
<div class="w-full max-w-md p-8 border rounded-lg bg-card">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Login</h1>
|
||||
|
||||
<div v-if="error" class="mb-4 p-3 bg-destructive/10 text-destructive rounded">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Tenant ID</label>
|
||||
<input
|
||||
v-model="tenantId"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
||||
placeholder="123"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Email</label>
|
||||
<input
|
||||
v-model="email"
|
||||
type="email"
|
||||
required
|
||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
||||
placeholder="user@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Password</label>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
required
|
||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="w-full px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{{ loading ? 'Logging in...' : 'Login' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 text-center text-sm text-muted-foreground">
|
||||
Don't have an account?
|
||||
<NuxtLink to="/register" class="text-primary hover:underline">
|
||||
Register
|
||||
<div class="grid min-h-svh lg:grid-cols-2">
|
||||
<div class="flex flex-col gap-4 p-6 md:p-10">
|
||||
<div class="flex justify-center gap-2 md:justify-start">
|
||||
<NuxtLink to="/" class="flex items-center gap-2 font-medium">
|
||||
<div class="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
|
||||
<LayoutGrid class="size-4" />
|
||||
</div>
|
||||
Neo Platform
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-center">
|
||||
<div class="w-full max-w-xs">
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-muted relative hidden lg:block">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?w=1200&auto=format&fit=crop&q=80"
|
||||
alt="Login cover"
|
||||
class="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const config = useRuntimeConfig()
|
||||
const router = useRouter()
|
||||
|
||||
const tenantId = ref('123')
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
const response = await fetch(`${config.public.apiBaseUrl}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-tenant-id': tenantId.value,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email.value,
|
||||
password: password.value,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(data.message || 'Login failed')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Store credentials
|
||||
localStorage.setItem('tenantId', tenantId.value)
|
||||
localStorage.setItem('token', data.access_token)
|
||||
localStorage.setItem('user', JSON.stringify(data.user))
|
||||
|
||||
// Redirect to home
|
||||
router.push('/')
|
||||
} catch (e: any) {
|
||||
error.value = e.message || 'Login failed'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,87 +1,97 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-background flex items-center justify-center">
|
||||
<div class="w-full max-w-md p-8 border rounded-lg bg-card">
|
||||
<h1 class="text-3xl font-bold mb-6 text-center">Register</h1>
|
||||
|
||||
<div v-if="error" class="mb-4 p-3 bg-destructive/10 text-destructive rounded">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-if="success" class="mb-4 p-3 bg-green-500/10 text-green-600 rounded">
|
||||
Registration successful! Redirecting to login...
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleRegister" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Tenant ID</label>
|
||||
<input
|
||||
v-model="tenantId"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
||||
placeholder="123"
|
||||
/>
|
||||
<div class="min-h-screen bg-background flex items-center justify-center p-4">
|
||||
<Card class="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-2xl text-center">Create Account</CardTitle>
|
||||
<CardDescription class="text-center">
|
||||
Sign up to get started with Neo Platform
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="error" class="mb-4 p-3 bg-destructive/10 text-destructive rounded text-sm">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Email</label>
|
||||
<input
|
||||
v-model="email"
|
||||
type="email"
|
||||
required
|
||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
||||
placeholder="user@example.com"
|
||||
/>
|
||||
<div v-if="success" class="mb-4 p-3 bg-green-500/10 text-green-600 rounded text-sm">
|
||||
Registration successful! Redirecting to login...
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Password</label>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
required
|
||||
minlength="6"
|
||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
<form @submit.prevent="handleRegister" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="tenantId">Tenant ID</Label>
|
||||
<Input
|
||||
id="tenantId"
|
||||
v-model="tenantId"
|
||||
type="text"
|
||||
required
|
||||
placeholder="123"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">First Name (Optional)</label>
|
||||
<input
|
||||
v-model="firstName"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
||||
placeholder="John"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="user@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Last Name (Optional)</label>
|
||||
<input
|
||||
v-model="lastName"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
required
|
||||
minlength="6"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">Must be at least 6 characters</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="w-full px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{{ loading ? 'Registering...' : 'Register' }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="firstName">First Name</Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
v-model="firstName"
|
||||
type="text"
|
||||
placeholder="John"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center text-sm text-muted-foreground">
|
||||
Already have an account?
|
||||
<NuxtLink to="/login" class="text-primary hover:underline">
|
||||
Login
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="lastName">Last Name</Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
v-model="lastName"
|
||||
type="text"
|
||||
placeholder="Doe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="w-full"
|
||||
>
|
||||
{{ loading ? 'Creating account...' : 'Create Account' }}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter class="flex justify-center">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Already have an account?
|
||||
<NuxtLink to="/login" class="text-primary hover:underline font-medium">
|
||||
Login
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user