Added login design

This commit is contained in:
Francisco Gaona
2025-11-25 18:58:49 +01:00
parent 484af68571
commit 150edfaf41
21 changed files with 9715 additions and 187 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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"

View 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>

View File

@@ -0,0 +1 @@
export { default as Input } from "./Input.vue"

View 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>

View File

@@ -0,0 +1 @@
export { default as Label } from "./Label.vue"