register users
This commit is contained in:
@@ -27,4 +27,8 @@ body,
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: 'TikTok Sans', sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
||||
22
frontend/components/ui/card/Card.vue
Normal file
22
frontend/components/ui/card/Card.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card"
|
||||
:class="
|
||||
cn(
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
frontend/components/ui/card/CardAction.vue
Normal file
17
frontend/components/ui/card/CardAction.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card-action"
|
||||
:class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
frontend/components/ui/card/CardContent.vue
Normal file
17
frontend/components/ui/card/CardContent.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card-content"
|
||||
:class="cn('px-6', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
frontend/components/ui/card/CardDescription.vue
Normal file
17
frontend/components/ui/card/CardDescription.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
data-slot="card-description"
|
||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
||||
17
frontend/components/ui/card/CardFooter.vue
Normal file
17
frontend/components/ui/card/CardFooter.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
:class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
frontend/components/ui/card/CardHeader.vue
Normal file
17
frontend/components/ui/card/CardHeader.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="card-header"
|
||||
:class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
17
frontend/components/ui/card/CardTitle.vue
Normal file
17
frontend/components/ui/card/CardTitle.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3
|
||||
data-slot="card-title"
|
||||
:class="cn('leading-none font-semibold', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
||||
7
frontend/components/ui/card/index.ts
Normal file
7
frontend/components/ui/card/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { default as Card } from './Card.vue'
|
||||
export { default as CardAction } from './CardAction.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'
|
||||
5
frontend/layouts/auth.vue
Normal file
5
frontend/layouts/auth.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<main class="flex h-full w-full items-center justify-center">
|
||||
<slot />
|
||||
</main>
|
||||
</template>
|
||||
@@ -90,9 +90,3 @@ const breadcrumbs = computed(() => getBreadcrumbs(route.path));
|
||||
</main>
|
||||
</SidebarProvider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'TikTok Sans', sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
||||
0
frontend/lib/api/auth/index.ts
Normal file
0
frontend/lib/api/auth/index.ts
Normal file
42
frontend/lib/api/auth/register.ts
Normal file
42
frontend/lib/api/auth/register.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { toast } from 'vue-sonner';
|
||||
import type { ApiResponse } from '~/types/api';
|
||||
|
||||
export async function registerUser(
|
||||
username: string,
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<{ success: boolean }> {
|
||||
const { data, error } = await useFetch<ApiResponse<undefined>>(
|
||||
getApiUrl('auth/register'),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: username,
|
||||
email: email,
|
||||
password: password,
|
||||
}),
|
||||
responseType: 'json',
|
||||
}
|
||||
);
|
||||
|
||||
if (data.value == null) {
|
||||
toast.error('Register', {
|
||||
description: error.value?.data ?? 'Failed to register',
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
toast.success('Register', {
|
||||
description: `Successfully registered user ${username}`,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
52
frontend/pages/login.vue
Normal file
52
frontend/pages/login.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
} from '@/components/ui/card';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'auth',
|
||||
});
|
||||
|
||||
// TODO: Get this from the backend
|
||||
const OPEN_ID = false;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="w-full max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email and password to your account.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="grid gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">Password</Label>
|
||||
<Input id="password" type="password" required />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter class="flex-col gap-2">
|
||||
<Button class="w-full">Sign in</Button>
|
||||
<Button class="w-full" variant="outline" :disabled="!OPEN_ID"
|
||||
>OpenID Connect</Button
|
||||
>
|
||||
<NuxtLink to="/register" class="w-full">
|
||||
<Button class="w-full" variant="ghost">Register instead</Button>
|
||||
</NuxtLink>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</template>
|
||||
95
frontend/pages/register.vue
Normal file
95
frontend/pages/register.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
} from '@/components/ui/card';
|
||||
import { registerUser } from '~/lib/api/auth/register';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'auth',
|
||||
});
|
||||
|
||||
const registering = ref(false);
|
||||
const username = ref('');
|
||||
const email = ref('');
|
||||
const password = ref('');
|
||||
|
||||
const allFieldsValid = computed(
|
||||
() =>
|
||||
username.value.trim().length > 0 &&
|
||||
email.value.trim().length > 0 &&
|
||||
password.value.trim().length > 0
|
||||
);
|
||||
|
||||
async function submit() {
|
||||
registering.value = true;
|
||||
|
||||
const { success } = await registerUser(
|
||||
username.value,
|
||||
email.value,
|
||||
password.value
|
||||
);
|
||||
|
||||
if (success) {
|
||||
await navigateTo({ path: '/login' });
|
||||
return;
|
||||
}
|
||||
|
||||
registering.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="w-full max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-2xl">Register</CardTitle>
|
||||
<CardDescription> Create a new user account </CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="grid gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="username">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
v-model="username"
|
||||
type="username"
|
||||
placeholder="409"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter class="flex-col gap-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
:disabled="!allFieldsValid || registering"
|
||||
@click="submit"
|
||||
>Register</Button
|
||||
>
|
||||
<NuxtLink to="/login" class="w-full">
|
||||
<Button class="w-full" variant="ghost">Log in instead</Button>
|
||||
</NuxtLink>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -2,3 +2,6 @@ export type ApiResponse<T> = {
|
||||
status: number;
|
||||
data: T;
|
||||
};
|
||||
|
||||
// TODO: Fix
|
||||
export type ApiError = string;
|
||||
|
||||
Reference in New Issue
Block a user