feat: susun semula layout urus template sijil
- 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>
This commit is contained in:
@@ -17,14 +17,71 @@
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="row g-4">
|
||||
{{-- Left: Current template --}}
|
||||
<div class="col-md-7">
|
||||
@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)
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
|
||||
{{-- ── 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')
|
||||
@@ -33,52 +90,64 @@
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="text-center mb-3">
|
||||
<div class="cert-preview-wrapper border rounded overflow-hidden" style="max-height:380px;">
|
||||
<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 w-100"
|
||||
style="object-fit:contain;">
|
||||
class="img-fluid"
|
||||
style="max-width:100%; max-height:{{ $isPortrait ? '520px' : '340px' }}; object-fit:contain;">
|
||||
</div>
|
||||
<div class="mt-2 text-muted small">{{ $template->original_filename }}</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>
|
||||
|
||||
{{-- Test Generate --}}
|
||||
<div class="border rounded p-3 bg-light">
|
||||
<label class="form-label small fw-medium">Jana Pratonton</label>
|
||||
{{-- 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()">
|
||||
<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>
|
||||
|
||||
{{-- Config Editor --}}
|
||||
<div class="card border-0 shadow-sm">
|
||||
{{-- 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">
|
||||
@php $config = $template->config_json ?? []; $fields = $config['fields'] ?? []; @endphp
|
||||
<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')
|
||||
|
||||
<p class="text-muted small mb-3">
|
||||
Koordinat X dan Y dikira dari sudut kiri atas imej (piksel).
|
||||
Imej template: <strong>{{ $config['width'] ?? '—' }} × {{ $config['height'] ?? '—' }}</strong> piksel.
|
||||
</p>
|
||||
|
||||
{{-- Name field --}}
|
||||
{{-- Nama Peserta --}}
|
||||
<div class="card border mb-3">
|
||||
<div class="card-header py-2 bg-light">
|
||||
<span class="fw-medium small">Nama Peserta</span>
|
||||
@@ -86,12 +155,12 @@
|
||||
<div class="card-body py-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-4">
|
||||
<label class="form-label small">X (Piksel)</label>
|
||||
<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 (Piksel)</label>
|
||||
<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>
|
||||
@@ -102,7 +171,8 @@
|
||||
</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"
|
||||
<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">
|
||||
@@ -117,9 +187,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Certificate No (optional) --}}
|
||||
{{-- No. Sijil (pilihan) --}}
|
||||
<div class="card border mb-4">
|
||||
<div class="card-header py-2 bg-light d-flex justify-content-between">
|
||||
<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"
|
||||
@@ -128,15 +198,16 @@
|
||||
<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="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</label>
|
||||
<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</label>
|
||||
<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>
|
||||
@@ -147,7 +218,8 @@
|
||||
</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"
|
||||
<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">
|
||||
@@ -168,9 +240,15 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.row -->
|
||||
|
||||
@else
|
||||
{{-- Upload Form --}}
|
||||
|
||||
{{-- 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>
|
||||
@@ -178,11 +256,13 @@
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info small mb-4">
|
||||
<i class="bi bi-lightbulb me-1"></i>
|
||||
Muat naik imej template sijil dalam format <strong>JPG atau PNG</strong>.
|
||||
Saiz maksimum: <strong>10MB</strong>. Resolusi disyorkan: <strong>1754 × 1240px</strong> (A4 landscape 150dpi).
|
||||
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">
|
||||
<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>
|
||||
@@ -195,7 +275,7 @@
|
||||
|
||||
<div id="imagePreviewBox" class="mb-4 d-none">
|
||||
<label class="form-label small text-muted">Pratonton:</label>
|
||||
<div class="border rounded overflow-hidden">
|
||||
<div id="uploadPreviewWrapper" class="border rounded overflow-hidden">
|
||||
<img id="imagePreviewEl" src="" alt="preview" class="img-fluid w-100">
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,46 +286,79 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Right: Tips --}}
|
||||
<div class="col-md-5">
|
||||
<div class="card border-0 bg-light">
|
||||
<div class="card-body p-4">
|
||||
<h6 class="fw-semibold mb-3"><i class="bi bi-info-circle me-2 text-primary"></i>Panduan Template</h6>
|
||||
<ul class="small text-muted mb-0">
|
||||
<li class="mb-2">Gunakan imej resolusi tinggi (≥1600px lebar) untuk hasil cetak berkualiti.</li>
|
||||
<li class="mb-2">Pastikan ruang untuk nama peserta tidak dihalang oleh grafik template.</li>
|
||||
<li class="mb-2">Koordinat (0,0) adalah sudut <strong>kiri atas</strong> imej.</li>
|
||||
<li class="mb-2">Gunakan butang <strong>Pratonton</strong> untuk menyemak kedudukan teks sebelum jana sijil sebenar.</li>
|
||||
<li class="mb-0">Sijil dijana dalam format JPG.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function previewImage(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
document.getElementById('imagePreviewEl').src = e.target.result;
|
||||
document.getElementById('imagePreviewBox').classList.remove('d-none');
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
// ── 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');
|
||||
|
||||
Reference in New Issue
Block a user