Frontend Components - luckydeva03/barbershop_app GitHub Wiki

🎨 Frontend Components

Panduan lengkap komponen frontend, UI/UX, dan teknologi yang digunakan dalam barbershop management system.

🛠️ Tech Stack Frontend

Core Technologies

  • CSS Framework: Bootstrap 5.3+ & Tailwind CSS
  • JavaScript: Vanilla JavaScript ES6+
  • Build Tool: Vite dengan Hot Module Replacement (HMR)
  • Icons: Feather Icons, Bootstrap Icons, Font Awesome
  • UI Libraries: Choices.js, DataTables, Perfect Scrollbar

Additional Libraries

  • Charts: Chart.js untuk analytics
  • Maps: Google Maps API
  • Notifications: Toastify.js
  • File Upload: FilePond
  • Rich Text: TinyMCE
  • Rating: Rater.js

🎨 Design System

Color Palette

:root {
    /* Primary Colors */
    --primary-50: #eff6ff;
    --primary-100: #dbeafe;
    --primary-500: #3b82f6;
    --primary-600: #2563eb;
    --primary-700: #1d4ed8;
    
    /* Secondary Colors */
    --secondary-50: #f8fafc;
    --secondary-500: #64748b;
    --secondary-600: #475569;
    
    /* Success Colors */
    --success-50: #f0fdf4;
    --success-500: #22c55e;
    --success-600: #16a34a;
    
    /* Warning Colors */
    --warning-50: #fffbeb;
    --warning-500: #f59e0b;
    --warning-600: #d97706;
    
    /* Danger Colors */
    --danger-50: #fef2f2;
    --danger-500: #ef4444;
    --danger-600: #dc2626;
}

Typography

/* Font Family */
body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}

/* Heading Styles */
.h1, h1 { font-size: 2.5rem; font-weight: 700; }
.h2, h2 { font-size: 2rem; font-weight: 600; }
.h3, h3 { font-size: 1.75rem; font-weight: 600; }
.h4, h4 { font-size: 1.5rem; font-weight: 500; }
.h5, h5 { font-size: 1.25rem; font-weight: 500; }
.h6, h6 { font-size: 1rem; font-weight: 500; }

/* Text Utilities */
.text-muted { color: var(--secondary-500); }
.text-small { font-size: 0.875rem; }
.text-xs { font-size: 0.75rem; }

🧩 Component Architecture

Layout Components

1. Main Application Layout (layouts/app.blade.php)

<!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', 'Barbershop Management') }}</title>
    
    <!-- Fonts -->
    <link rel="preconnect" href="https://fonts.bunny.net">
    <link href="https://fonts.bunny.net/css?family=inter:400,500,600,700" rel="stylesheet" />
    
    <!-- Styles -->
    @vite(['resources/css/app.css', 'resources/js/app.js'])
    @stack('styles')
</head>
<body class="font-sans antialiased">
    <div id="app">
        <!-- Navigation -->
        @include('components.navigation.main-nav')
        
        <!-- Main Content -->
        <main class="py-4">
            @yield('content')
        </main>
        
        <!-- Footer -->
        @include('components.footer')
    </div>
    
    <!-- Toast Container -->
    <div id="toast-container"></div>
    
    @stack('scripts')
</body>
</html>

2. Admin Layout (layouts/admin.blade.php)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title') - Admin Dashboard</title>
    
    @vite(['resources/css/admin.css', 'resources/js/admin.js'])
    @stack('styles')
</head>
<body>
    <div class="wrapper">
        <!-- Sidebar -->
        @include('components.admin.sidebar')
        
        <!-- Main Content -->
        <div class="main-content">
            <!-- Top Navigation -->
            @include('components.admin.topbar')
            
            <!-- Page Content -->
            <div class="content-wrapper">
                @if(session('success'))
                    @include('components.alerts.success', ['message' => session('success')])
                @endif
                
                @if(session('error'))
                    @include('components.alerts.error', ['message' => session('error')])
                @endif
                
                @yield('content')
            </div>
        </div>
    </div>
    
    @stack('scripts')
