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({
|
export default defineNuxtConfig({
|
||||||
devtools: { enabled: true },
|
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: {
|
colorMode: {
|
||||||
classSuffix: '',
|
classSuffix: '',
|
||||||
@@ -28,5 +43,18 @@ export default defineNuxtConfig({
|
|||||||
strict: true,
|
strict: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
hmr: {
|
||||||
|
clientPort: 3001,
|
||||||
|
},
|
||||||
|
allowedHosts: [
|
||||||
|
'jupiter.routebox.co',
|
||||||
|
'localhost',
|
||||||
|
'127.0.0.1',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
compatibilityDate: '2024-01-01',
|
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": {
|
"dependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.11.4",
|
"@nuxtjs/tailwindcss": "^6.11.4",
|
||||||
"@vueuse/core": "^10.7.2",
|
"@vueuse/core": "^10.11.1",
|
||||||
"nuxt": "^3.10.0",
|
|
||||||
"vue": "^3.4.15",
|
|
||||||
"vue-router": "^4.2.5",
|
|
||||||
"radix-vue": "^1.4.1",
|
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.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",
|
"tailwind-merge": "^2.2.1",
|
||||||
"lucide-vue-next": "^0.309.0"
|
"vue": "^3.4.15",
|
||||||
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxtjs/color-mode": "^3.3.2",
|
"@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>
|
<template>
|
||||||
<div class="min-h-screen bg-background flex items-center justify-center">
|
<div class="grid min-h-svh lg:grid-cols-2">
|
||||||
<div class="w-full max-w-md p-8 border rounded-lg bg-card">
|
<div class="flex flex-col gap-4 p-6 md:p-10">
|
||||||
<h1 class="text-3xl font-bold mb-6 text-center">Login</h1>
|
<div class="flex justify-center gap-2 md:justify-start">
|
||||||
|
<NuxtLink to="/" class="flex items-center gap-2 font-medium">
|
||||||
<div v-if="error" class="mb-4 p-3 bg-destructive/10 text-destructive rounded">
|
<div class="bg-primary text-primary-foreground flex size-6 items-center justify-center rounded-md">
|
||||||
{{ error }}
|
<LayoutGrid class="size-4" />
|
||||||
</div>
|
</div>
|
||||||
|
Neo Platform
|
||||||
<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
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<template>
|
||||||
<div class="min-h-screen bg-background flex items-center justify-center">
|
<div class="min-h-screen bg-background flex items-center justify-center p-4">
|
||||||
<div class="w-full max-w-md p-8 border rounded-lg bg-card">
|
<Card class="w-full max-w-md">
|
||||||
<h1 class="text-3xl font-bold mb-6 text-center">Register</h1>
|
<CardHeader>
|
||||||
|
<CardTitle class="text-2xl text-center">Create Account</CardTitle>
|
||||||
<div v-if="error" class="mb-4 p-3 bg-destructive/10 text-destructive rounded">
|
<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 }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="success" class="mb-4 p-3 bg-green-500/10 text-green-600 rounded">
|
<div v-if="success" class="mb-4 p-3 bg-green-500/10 text-green-600 rounded text-sm">
|
||||||
Registration successful! Redirecting to login...
|
Registration successful! Redirecting to login...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="handleRegister" class="space-y-4">
|
<form @submit.prevent="handleRegister" class="space-y-4">
|
||||||
<div>
|
<div class="space-y-2">
|
||||||
<label class="block text-sm font-medium mb-2">Tenant ID</label>
|
<Label for="tenantId">Tenant ID</Label>
|
||||||
<input
|
<Input
|
||||||
|
id="tenantId"
|
||||||
v-model="tenantId"
|
v-model="tenantId"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
|
||||||
placeholder="123"
|
placeholder="123"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="space-y-2">
|
||||||
<label class="block text-sm font-medium mb-2">Email</label>
|
<Label for="email">Email</Label>
|
||||||
<input
|
<Input
|
||||||
|
id="email"
|
||||||
v-model="email"
|
v-model="email"
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
|
||||||
placeholder="user@example.com"
|
placeholder="user@example.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="space-y-2">
|
||||||
<label class="block text-sm font-medium mb-2">Password</label>
|
<Label for="password">Password</Label>
|
||||||
<input
|
<Input
|
||||||
|
id="password"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
minlength="6"
|
minlength="6"
|
||||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
/>
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground">Must be at least 6 characters</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<label class="block text-sm font-medium mb-2">First Name (Optional)</label>
|
<div class="space-y-2">
|
||||||
<input
|
<Label for="firstName">First Name</Label>
|
||||||
|
<Input
|
||||||
|
id="firstName"
|
||||||
v-model="firstName"
|
v-model="firstName"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
|
||||||
placeholder="John"
|
placeholder="John"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="space-y-2">
|
||||||
<label class="block text-sm font-medium mb-2">Last Name (Optional)</label>
|
<Label for="lastName">Last Name</Label>
|
||||||
<input
|
<Input
|
||||||
|
id="lastName"
|
||||||
v-model="lastName"
|
v-model="lastName"
|
||||||
type="text"
|
type="text"
|
||||||
class="w-full px-3 py-2 border rounded-md bg-background"
|
|
||||||
placeholder="Doe"
|
placeholder="Doe"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="w-full px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50"
|
class="w-full"
|
||||||
>
|
>
|
||||||
{{ loading ? 'Registering...' : 'Register' }}
|
{{ loading ? 'Creating account...' : 'Create Account' }}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
</CardContent>
|
||||||
<div class="mt-4 text-center text-sm text-muted-foreground">
|
<CardFooter class="flex justify-center">
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
Already have an account?
|
Already have an account?
|
||||||
<NuxtLink to="/login" class="text-primary hover:underline">
|
<NuxtLink to="/login" class="text-primary hover:underline font-medium">
|
||||||
Login
|
Login
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user