DEV Community

Cover image for Laravel Vue SPA Admin Panel with Taildwindcss
syed kamruzzaman
syed kamruzzaman

Posted on

Laravel Vue SPA Admin Panel with Taildwindcss

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

Step-1

Install Laravel latest app

composer create-project laravel/laravel spa-admin-panel
Enter fullscreen mode Exit fullscreen mode

Step-2

Install necessary packages.

npm install vue
npm install vue-router
npm install pinia
npm install  @vitejs/plugin-vue
Enter fullscreen mode Exit fullscreen mode

Step-3

Install Tailwindcss and setup Tailwindcss config file

npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

Open tailwind.config.js and do this

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./resources/**/*.blade.php",
    "./resources/**/*.js",
    "./resources/**/*.vue",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Then go to resources folder then css folder and open app.css and write this code-

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

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',
        },
    },
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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

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;
Enter fullscreen mode Exit fullscreen mode

Step-8

Make layout component.
Image description

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Step-9

Now we make some pages.

  1. Dashboard

  2. About

  3. Login

  4. Register

  5. PageNotFound

Image description

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>
Enter fullscreen mode Exit fullscreen mode

Desktop View and Menu Unextended
Desktop View and Menu Unextended

Desktop View and Menu Extended
Image description

Mobile View and Menu Unextended
Image description

Mobile View and Menu Extended
Image description

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>
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

Desktop View and Menu Unextended
Image description

Desktop View and Menu Extended
Image description

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>

Enter fullscreen mode Exit fullscreen mode

Desktop View and Menu Unextended
Image description

Desktop View and Menu Extended
Image description

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>
Enter fullscreen mode Exit fullscreen mode

Image description

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", ".*");
Enter fullscreen mode Exit fullscreen mode

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)