Laravel Vue SPA Admin Panel with Taildwindcss
Tags: laravel, vue, tailwindcss, vite
Published: 2024-01-14 09:50:58

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 -

[Project Overview](https://youtu.be/chKGnSIX5rc)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0bucl22xigrnlea37e7i.png)

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

Make layout component.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/avitbgdxw13cnslzrvnk.png)

**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.

1. Dashboard

2. About

3. Login

4. Register

5. PageNotFound

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qhk1d1wjuvcl74vz4x1r.png)

**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 Unextended](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n4n31mep9g11odmqi5r8.png)


Desktop View and Menu Extended
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pbhtfc4jthv1qpywbg72.png)


Mobile View and Menu Unextended
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w77m76zm6qsf6z4ntjbq.png)

Mobile View and Menu Extended
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n8jj2nh7kj1esi65igqa.png)



**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
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o9wxe7rq6arna3dkify0.png)

Desktop View and Menu Extended
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wm25jmq6179dtx6yfbbc.png)

**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
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dwarh6xjw9kjhcrud9rw.png)

Desktop View and Menu Extended
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w15j790xrh2zebnl74jn.png)

**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>
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8x66gu2ka5vgnwbq5drz.png)

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 😇]