</body>
</html>

Navigation Components

1. Main Navigation (components/navigation/main-nav.blade.php)

<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
    <div class="container">
        <!-- Brand -->
        <a class="navbar-brand" href="{{ route('home') }}">
            <img src="{{ asset('images/logo.png') }}" alt="Logo" height="32">
            {{ config('app.name') }}
        </a>
        
        <!-- Mobile Toggle -->
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
            <span class="navbar-toggler-icon"></span>
        </button>
        
        <!-- Navigation Links -->
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav me-auto">
                <li class="nav-item">
                    <a class="nav-link {{ request()->routeIs('home') ? 'active' : '' }}" 
                       href="{{ route('home') }}">Home</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link {{ request()->routeIs('stores.*') ? 'active' : '' }}" 
                       href="{{ route('stores.index') }}">Stores</a>
                </li>
            </ul>
            
            <!-- User Menu -->
            <ul class="navbar-nav">
                @auth
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" 
                           data-bs-toggle="dropdown">
                            @if(auth()->user()->profile_photo)
                                <img src="{{ auth()->user()->profile_photo }}" 
                                     alt="Profile" class="rounded-circle me-1" width="24" height="24">
                            @endif
                            {{ auth()->user()->name }}
                        </a>
                        <ul class="dropdown-menu">
                            <li><a class="dropdown-item" href="{{ route('dashboard') }}">Dashboard</a></li>
                            <li><a class="dropdown-item" href="{{ route('profile.edit') }}">Profile</a></li>
                            <li><hr class="dropdown-divider"></li>
                            <li>
                                <form method="POST" action="{{ route('logout') }}">
                                    @csrf
                                    <button type="submit" class="dropdown-item">Logout</button>
                                </form>
                            </li>
                        </ul>
                    </li>
                @else
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('login') }}">Login</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ route('register') }}">Register</a>
                    </li>
                @endauth
            </ul>
        </div>
    </div>
</nav>

2. Admin Sidebar (components/admin/sidebar.blade.php)

<div class="sidebar">
    <!-- Logo -->
    <div class="sidebar-brand">
        <img src="{{ asset('images/logo.png') }}" alt="Logo" height="32">
        <span>Admin Panel</span>
    </div>
    
    <!-- Navigation -->
    <nav class="sidebar-nav">
        <ul class="nav flex-column">
            <li class="nav-item">
                <a class="nav-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}" 
                   href="{{ route('admin.dashboard') }}">
                    <i data-feather="home"></i>
                    Dashboard
                </a>
            </li>
            
            <li class="nav-item">
                <a class="nav-link {{ request()->routeIs('admin.users.*') ? 'active' : '' }}" 
                   href="{{ route('admin.users.index') }}">
                    <i data-feather="users"></i>
                    Users
                </a>
            </li>
            
            <li class="nav-item">
                <a class="nav-link {{ request()->routeIs('admin.codes.*') ? 'active' : '' }}" 
                   href="{{ route('admin.codes.index') }}">
                    <i data-feather="tag"></i>
                    Redeem Codes
                </a>
            </li>
            
            <li class="nav-item">
                <a class="nav-link {{ request()->routeIs('admin.reviews.*') ? 'active' : '' }}" 
                   href="{{ route('admin.reviews.index') }}">
                    <i data-feather="star"></i>
                    Reviews
                </a>
            </li>
            
            <li class="nav-item">
                <a class="nav-link {{ request()->routeIs('admin.analytics.*') ? 'active' : '' }}" 
                   href="{{ route('admin.analytics.index') }}">
                    <i data-feather="bar-chart-2"></i>
                    Analytics
                </a>
            </li>
        </ul>
    </nav>
</div>

Card Components

