Sometimes we work on an app That’s time, we need a simple Admin Panel. But most of the Admin Panel has a lot of components and features. But we need a simple Admin Panel for custom modification as design and logic demand. Today I discuss this simple SPA admin panel.
Below you can see the short video about this project -
Step-1
Install Laravel latest app
composer create-project laravel/laravel spa-admin-panel
Step-2
Install necessary packages.
npm install vue
npm install vue-router
npm install pinia
npm install @vitejs/plugin-vue
Step-3
Install Tailwindcss and setup Tailwindcss config file
npx tailwindcss init -p
Open tailwind.config.js and do this
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./resources/**/*.blade.php",
"./resources/**/*.js",
"./resources/**/*.vue",
],
theme: {
extend: {},
},
plugins: [],
}
Then go to resources folder then css folder and open app.css and write this code-
@tailwind base;
@tailwind components;
@tailwind utilities;
Now you finished your tailwindcss setup
Step-4
Vite configure setup. Open your vite.config.js and do this
import vue from '@vitejs/plugin-vue';
import laravel from 'laravel-vite-plugin';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
vue(),
],
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js',
},
},
});
Then open welcome.blade.php and do this
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,600&display=swap" rel="stylesheet" />
@vite(['resources/js/app.js', 'resources/css/app.css'])
</head>
<body >
<div id="app">
<app-component></app-component>
</div>
</body>
</html>
Step-5
App-component defined on app.js. Go to resources / js folder and open app.js.
import { createPinia } from 'pinia';
import { createApp } from "vue";
import './bootstrap';
//Component import
import AppComponent from './App.vue';
import router from "./routes/index.js";
//define
const app = createApp({});
const pinia = createPinia()
//define as component
app.component("app-component", AppComponent);
//use package
app.use(pinia)
app.use(router)
app.mount("#app");
Here we configure vue, pinia, vue-router and then define app-component.
Step-6
Make some folders and App.vue file inside js folder below like this
Step-7
Configure router file. Go to resources / js / routes and create index.js file. Write this code
import { createRouter, createWebHistory } from "vue-router";
import AuthLayout from "../components/layouts/AuthLayout.vue";
import DefaultLayout from "../components/layouts/DefaultLayout.vue";
import Page404 from "../views/PageNotFound.vue";
import About from "../views/about/About.vue";
import Login from "../views/auth/Login.vue";
import Register from "../views/auth/Register.vue";
import Dashboard from "../views/dashboard/Dashboard.vue";
const routes = [
{
path: "/",
redirect: "/dashboard",
component: DefaultLayout,
children: [
{ path: "/dashboard", name: "Dashboard", component: Dashboard, meta: { title: "Dashboard" } },
{ path: '/about', name: "About", component: About },
],
},
{
path: "/auth",
redirect: "/login",
name: "Auth",
component: AuthLayout,
children: [
{ path: "/login", name: "Login", component: Login },
{ path: "/register", name: "Register", component: Register },
],
},
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: Page404,
},
];
const router = createRouter( {
history: createWebHistory(),
routes,
} );
// Update the document title on each route change
router.beforeEach((to, from, next) => {
document.title = to.meta.title || 'Kamruzzaman';
next();
});
export default router;
Step-8
AuthLayout.vue
<template>
<section class="flex flex-col h-screen">
<!-- navbar -->
<nav class="w-full py-4 bg-blue-600 px-3 min-h-[10%] flex justify-center">
<img src="../../assets/coffeeLogo.png" alt="coffeeLogo" class="h-11">
</nav>
<router-view />
<!-- footer -->
<footer class="w-full py-3 bg-blue-800 text-center text-white min-h-[5%]">
This is coypright@syed kamruzzaman, just fun 😂
</footer>
</section>
</template>
DefaultLayout.vue
<template>
<div class="relative min-h-screen">
<Sidebar />
<!-- navbar -->
<div
class="bg-gray-700 py-4 px-4 text-light-grey flex justify-between items-center fixed left-0 right-0 z-10"
>
<ToggleIcon
class="my-1 w-6 h-6 sm:w-7 sm:h-7 text-gray-100 bg-dark-blue cursor-pointer md:hidden"
@click="toggleSidebar"
/>
<div class="cursor-pointer">
<router-link :to="{ name: 'Dashboard' }">
<img
src="../../assets/coffeeLogo.png"
class="h-8"
/>
</router-link>
</div>
<div class="flex gap-4">
<div class="flex items-center gap-4">
<h1
class="cursor-pointer font-renner_medium text-white"
title="Representative"
>
Hi, Kamruzzaman
</h1>
</div>
<div class="dropdown inline-block relative">
<UserIcon class="w-9 h-9 cursor-pointer fill-white" />
<ul
class="dropdown-menu min-w-max absolute right-0 hidden text-gray-700 pt-5"
>
<li
class="bg-gray-200 hover:bg-gray-400 py-2 px-4 block whitespace-no-wrap cursor-pointer"
@click="logout"
>
logout
</li>
</ul>
</div>
</div>
</div>
<!-- navbar -->
<!-- main section -->
<section
class="relative pt-[70px] md:pl-[256px]"
:class="[showSideBar ? '' : 'md:pl-[66px]']"
>
<router-view></router-view>
</section>
<!-- /main section -->
<!-- Footer Section -->
<div class="bg-gray-700 py-4 px-4 text-light-grey flex justify-center items-center fixed left-0 right-0 bottom-0 z-10">
<p class="text-white mx-auto">copyright@syedKamruzzamn</p>
</div>
<!-- End Footer Section -->
</div>
</template>
<script setup>
import { computed } from 'vue';
import { basicStore } from '../../store/basicStore';
import ToggleIcon from "../icons/ToggleIcon.vue";
import UserIcon from "../icons/UserIcon.vue";
import Sidebar from "./Sidebar.vue";
const basicStoreInfo = basicStore();
const toggleSidebar =()=>{
basicStoreInfo.showSideBar = !basicStoreInfo.showSideBar;
}
const logout =()=>{
alert("hello Ripon");
}
const showSideBar = computed(() => basicStoreInfo.showSideBar)
</script>
<style scoped>
.dropdown:hover .dropdown-menu {
display: block;
}
</style>
Sidebar.vue
<template>
<!-- desktop sidebar -->
<div
class="bg-gray-300 text-light-grey h-screen hidden w-full md:block md:fixed md:top-0 md:bottom-0 md:z-10 md:pt-[68px]"
:class="[showSideBar ? 'md:w-64' : 'md:w-16']"
>
<ChevronLeftIcon
class="relative -right-48 ml-5 my-2 w-8 h-8 stroke-2 text-light-grey bg-dark-blue cursor-pointer"
v-if="showSideBar"
@click="toggleSidebar"
/>
<ChevronRightIcon
class="ml-5 my-2 w-8 h-8 stroke-2 text-light-grey bg-dark-blue cursor-pointer"
v-if="!showSideBar"
@click="toggleSidebar"
/>
<nav class="relative">
<SidebarItem
to="Dashboard"
class="group relative"
:class="{
['router-link-active router-link-exact-active']:
$route.path.match('dashboard') !== null,
}"
>
<CompanyIcon class="w-10 h-10 fill-white" />
<span class="group-hover:opacity-100 transition-opacity bg-gray-800 px-3 text-sm text-gray-100 rounded-md absolute left-1/2 -translate-x-1/2 translate-y-full opacity-0 m-4 mx-auto"
v-if="!showSideBar"
>Dashboard</span>
<span
class="sidebar-item text-white"
:class="[showSideBar ? 'block' : 'hidden']"
>Dashboard</span
>
</SidebarItem>
<SidebarItem
to="About"
class="group relative"
:class="{
['router-link-active router-link-exact-active']:
$route.path.match('about') !== null,
}"
>
<OrdersIcon class="w-6 h-10 fill-white ml-2" />
<span class="group-hover:opacity-100 transition-opacity bg-gray-800 px-3 text-sm text-gray-100 rounded-md absolute left-1/2 -translate-x-1/2 translate-y-full opacity-0 m-4 mx-auto"
v-if="!showSideBar"
>About</span>
<span
class="sidebar-item ml-2 text-white"
:class="[showSideBar ? 'block' : 'hidden']"
>About</span
>
</SidebarItem>
</nav>
</div>
<!-- /desktop sidebar -->
<!-- mobile side bar -->
<MobileSidebar />
<!-- /mobile side bar -->
</template>
<script setup>
import ChevronLeftIcon from "../icons/ChevronLeftIcon.vue";
import ChevronRightIcon from "../icons/ChevronRightIcon.vue";
import CompanyIcon from "../icons/CompnayIcon.vue";
import OrdersIcon from "../icons/OrdersIcon.vue";
import SidebarItem from "./SidebarItem.vue";
import { computed, ref } from 'vue';
import { basicStore } from '../../store/basicStore';
import MobileSidebar from "./MobileSidebar.vue";
const basicStoreInfo = basicStore();
const show = ref(true)
const toggleSidebar =()=>{
basicStoreInfo.showSideBar = !basicStoreInfo.showSideBar;
}
const showSideBar = computed(() => basicStoreInfo.showSideBar)
</script>
SidebarItem.vue
<template>
<router-link
:to="{ name: to }"
class="flex items-center gap-2 px-3 py-2 hover:bg-light-gold transition duration-200 cursor-pointer"
>
<slot></slot>
</router-link>
</template>
<script>
export default {
props: {
to: {
required: true,
type: String,
default: "Dashboard",
},
},
computed: {
routeName() {
return this.$route.name;
},
},
};
</script>
<style scoped>
.router-link-active,
.router-link-exact-active {
background: #4c4c54;
}
</style>
MobileSidebar.vue
<template>
<!-- mobile side bar -->
<div
class="fixed bg-gray-600 h-screen w-full z-10 md:hidden transition-all duration-500 ease-in-out"
:class="[showSideBar ? '-top-[1px] sm:70px block' : '-top-[900px]']"
>
<nav class="mt-[80px]">
<SidebarItem
to="Dashboard"
:class="{
['router-link-active router-link-exact-active']:
$route.path.match('dashboard') !== null,
}"
@click="toggleSidebar"
>
<CompanyIcon class="w-10 h-10 fill-white text-light-grey" />
<span class="sidebar-item text-gray-100">Dashboard </span>
</SidebarItem>
<SidebarItem
to="About"
:class="{
['router-link-active router-link-exact-active']:
$route.path.match('about') !== null,
}"
@click="toggleSidebar"
>
<CompanyIcon class="w-10 h-10 fill-white text-light-grey" />
<span class="sidebar-item text-gray-100">About </span>
</SidebarItem>
</nav>
</div>
<!-- /mobile side bar -->
</template>
<script setup>
import { computed } from 'vue';
import { basicStore } from '../../store/basicStore';
import CompanyIcon from "../icons/CompnayIcon.vue";
import SidebarItem from "./SidebarItem.vue";
const basicStoreInfo = basicStore();
const toggleSidebar =()=>{
basicStoreInfo.showSideBar = !basicStoreInfo.showSideBar;
}
const showSideBar = computed(() => basicStoreInfo.showSideBar)
</script>
Step-9
Now we make some pages.
Dashboard
About
Login
Register
PageNotFound
Dashboard.vue
<template>
<div class="px-5 py-5">
<h1 class="text-3xl mb-3">Dashboard Page </h1>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Iure nihil nam labore deleniti ratione eius consectetur vel illo nemo reiciendis ullam, incidunt quam doloribus praesentium id, corrupti rem. Ullam dolores, non voluptate quis maxime ducimus temporibus repudiandae nihil quasi? Similique.
</p>
</div>
</template>
Desktop View and Menu Unextended
Desktop View and Menu Extended
Mobile View and Menu Unextended
About.vue
<template>
<div class="px-5 py-5">
<h1 class="text-3xl mb-3">About Page</h1>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Iure nihil nam labore deleniti ratione eius consectetur vel illo nemo reiciendis ullam, incidunt quam doloribus praesentium id, corrupti rem. Ullam dolores, non voluptate quis maxime ducimus temporibus repudiandae nihil quasi? Similique, accusamus eos sunt fugit tempora sit necessitatibus vero placeat corrupti voluptatem pariatur aliquid debitis corporis iusto. Suscipit, sunt!.
</p>
</div>
</template>
<script setup>
</script>
Design Look Similar Dashboard.
Login.vue
<template>
<div class="min-h-[85%] flex items-center justify-center bg-gray-100">
<div class="bg-white p-8 rounded shadow-md">
<h1 class="text-2xl font-semibold mb-6">Login Page</h1>
<!-- Form -->
<form @submit.prevent="login">
<!-- Username Input -->
<div class="mb-4">
<label for="username" class="block text-gray-600 text-sm font-medium mb-2">Username:</label>
<input
type="text"
id="username"
v-model="username"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500"
required
/>
</div>
<!-- Password Input -->
<div class="mb-6">
<label for="password" class="block text-gray-600 text-sm font-medium mb-2">Password:</label>
<input
type="password"
id="password"
v-model="password"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500"
required
/>
</div>
<!-- Submit Button -->
<button
type="submit"
class="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600 focus:outline-none focus:shadow-outline-blue"
>
Login
</button>
</form>
</div>
</div>
</template>
<script>
export default {
data() {
return {
username: '',
password: '',
};
},
methods: {
login() {
// Handle login logic here
console.log('Logging in...');
},
},
};
</script>
Desktop View and Menu Unextended
Desktop View and Menu Extended
Register.vue
<template>
<div class="min-h-[85%] flex items-center justify-center bg-gray-100">
<div class="bg-white p-8 rounded shadow-md">
<h1 class="text-2xl font-semibold mb-6">Register Page</h1>
<!-- Form -->
<form @submit.prevent="register">
<!-- Username Input -->
<div class="mb-4">
<label for="username" class="block text-gray-600 text-sm font-medium mb-2">Username:</label>
<input
type="text"
id="username"
v-model="username"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500"
required
/>
</div>
<!-- Email Input -->
<div class="mb-4">
<label for="email" class="block text-gray-600 text-sm font-medium mb-2">Email:</label>
<input
type="email"
id="email"
v-model="email"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500"
required
/>
</div>
<!-- Password Input -->
<div class="mb-6">
<label for="password" class="block text-gray-600 text-sm font-medium mb-2">Password:</label>
<input
type="password"
id="password"
v-model="password"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500"
required
/>
</div>
<!-- Confirm Password Input -->
<div class="mb-6">
<label for="confirmPassword" class="block text-gray-600 text-sm font-medium mb-2">Confirm Password:</label>
<input
type="password"
id="confirmPassword"
v-model="confirmPassword"
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500"
required
/>
</div>
<!-- Submit Button -->
<button
type="submit"
class="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600 focus:outline-none focus:shadow-outline-blue"
>
Register
</button>
</form>
</div>
</div>
</template>
<script>
export default {
data() {
return {
username: '',
email: '',
password: '',
confirmPassword: '',
};
},
methods: {
register() {
// Handle registration logic here
console.log('Registering...');
},
},
};
</script>
Desktop View and Menu Unextended
Desktop View and Menu Extended
PageNotFound.vue
<template>
<div class="min-h-screen flex items-center justify-center bg-gray-100">
<div class="text-center">
<h1 class="text-4xl font-bold mb-6">404 Not Found</h1>
<p class="text-gray-600 text-lg mb-8">The page you are looking for does not exist.</p>
<router-link to="/" class="text-blue-500 hover:underline">Go back to the home page</router-link>
</div>
</div>
</template>
Now we have done all the necessary things. we will now configure our main route. Go to routes folder and open web.php file and do this
Route::get('/{any}', function () {
return view('welcome');
})->where("any", ".*");
Now we finished our full SPA app processing.
Full Project GitHub
https://github.com/kamruzzamanripon/laravel-vue-pinia-admin-panel-boilerplate
That’s all. Happy Learning :) .
[if it is helpful, giving a star to the repository 😇]
Top comments (0)