CoreUI Free + React (Vite) — Admin Dashboard Tutorial

Responsive • Light Theme • สร้าง Sidebar + Header + Routing (React Router v6) + Dashboard KPI Cards (ย่อ Sidebar เหลือ Icon)

ผู้สอน: ผู้ช่วยศาสตราจารย์ ปองพล นิลพฤกษ์
Tech: Vite + ReactCoreUI FreeBootstrap 5 utilitiesReact Router v6
ผลลัพธ์: Layout + SPA Routing + UI Dashboard

Step 1: สร้างโปรเจกต์ React ด้วย Vite Terminal

เป้าหมาย: สร้างโปรเจกต์ชื่อ coreui-react แล้วรันให้เห็นหน้าเริ่มต้น

npm create vite@latest coreui-react -- --template react
cd coreui-react
npm install
npm run dev
เช็กผล: เปิด URL ที่ Vite แสดง แล้วเห็นหน้าเริ่มต้นของ React
แนวคิด: Vite ช่วยให้ dev server โหลดเร็ว เหมาะกับการเรียน/สอน

Step 2: ติดตั้ง CoreUI + Router + Icons Terminal

ติดตั้ง UI components, Router และไอคอน เพื่อทำหน้า Admin ให้ดูมืออาชีพขึ้น

npm i @coreui/react @coreui/coreui @popperjs/core
npm i react-router-dom
npm i bootstrap-icons

Step 3: Import CSS/JS ใน main.jsx src/main.jsx

Import CoreUI CSS/JS + Bootstrap Icons + Global CSS ของเรา

ไฟล์: src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'

// CoreUI
import '@coreui/coreui/dist/css/coreui.min.css'
import '@coreui/coreui/dist/js/coreui.bundle.min.js'

// Bootstrap Icons
import 'bootstrap-icons/font/bootstrap-icons.css'

// Global CSS ของเรา
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

Step 4: จัดโครงสร้างโฟลเดอร์ Project Structure

แยก Layout / Pages / Routes เพื่อให้อ่านง่ายและขยายต่อได้

src/
  layout/
    AppLayout.jsx
    AppHeader.jsx
    AppSidebar.jsx
  pages/
    Dashboard.jsx
    Users.jsx
  routes/
    AppRoutes.jsx
  index.css
  App.jsx
  main.jsx

Step 5: สร้าง index.css (Sidebar UX) src/index.css

ทำเมนูให้ดูเป็น list ปกติ, active/hover สวย, และตอนย่อ sidebar ให้ซ่อน label

ไฟล์: src/index.css
.sidebar-nav .nav-link{
  padding: .55rem .75rem;
  border-radius: .5rem;
  line-height: 1.2;
}
.sidebar-nav .nav-link:hover{
  background: #f6f8fb;
}
.sidebar-nav .nav-link.active{
  background: #eef2ff;
  color: #1f2a44;
  font-weight: 600;
  border-left: 3px solid #4f46e5;
}
.sidebar.sidebar-narrow .nav-label{
  display:none;
}

