chore: initial Laravel 13 project setup for eCert MBIP
- Laravel 13.9 + PHP 8.5 + MySQL - Bootstrap 5.3 + jQuery 3.7 + Chart.js (replacing Alpine/Tailwind) - Packages: intervention/image, dompdf, simple-qrcode, league/csv, laravel/breeze, laravel/boost - 17 database migrations: users, programs, qr_codes, participants, attendances, certificates, questionnaires, email_logs, audit_logs - 13 Eloquent models with full relationships - Admin layout (Bootstrap 5 sidebar) + public layout (mobile-first) - Rate limiters: checkin (60/min), certificate (30/min) - Admin seeder: admin@mbip.gov.my - Storage directories + symlink configured Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
154
resources/views/layouts/admin.blade.php
Normal file
154
resources/views/layouts/admin.blade.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ms">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>@yield('title', 'eCert MBIP') — Admin</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body>
|
||||
<div class="d-flex">
|
||||
|
||||
{{-- Sidebar --}}
|
||||
<nav class="sidebar d-none d-md-flex flex-column" style="min-height:100vh; width:260px; flex-shrink:0;">
|
||||
<div class="sidebar-brand">
|
||||
<h5><i class="bi bi-award-fill me-2"></i>eCert MBIP</h5>
|
||||
<small>Sistem Pengurusan Sijil Digital</small>
|
||||
</div>
|
||||
|
||||
<ul class="nav flex-column mt-2 px-1 flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.dashboard') }}"
|
||||
class="nav-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
|
||||
<i class="bi bi-grid-1x2-fill"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item mt-2">
|
||||
<small class="text-white-50 px-3" style="font-size:.7rem; text-transform:uppercase; letter-spacing:.8px;">Program</small>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.programs.index') }}"
|
||||
class="nav-link {{ request()->routeIs('admin.programs.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-calendar-event-fill"></i> Senarai Program
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item mt-2">
|
||||
<small class="text-white-50 px-3" style="font-size:.7rem; text-transform:uppercase; letter-spacing:.8px;">Soalselidik</small>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.questionnaires.index') }}"
|
||||
class="nav-link {{ request()->routeIs('admin.questionnaires.*') ? 'active' : '' }}">
|
||||
<i class="bi bi-clipboard2-check-fill"></i> Set Soalselidik
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="px-3 pb-3 mt-auto">
|
||||
<div class="border-top border-white border-opacity-25 pt-3">
|
||||
<small class="text-white-50 d-block mb-1">{{ auth()->user()->name }}</small>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm btn-outline-light w-100">
|
||||
<i class="bi bi-box-arrow-right me-1"></i> Log Keluar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{{-- Main Content --}}
|
||||
<div class="main-content flex-grow-1">
|
||||
|
||||
{{-- Top Navbar (mobile) --}}
|
||||
<nav class="navbar navbar-light bg-white border-bottom d-md-none px-3">
|
||||
<span class="navbar-brand fw-bold text-primary mb-0">
|
||||
<i class="bi bi-award-fill me-1"></i>eCert MBIP
|
||||
</span>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="offcanvas" data-bs-target="#mobileSidebar">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{{-- Page Header --}}
|
||||
@if (isset($header) || View::hasSection('header'))
|
||||
<div class="page-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="mb-0 fw-semibold">@yield('header')</h5>
|
||||
@if(View::hasSection('breadcrumb'))
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0 mt-1" style="font-size:.82rem;">
|
||||
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
|
||||
@yield('breadcrumb')
|
||||
</ol>
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
<div>@yield('header-actions')</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Flash Messages --}}
|
||||
<div class="px-4 pt-3">
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>{{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>{{ session('error') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
@if(session('warning'))
|
||||
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-circle-fill me-2"></i>{{ session('warning') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Page Content --}}
|
||||
<div class="p-4">
|
||||
@yield('content')
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Mobile Offcanvas Sidebar --}}
|
||||
<div class="offcanvas offcanvas-start" tabindex="-1" id="mobileSidebar" style="width:260px; background: var(--mbip-primary);">
|
||||
<div class="offcanvas-header border-bottom border-white border-opacity-25">
|
||||
<h6 class="text-white mb-0"><i class="bi bi-award-fill me-2"></i>eCert MBIP</h6>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body p-0">
|
||||
<ul class="nav flex-column mt-2 px-1">
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.dashboard') }}" class="nav-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}" style="color:rgba(255,255,255,.85);">
|
||||
<i class="bi bi-grid-1x2-fill me-2"></i>Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.programs.index') }}" class="nav-link {{ request()->routeIs('admin.programs.*') ? 'active' : '' }}" style="color:rgba(255,255,255,.85);">
|
||||
<i class="bi bi-calendar-event-fill me-2"></i>Senarai Program
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ route('admin.questionnaires.index') }}" class="nav-link {{ request()->routeIs('admin.questionnaires.*') ? 'active' : '' }}" style="color:rgba(255,255,255,.85);">
|
||||
<i class="bi bi-clipboard2-check-fill me-2"></i>Set Soalselidik
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
36
resources/views/layouts/app.blade.php
Normal file
36
resources/views/layouts/app.blade.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<div class="min-h-screen bg-gray-100">
|
||||
@include('layouts.navigation')
|
||||
|
||||
<!-- Page Heading -->
|
||||
@isset($header)
|
||||
<header class="bg-white shadow">
|
||||
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
||||
{{ $header }}
|
||||
</div>
|
||||
</header>
|
||||
@endisset
|
||||
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
{{ $slot }}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
30
resources/views/layouts/guest.blade.php
Normal file
30
resources/views/layouts/guest.blade.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="font-sans text-gray-900 antialiased">
|
||||
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
|
||||
<div>
|
||||
<a href="/">
|
||||
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
100
resources/views/layouts/navigation.blade.php
Normal file
100
resources/views/layouts/navigation.blade.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
|
||||
<!-- Primary Navigation Menu -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex">
|
||||
<!-- Logo -->
|
||||
<div class="shrink-0 flex items-center">
|
||||
<a href="{{ route('dashboard') }}">
|
||||
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-nav-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
<x-dropdown align="right" width="48">
|
||||
<x-slot name="trigger">
|
||||
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
|
||||
<div>{{ Auth::user()->name }}</div>
|
||||
|
||||
<div class="ms-1">
|
||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<x-dropdown-link :href="route('profile.edit')">
|
||||
{{ __('Profile') }}
|
||||
</x-dropdown-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<x-dropdown-link :href="route('logout')"
|
||||
onclick="event.preventDefault();
|
||||
this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-dropdown-link>
|
||||
</form>
|
||||
</x-slot>
|
||||
</x-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger -->
|
||||
<div class="-me-2 flex items-center sm:hidden">
|
||||
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
|
||||
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
||||
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Navigation Menu -->
|
||||
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||
<div class="pt-2 pb-3 space-y-1">
|
||||
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||
{{ __('Dashboard') }}
|
||||
</x-responsive-nav-link>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
<div class="pt-4 pb-1 border-t border-gray-200">
|
||||
<div class="px-4">
|
||||
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
|
||||
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<x-responsive-nav-link :href="route('profile.edit')">
|
||||
{{ __('Profile') }}
|
||||
</x-responsive-nav-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<x-responsive-nav-link :href="route('logout')"
|
||||
onclick="event.preventDefault();
|
||||
this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
</x-responsive-nav-link>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
63
resources/views/layouts/public.blade.php
Normal file
63
resources/views/layouts/public.blade.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ms">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>@yield('title', 'eCert MBIP')</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<style>
|
||||
body { background: #f0f4f8; min-height: 100vh; }
|
||||
.public-container { max-width: 480px; margin: 0 auto; }
|
||||
</style>
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{-- Header Brand --}}
|
||||
<div class="public-hero">
|
||||
<div class="public-container px-3 pt-2 pb-3">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-award-fill fs-4 me-2 opacity-75"></i>
|
||||
<span class="fw-bold" style="font-size:.9rem; letter-spacing:.5px;">eCert MBIP</span>
|
||||
</div>
|
||||
@yield('hero')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Main Content --}}
|
||||
<div class="public-container px-3 py-4">
|
||||
|
||||
{{-- Flash Messages --}}
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show mb-4" role="alert">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>{{ session('success') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>{{ session('error') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
@if(session('warning'))
|
||||
<div class="alert alert-warning alert-dismissible fade show mb-4" role="alert">
|
||||
<i class="bi bi-exclamation-circle-fill me-2"></i>{{ session('warning') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@yield('content')
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Footer --}}
|
||||
<div class="text-center py-4">
|
||||
<small class="text-muted">Majlis Bandaraya Ipoh Perak © {{ date('Y') }}</small>
|
||||
</div>
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user