This commit is contained in:
490
.laravel-guide/README.md
Normal file
490
.laravel-guide/README.md
Normal file
@@ -0,0 +1,490 @@
|
||||
# Free Laravel Vue 3.x Tailwind 4.x Dashboard
|
||||
|
||||
[](https://justboil.github.io/admin-one-vue-tailwind/)
|
||||
|
||||
This guide will help you integrate your Laravel application with [Admin One - free Vue 3 Tailwind 4 Admin Dashboard with dark mode](https://github.com/justboil/admin-one-vue-tailwind).
|
||||
|
||||
**Admin One** is simple, fast and free Vue.js 3.x Tailwind CSS 4.x admin dashboard with Laravel 12.x integration.
|
||||
|
||||
- Built with **Vue.js 3**, **Tailwind CSS 4** framework & **Composition API**
|
||||
- **Laravel** build tools
|
||||
- **Laravel Jetstream** with **Inertia + Vue** stack
|
||||
- **SFC** `<script setup>` [Info](https://v3.vuejs.org/api/sfc-script-setup.html)
|
||||
- **Pinia** state library (official Vuex 5)
|
||||
- **Dark mode**
|
||||
- **Styled** scrollbars
|
||||
- **Production CSS** is only **≈38kb**
|
||||
- Reusable components
|
||||
- Free under MIT License
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Install](#install)
|
||||
- [Copy styles, components and scripts](#copy-styles-components-and-scripts)
|
||||
- [Add pages](#add-pages)
|
||||
- [Fix router links](#fix-router-links)
|
||||
- [Add Inertia-related stuff](#add-inertia-related-stuff)
|
||||
- [Upgrading to Premium version](#upgrading-to-premium-version)
|
||||
- [Optional steps](#optional-steps)
|
||||
- [Laravel & Inertia docs](#laravel--inertia-docs)
|
||||
|
||||
## Install
|
||||
|
||||
### Install Laravel
|
||||
|
||||
First, [install Laravel](https://laravel.com/docs/installation) application
|
||||
|
||||
### Install Jetstream
|
||||
|
||||
Then `cd` to project dir and install Jetstream with Inertia Vue stack
|
||||
|
||||
```bash
|
||||
composer require laravel/jetstream
|
||||
|
||||
php artisan jetstream:install inertia
|
||||
|
||||
php artisan migrate
|
||||
|
||||
npm install
|
||||
```
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```bash
|
||||
npm i pinia @mdi/js chart.js numeral -D
|
||||
```
|
||||
|
||||
## Copy styles, components and scripts
|
||||
|
||||
**Before you start,** we recommend to rename Laravel Jetsreams's original folders (so you'll keep them for future reference) — `resources/js/Components` `resources/js/Layouts` `resources/js/Pages` to something like ComponentsJetsteam, LayoutsJetstream, etc.
|
||||
|
||||
Now clone [justboil/admin-one-vue-tailwind](https://github.com/justboil/admin-one-vue-tailwind) project somewhere locally (into any separate folder)
|
||||
|
||||
Next, copy these files **from justboil/admin-one-vue-tailwind project** directory **to laravel project** directory:
|
||||
|
||||
- Copy `tailwind.config.js` to `/`
|
||||
- Copy `src/components` `src/layouts` `src/stores` `src/colors.js` `src/config.js` `src/menuAside.js` `src/menuNavBar.js` to `resources/js/`
|
||||
- Copy `.laravel-guide/resources/js/` to `resources/js/`
|
||||
- Delete `resources/css/app.css`
|
||||
- Copy `src/css` to `resources/css`
|
||||
|
||||
### [optional] lowecase vs Capitalized folder names
|
||||
|
||||
Fresh Laravel install with Jetstream provides **Capitalized** folder names such as `Components`, `Layouts`, etc. For the sake of simplicity we just follow Vue conventions with lowercase folder names. However, you may opt-in to capitalize folder names:
|
||||
|
||||
- Make sure you've removed original Laravel Jetstream's `resources/js/Layouts` and `resources/js/Components` folders
|
||||
- Rename the folders you've copied in the previous section: `resources/js/layouts` to `Layouts`; `components` to `Components`; `stores` to `Stores`
|
||||
- Replace everywhere in imports: `@/layouts/` with `@/Layouts/`; `@/components/` with `@/Components/`; `@/stores/` with `@/Stores/`
|
||||
|
||||
### In tailwind.config.js
|
||||
|
||||
Please make sure, you've copied template's `tailwind.config.js`. Then replace `content`, to reflect Laravel's structure:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
content: [
|
||||
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
|
||||
'./storage/framework/views/*.php',
|
||||
'./resources/views/**/*.blade.php',
|
||||
'./resources/js/**/*.vue',
|
||||
'./resources/js/**/*.js'
|
||||
]
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### In resources/views/app.blade.php
|
||||
|
||||
- Remove `<link rel="stylesheet" href="https://fonts.bunny.net/css2?family=Nunito:wght@400;600;700&display=swap">`
|
||||
|
||||
## Add Pages
|
||||
|
||||
Let's just add first page. You can repeat these steps for other pages, if you wish to.
|
||||
|
||||
First, copy `src/views/HomeView.vue` (from justboil/admin-one-vue-tailwind project) to `resources/js/Pages/` (in your Laravel project).
|
||||
|
||||
Then, open `resources/js/Pages/HomeView.vue` and add `<Head>`:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { Head } from '@inertiajs/vue3'
|
||||
// ...
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutAuthenticated>
|
||||
<Head title="Dashboard" />
|
||||
<!-- ... -->
|
||||
</LayoutAuthenticated>
|
||||
</template>
|
||||
```
|
||||
|
||||
Add route in `routes/web.php`. There's a `/dashboard` route already defined by default, so just replace `Inertia::render('Dashboard')` with `Inertia::render('HomeView')`:
|
||||
|
||||
```php
|
||||
Route::get('/dashboard', function () {
|
||||
return Inertia::render('HomeView');
|
||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
||||
```
|
||||
|
||||
## Fix router links
|
||||
|
||||
Here we replace RouterLink with Inertia Link.
|
||||
|
||||
### resources/js/menuAside.js and resources/js/menuNavBar.js
|
||||
|
||||
Optionally, you can pass menu via Inertia shared props, so you'll be able to control it with PHP. Here we'd just use JS.
|
||||
|
||||
`to` should be replaced with `route` which specifies route name defined in `routes/web.php`. For external links `href` should be used instead. Here's an example for `menuAside.js`:
|
||||
|
||||
```javascript
|
||||
export default [
|
||||
'General',
|
||||
[
|
||||
{
|
||||
route: 'dashboard',
|
||||
icon: mdiMonitor,
|
||||
label: 'Dashboard'
|
||||
},
|
||||
// {
|
||||
// route: "another-route-name",
|
||||
// icon: mdiMonitor,
|
||||
// label: "Dashboard 2",
|
||||
// },
|
||||
{
|
||||
href: 'https://example.com/',
|
||||
icon: mdiMonitor,
|
||||
label: 'Example.com'
|
||||
}
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
Route names reflect ones defined in `routes/web.php`:
|
||||
|
||||
```php
|
||||
Route::middleware(['auth:sanctum', 'verified'])->get('/dashboard', function () {
|
||||
return Inertia::render('Home');
|
||||
})->name('dashboard');
|
||||
|
||||
// Route::middleware(['auth:sanctum', 'verified'])->get('/dashboard-2', function () {
|
||||
// return Inertia::render('Home2');
|
||||
// })->name('another-route-name');
|
||||
```
|
||||
|
||||
Now, let's update vue files, to make them work with route names and Inertia links.
|
||||
|
||||
### resources/js/components/AsideMenuItem.vue
|
||||
|
||||
Replace `RouterLink` imported from `vue-router` with `Link` import in `<script setup>` and add consts:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { Link } from '@inertiajs/vue3'
|
||||
// import { RouterLink } from "vue-router";
|
||||
// ...
|
||||
|
||||
// Add itemHref
|
||||
const itemHref = computed(() => (props.item.route ? route(props.item.route) : props.item.href))
|
||||
|
||||
// Add activeInactiveStyle
|
||||
const activeInactiveStyle = computed(() =>
|
||||
props.item.route && route().current(props.item.route)
|
||||
? darkModeStore.asideMenuItemActiveStyle
|
||||
: ''
|
||||
)
|
||||
|
||||
// ...
|
||||
</script>
|
||||
```
|
||||
|
||||
In `<template>` section:
|
||||
|
||||
- In `<component>` remove `v-slot` and `:to` attributes; replace `:is` with `:is="item.route ? Link : 'a'"` and `:href` with `:href="itemHref"`
|
||||
- Inside `<component>` replace `:class` attribute for both `<BaseIcon>` components, `<span>` and another `<BaseIcon>` with `:class="activeInactiveStyle"`
|
||||
- ...and for `<span>` (also inside `<component>`) replace `:class` attribute with `:class="[{ 'pr-12': !hasDropdown }, activeInactiveStyle]"`
|
||||
|
||||
### resources/js/components/BaseButton.vue
|
||||
|
||||
Replace `RouterLink` imported from `vue-router` with `Link` import in `<script setup>`:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { Link } from '@inertiajs/vue3'
|
||||
// import { RouterLink } from "vue-router";
|
||||
// ...
|
||||
</script>
|
||||
```
|
||||
|
||||
Replace `to` prop declaration with `routeName`:
|
||||
|
||||
```javascript
|
||||
const props = defineProps({
|
||||
// ...
|
||||
routeName: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
Fix `const is` declaration, so it returns the `Link` component when `props.routeName` is set:
|
||||
|
||||
```javascript
|
||||
const is = computed(() => {
|
||||
if (props.as) {
|
||||
return props.as
|
||||
}
|
||||
|
||||
if (props.routeName) {
|
||||
return Link
|
||||
}
|
||||
|
||||
if (props.href) {
|
||||
return 'a'
|
||||
}
|
||||
|
||||
return 'button'
|
||||
})
|
||||
```
|
||||
|
||||
Remove `:to` and replace `:href` in `<component>` with `:href="routeName ? route(routeName) : href"`:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<component
|
||||
:is="is"
|
||||
:class="componentClass"
|
||||
:href="routeName ? route(routeName) : href"
|
||||
:type="computedType"
|
||||
:target="target"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<!-- ... -->
|
||||
</component>
|
||||
</template>
|
||||
```
|
||||
|
||||
### resources/js/components/NavBarItem.vue
|
||||
|
||||
Replace `RouterLink` imported from `vue-router` with `Link` import in `<script setup>`:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { Link } from '@inertiajs/vue3'
|
||||
// import { RouterLink } from "vue-router";
|
||||
// ...
|
||||
|
||||
// Add itemHref
|
||||
const itemHref = computed(() => (props.item.route ? route(props.item.route) : props.item.href))
|
||||
|
||||
// Update `const is` to return `Link` when `props.routeName` is set:
|
||||
const is = computed(() => {
|
||||
if (props.item.href) {
|
||||
return 'a'
|
||||
}
|
||||
|
||||
if (props.item.route) {
|
||||
return Link
|
||||
}
|
||||
|
||||
return 'div'
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
Then, remove `:to` attribute and set `:href` attribute to `:href="itemHref"` in `<component>`.
|
||||
|
||||
## Add Inertia-related stuff
|
||||
|
||||
### resources/js/layouts/LayoutAuthenticated.vue
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// Remove vue-router stuff:
|
||||
|
||||
// import { useRouter } from 'vue-router'
|
||||
|
||||
// const router = useRouter()
|
||||
|
||||
// router.beforeEach(() => {
|
||||
// isAsideMobileExpanded.value = false
|
||||
// isAsideLgActive.value = false
|
||||
// })
|
||||
|
||||
// Add:
|
||||
|
||||
import { router } from '@inertiajs/vue3'
|
||||
|
||||
router.on('navigate', () => {
|
||||
isAsideMobileExpanded.value = false
|
||||
isAsideLgActive.value = false
|
||||
})
|
||||
|
||||
// Replace `isLogout` logic:
|
||||
|
||||
const menuClick = (event, item) => {
|
||||
// ...
|
||||
|
||||
if (item.isLogout) {
|
||||
// Add:
|
||||
router.post(route('logout'))
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
</script>
|
||||
```
|
||||
|
||||
### resources/js/components/UserAvatarCurrentUser.vue
|
||||
|
||||
Let's fetch user avatar initials based on username stored in database.
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { usePage } from '@inertiajs/vue3'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
|
||||
const userName = computed(() => usePage().props.auth.user.name)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UserAvatar :username="userName" api="initials">
|
||||
<slot />
|
||||
</UserAvatar>
|
||||
</template>
|
||||
```
|
||||
|
||||
### resources/js/components/NavBarItem.vue
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// Add usePage:
|
||||
import { usePage } from '@inertiajs/vue3'
|
||||
// Remove unused useMainStore:
|
||||
// import { useMainStore } from '@/stores/main.js'
|
||||
// ...
|
||||
|
||||
// Update itemLabel:
|
||||
const itemLabel = computed(() =>
|
||||
props.item.isCurrentUser ? usePage().props.auth.user.name : props.item.label
|
||||
)
|
||||
|
||||
// ...
|
||||
</script>
|
||||
```
|
||||
|
||||
## Upgrading to Premium version
|
||||
|
||||
Please make sure you have completed all previous steps in this guide, so you have everything up and running. Then download and uzip the Premium version files. Next, follow steps described below.
|
||||
|
||||
### Add deps
|
||||
|
||||
```bash
|
||||
npm i @headlessui/vue -D
|
||||
```
|
||||
|
||||
### Copy files
|
||||
|
||||
Copy files to your Laravel project (overwrite free version ones or merge if you have changed something):
|
||||
|
||||
- Copy `src/components/Premium` to `resources/js/components/Premium`
|
||||
- Copy `src/stores` to `resources/js/stores`
|
||||
- Copy `src/config.js` to `resources/js/config.js`
|
||||
- Copy `src/sampleButtonMenuOptions.js` to `resources/js/sampleButtonMenuOptions.js`
|
||||
- Copy `src/colorsPremium.js` to `resources/js/colorsPremium.js`
|
||||
|
||||
### Update tailwind.config.js
|
||||
|
||||
Replace `tailwind.config.js` in your Laravel project with the Premium one. Make sure to preserve `module.exports.content`:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
content: [
|
||||
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
|
||||
'./storage/framework/views/*.php',
|
||||
'./resources/views/**/*.blade.php',
|
||||
'./resources/js/**/*.vue',
|
||||
'./resources/js/**/*.js'
|
||||
]
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Update resources/js/app.js
|
||||
|
||||
Add layout store to `resources/js/app.js`:
|
||||
|
||||
```js
|
||||
// Add layout store
|
||||
import { useLayoutStore } from '@/stores/layout.js'
|
||||
|
||||
const layoutStore = useLayoutStore(pinia)
|
||||
|
||||
layoutStore.responsiveLayoutControl()
|
||||
window.onresize = () => layoutStore.responsiveLayoutControl()
|
||||
```
|
||||
|
||||
### Update resources/js/layouts/LayoutAuthenticated.vue
|
||||
|
||||
Replace contents of `resources/js/layouts/LayoutAuthenticated.vue` with contents of `src/js/layouts/LayoutAuthenticated.vue` (from the Premium version)
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
// Replace router use:
|
||||
|
||||
// import { useRouter } from "vue-router";
|
||||
import { router } from '@inertiajs/vue3'
|
||||
|
||||
// const router = useRouter();
|
||||
|
||||
// router.beforeEach(() => {
|
||||
// layoutStore.isAsideMobileExpanded = false;
|
||||
// });
|
||||
|
||||
router.on('navigate', () => {
|
||||
layoutStore.isAsideMobileExpanded = false
|
||||
})
|
||||
|
||||
// Add logout:
|
||||
|
||||
const menuClick = (event, item) => {
|
||||
// ...
|
||||
|
||||
if (item.isLogout) {
|
||||
router.post(route('logout'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Use premium login and signup layouts
|
||||
|
||||
Optionally, you may update layouts of login and signup, located at `resources/js/Pages/Auth` with Premium version layouts from `src/views/Premium/LoginView.vue` and `src/views/Premium/SignupView.vue`
|
||||
|
||||
## Optional steps
|
||||
|
||||
### Default style
|
||||
|
||||
It's likely, you'll use only one app style. Follow [this guide](https://justboil.github.io/docs/customization/#default-style) to set one of choice.
|
||||
|
||||
### Fix .editorconfig
|
||||
|
||||
Add to .editorconfig:
|
||||
|
||||
```editorconfig
|
||||
[*.{js,jsx,ts,tsx,vue,html,css}]
|
||||
indent_size = 2
|
||||
```
|
||||
|
||||
### resources/js/bootstrap.js
|
||||
|
||||
Global `lodash` and `axios` aren't needed, as we import them directly when necessary.
|
||||
|
||||
## Laravel & Inertia docs
|
||||
|
||||
- [Laravel Docs](https://laravel.com/docs)
|
||||
- [Laravel Jetstream Docs](https://jetstream.laravel.com/)
|
||||
- [Inertia](https://inertiajs.com/)
|
78
.laravel-guide/resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
78
.laravel-guide/resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup>
|
||||
import { useForm, Head } from '@inertiajs/vue3'
|
||||
import { ref } from 'vue'
|
||||
import LayoutGuest from '@/layouts/LayoutGuest.vue'
|
||||
import SectionFullScreen from '@/components/SectionFullScreen.vue'
|
||||
import CardBox from '@/components/CardBox.vue'
|
||||
import FormControl from '@/components/FormControl.vue'
|
||||
import FormField from '@/components/FormField.vue'
|
||||
import BaseDivider from '@/components/BaseDivider.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import FormValidationErrors from '@/components/FormValidationErrors.vue'
|
||||
|
||||
const form = useForm({
|
||||
password: ''
|
||||
})
|
||||
|
||||
const passwordInput = ref(null)
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('password.confirm'), {
|
||||
onFinish: () => {
|
||||
form.reset()
|
||||
|
||||
passwordInput.value?.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutGuest>
|
||||
<Head title="Secure Area" />
|
||||
|
||||
<SectionFullScreen
|
||||
v-slot="{ cardClass }"
|
||||
bg="purplePink"
|
||||
>
|
||||
<CardBox
|
||||
:class="cardClass"
|
||||
is-form
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<FormValidationErrors />
|
||||
|
||||
<FormField>
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
This is a secure area of the application. Please confirm your password before continuing.
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Password"
|
||||
label-for="password"
|
||||
help="Please enter your password to confirm"
|
||||
>
|
||||
<FormControl
|
||||
id="password"
|
||||
@set-ref="passwordInput = $event"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="info"
|
||||
label="Confirm"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
/>
|
||||
</CardBox>
|
||||
</SectionFullScreen>
|
||||
</LayoutGuest>
|
||||
</template>
|
91
.laravel-guide/resources/js/Pages/Auth/ForgotPassword.vue
Normal file
91
.laravel-guide/resources/js/Pages/Auth/ForgotPassword.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { useForm, Head, Link } from '@inertiajs/vue3'
|
||||
import { mdiEmail } from '@mdi/js'
|
||||
import LayoutGuest from '@/layouts/LayoutGuest.vue'
|
||||
import SectionFullScreen from '@/components/SectionFullScreen.vue'
|
||||
import CardBox from '@/components/CardBox.vue'
|
||||
import FormField from '@/components/FormField.vue'
|
||||
import FormControl from '@/components/FormControl.vue'
|
||||
import BaseDivider from '@/components/BaseDivider.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import FormValidationErrors from '@/components/FormValidationErrors.vue'
|
||||
import NotificationBarInCard from '@/components/NotificationBarInCard.vue'
|
||||
import BaseLevel from '@/components/BaseLevel.vue'
|
||||
|
||||
defineProps({
|
||||
status: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const form = useForm({
|
||||
email: ''
|
||||
})
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('password.email'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutGuest>
|
||||
<Head title="Forgot Password" />
|
||||
|
||||
<SectionFullScreen
|
||||
v-slot="{ cardClass }"
|
||||
bg="purplePink"
|
||||
>
|
||||
<CardBox
|
||||
:class="cardClass"
|
||||
is-form
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<FormValidationErrors />
|
||||
|
||||
<NotificationBarInCard
|
||||
v-if="status"
|
||||
color="info"
|
||||
>
|
||||
{{ status }}
|
||||
</NotificationBarInCard>
|
||||
|
||||
<FormField>
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Email"
|
||||
help="Please enter your email"
|
||||
>
|
||||
<FormControl
|
||||
v-model="form.email"
|
||||
:icon="mdiEmail"
|
||||
autocomplete="email"
|
||||
type="email"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseLevel>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="info"
|
||||
label="Email link"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
/>
|
||||
<Link
|
||||
:href="route('login')"
|
||||
>
|
||||
Back to login
|
||||
</Link>
|
||||
</BaseLevel>
|
||||
</CardBox>
|
||||
</SectionFullScreen>
|
||||
</LayoutGuest>
|
||||
</template>
|
129
.laravel-guide/resources/js/Pages/Auth/Login.vue
Normal file
129
.laravel-guide/resources/js/Pages/Auth/Login.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<script setup>
|
||||
import { useForm, Head, Link } from '@inertiajs/vue3'
|
||||
import { mdiAccount, mdiAsterisk } from '@mdi/js'
|
||||
import LayoutGuest from '@/layouts/LayoutGuest.vue'
|
||||
import SectionFullScreen from '@/components/SectionFullScreen.vue'
|
||||
import CardBox from '@/components/CardBox.vue'
|
||||
import FormCheckRadioGroup from '@/components/FormCheckRadioGroup.vue'
|
||||
import FormField from '@/components/FormField.vue'
|
||||
import FormControl from '@/components/FormControl.vue'
|
||||
import BaseDivider from '@/components/BaseDivider.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import BaseButtons from '@/components/BaseButtons.vue'
|
||||
import FormValidationErrors from '@/components/FormValidationErrors.vue'
|
||||
import NotificationBarInCard from '@/components/NotificationBarInCard.vue'
|
||||
import BaseLevel from '@/components/BaseLevel.vue'
|
||||
|
||||
const props = defineProps({
|
||||
canResetPassword: Boolean,
|
||||
status: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const form = useForm({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: []
|
||||
})
|
||||
|
||||
const submit = () => {
|
||||
form
|
||||
.transform(data => ({
|
||||
... data,
|
||||
remember: form.remember && form.remember.length ? 'on' : ''
|
||||
}))
|
||||
.post(route('login'), {
|
||||
onFinish: () => form.reset('password'),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutGuest>
|
||||
<Head title="Login" />
|
||||
|
||||
<SectionFullScreen
|
||||
v-slot="{ cardClass }"
|
||||
bg="purplePink"
|
||||
>
|
||||
<CardBox
|
||||
:class="cardClass"
|
||||
is-form
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<FormValidationErrors />
|
||||
|
||||
<NotificationBarInCard
|
||||
v-if="status"
|
||||
color="info"
|
||||
>
|
||||
{{ status }}
|
||||
</NotificationBarInCard>
|
||||
|
||||
<FormField
|
||||
label="Email"
|
||||
label-for="email"
|
||||
help="Please enter your email"
|
||||
>
|
||||
<FormControl
|
||||
v-model="form.email"
|
||||
:icon="mdiAccount"
|
||||
id="email"
|
||||
autocomplete="email"
|
||||
type="email"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Password"
|
||||
label-for="password"
|
||||
help="Please enter your password"
|
||||
>
|
||||
<FormControl
|
||||
v-model="form.password"
|
||||
:icon="mdiAsterisk"
|
||||
type="password"
|
||||
id="password"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormCheckRadioGroup
|
||||
v-model="form.remember"
|
||||
name="remember"
|
||||
:options="{ remember: 'Remember' }"
|
||||
/>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseLevel>
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="info"
|
||||
label="Login"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
/>
|
||||
<BaseButton
|
||||
v-if="canResetPassword"
|
||||
route-name="password.request"
|
||||
color="info"
|
||||
outline
|
||||
label="Remind"
|
||||
/>
|
||||
</BaseButtons>
|
||||
<Link
|
||||
:href="route('register')"
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
</BaseLevel>
|
||||
</CardBox>
|
||||
</SectionFullScreen>
|
||||
</LayoutGuest>
|
||||
</template>
|
131
.laravel-guide/resources/js/Pages/Auth/Register.vue
Normal file
131
.laravel-guide/resources/js/Pages/Auth/Register.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<script setup>
|
||||
import { useForm, usePage, Head } from "@inertiajs/vue3";
|
||||
import { computed } from "vue";
|
||||
import { mdiAccount, mdiEmail, mdiFormTextboxPassword } from "@mdi/js";
|
||||
import LayoutGuest from "@/layouts/LayoutGuest.vue";
|
||||
import SectionFullScreen from "@/components/SectionFullScreen.vue";
|
||||
import CardBox from "@/components/CardBox.vue";
|
||||
import FormCheckRadioGroup from "@/components/FormCheckRadioGroup.vue";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
import FormControl from "@/components/FormControl.vue";
|
||||
import BaseDivider from "@/components/BaseDivider.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import BaseButtons from "@/components/BaseButtons.vue";
|
||||
import FormValidationErrors from "@/components/FormValidationErrors.vue";
|
||||
|
||||
const form = useForm({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
password_confirmation: "",
|
||||
terms: [],
|
||||
});
|
||||
|
||||
const hasTermsAndPrivacyPolicyFeature = computed(
|
||||
() => usePage().props.jetstream?.hasTermsAndPrivacyPolicyFeature
|
||||
);
|
||||
|
||||
const submit = () => {
|
||||
form
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
terms: form.terms && form.terms.length,
|
||||
}))
|
||||
.post(route("register"), {
|
||||
onFinish: () => form.reset("password", "password_confirmation"),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutGuest>
|
||||
<Head title="Register" />
|
||||
|
||||
<SectionFullScreen v-slot="{ cardClass }" bg="purplePink">
|
||||
<CardBox
|
||||
:class="cardClass"
|
||||
class="my-24"
|
||||
is-form
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<FormValidationErrors />
|
||||
|
||||
<FormField label="Name" label-for="name" help="Please enter your name">
|
||||
<FormControl
|
||||
v-model="form.name"
|
||||
id="name"
|
||||
:icon="mdiAccount"
|
||||
autocomplete="name"
|
||||
type="text"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Email"
|
||||
label-for="email"
|
||||
help="Please enter your email"
|
||||
>
|
||||
<FormControl
|
||||
v-model="form.email"
|
||||
id="email"
|
||||
:icon="mdiEmail"
|
||||
autocomplete="email"
|
||||
type="email"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Password"
|
||||
label-for="password"
|
||||
help="Please enter new password"
|
||||
>
|
||||
<FormControl
|
||||
v-model="form.password"
|
||||
id="password"
|
||||
:icon="mdiFormTextboxPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Confirm Password"
|
||||
label-for="password_confirmation"
|
||||
help="Please confirm your password"
|
||||
>
|
||||
<FormControl
|
||||
v-model="form.password_confirmation"
|
||||
id="password_confirmation"
|
||||
:icon="mdiFormTextboxPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormCheckRadioGroup
|
||||
v-if="hasTermsAndPrivacyPolicyFeature"
|
||||
v-model="form.terms"
|
||||
name="remember"
|
||||
:options="{ agree: 'I agree to the Terms' }"
|
||||
/>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="info"
|
||||
label="Register"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
/>
|
||||
<BaseButton route-name="login" color="info" outline label="Login" />
|
||||
</BaseButtons>
|
||||
</CardBox>
|
||||
</SectionFullScreen>
|
||||
</LayoutGuest>
|
||||
</template>
|
111
.laravel-guide/resources/js/Pages/Auth/ResetPassword.vue
Normal file
111
.laravel-guide/resources/js/Pages/Auth/ResetPassword.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script setup>
|
||||
import { useForm, Head, Link } from '@inertiajs/vue3'
|
||||
import { mdiEmail, mdiFormTextboxPassword } from '@mdi/js'
|
||||
import LayoutGuest from '@/layouts/LayoutGuest.vue'
|
||||
import SectionFullScreen from '@/components/SectionFullScreen.vue'
|
||||
import CardBox from '@/components/CardBox.vue'
|
||||
import FormField from '@/components/FormField.vue'
|
||||
import FormControl from '@/components/FormControl.vue'
|
||||
import BaseDivider from '@/components/BaseDivider.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import FormValidationErrors from '@/components/FormValidationErrors.vue'
|
||||
|
||||
const props = defineProps({
|
||||
email: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const form = useForm({
|
||||
token: props.token,
|
||||
email: props.email,
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
|
||||
const submit = () => {
|
||||
form
|
||||
.post(route('password.update'), {
|
||||
onFinish: () => form.reset('password', 'password_confirmation'),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutGuest>
|
||||
<Head title="Reset Password" />
|
||||
|
||||
<SectionFullScreen
|
||||
v-slot="{ cardClass }"
|
||||
bg="purplePink"
|
||||
>
|
||||
<CardBox
|
||||
:class="cardClass"
|
||||
is-form
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<FormValidationErrors />
|
||||
|
||||
<FormField
|
||||
label="Email"
|
||||
label-for="email"
|
||||
help="Please enter your email"
|
||||
>
|
||||
<FormControl
|
||||
v-model="form.email"
|
||||
:icon="mdiEmail"
|
||||
autocomplete="email"
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Password"
|
||||
label-for="password"
|
||||
help="Please enter new password"
|
||||
>
|
||||
<FormControl
|
||||
v-model="form.password"
|
||||
:icon="mdiFormTextboxPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
id="password"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label="Confirm Password"
|
||||
label-for="password_confirmation"
|
||||
help="Please confirm new password"
|
||||
>
|
||||
<FormControl
|
||||
v-model="form.password_confirmation"
|
||||
:icon="mdiFormTextboxPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
id="password_confirmation"
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="info"
|
||||
label="Reset password"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
/>
|
||||
</CardBox>
|
||||
</SectionFullScreen>
|
||||
</LayoutGuest>
|
||||
</template>
|
126
.laravel-guide/resources/js/Pages/Auth/TwoFactorChallenge.vue
Normal file
126
.laravel-guide/resources/js/Pages/Auth/TwoFactorChallenge.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<script setup>
|
||||
import { useForm, Head } from '@inertiajs/vue3'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import LayoutGuest from '@/layouts/LayoutGuest.vue'
|
||||
import SectionFullScreen from '@/components/SectionFullScreen.vue'
|
||||
import CardBox from '@/components/CardBox.vue'
|
||||
import FormControl from '@/components/FormControl.vue'
|
||||
import FormField from '@/components/FormField.vue'
|
||||
import BaseDivider from '@/components/BaseDivider.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import FormValidationErrors from '@/components/FormValidationErrors.vue'
|
||||
import BaseLevel from '@/components/BaseLevel.vue'
|
||||
|
||||
const recovery = ref(false)
|
||||
|
||||
const form = useForm({
|
||||
code: '',
|
||||
recovery_code: ''
|
||||
})
|
||||
|
||||
const recoveryCodeInput = ref(null)
|
||||
const codeInput = ref(null)
|
||||
|
||||
const toggleRecovery = async () => {
|
||||
recovery.value ^= true
|
||||
|
||||
await nextTick()
|
||||
|
||||
if (recovery.value) {
|
||||
recoveryCodeInput.value?.focus()
|
||||
form.code = ''
|
||||
} else {
|
||||
codeInput.value?.focus()
|
||||
form.recovery_code = ''
|
||||
}
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('two-factor.login'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutGuest>
|
||||
<Head title="Two-factor Confirmation" />
|
||||
|
||||
<SectionFullScreen
|
||||
v-slot="{ cardClass }"
|
||||
bg="purplePink"
|
||||
>
|
||||
<CardBox
|
||||
:class="cardClass"
|
||||
is-form
|
||||
@submit.prevent="submit"
|
||||
>
|
||||
<FormValidationErrors />
|
||||
|
||||
<FormField>
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
<template v-if="! recovery">
|
||||
Please confirm access to your account by entering the authentication code provided by your authenticator application.
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
Please confirm access to your account by entering one of your emergency recovery codes.
|
||||
</template>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-if="!recovery"
|
||||
label="Code"
|
||||
label-for="code"
|
||||
help="Please enter one-time code"
|
||||
>
|
||||
<FormControl
|
||||
id="code"
|
||||
@set-ref="codeInput = $event"
|
||||
v-model="form.code"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autofocus
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-else
|
||||
label="Recovery Code"
|
||||
label-for="recovery_code"
|
||||
help="Please enter recovery code"
|
||||
>
|
||||
<FormControl
|
||||
id="recovery_code"
|
||||
@set-ref="recoveryCodeInput = $event"
|
||||
v-model="form.recovery_code"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseLevel>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="info"
|
||||
label="Log in"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
/>
|
||||
<button @click.prevent="toggleRecovery">
|
||||
<template v-if="!recovery">
|
||||
Use a recovery code
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
Use an authentication code
|
||||
</template>
|
||||
</button>
|
||||
</BaseLevel>
|
||||
</CardBox>
|
||||
</SectionFullScreen>
|
||||
</LayoutGuest>
|
||||
</template>
|
70
.laravel-guide/resources/js/Pages/Auth/VerifyEmail.vue
Normal file
70
.laravel-guide/resources/js/Pages/Auth/VerifyEmail.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup>
|
||||
import { useForm, Head, Link } from "@inertiajs/vue3";
|
||||
import { computed } from "vue";
|
||||
import LayoutGuest from "@/layouts/LayoutGuest.vue";
|
||||
import SectionFullScreen from "@/components/SectionFullScreen.vue";
|
||||
import CardBox from "@/components/CardBox.vue";
|
||||
import FormField from "@/components/FormField.vue";
|
||||
import BaseDivider from "@/components/BaseDivider.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import FormValidationErrors from "@/components/FormValidationErrors.vue";
|
||||
import NotificationBarInCard from "@/components/NotificationBarInCard.vue";
|
||||
import BaseLevel from "@/components/BaseLevel.vue";
|
||||
|
||||
const props = defineProps({
|
||||
status: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm();
|
||||
|
||||
const verificationLinkSent = computed(
|
||||
() => props.status === "verification-link-sent"
|
||||
);
|
||||
|
||||
const submit = () => {
|
||||
form.post(route("verification.send"));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutGuest>
|
||||
<Head title="Email Verification" />
|
||||
|
||||
<SectionFullScreen v-slot="{ cardClass }" bg="purplePink">
|
||||
<CardBox :class="cardClass" is-form @submit.prevent="submit">
|
||||
<FormValidationErrors />
|
||||
|
||||
<NotificationBarInCard v-if="verificationLinkSent" color="info">
|
||||
A new verification link has been sent to the email address you
|
||||
provided during registration.
|
||||
</NotificationBarInCard>
|
||||
|
||||
<FormField>
|
||||
<div class="mb-4 text-sm text-gray-600">
|
||||
Thanks for signing up! Before getting started, could you verify your
|
||||
email address by clicking on the link we just emailed to you? If you
|
||||
didn't receive the email, we will gladly send you another.
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
|
||||
<BaseLevel>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
color="info"
|
||||
label="Resend Verification Email"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
/>
|
||||
<Link :href="route('logout')" method="post" as="button">
|
||||
Logout
|
||||
</Link>
|
||||
</BaseLevel>
|
||||
</CardBox>
|
||||
</SectionFullScreen>
|
||||
</LayoutGuest>
|
||||
</template>
|
37
.laravel-guide/resources/js/app.js
Normal file
37
.laravel-guide/resources/js/app.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import '../css/main.css'
|
||||
|
||||
import { createPinia } from 'pinia'
|
||||
// import { useDarkModeStore } from '@/stores/darkMode.js'
|
||||
import { createApp, h } from 'vue'
|
||||
import { createInertiaApp } from '@inertiajs/vue3'
|
||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'
|
||||
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m'
|
||||
|
||||
const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
createInertiaApp({
|
||||
title: (title) => `${title} - ${appName}`,
|
||||
resolve: (name) =>
|
||||
resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
|
||||
setup({ el, App, props, plugin }) {
|
||||
return createApp({ render: () => h(App, props) })
|
||||
.use(plugin)
|
||||
.use(pinia)
|
||||
.use(ZiggyVue, Ziggy)
|
||||
.mount(el)
|
||||
},
|
||||
progress: {
|
||||
color: '#4B5563'
|
||||
}
|
||||
})
|
||||
|
||||
// const darkModeStore = useDarkModeStore(pinia)
|
||||
|
||||
// if (
|
||||
// (!localStorage['darkMode'] && window.matchMedia('(prefers-color-scheme: dark)').matches) ||
|
||||
// localStorage['darkMode'] === '1'
|
||||
// ) {
|
||||
// darkModeStore.set(true)
|
||||
// }
|
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { usePage } from "@inertiajs/vue3";
|
||||
import NotificationBarInCard from "@/components/NotificationBarInCard.vue";
|
||||
|
||||
const errors = computed(() => usePage().props.errors);
|
||||
|
||||
const hasErrors = computed(() => Object.keys(errors.value).length > 0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NotificationBarInCard v-if="hasErrors" color="danger">
|
||||
<b>Whoops! Something went wrong.</b>
|
||||
<span v-for="(error, key) in errors" :key="key">{{ error }}</span>
|
||||
</NotificationBarInCard>
|
||||
</template>
|
Reference in New Issue
Block a user