Step 6: สร้าง Pages (Dashboard/Users) src/pages/*

สร้างหน้าเพื่อลองสลับ route และทำ Dashboard เป็น KPI Cards + Activities

ไฟล์: src/pages/Users.jsx
import { CCard, CCardBody } from "@coreui/react";

export default function Users() {
  return (
    <CCard className="shadow-sm border-0 rounded-4">
      <CCardBody className="p-4">
        <h5 className="fw-semibold mb-2">Users</h5>
        <div className="text-body-secondary">
          หน้านี้เตรียมไว้สำหรับทำ Table + Search + Pagination
        </div>
      </CCardBody>
    </CCard>
  );
}
ไฟล์: src/pages/Dashboard.jsx
import {
  CCard,
  CCardBody,
  CCardHeader,
  CRow,
  CCol,
  CProgress,
  CTable,
  CTableHead,
  CTableRow,
  CTableHeaderCell,
  CTableBody,
  CTableDataCell,
  CBadge,
} from "@coreui/react";

function StatCard({ title, value, delta, icon, hint, progress }) {
  return (
    <CCard className="shadow-sm border-0 rounded-4 h-100">
      <CCardBody className="p-3 p-md-4">
        <div className="d-flex align-items-start justify-content-between">
          <div>
            <div className="text-body-secondary small">{title}</div>
            <div className="fs-4 fw-semibold mt-1">{value}</div>
            <div className="small mt-2">
              <CBadge color={delta.startsWith("+") ? "success" : "danger"} className="me-2">
                {delta}
              </CBadge>
              <span className="text-body-secondary">{hint}</span>
            </div>
          </div>

          <div
            className="rounded-4 d-inline-flex align-items-center justify-content-center bg-light"
            style={{ width: 44, height: 44 }}
          >
            <i className={`bi ${icon} fs-5`} />
          </div>
        </div>

        {typeof progress === "number" && (
          <div className="mt-3">
            <CProgress value={progress} />
            <div className="small text-body-secondary mt-1">{progress}% of target</div>
          </div>
        )}
      </CCardBody>
    </CCard>
  );
}

export default function Dashboard() {
  return (
    <div className="pb-4">
      <CRow className="g-3 g-md-4">
        <CCol xs={12} sm={6} xl={3}>
          <StatCard title="Active Users" value="1,248" delta="+8.2%" icon="bi-people" hint="เทียบสัปดาห์ก่อน" progress={72} />
        </CCol>
        <CCol xs={12} sm={6} xl={3}>
          <StatCard title="Revenue" value="฿ 342,900" delta="+4.6%" icon="bi-cash-stack" hint="รายได้เดือนนี้" progress={58} />
        </CCol>
        <CCol xs={12} sm={6} xl={3}>
          <StatCard title="New Orders" value="392" delta="-1.3%" icon="bi-bag-check" hint="คำสั่งซื้อใหม่" progress={41} />
        </CCol>
        <CCol xs={12} sm={6} xl={3}>
          <StatCard title="System Health" value="99.9%" delta="+0.1%" icon="bi-shield-check" hint="Uptime 30 วัน" progress={99} />
        </CCol>
      </CRow>

      <CRow className="g-3 g-md-4 mt-1">
        <CCol lg={7}>
          <CCard className="shadow-sm border-0 rounded-4 h-100">
            <CCardHeader className="bg-white border-0 pt-4 px-4">
              <div className="d-flex align-items-center justify-content-between">
                <div className="fw-semibold">Recent Activities</div>
                <span className="small text-body-secondary">Updated just now</span>
              </div>
            </CCardHeader>
            <CCardBody className="px-4 pb-4">
              <CTable responsive className="align-middle mb-0">
                <CTableHead>
                  <CTableRow>
                    <CTableHeaderCell>Time</CTableHeaderCell>
                    <CTableHeaderCell>Action</CTableHeaderCell>
                    <CTableHeaderCell>Status</CTableHeaderCell>
                  </CTableRow>
                </CTableHead>
                <CTableBody>
                  {[
                    { t: "09:12", a: "User login: user_102", s: "success" },
                    { t: "09:25", a: "Create order: #A-3921", s: "success" },
                    { t: "10:02", a: "Payment pending: #A-3918", s: "warning" },
                    { t: "10:18", a: "Failed login attempt", s: "danger" },
                  ].map((r, i) => (
                    <CTableRow key={i}>
                      <CTableDataCell className="text-body-secondary">{r.t}</CTableDataCell>
                      <CTableDataCell>{r.a}</CTableDataCell>
                      <CTableDataCell>
                        <CBadge color={r.s}>{r.s.toUpperCase()}</CBadge>
                      </CTableDataCell>
                    </CTableRow>
                  ))}
                </CTableBody>
              </CTable>
            </CCardBody>
          </CCard>
        </CCol>

        <CCol lg={5}>
          <CCard className="shadow-sm border-0 rounded-4 h-100">
            <CCardHeader className="bg-white border-0 pt-4 px-4">
              <div className="fw-semibold">Quick Insights</div>
            </CCardHeader>
            <CCardBody className="px-4 pb-4">
              <div className="mb-3">
                <div className="d-flex justify-content-between small">
                  <span className="text-body-secondary">Conversion Rate</span>
                  <span className="fw-semibold">3.4%</span>
                </div>
                <CProgress value={34} className="mt-2" />
              </div>

              <div className="mb-3">
                <div className="d-flex justify-content-between small">
                  <span className="text-body-secondary">Support Tickets</span>
                  <span className="fw-semibold">18 open</span>
                </div>
                <CProgress value={18} className="mt-2" />
              </div>

              <div className="mb-0">
                <div className="d-flex justify-content-between small">
                  <span className="text-body-secondary">Storage Used</span>
                  <span className="fw-semibold">62%</span>
                </div>
                <CProgress value={62} className="mt-2" />
              </div>

              <div className="mt-4 p-3 rounded-4 bg-light">
                <div className="fw-semibold">Next step</div>
                <div className="text-body-secondary small">
                  เพิ่มหน้า Reports และทำ Table/Filter เพื่อเป็นงานปลายภาค
                </div>
              </div>
            </CCardBody>
          </CCard>
        </CCol>
      </CRow>
    </div>
  );
}

Step 7: สร้าง Routes (React Router v6) — Diagram Routing

SPA (Single Page Application) คือเว็บที่ “ไม่ reload ทั้งหน้า” เวลาเปลี่ยนหน้า แต่จะสลับคอมโพเนนต์ตาม URL แทน
แนวคิด: URL เปลี่ยน → Router เลือก Route → แสดงคอมโพเนนต์ในพื้นที่ Content

Browser URL ตัวอย่าง: / หรือ /users BrowserRouter จับ URL แล้วเลือก Route AppLayout โครงถาวร: Sidebar + Header AppRoutes (Routes + Route) path: "/" element: <Dashboard /> path: "/users" element: <Users /> URL เปลี่ยน → Router เลือก Route → แสดงคอมโพเนนต์ในพื้นที่ Content (ไม่ reload ทั้งหน้า)
Router Flow Render Content

ต่อไปคือโค้ดประกาศ Route จริง ๆ: สร้างไฟล์ src/routes/AppRoutes.jsx โดยกำหนดว่า URL ไหน แสดงคอมโพเนนต์อะไร

ไฟล์: src/routes/AppRoutes.jsx
import { Routes, Route } from "react-router-dom";
import Dashboard from "../pages/Dashboard";
import Users from "../pages/Users";

export default function AppRoutes() {
  return (
    <Routes>
      <Route path="/" element={<Dashboard />} />
      <Route path="/users" element={<Users />} />
    </Routes>
  );
}

Step 8: สร้าง Sidebar (narrow) src/layout/AppSidebar.jsx

ใช้ <NavLink> render ลิงก์เอง เพื่อให้คลิกเปลี่ยนหน้าแน่นอน (SPA) และยังคงสไตล์ CoreUI ด้วย class nav-link

ไฟล์: src/layout/AppSidebar.jsx
import {
  CSidebar,
  CSidebarBrand,
  CSidebarNav,
  CNavItem,
  CNavTitle,
} from "@coreui/react";
import { NavLink } from "react-router-dom";

export default function AppSidebar({ visible, setVisible, narrow }) {
  return (
    <CSidebar
      visible={visible}
      onVisibleChange={(val) => setVisible(val)}
      narrow={narrow}
      unfoldable
      className="border-end bg-white"
    >
      <CSidebarBrand className="px-3 py-3 border-bottom">
        <div className="d-flex align-items-center gap-2">
          <div
            className="d-flex align-items-center justify-content-center rounded"
            style={{ width: 32, height: 32, background: "#eef2ff", color: "#4f46e5" }}
          >
            <i className="bi bi-grid-1x2-fill" />
          </div>

          <div className="nav-label">
            <div className="fw-semibold">CoreUI Admin</div>
            <div className="small text-body-secondary">Free Version</div>
          </div>
        </div>
      </CSidebarBrand>

      <CSidebarNav className="pt-2">
        <CNavTitle className="text-uppercase small text-body-secondary px-3 mt-2 mb-1">
          Overview
        </CNavTitle>

        <CNavItem>
          <NavLink
            to="/"
            className={({ isActive }) =>
              `nav-link d-flex align-items-center ${isActive ? "active" : ""}`
            }
          >
            <i className="bi bi-speedometer2 me-2" />
            <span className="nav-label">Dashboard</span>
          </NavLink>
        </CNavItem>

        <CNavItem>
          <NavLink
            to="/users"
            className={({ isActive }) =>
              `nav-link d-flex align-items-center ${isActive ? "active" : ""}`
            }
          >
            <i className="bi bi-people me-2" />
            <span className="nav-label">Users</span>
          </NavLink>
        </CNavItem>

        <CNavTitle className="text-uppercase small text-body-secondary px-3 mt-3 mb-1">
          Management
        </CNavTitle>

        <CNavItem>
          <a className="nav-link d-flex align-items-center" href="#">
            <i className="bi bi-receipt me-2" />
            <span className="nav-label">Reports</span>
            <span className="ms-auto badge bg-secondary nav-label">Soon</span>
          </a>
        </CNavItem>

        <CNavItem>
          <a className="nav-link d-flex align-items-center" href="#">
            <i className="bi bi-gear me-2" />
            <span className="nav-label">Settings</span>
          </a>
        </CNavItem>
      </CSidebarNav>

      <div className="mt-auto px-3 py-3 border-top small text-body-secondary">
        <span className="nav-label">ผู้สอน: ผู้ช่วยศาสตราจารย์ ปองพล นิลพฤกษ์</span>
      </div>
    </CSidebar>
  );
}

Step 9: สร้าง Header (ชิดขวา) src/layout/AppHeader.jsx

แยกกลุ่มซ้าย/ขวา และใช้ ms-auto ดัน Search/Bell/Profile ไปชิดมุมขวา

ไฟล์: src/layout/AppHeader.jsx
import {
  CHeader,
  CContainer,
  CHeaderBrand,
  CHeaderNav,
  CNavItem,
  CNavLink,
  CButton,
  CDropdown,
  CDropdownToggle,
  CDropdownMenu,
  CDropdownItem,
  CForm,
  CFormInput,
} from "@coreui/react";

export default function AppHeader({
  sidebarVisible,
  setSidebarVisible,
  sidebarNarrow,
  setSidebarNarrow,
}) {
  return (
    <CHeader position="sticky" className="border-bottom bg-white">
      <CContainer fluid className="py-2">
        <div className="d-flex align-items-center w-100">

          {/* LEFT */}
          <div className="d-flex align-items-center gap-2">
            <CButton
              color="light"
              className="border shadow-sm"
              onClick={() => setSidebarNarrow(!sidebarNarrow)}
              aria-label="Collapse sidebar"
              title="Collapse sidebar"
            >
              <i
                className={`bi ${
                  sidebarNarrow ? "bi-layout-sidebar-inset" : "bi-layout-sidebar"
                } fs-5`}
              />
            </CButton>

            <CButton
              color="light"
              className="border shadow-sm d-md-none"
              onClick={() => setSidebarVisible(!sidebarVisible)}
              aria-label="Show sidebar"
              title="Show sidebar (mobile)"
            >
              <i className="bi bi-list fs-5" />
            </CButton>

            <CHeaderBrand className="mb-0 fw-semibold">
              CoreUI Admin
            </CHeaderBrand>
          </div>

          {/* RIGHT */}
          <div className="d-flex align-items-center gap-2 ms-auto">
            <div className="d-none d-md-block" style={{ width: 320 }}>
              <CForm className="w-100">
                <CFormInput type="search" placeholder="Search…" className="shadow-sm" />
              </CForm>
            </div>

            <CHeaderNav className="align-items-center">
              <CNavItem>
                <CNavLink href="#" className="text-body">
                  <i className="bi bi-bell fs-5" />
                </CNavLink>
              </CNavItem>
            </CHeaderNav>

            <CDropdown alignment="end">
              <CDropdownToggle color="light" className="border shadow-sm">
                <i className="bi bi-person-circle fs-5" />
                <span className="ms-2 d-none d-sm-inline">Admin</span>
              </CDropdownToggle>
              <CDropdownMenu>
                <CDropdownItem href="#">Profile</CDropdownItem>
                <CDropdownItem href="#">Settings</CDropdownItem>
                <CDropdownItem href="#">Logout</CDropdownItem>
              </CDropdownMenu>
            </CDropdown>
          </div>

        </div>
      </CContainer>
    </CHeader>
  );
}