1. Stats Card (components/cards/stats-card.blade.php)

<div class="card stats-card">
    <div class="card-body">
        <div class="d-flex justify-content-between align-items-center">
            <div>
                <h3 class="card-title h5 mb-0">{{ $title }}</h3>
                <p class="card-subtitle text-muted mb-0">{{ $subtitle ?? '' }}</p>
            </div>
            <div class="stats-icon">
                <i data-feather="{{ $icon }}" class="text-{{ $color ?? 'primary' }}"></i>
            </div>
        </div>
        
        <div class="mt-3">
            <h2 class="display-6 fw-bold text-{{ $color ?? 'primary' }}">{{ $value }}</h2>
            
            @if(isset($change))
                <small class="text-{{ $change > 0 ? 'success' : 'danger' }}">
                    <i data-feather="{{ $change > 0 ? 'trending-up' : 'trending-down' }}"></i>
                    {{ abs($change) }}% from last month
                </small>
            @endif
        </div>
    </div>
</div>

{{-- Usage Example:
@include('components.cards.stats-card', [
    'title' => 'Total Users',
    'value' => '1,234',
    'icon' => 'users',
    'color' => 'primary',
    'change' => 12.5
])
--}}

2. Store Card (components/cards/store-card.blade.php)

<div class="card store-card h-100">
    @if($store->image_url)
        <img src="{{ $store->image_url }}" class="card-img-top" alt="{{ $store->name }}">
    @endif
    
    <div class="card-body">
        <h5 class="card-title">{{ $store->name }}</h5>
        <p class="card-text text-muted">{{ Str::limit($store->description, 100) }}</p>
        
        <!-- Rating -->
        <div class="d-flex align-items-center mb-2">
            <div class="rating-stars me-2">
                @for($i = 1; $i <= 5; $i++)
                    <i class="fas fa-star {{ $i <= $store->average_rating ? 'text-warning' : 'text-muted' }}"></i>
                @endfor
            </div>
            <small class="text-muted">
                {{ number_format($store->average_rating, 1) }} 
                ({{ $store->reviews_count }} reviews)
            </small>
        </div>
        
        <!-- Address -->
        <p class="card-text">
            <i data-feather="map-pin" class="me-1"></i>
            {{ $store->address }}
        </p>
        
        <!-- Phone -->
        @if($store->phone)
            <p class="card-text">
                <i data-feather="phone" class="me-1"></i>
                {{ $store->phone }}
            </p>
        @endif
    </div>
    
    <div class="card-footer">
        <div class="d-grid gap-2">
            <a href="{{ route('stores.show', $store) }}" class="btn btn-outline-primary">
                View Details
            </a>
            
            @if($store->phone)
                <a href="https://wa.me/{{ preg_replace('/[^0-9]/', '', $store->phone) }}?text={{ urlencode('Halo! Saya ingin booking di ' . $store->name) }}" 
                   class="btn btn-success" target="_blank">
                    <i class="fab fa-whatsapp me-1"></i>
                    Book via WhatsApp
                </a>
            @endif
        </div>
    </div>
</div>

Form Components

1. Input Component (components/forms/input.blade.php)

<div class="mb-3">
    @if($label ?? false)
        <label for="{{ $id ?? $name }}" class="form-label">
            {{ $label }}
            @if($required ?? false)
                <span class="text-danger">*</span>
            @endif
        </label>
    @endif
    
    <input 
        type="{{ $type ?? 'text' }}" 
        class="form-control @error($name) is-invalid @enderror" 
        id="{{ $id ?? $name }}"
        name="{{ $name }}"
        value="{{ old($name, $value ?? '') }}"
        placeholder="{{ $placeholder ?? '' }}"
        {{ ($required ?? false) ? 'required' : '' }}
        {{ $attributes ?? '' }}
    >
    
    @error($name)
        <div class="invalid-feedback">
            {{ $message }}
        </div>
    @enderror
    
    @if($help ?? false)
        <div class="form-text">{{ $help }}</div>
    @endif
