Init
This commit is contained in:
2
resources/css/app.css
Normal file
2
resources/css/app.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
99
resources/js/Pages/Auth/CompleteProfile.vue
Normal file
99
resources/js/Pages/Auth/CompleteProfile.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import type { AuthFormField, FormSubmitEvent } from '@nuxt/ui'
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
import * as v from 'valibot'
|
||||
import { computed } from 'vue'
|
||||
import AuthLayout from '@/layouts/AuthLayout.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
socialiteUser: {
|
||||
email: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
suggested_username: string
|
||||
provider: string
|
||||
}
|
||||
}>()
|
||||
|
||||
const form = useForm({
|
||||
username: props.socialiteUser.suggested_username,
|
||||
first_name: props.socialiteUser.first_name,
|
||||
last_name: props.socialiteUser.last_name,
|
||||
})
|
||||
|
||||
const fields: AuthFormField[] = [
|
||||
{
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
label: 'Username',
|
||||
placeholder: 'Choose a unique username',
|
||||
required: true,
|
||||
hint: 'The suggested username was already taken. Please choose a different one.',
|
||||
},
|
||||
{
|
||||
name: 'first_name',
|
||||
type: 'text',
|
||||
label: 'First name',
|
||||
placeholder: 'Enter your first name',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'last_name',
|
||||
type: 'text',
|
||||
label: 'Last name',
|
||||
placeholder: 'Enter your last name',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
|
||||
const schema = v.object({
|
||||
username: v.pipe(
|
||||
v.string('Username is required'),
|
||||
v.nonEmpty('Username is required'),
|
||||
v.minLength(3, 'Username must be at least 3 characters'),
|
||||
v.regex(/^[\w-]+$/, 'Username can only contain letters, numbers, dashes and underscores'),
|
||||
),
|
||||
first_name: v.pipe(v.string('First name is required'), v.nonEmpty('First name is required')),
|
||||
last_name: v.pipe(v.string('Last name is required'), v.nonEmpty('Last name is required')),
|
||||
})
|
||||
|
||||
type Schema = v.InferOutput<typeof schema>
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
form.username = event.data.username
|
||||
form.first_name = event.data.first_name
|
||||
form.last_name = event.data.last_name
|
||||
|
||||
form.post('/complete-profile')
|
||||
}
|
||||
|
||||
const hasErrors = computed(() => Object.keys(form.errors).length > 0)
|
||||
const firstError = computed(() => Object.values(form.errors)[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthLayout>
|
||||
<UAuthForm
|
||||
:schema="schema"
|
||||
:fields="fields"
|
||||
:submit="{ label: 'Complete registration', loading: form.processing }"
|
||||
:disabled="form.processing"
|
||||
:ui="{ header: 'hidden' }"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<template v-if="hasErrors" #validation>
|
||||
<UAlert
|
||||
color="error"
|
||||
icon="i-lucide-alert-circle"
|
||||
:title="firstError"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<span class="text-muted">
|
||||
Signing up with {{ socialiteUser.email }}
|
||||
</span>
|
||||
</template>
|
||||
</UAuthForm>
|
||||
</AuthLayout>
|
||||
</template>
|
||||
75
resources/js/Pages/Auth/ForgotPassword.vue
Normal file
75
resources/js/Pages/Auth/ForgotPassword.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import type { AuthFormField, FormSubmitEvent } from '@nuxt/ui'
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
import * as v from 'valibot'
|
||||
import { computed } from 'vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import AuthLayout from '@/layouts/AuthLayout.vue'
|
||||
|
||||
const { config, flash } = useAuth()
|
||||
|
||||
const form = useForm({
|
||||
email: '',
|
||||
})
|
||||
|
||||
const fields: AuthFormField[] = [
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
label: 'Email',
|
||||
placeholder: 'Enter your email',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
|
||||
const schema = v.object({
|
||||
email: v.pipe(v.string('Email is required'), v.nonEmpty('Email is required'), v.email('Please enter a valid email')),
|
||||
})
|
||||
|
||||
type Schema = v.InferOutput<typeof schema>
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
form.email = event.data.email
|
||||
form.post('/forgot-password')
|
||||
}
|
||||
|
||||
const hasErrors = computed(() => Object.keys(form.errors).length > 0)
|
||||
const firstError = computed(() => Object.values(form.errors)[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthLayout>
|
||||
<UAuthForm
|
||||
:schema="schema"
|
||||
:fields="fields"
|
||||
:submit="{ label: config.forgotPassword.submit_label, loading: form.processing }"
|
||||
:disabled="form.processing"
|
||||
:ui="{ header: 'hidden' }"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<template v-if="hasErrors || flash.status" #validation>
|
||||
<UAlert
|
||||
v-if="hasErrors"
|
||||
color="error"
|
||||
icon="i-lucide-alert-circle"
|
||||
:title="firstError"
|
||||
/>
|
||||
<UAlert
|
||||
v-if="flash.status"
|
||||
color="success"
|
||||
icon="i-lucide-check-circle"
|
||||
:title="flash.status"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<span class="text-muted">
|
||||
Remember your password?
|
||||
<ULink to="/login" class="text-primary font-medium">
|
||||
Sign in
|
||||
</ULink>
|
||||
</span>
|
||||
</template>
|
||||
</UAuthForm>
|
||||
</AuthLayout>
|
||||
</template>
|
||||
111
resources/js/Pages/Auth/Login.vue
Normal file
111
resources/js/Pages/Auth/Login.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import type { AuthFormField, FormSubmitEvent } from '@nuxt/ui'
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
import * as v from 'valibot'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import AuthLayout from '@/layouts/AuthLayout.vue'
|
||||
|
||||
const { config, flash } = useAuth()
|
||||
|
||||
const form = useForm({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
})
|
||||
|
||||
const rememberMe = ref(false)
|
||||
|
||||
const fields: AuthFormField[] = [
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
label: 'Email',
|
||||
placeholder: 'Enter your email',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
label: 'Password',
|
||||
placeholder: 'Enter your password',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
|
||||
const providers = computed(() =>
|
||||
config.value.providers.map(provider => ({
|
||||
label: provider.label,
|
||||
icon: provider.icon,
|
||||
to: `/auth/${provider.key}`,
|
||||
})),
|
||||
)
|
||||
|
||||
const schema = v.object({
|
||||
email: v.pipe(v.string('Email is required'), v.nonEmpty('Email is required'), v.email('Please enter a valid email')),
|
||||
password: v.pipe(v.string('Password is required'), v.nonEmpty('Password is required')),
|
||||
remember: v.optional(v.boolean()),
|
||||
})
|
||||
|
||||
type Schema = v.InferOutput<typeof schema>
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
form.email = event.data.email
|
||||
form.password = event.data.password
|
||||
form.remember = rememberMe.value
|
||||
|
||||
form.post('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthLayout>
|
||||
<UAuthForm
|
||||
:schema="schema"
|
||||
:fields="fields"
|
||||
:providers="providers"
|
||||
:submit="{ label: config.login.submit_label, loading: form.processing }"
|
||||
:disabled="form.processing"
|
||||
:ui="{ header: 'hidden' }"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<template v-if="config.features.password_reset" #password-hint>
|
||||
<ULink to="/forgot-password" class="text-primary font-medium" tabindex="-1">
|
||||
Forgot password?
|
||||
</ULink>
|
||||
</template>
|
||||
|
||||
<template v-if="form.errors.email || flash.status" #validation>
|
||||
<UAlert
|
||||
v-if="form.errors.email"
|
||||
color="error"
|
||||
icon="i-lucide-alert-circle"
|
||||
:title="form.errors.email"
|
||||
/>
|
||||
<UAlert
|
||||
v-if="flash.status"
|
||||
color="success"
|
||||
icon="i-lucide-check-circle"
|
||||
:title="flash.status"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between">
|
||||
<UCheckbox
|
||||
v-if="config.features.remember_me"
|
||||
v-model="rememberMe"
|
||||
label="Remember me"
|
||||
/>
|
||||
<span v-else />
|
||||
<span v-if="config.features.registration" class="text-sm text-muted">
|
||||
Don't have an account?
|
||||
<ULink to="/register" class="text-primary font-medium">
|
||||
Sign up
|
||||
</ULink>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</UAuthForm>
|
||||
</AuthLayout>
|
||||
</template>
|
||||
155
resources/js/Pages/Auth/Register.vue
Normal file
155
resources/js/Pages/Auth/Register.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import type { AuthFormField, FormSubmitEvent } from '@nuxt/ui'
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
import * as v from 'valibot'
|
||||
import { computed } from 'vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import AuthLayout from '@/layouts/AuthLayout.vue'
|
||||
|
||||
const { config } = useAuth()
|
||||
|
||||
const form = useForm({
|
||||
username: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
|
||||
const fields: AuthFormField[] = [
|
||||
{
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
label: 'Username',
|
||||
placeholder: 'Choose a username',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'first_name',
|
||||
type: 'text',
|
||||
label: 'First name',
|
||||
placeholder: 'Enter your first name',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'last_name',
|
||||
type: 'text',
|
||||
label: 'Last name',
|
||||
placeholder: 'Enter your last name',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
label: 'Email',
|
||||
placeholder: 'Enter your email',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
label: 'Password',
|
||||
placeholder: 'Create a password',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'password_confirmation',
|
||||
type: 'password',
|
||||
label: 'Confirm password',
|
||||
placeholder: 'Confirm your password',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
|
||||
const providers = computed(() =>
|
||||
config.value.providers.map(provider => ({
|
||||
label: provider.label,
|
||||
icon: provider.icon,
|
||||
to: `/auth/${provider.key}`,
|
||||
})),
|
||||
)
|
||||
|
||||
const schema = v.pipe(
|
||||
v.object({
|
||||
username: v.pipe(
|
||||
v.string('Username is required'),
|
||||
v.nonEmpty('Username is required'),
|
||||
v.minLength(3, 'Username must be at least 3 characters'),
|
||||
v.regex(/^[\w-]+$/, 'Username can only contain letters, numbers, dashes and underscores'),
|
||||
),
|
||||
first_name: v.pipe(v.string('First name is required'), v.nonEmpty('First name is required')),
|
||||
last_name: v.pipe(v.string('Last name is required'), v.nonEmpty('Last name is required')),
|
||||
email: v.pipe(v.string('Email is required'), v.nonEmpty('Email is required'), v.email('Please enter a valid email')),
|
||||
password: v.pipe(v.string('Password is required'), v.nonEmpty('Password is required'), v.minLength(8, 'Password must be at least 8 characters')),
|
||||
password_confirmation: v.pipe(v.string('Please confirm your password'), v.nonEmpty('Please confirm your password')),
|
||||
}),
|
||||
v.forward(
|
||||
v.partialCheck(
|
||||
[['password'], ['password_confirmation']],
|
||||
input => input.password === input.password_confirmation,
|
||||
'Passwords do not match',
|
||||
),
|
||||
['password_confirmation'],
|
||||
),
|
||||
)
|
||||
|
||||
type Schema = v.InferOutput<typeof schema>
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
form.username = event.data.username
|
||||
form.first_name = event.data.first_name
|
||||
form.last_name = event.data.last_name
|
||||
form.email = event.data.email
|
||||
form.password = event.data.password
|
||||
form.password_confirmation = event.data.password_confirmation
|
||||
|
||||
form.post('/register')
|
||||
}
|
||||
|
||||
const hasErrors = computed(() => Object.keys(form.errors).length > 0)
|
||||
const firstError = computed(() => Object.values(form.errors)[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthLayout>
|
||||
<UAuthForm
|
||||
:schema="schema"
|
||||
:fields="fields"
|
||||
:providers="providers"
|
||||
:submit="{ label: config.register.submit_label, loading: form.processing }"
|
||||
:disabled="form.processing"
|
||||
:ui="{ header: 'hidden' }"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<template v-if="hasErrors" #validation>
|
||||
<UAlert
|
||||
color="error"
|
||||
icon="i-lucide-alert-circle"
|
||||
:title="firstError"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div v-if="config.legal.show_in_register && (config.legal.terms_url || config.legal.privacy_url)" class="mb-2">
|
||||
By creating an account, you agree to our
|
||||
<ULink v-if="config.legal.terms_url" :to="config.legal.terms_url" class="text-primary font-medium">
|
||||
Terms of Service
|
||||
</ULink>
|
||||
<template v-if="config.legal.terms_url && config.legal.privacy_url">
|
||||
and
|
||||
</template>
|
||||
<ULink v-if="config.legal.privacy_url" :to="config.legal.privacy_url" class="text-primary font-medium">
|
||||
Privacy Policy
|
||||
</ULink>.
|
||||
</div>
|
||||
<span class="text-muted">
|
||||
Already have an account?
|
||||
<ULink to="/login" class="text-primary font-medium">
|
||||
Sign in
|
||||
</ULink>
|
||||
</span>
|
||||
</template>
|
||||
</UAuthForm>
|
||||
</AuthLayout>
|
||||
</template>
|
||||
105
resources/js/Pages/Auth/ResetPassword.vue
Normal file
105
resources/js/Pages/Auth/ResetPassword.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import type { AuthFormField, FormSubmitEvent } from '@nuxt/ui'
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
import * as v from 'valibot'
|
||||
import { computed } from 'vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import AuthLayout from '@/layouts/AuthLayout.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
email: string
|
||||
token: string
|
||||
}>()
|
||||
|
||||
const { config } = useAuth()
|
||||
|
||||
const form = useForm({
|
||||
token: props.token,
|
||||
email: props.email,
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
|
||||
const fields: AuthFormField[] = [
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
label: 'Email',
|
||||
placeholder: 'Enter your email',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
label: 'New password',
|
||||
placeholder: 'Enter your new password',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'password_confirmation',
|
||||
type: 'password',
|
||||
label: 'Confirm password',
|
||||
placeholder: 'Confirm your new password',
|
||||
required: true,
|
||||
},
|
||||
]
|
||||
|
||||
const schema = v.pipe(
|
||||
v.object({
|
||||
email: v.pipe(v.string('Email is required'), v.nonEmpty('Email is required'), v.email('Please enter a valid email')),
|
||||
password: v.pipe(v.string('Password is required'), v.nonEmpty('Password is required'), v.minLength(8, 'Password must be at least 8 characters')),
|
||||
password_confirmation: v.pipe(v.string('Please confirm your password'), v.nonEmpty('Please confirm your password')),
|
||||
}),
|
||||
v.forward(
|
||||
v.partialCheck(
|
||||
[['password'], ['password_confirmation']],
|
||||
input => input.password === input.password_confirmation,
|
||||
'Passwords do not match',
|
||||
),
|
||||
['password_confirmation'],
|
||||
),
|
||||
)
|
||||
|
||||
type Schema = v.InferOutput<typeof schema>
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
form.email = event.data.email
|
||||
form.password = event.data.password
|
||||
form.password_confirmation = event.data.password_confirmation
|
||||
|
||||
form.post('/reset-password')
|
||||
}
|
||||
|
||||
const hasErrors = computed(() => Object.keys(form.errors).length > 0)
|
||||
const firstError = computed(() => Object.values(form.errors)[0])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthLayout>
|
||||
<UAuthForm
|
||||
:schema="schema"
|
||||
:fields="fields"
|
||||
:submit="{ label: config.resetPassword.submit_label, loading: form.processing }"
|
||||
:disabled="form.processing"
|
||||
:ui="{ header: 'hidden' }"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<template v-if="hasErrors" #validation>
|
||||
<UAlert
|
||||
color="error"
|
||||
icon="i-lucide-alert-circle"
|
||||
:title="firstError"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<span class="text-muted">
|
||||
Remember your password?
|
||||
<ULink to="/login" class="text-primary font-medium">
|
||||
Sign in
|
||||
</ULink>
|
||||
</span>
|
||||
</template>
|
||||
</UAuthForm>
|
||||
</AuthLayout>
|
||||
</template>
|
||||
60
resources/js/Pages/Welcome.vue
Normal file
60
resources/js/Pages/Welcome.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
defineProps<{
|
||||
appName: string
|
||||
}>()
|
||||
|
||||
const { user, isAuthenticated, config } = useAuth()
|
||||
|
||||
const logoutForm = useForm({})
|
||||
|
||||
function logout() {
|
||||
logoutForm.post('/logout')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<div class="min-h-screen flex flex-col bg-gray-100 dark:bg-gray-900">
|
||||
<header class="flex items-center justify-end gap-4 p-4">
|
||||
<template v-if="isAuthenticated">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ user?.full_name }}
|
||||
</span>
|
||||
<UButton variant="outline" :loading="logoutForm.processing" @click="logout">
|
||||
Logout
|
||||
</UButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<UButton to="/login" variant="ghost">
|
||||
Login
|
||||
</UButton>
|
||||
<UButton v-if="config.features.registration" to="/register">
|
||||
Register
|
||||
</UButton>
|
||||
</template>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl font-bold text-gray-900 dark:text-white">
|
||||
Welcome to {{ appName }}
|
||||
</h1>
|
||||
<p class="mt-4 text-gray-600 dark:text-gray-400">
|
||||
Inertia.js with Vue and Nuxt UI is ready to go!
|
||||
</p>
|
||||
<div v-if="!isAuthenticated" class="mt-8 flex gap-4 justify-center">
|
||||
<UButton to="/register">
|
||||
Get Started
|
||||
</UButton>
|
||||
<UButton variant="outline">
|
||||
Learn More
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UApp>
|
||||
</template>
|
||||
25
resources/js/app.ts
Normal file
25
resources/js/app.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { DefineComponent } from 'vue'
|
||||
import { createInertiaApp } from '@inertiajs/vue3'
|
||||
|
||||
import ui from '@nuxt/ui/vue-plugin'
|
||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
|
||||
import { createApp, h } from 'vue'
|
||||
import '../css/app.css'
|
||||
import './bootstrap'
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel'
|
||||
|
||||
createInertiaApp({
|
||||
title: title => (title ? `${title} - ${appName}` : appName),
|
||||
resolve: name =>
|
||||
resolvePageComponent(
|
||||
`./Pages/${name}.vue`,
|
||||
import.meta.glob<DefineComponent>('./Pages/**/*.vue'),
|
||||
),
|
||||
setup({ el, App, props, plugin }) {
|
||||
createApp({ render: () => h(App, props) })
|
||||
.use(plugin)
|
||||
.use(ui)
|
||||
.mount(el)
|
||||
},
|
||||
})
|
||||
7
resources/js/bootstrap.js
vendored
Normal file
7
resources/js/bootstrap.js
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import axios from 'axios'
|
||||
|
||||
window.axios = axios
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
|
||||
window.axios.defaults.withCredentials = true
|
||||
window.axios.defaults.withXSRFToken = true
|
||||
44
resources/js/components/common/Logo.vue
Normal file
44
resources/js/components/common/Logo.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
showText?: boolean
|
||||
}>(), {
|
||||
size: 'md',
|
||||
showText: true,
|
||||
})
|
||||
|
||||
const { config } = useAuth()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-3">
|
||||
<!--
|
||||
Replace this placeholder with your actual logo:
|
||||
<img src="/images/logo.svg" alt="Logo" :class="iconSizeClass">
|
||||
-->
|
||||
<div
|
||||
class="flex items-center justify-center rounded-xl bg-primary-500 text-white font-bold"
|
||||
:class="{
|
||||
'size-8 text-sm': size === 'sm',
|
||||
'size-12 text-xl': size === 'md',
|
||||
'size-16 text-2xl': size === 'lg',
|
||||
}"
|
||||
>
|
||||
{{ config.appName.charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="showText"
|
||||
class="font-semibold text-gray-900 dark:text-white"
|
||||
:class="{
|
||||
'text-lg': size === 'sm',
|
||||
'text-2xl': size === 'md',
|
||||
'text-3xl': size === 'lg',
|
||||
}"
|
||||
>
|
||||
{{ config.appName }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
46
resources/js/components/common/__tests__/Logo.test.ts
Normal file
46
resources/js/components/common/__tests__/Logo.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { computed } from 'vue'
|
||||
import Logo from '../Logo.vue'
|
||||
|
||||
// Mock the useAuth composable
|
||||
vi.mock('@/composables/useAuth', () => ({
|
||||
useAuth: () => ({
|
||||
config: computed(() => ({ appName: 'TestApp' })),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('logo', () => {
|
||||
it('renders the first letter of the app name', () => {
|
||||
const wrapper = mount(Logo)
|
||||
expect(wrapper.text()).toContain('T')
|
||||
})
|
||||
|
||||
it('renders the app name when showText is true', () => {
|
||||
const wrapper = mount(Logo, {
|
||||
props: { showText: true },
|
||||
})
|
||||
expect(wrapper.text()).toContain('TestApp')
|
||||
})
|
||||
|
||||
it('does not render the app name when showText is false', () => {
|
||||
const wrapper = mount(Logo, {
|
||||
props: { showText: false },
|
||||
})
|
||||
expect(wrapper.text()).not.toContain('TestApp')
|
||||
})
|
||||
|
||||
it('applies correct size classes for sm', () => {
|
||||
const wrapper = mount(Logo, {
|
||||
props: { size: 'sm' },
|
||||
})
|
||||
expect(wrapper.html()).toContain('size-8')
|
||||
})
|
||||
|
||||
it('applies correct size classes for lg', () => {
|
||||
const wrapper = mount(Logo, {
|
||||
props: { size: 'lg' },
|
||||
})
|
||||
expect(wrapper.html()).toContain('size-16')
|
||||
})
|
||||
})
|
||||
23
resources/js/composables/useAuth.ts
Normal file
23
resources/js/composables/useAuth.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { AuthConfig, Flash, User } from '@/types'
|
||||
import { usePage } from '@inertiajs/vue3'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export function useAuth() {
|
||||
const page = usePage<{
|
||||
auth: { user: User | null }
|
||||
flash: Flash
|
||||
authConfig: AuthConfig
|
||||
}>()
|
||||
|
||||
const user = computed(() => page.props.auth.user)
|
||||
const isAuthenticated = computed(() => !!page.props.auth.user)
|
||||
const flash = computed(() => page.props.flash)
|
||||
const config = computed(() => page.props.authConfig)
|
||||
|
||||
return {
|
||||
user,
|
||||
isAuthenticated,
|
||||
flash,
|
||||
config,
|
||||
}
|
||||
}
|
||||
21
resources/js/layouts/AuthLayout.vue
Normal file
21
resources/js/layouts/AuthLayout.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import Logo from '@/components/common/Logo.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<div class="min-h-screen flex flex-col items-center justify-center bg-gradient-to-br from-primary-50 to-primary-100 dark:from-gray-900 dark:to-gray-800 p-4">
|
||||
<div class="w-full max-w-md">
|
||||
<UPageCard :ui="{ wrapper: 'flex flex-col flex-1 items-center', header: 'mb-4' }">
|
||||
<template #header>
|
||||
<slot name="logo">
|
||||
<Logo />
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
</UPageCard>
|
||||
</div>
|
||||
</div>
|
||||
</UApp>
|
||||
</template>
|
||||
36
resources/js/types/auth/config.ts
Normal file
36
resources/js/types/auth/config.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export interface AuthFeatures {
|
||||
registration: boolean
|
||||
password_reset: boolean
|
||||
remember_me: boolean
|
||||
email_verification: boolean
|
||||
}
|
||||
|
||||
export interface AuthPageConfig {
|
||||
title: string
|
||||
description: string
|
||||
icon: string
|
||||
submit_label: string
|
||||
}
|
||||
|
||||
export interface AuthProvider {
|
||||
key: string
|
||||
label: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
export interface AuthLegal {
|
||||
terms_url: string | null
|
||||
privacy_url: string | null
|
||||
show_in_register: boolean
|
||||
}
|
||||
|
||||
export interface AuthConfig {
|
||||
appName: string
|
||||
features: AuthFeatures
|
||||
login: AuthPageConfig
|
||||
register: AuthPageConfig
|
||||
forgotPassword: AuthPageConfig
|
||||
resetPassword: AuthPageConfig
|
||||
providers: AuthProvider[]
|
||||
legal: AuthLegal
|
||||
}
|
||||
1
resources/js/types/auth/index.ts
Normal file
1
resources/js/types/auth/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './config'
|
||||
4
resources/js/types/index.ts
Normal file
4
resources/js/types/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Re-export all types for convenience
|
||||
export * from './auth'
|
||||
export * from './inertia'
|
||||
export * from './models'
|
||||
6
resources/js/types/inertia/flash.ts
Normal file
6
resources/js/types/inertia/flash.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Flash {
|
||||
success: string | null
|
||||
error: string | null
|
||||
message: string | null
|
||||
status: string | null
|
||||
}
|
||||
2
resources/js/types/inertia/index.ts
Normal file
2
resources/js/types/inertia/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './flash'
|
||||
export * from './page-props'
|
||||
11
resources/js/types/inertia/page-props.ts
Normal file
11
resources/js/types/inertia/page-props.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { AuthConfig } from '../auth'
|
||||
import type { User } from '../models'
|
||||
import type { Flash } from './flash'
|
||||
|
||||
export interface PageProps {
|
||||
auth: {
|
||||
user: User | null
|
||||
}
|
||||
flash: Flash
|
||||
authConfig: AuthConfig
|
||||
}
|
||||
1
resources/js/types/models/index.ts
Normal file
1
resources/js/types/models/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './user'
|
||||
11
resources/js/types/models/user.ts
Normal file
11
resources/js/types/models/user.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface User {
|
||||
id: number
|
||||
username: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
full_name: string
|
||||
email: string
|
||||
email_verified_at: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
14
resources/views/app.blade.php
Normal file
14
resources/views/app.blade.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@inertiaHead
|
||||
@vite('resources/js/app.ts')
|
||||
</head>
|
||||
<body>
|
||||
<div class="isolate">
|
||||
@inertia
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
277
resources/views/welcome.blade.php
Normal file
277
resources/views/welcome.blade.php
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user