Step 10: รวม Layout + Router src/layout/AppLayout.jsx + src/App.jsx

AppLayout คุม state ของ sidebar (visible/narrow) แล้วครอบทั้งแอปด้วย BrowserRouter

ไฟล์: src/layout/AppLayout.jsx
import { useState } from "react";
import { CContainer } from "@coreui/react";
import AppHeader from "./AppHeader";
import AppSidebar from "./AppSidebar";
import AppRoutes from "../routes/AppRoutes";

export default function AppLayout() {
  const [sidebarVisible, setSidebarVisible] = useState(true);
  const [sidebarNarrow, setSidebarNarrow] = useState(false);

  return (
    <div className="d-flex min-vh-100 bg-light">
      <AppSidebar
        visible={sidebarVisible}
        setVisible={setSidebarVisible}
        narrow={sidebarNarrow}
      />

      <div className="flex-grow-1">
        <AppHeader
          sidebarVisible={sidebarVisible}
          setSidebarVisible={setSidebarVisible}
          sidebarNarrow={sidebarNarrow}
          setSidebarNarrow={setSidebarNarrow}
        />

        <main className="py-4">
          <CContainer lg>
            <div className="mb-3">
              <h4 className="mb-1 fw-semibold">Dashboard</h4>
              <div className="text-body-secondary">ภาพรวมข้อมูลสำคัญของระบบ</div>
            </div>

            <AppRoutes />
          </CContainer>
        </main>
      </div>
    </div>
  );
}
ไฟล์: src/App.jsx
import { BrowserRouter } from "react-router-dom";
import AppLayout from "./layout/AppLayout";

export default function App() {
  return (
    <BrowserRouter>
      <AppLayout />
    </BrowserRouter>
  );
}

Step 11: Checklist + Run npm run dev

รันแล้วตรวจสอบว่าได้ครบ: Routing ทำงาน, Header ชิดขวา, Sidebar ย่อเหลือ icon, Dashboard มี KPI

npm run dev
Routing: คลิก Dashboard/Users แล้ว URL และ Content เปลี่ยน (ไม่ reload)
Header: Search + Bell + Profile ชิดมุมขวา
Sidebar: กดย่อแล้วเหลือ icon
Dashboard: มี KPI Cards + Activities + Insights