</div>

{{-- Usage Example:
@include('components.forms.input', [
    'name' => 'email',
    'label' => 'Email Address',
    'type' => 'email',
    'required' => true,
    'help' => 'We will never share your email with anyone.'
])
--}}

2. Select Component (components/forms/select.blade.php)

<div class="mb-3">
    @if($label ?? false)
        <label for="{{ $id ?? $name }}" class="form-label">
            {{ $label }}
            @if($required ?? false)
                <span class="text-danger">*</span>
            @endif
        </label>
    @endif
    
    <select 
        class="form-select @error($name) is-invalid @enderror" 
        id="{{ $id ?? $name }}"
        name="{{ $name }}"
        {{ ($required ?? false) ? 'required' : '' }}
        {{ $attributes ?? '' }}
    >
        @if($placeholder ?? false)
            <option value="">{{ $placeholder }}</option>
        @endif
        
        @foreach($options as $value => $text)
            <option value="{{ $value }}" {{ old($name, $selected ?? '') == $value ? 'selected' : '' }}>
                {{ $text }}
            </option>
        @endforeach
    </select>
    
    @error($name)
        <div class="invalid-feedback">
            {{ $message }}
        </div>
    @enderror
    
    @if($help ?? false)
        <div class="form-text">{{ $help }}</div>
    @endif
</div>

Alert Components

1. Success Alert (components/alerts/success.blade.php)

<div class="alert alert-success alert-dismissible fade show" role="alert">
    <i data-feather="check-circle" class="me-2"></i>
    {{ $message }}
    <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>

2. Error Alert (components/alerts/error.blade.php)

<div class="alert alert-danger alert-dismissible fade show" role="alert">
    <i data-feather="alert-circle" class="me-2"></i>
    {{ $message }}
    <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>

Modal Components

1. Confirmation Modal (components/modals/confirm-modal.blade.php)

<div class="modal fade" id="{{ $id }}" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">{{ $title }}</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                {{ $message }}
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
                <button type="button" class="btn btn-{{ $type ?? 'danger' }}" id="{{ $id }}-confirm">
                    {{ $confirmText ?? 'Confirm' }}
                </button>
            </div>
        </div>
    </div>
</div>

<script>
document.getElementById('{{ $id }}-confirm').addEventListener('click', function() {
    @if($action ?? false)
        {{ $action }}
    @endif
});
</script>

📱 JavaScript Components

1. Toast Notifications (resources/js/components/toast.js)

class ToastManager {
    constructor() {
        this.container = document.getElementById('toast-container') || this.createContainer();
    }
    
    createContainer() {
        const container = document.createElement('div');
        container.id = 'toast-container';
        container.className = 'toast-container position-fixed top-0 end-0 p-3';
        document.body.appendChild(container);
        return container;
    }
    
    show(message, type = 'success', duration = 5000) {
        const toast = document.createElement('div');
        toast.className = `toast align-items-center text-bg-${type} border-0`;
        toast.setAttribute('role', 'alert');
        
        toast.innerHTML = `
            <div class="d-flex">
                <div class="toast-body">
                    ${message}
                </div>
                <button type="button" class="btn-close btn-close-white me-2 m-auto" 
                        data-bs-dismiss="toast"></button>
            </div>
        `;
        
        this.container.appendChild(toast);
        
        const bsToast = new bootstrap.Toast(toast, { delay: duration });
        bsToast.show();
        
        toast.addEventListener('hidden.bs.toast', () => {
            toast.remove();
        });
    }
    
    success(message) { this.show(message, 'success'); }
    error(message) { this.show(message, 'danger'); }
    warning(message) { this.show(message, 'warning'); }
    info(message) { this.show(message, 'info'); }
}

// Global toast instance
window.toast = new ToastManager();

