- Panduan Template di bahagian atas, boleh lipat/kembang - Template Aktif (kiri) bersebelahan Konfigurasi Teks (kanan) — col-lg-6 - Auto-detect portrait/landscape dari naturalWidth/naturalHeight imej - Portrait: max-height 520px | Landscape: max-height 340px - Badge orientasi (hijau=Landscape, biru=Portrait) dalam header kad - Laras tinggi juga untuk pratonton upload form Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
395 lines
20 KiB
PHP
395 lines
20 KiB
PHP
@extends('layouts.admin')
|
||
|
||
@section('title', 'Template Sijil — ' . $program->title)
|
||
@section('header', 'Template Sijil')
|
||
|
||
@section('breadcrumb')
|
||
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
|
||
<li class="breadcrumb-item"><a href="{{ route('admin.programs.show', $program) }}">{{ Str::limit($program->title, 30) }}</a></li>
|
||
<li class="breadcrumb-item active">Template Sijil</li>
|
||
@endsection
|
||
|
||
@section('header-actions')
|
||
<a href="{{ route('admin.programs.show', $program) }}#tab-template" class="btn btn-sm btn-outline-secondary">
|
||
<i class="bi bi-arrow-left me-1"></i> Kembali
|
||
</a>
|
||
@endsection
|
||
|
||
@section('content')
|
||
|
||
@php
|
||
$config = $template?->config_json ?? [];
|
||
$fields = $config['fields'] ?? [];
|
||
$imgWidth = $config['width'] ?? 0;
|
||
$imgHeight = $config['height'] ?? 0;
|
||
$isPortrait = $imgHeight > $imgWidth && $imgWidth > 0;
|
||
@endphp
|
||
|
||
{{-- ── Panduan Template (atas, boleh lipat) ────────────────────────────── --}}
|
||
<div class="card border-0 bg-light mb-4">
|
||
<div class="card-header bg-transparent border-0 py-2 d-flex justify-content-between align-items-center"
|
||
role="button" data-bs-toggle="collapse" data-bs-target="#guidePanel" aria-expanded="false">
|
||
<span class="fw-semibold small"><i class="bi bi-info-circle me-2 text-primary"></i>Panduan Template</span>
|
||
<i class="bi bi-chevron-down small text-muted" id="guideChevron"></i>
|
||
</div>
|
||
<div class="collapse" id="guidePanel">
|
||
<div class="card-body pt-0 pb-3">
|
||
<div class="row g-3">
|
||
<div class="col-md-4">
|
||
<div class="d-flex gap-2">
|
||
<i class="bi bi-aspect-ratio text-primary mt-1 flex-shrink-0"></i>
|
||
<div class="small text-muted">
|
||
Resolusi disyorkan <strong>1754 × 1240px</strong> (A4 landscape 150dpi)
|
||
atau <strong>1240 × 1754px</strong> (portrait).
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="d-flex gap-2">
|
||
<i class="bi bi-cursor text-primary mt-1 flex-shrink-0"></i>
|
||
<div class="small text-muted">
|
||
Koordinat <strong>(0, 0)</strong> adalah sudut kiri atas imej.
|
||
X bertambah ke kanan, Y bertambah ke bawah.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<div class="d-flex gap-2">
|
||
<i class="bi bi-eye text-primary mt-1 flex-shrink-0"></i>
|
||
<div class="small text-muted">
|
||
Guna butang <strong>Pratonton</strong> untuk semak kedudukan teks
|
||
sebelum jana sijil sebenar.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@if($template)
|
||
|
||
{{-- ── Template Aktif (kiri) + Konfigurasi (kanan) ─────────────────────── --}}
|
||
<div class="row g-4">
|
||
|
||
{{-- Kiri: Template Aktif --}}
|
||
<div class="col-lg-6">
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||
<div class="d-flex align-items-center gap-2">
|
||
<h6 class="mb-0 fw-semibold"><i class="bi bi-image me-2 text-primary"></i>Template Aktif</h6>
|
||
<span id="orientationBadge" class="badge bg-secondary" style="font-size:.7rem;">
|
||
{{ $isPortrait ? 'Portrait' : ($imgWidth > 0 ? 'Landscape' : '—') }}
|
||
</span>
|
||
</div>
|
||
<form method="POST" action="{{ route('admin.programs.template.destroy', $program) }}"
|
||
onsubmit="return confirm('Padam template sijil ini? Tindakan ini tidak boleh diundur.')">
|
||
@csrf @method('DELETE')
|
||
<button class="btn btn-sm btn-outline-danger">
|
||
<i class="bi bi-trash me-1"></i> Padam
|
||
</button>
|
||
</form>
|
||
</div>
|
||
<div class="card-body d-flex flex-column">
|
||
|
||
{{-- Image viewer — tinggi berubah ikut orientasi --}}
|
||
<div id="previewWrapper"
|
||
class="border rounded overflow-hidden mb-2 d-flex align-items-center justify-content-center bg-light"
|
||
style="max-height:{{ $isPortrait ? '520px' : '340px' }}; transition: max-height .3s ease;">
|
||
<img src="{{ route('admin.programs.template.preview', $program) }}"
|
||
id="templatePreview"
|
||
alt="Template Preview"
|
||
class="img-fluid"
|
||
style="max-width:100%; max-height:{{ $isPortrait ? '520px' : '340px' }}; object-fit:contain;">
|
||
</div>
|
||
<div class="text-muted small text-center mb-3">
|
||
{{ $template->original_filename }}
|
||
@if($imgWidth > 0)
|
||
· <span id="dimensionLabel">{{ $imgWidth }} × {{ $imgHeight }} px</span>
|
||
@endif
|
||
</div>
|
||
|
||
{{-- Jana Pratonton --}}
|
||
<div class="border rounded p-3 bg-light mt-auto">
|
||
<label class="form-label small fw-medium mb-2">Jana Pratonton</label>
|
||
<div class="row g-2">
|
||
<div class="col-8">
|
||
<input type="text" id="sampleName" class="form-control form-control-sm"
|
||
value="NAMA PESERTA CONTOH" placeholder="Nama untuk pratonton">
|
||
</div>
|
||
<div class="col-4">
|
||
<button type="button" id="previewBtn" class="btn btn-sm btn-primary w-100"
|
||
onclick="loadPreview()">
|
||
<i class="bi bi-eye me-1"></i> Pratonton
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Kanan: Konfigurasi Kedudukan Teks --}}
|
||
<div class="col-lg-6">
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<div class="card-header bg-white py-3">
|
||
<h6 class="mb-0 fw-semibold"><i class="bi bi-sliders me-2 text-warning"></i>Konfigurasi Kedudukan Teks</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<p class="text-muted small mb-3">
|
||
Koordinat X dan Y dikira dari sudut kiri atas imej (piksel).
|
||
@if($imgWidth > 0)
|
||
Saiz imej: <strong>{{ $imgWidth }} × {{ $imgHeight }} px</strong>.
|
||
@endif
|
||
</p>
|
||
|
||
<form method="POST" action="{{ route('admin.programs.template.config', $program) }}">
|
||
@csrf @method('PUT')
|
||
|
||
{{-- Nama Peserta --}}
|
||
<div class="card border mb-3">
|
||
<div class="card-header py-2 bg-light">
|
||
<span class="fw-medium small">Nama Peserta</span>
|
||
</div>
|
||
<div class="card-body py-3">
|
||
<div class="row g-2">
|
||
<div class="col-4">
|
||
<label class="form-label small">X (px)</label>
|
||
<input type="number" name="fields[name][x]" class="form-control form-control-sm"
|
||
value="{{ $fields['name']['x'] ?? 800 }}">
|
||
</div>
|
||
<div class="col-4">
|
||
<label class="form-label small">Y (px)</label>
|
||
<input type="number" name="fields[name][y]" class="form-control form-control-sm"
|
||
value="{{ $fields['name']['y'] ?? 400 }}">
|
||
</div>
|
||
<div class="col-4">
|
||
<label class="form-label small">Saiz Font</label>
|
||
<input type="number" name="fields[name][font_size]" class="form-control form-control-sm"
|
||
value="{{ $fields['name']['font_size'] ?? 52 }}">
|
||
</div>
|
||
<div class="col-4">
|
||
<label class="form-label small">Warna</label>
|
||
<input type="color" name="fields[name][font_color]"
|
||
class="form-control form-control-color form-control-sm"
|
||
value="{{ $fields['name']['font_color'] ?? '#1a3a6b' }}">
|
||
</div>
|
||
<div class="col-4">
|
||
<label class="form-label small">Align</label>
|
||
<select name="fields[name][align]" class="form-select form-select-sm">
|
||
<option value="left" {{ ($fields['name']['align'] ?? 'center') === 'left' ? 'selected' : '' }}>Kiri</option>
|
||
<option value="center" {{ ($fields['name']['align'] ?? 'center') === 'center' ? 'selected' : '' }}>Tengah</option>
|
||
<option value="right" {{ ($fields['name']['align'] ?? 'center') === 'right' ? 'selected' : '' }}>Kanan</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- No. Sijil (pilihan) --}}
|
||
<div class="card border mb-4">
|
||
<div class="card-header py-2 bg-light d-flex justify-content-between align-items-center">
|
||
<span class="fw-medium small">No. Sijil <span class="text-muted">(Pilihan)</span></span>
|
||
<div class="form-check form-switch mb-0">
|
||
<input class="form-check-input" type="checkbox" id="showCertNo"
|
||
onchange="toggleCertNo(this)"
|
||
{{ isset($fields['certificate_no']) ? 'checked' : '' }}>
|
||
<label class="form-check-label small" for="showCertNo">Papar</label>
|
||
</div>
|
||
</div>
|
||
<div class="card-body py-3" id="certNoFields"
|
||
{{ isset($fields['certificate_no']) ? '' : 'style=display:none' }}>
|
||
<div class="row g-2">
|
||
<div class="col-4">
|
||
<label class="form-label small">X (px)</label>
|
||
<input type="number" name="fields[certificate_no][x]" class="form-control form-control-sm"
|
||
value="{{ $fields['certificate_no']['x'] ?? 800 }}">
|
||
</div>
|
||
<div class="col-4">
|
||
<label class="form-label small">Y (px)</label>
|
||
<input type="number" name="fields[certificate_no][y]" class="form-control form-control-sm"
|
||
value="{{ $fields['certificate_no']['y'] ?? 460 }}">
|
||
</div>
|
||
<div class="col-4">
|
||
<label class="form-label small">Saiz Font</label>
|
||
<input type="number" name="fields[certificate_no][font_size]" class="form-control form-control-sm"
|
||
value="{{ $fields['certificate_no']['font_size'] ?? 28 }}">
|
||
</div>
|
||
<div class="col-4">
|
||
<label class="form-label small">Warna</label>
|
||
<input type="color" name="fields[certificate_no][font_color]"
|
||
class="form-control form-control-color form-control-sm"
|
||
value="{{ $fields['certificate_no']['font_color'] ?? '#555555' }}">
|
||
</div>
|
||
<div class="col-4">
|
||
<label class="form-label small">Align</label>
|
||
<select name="fields[certificate_no][align]" class="form-select form-select-sm">
|
||
<option value="left" {{ ($fields['certificate_no']['align'] ?? 'center') === 'left' ? 'selected' : '' }}>Kiri</option>
|
||
<option value="center" {{ ($fields['certificate_no']['align'] ?? 'center') === 'center' ? 'selected' : '' }}>Tengah</option>
|
||
<option value="right" {{ ($fields['certificate_no']['align'] ?? 'center') === 'right' ? 'selected' : '' }}>Kanan</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="bi bi-save me-1"></i> Simpan Konfigurasi
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /.row -->
|
||
|
||
@else
|
||
|
||
{{-- Tiada template — form upload --}}
|
||
<div class="row g-4">
|
||
<div class="col-lg-7">
|
||
<div class="card border-0 shadow-sm">
|
||
<div class="card-header bg-white py-3">
|
||
<h6 class="mb-0 fw-semibold"><i class="bi bi-upload me-2 text-primary"></i>Muat Naik Template Sijil</h6>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="alert alert-info small mb-4">
|
||
<i class="bi bi-lightbulb me-1"></i>
|
||
Format <strong>JPG atau PNG</strong>, maksimum <strong>10MB</strong>.
|
||
Resolusi disyorkan: <strong>1754 × 1240px</strong> (A4 landscape) atau
|
||
<strong>1240 × 1754px</strong> (portrait).
|
||
</div>
|
||
|
||
<form method="POST" action="{{ route('admin.programs.template.store', $program) }}"
|
||
enctype="multipart/form-data">
|
||
@csrf
|
||
<div class="mb-4">
|
||
<label class="form-label fw-medium">Fail Template <span class="text-danger">*</span></label>
|
||
<input type="file" name="template_image" accept="image/jpeg,image/png"
|
||
class="form-control @error('template_image') is-invalid @enderror"
|
||
onchange="previewImage(this)">
|
||
@error('template_image')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||
<div class="form-text">Format: JPG, PNG — Maksimum 10MB</div>
|
||
</div>
|
||
|
||
<div id="imagePreviewBox" class="mb-4 d-none">
|
||
<label class="form-label small text-muted">Pratonton:</label>
|
||
<div id="uploadPreviewWrapper" class="border rounded overflow-hidden">
|
||
<img id="imagePreviewEl" src="" alt="preview" class="img-fluid w-100">
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary">
|
||
<i class="bi bi-upload me-1"></i> Muat Naik
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@endif
|
||
|
||
@endsection
|
||
|
||
@push('scripts')
|
||
<script>
|
||
// ── Auto-detect orientasi dan laras tinggi viewer ─────────────────────────────
|
||
function applyOrientation(naturalW, naturalH) {
|
||
const wrapper = document.getElementById('previewWrapper');
|
||
const img = document.getElementById('templatePreview');
|
||
const badge = document.getElementById('orientationBadge');
|
||
if (!wrapper || !img) return;
|
||
|
||
const isPortrait = naturalH > naturalW;
|
||
const maxH = isPortrait ? '520px' : '340px';
|
||
|
||
wrapper.style.maxHeight = maxH;
|
||
img.style.maxHeight = maxH;
|
||
|
||
if (badge) {
|
||
badge.textContent = isPortrait ? 'Portrait' : 'Landscape';
|
||
badge.className = 'badge ' + (isPortrait ? 'bg-info' : 'bg-success');
|
||
}
|
||
}
|
||
|
||
// Jalankan sekali apabila imej template dimuatkan
|
||
const templateImg = document.getElementById('templatePreview');
|
||
if (templateImg) {
|
||
if (templateImg.complete && templateImg.naturalWidth) {
|
||
applyOrientation(templateImg.naturalWidth, templateImg.naturalHeight);
|
||
} else {
|
||
templateImg.addEventListener('load', function () {
|
||
applyOrientation(this.naturalWidth, this.naturalHeight);
|
||
});
|
||
}
|
||
}
|
||
|
||
// ── Upload pratonton (form muat naik) ─────────────────────────────────────────
|
||
function previewImage(input) {
|
||
if (!input.files || !input.files[0]) return;
|
||
const reader = new FileReader();
|
||
reader.onload = e => {
|
||
const el = document.getElementById('imagePreviewEl');
|
||
const box = document.getElementById('imagePreviewBox');
|
||
el.src = e.target.result;
|
||
box.classList.remove('d-none');
|
||
|
||
// Laras pratonton upload ikut orientasi
|
||
el.onload = function () {
|
||
const wrap = document.getElementById('uploadPreviewWrapper');
|
||
if (wrap) wrap.style.maxHeight = this.naturalHeight > this.naturalWidth ? '520px' : '340px';
|
||
};
|
||
};
|
||
reader.readAsDataURL(input.files[0]);
|
||
}
|
||
|
||
// ── Toggle No. Sijil ──────────────────────────────────────────────────────────
|
||
function toggleCertNo(cb) {
|
||
document.getElementById('certNoFields').style.display = cb.checked ? '' : 'none';
|
||
}
|
||
|
||
// ── Toggle panduan (ikon chevron) ─────────────────────────────────────────────
|
||
document.getElementById('guidePanel')?.addEventListener('show.bs.collapse', () => {
|
||
document.getElementById('guideChevron').className = 'bi bi-chevron-up small text-muted';
|
||
});
|
||
document.getElementById('guidePanel')?.addEventListener('hide.bs.collapse', () => {
|
||
document.getElementById('guideChevron').className = 'bi bi-chevron-down small text-muted';
|
||
});
|
||
|
||
// ── Jana Pratonton ────────────────────────────────────────────────────────────
|
||
function loadPreview() {
|
||
const name = document.getElementById('sampleName').value.trim() || 'NAMA PESERTA CONTOH';
|
||
const img = document.getElementById('templatePreview');
|
||
const btn = document.getElementById('previewBtn');
|
||
const url = "{{ route('admin.programs.template.test', $program) }}";
|
||
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status"></span> Memuatkan...';
|
||
|
||
const form = new FormData();
|
||
form.append('_token', '{{ csrf_token() }}');
|
||
form.append('sample_name', name);
|
||
|
||
fetch(url, { method: 'POST', body: form })
|
||
.then(r => {
|
||
if (!r.ok) return r.json().then(j => { throw new Error(j.error || 'Ralat pelayan (' + r.status + ')'); });
|
||
return r.blob();
|
||
})
|
||
.then(blob => {
|
||
const prevSrc = img.src;
|
||
img.src = URL.createObjectURL(blob);
|
||
if (prevSrc.startsWith('blob:')) URL.revokeObjectURL(prevSrc);
|
||
})
|
||
.catch(err => {
|
||
alert('Gagal jana pratonton: ' + err.message);
|
||
})
|
||
.finally(() => {
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i class="bi bi-eye me-1"></i> Pratonton';
|
||
});
|
||
}
|
||
</script>
|
||
@endpush
|