2. Form Validation (resources/js/components/validation.js)

class FormValidator {
    constructor(form) {
        this.form = form;
        this.rules = {};
        this.init();
    }
    
    init() {
        this.form.addEventListener('submit', (e) => {
            if (!this.validate()) {
                e.preventDefault();
                e.stopPropagation();
            }
            this.form.classList.add('was-validated');
        });
        
        // Real-time validation
        this.form.querySelectorAll('input, select, textarea').forEach(field => {
            field.addEventListener('blur', () => this.validateField(field));
        });
    }
    
    addRule(fieldName, validator, message) {
        if (!this.rules[fieldName]) {
            this.rules[fieldName] = [];
        }
        this.rules[fieldName].push({ validator, message });
    }
    
    validateField(field) {
        const fieldName = field.name;
        const rules = this.rules[fieldName] || [];
        
        for (const rule of rules) {
            if (!rule.validator(field.value)) {
                this.showFieldError(field, rule.message);
                return false;
            }
        }
        
        this.clearFieldError(field);
        return true;
    }
    
    validate() {
        let isValid = true;
        
        this.form.querySelectorAll('input, select, textarea').forEach(field => {
            if (!this.validateField(field)) {
                isValid = false;
            }
        });
        
        return isValid;
    }
    
    showFieldError(field, message) {
        field.classList.add('is-invalid');
        
        let feedback = field.parentNode.querySelector('.invalid-feedback');
        if (!feedback) {
            feedback = document.createElement('div');
            feedback.className = 'invalid-feedback';
            field.parentNode.appendChild(feedback);
        }
        feedback.textContent = message;
    }
    
    clearFieldError(field) {
        field.classList.remove('is-invalid');
        const feedback = field.parentNode.querySelector('.invalid-feedback');
        if (feedback) {
            feedback.remove();
        }
    }
}

// Usage example
document.addEventListener('DOMContentLoaded', function() {
    const redeemForm = document.getElementById('redeem-form');
    if (redeemForm) {
        const validator = new FormValidator(redeemForm);
        
        validator.addRule('code', 
            value => value.length >= 4, 
            'Code must be at least 4 characters'
        );
        
        validator.addRule('code', 
            value => /^[A-Z0-9]+$/.test(value), 
            'Code can only contain letters and numbers'
        );
    }
});

3. DataTable Wrapper (resources/js/components/datatable.js)

class DataTableWrapper {
    constructor(tableSelector, options = {}) {
        this.table = document.querySelector(tableSelector);
        this.options = {
            pageLength: 25,
            responsive: true,
            order: [[0, 'desc']],
            language: {
                search: "Search:",
                lengthMenu: "Show _MENU_ entries per page",
                info: "Showing _START_ to _END_ of _TOTAL_ entries",
                paginate: {
                    first: "First",
                    last: "Last",
                    next: "Next",
                    previous: "Previous"
                }
            },
            ...options
        };
        
        this.init();
    }
    
    init() {
        if (this.table && typeof DataTable !== 'undefined') {
            this.dataTable = new DataTable(this.table, this.options);
            this.addCustomFilters();
        }
    }
    
    addCustomFilters() {
        // Add date range filter
        const dateFilter = document.querySelector('[data-filter="date"]');
        if (dateFilter) {
            dateFilter.addEventListener('change', (e) => {
                this.dataTable.draw();
            });
        }
        
        // Add status filter
        const statusFilter = document.querySelector('[data-filter="status"]');
        if (statusFilter) {
            statusFilter.addEventListener('change', (e) => {
                this.dataTable.column(3).search(e.target.value).draw();
            });
        }
    }
    
    refresh() {
        if (this.dataTable) {
            this.dataTable.ajax.reload();
        }
    }
}

// Initialize data tables
document.addEventListener('DOMContentLoaded', function() {
    // Users table
    if (document.querySelector('#users-table')) {
        new DataTableWrapper('#users-table', {
            ajax: '/admin/users/data',
            columns: [
                { data: 'id' },
                { data: 'name' },
                { data: 'email' },
                { data: 'total_points' },
                { data: 'created_at' },
                { data: 'actions', orderable: false }
            ]
        });
    }
    
    // Codes table
    if (document.querySelector('#codes-table')) {
        new DataTableWrapper('#codes-table');
    }
});

🎨 Styling Architecture

1. Component Styles (resources/css/components/)

/* resources/css/components/cards.css */
.stats-card {
    border: none;
    box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
    transition: box-shadow 0.15s ease-in-out;
}

.stats-card:hover {
    box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}

.stats-icon {
    width: 3rem;
    height: 3rem;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 0.5rem;
    background-color: var(--bs-light);
}

.store-card {
    transition: transform 0.2s ease-in-out;
}

.store-card:hover {
    transform: translateY(-2px);
}

.rating-stars .fa-star {
    font-size: 0.875rem;
}
/* resources/css/components/navigation.css */
.navbar-brand img {
    margin-right: 0.5rem;
}

.sidebar {
    width: 250px;
    height: 100vh;
    background-color: var(--bs-dark);
    position: fixed;
    left: 0;
    top: 0;
    z-index: 1000;
}

.sidebar-brand {
    padding: 1rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    display: flex;
    align-items: center;
    color: white;
}

.sidebar-nav {
    padding: 1rem 0;
}

.sidebar-nav .nav-link {
    color: rgba(255, 255, 255, 0.75);
    padding: 0.75rem 1rem;
    display: flex;
    align-items: center;
    text-decoration: none;
    transition: all 0.2s ease;
}

.sidebar-nav .nav-link:hover,
.sidebar-nav .nav-link.active {
    color: white;
    background-color: rgba(255, 255, 255, 0.1);
}

.sidebar-nav .nav-link i {
    margin-right: 0.5rem;
    width: 1.25rem;
}

2. Responsive Design

/* resources/css/responsive.css */
@media (max-width: 768px) {
    .sidebar {
        transform: translateX(-100%);
        transition: transform 0.3s ease;
    }
    
    .sidebar.show {
        transform: translateX(0);
    }
    
    .main-content {
        margin-left: 0;
    }
    
    .stats-card {
        margin-bottom: 1rem;
    }
    
    .store-card {
        margin-bottom: 1.5rem;
    }
}

@media (max-width: 576px) {
    .navbar-brand {
        font-size: 1rem;
    }
    
    .display-6 {
        font-size: 1.5rem;
    }
    
    .card-body {
        padding: 1rem;
    }
}

🚀 Performance Optimization

1. Lazy Loading

// resources/js/utils/lazy-loading.js
class LazyLoader {
    constructor() {
        this.imageObserver = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const img = entry.target;
                    img.src = img.dataset.src;
                    img.classList.remove('lazy');
                    this.imageObserver.unobserve(img);
                }
            });
        });
        
        this.init();
    }
    
    init() {
        const lazyImages = document.querySelectorAll('img[data-src]');
        lazyImages.forEach(img => {
            this.imageObserver.observe(img);
        });
    }
}

document.addEventListener('DOMContentLoaded', () => {
    new LazyLoader();
});

2. Asset Optimization

// vite.config.js
export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
    ],
    build: {
        rollupOptions: {
            output: {
                manualChunks: {
                    vendor: ['bootstrap'],
                    charts: ['chart.js'],
                    tables: ['datatables.net'],
                    maps: ['google-maps-api'],
                }
            }
        },
        cssCodeSplit: true,
        sourcemap: false,
        minify: 'terser',
        terserOptions: {
            compress: {
                drop_console: true,
                drop_debugger: true,
            }
        }
    }
});

Next: Customer Portal Guide untuk panduan lengkap fitur customer.

⚠️ **GitHub.com Fallback** ⚠️