Remove VS screen; fix cropper + sports edit save + headshot/logo mapping
- Remove VS intro from player and thumbnail VS card from match video cards.
All references (`msb-vs*`, `initVsIntro`, `setVsImg`, `HAS_VS`, `$vs`,
`$msbThumbVs`, `$hasMatchMeta`) are stripped from scoreboard partials,
video-card component, and scoreboard script. Related CSS keyframes
(`msbFadeIn/Out`, `msbDriftA/B`, `msbSweep`) removed.
- Image cropper (resources/views/components/image-cropper.blade.php):
replace unreliable third-party Cropme integration with a DIY cropper.
Pan/zoom/rotate wired directly with pointer + wheel events; on save,
the visible viewport is rendered to an offscreen canvas at the configured
output-width (with circular clip for `shape="circle"`). Also reparent the
overlay to <body> on open to escape any transformed Bootstrap-modal
ancestor's stacking context that clipped it.
- Sports match modal (resources/views/layouts/partials/sports-match-modal.blade.php):
set fighter photo croppers to 3:4 portrait (285x380) to match the VS
card's 228x304 frame; club logos + referee stay 1:1 square.
- SportsMatchController::fillFromRequest (line 174): coerce
`$this->clean($request->input('media', []))` to `[]` when it returns null
so array_merge doesn't crash under PHP 8's stricter types.
- SportsMatch::headerData(): read fighter headshots + club logos from the
canonical media.* keys (participant1_photo / participant2_photo /
club1_logo / club2_logo) that the uploader modal actually saves, with the
older p1_* / p2_* names kept as a secondary fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
19741f489b
commit
2b5e480c9a
@ -166,7 +166,9 @@ class SportsMatchController extends Controller
|
|||||||
// Media text fields (caption/alt/credit/public). Preserve existing image
|
// Media text fields (caption/alt/credit/public). Preserve existing image
|
||||||
// paths already on the record; handleImages() overwrites any replaced ones.
|
// paths already on the record; handleImages() overwrites any replaced ones.
|
||||||
$existingMedia = $match->media ?? [];
|
$existingMedia = $match->media ?? [];
|
||||||
$mediaText = $this->clean($request->input('media', []));
|
// clean() returns null when the array is entirely empty — coerce back to []
|
||||||
|
// so array_merge() doesn't blow up on PHP 8's stricter type checking.
|
||||||
|
$mediaText = $this->clean($request->input('media', [])) ?? [];
|
||||||
if (isset($mediaText['public'])) {
|
if (isset($mediaText['public'])) {
|
||||||
$mediaText['public'] = filter_var($mediaText['public'], FILTER_VALIDATE_BOOLEAN);
|
$mediaText['public'] = filter_var($mediaText['public'], FILTER_VALIDATE_BOOLEAN);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,15 +53,18 @@ class SportsMatch extends Model
|
|||||||
'name' => $trim($this->participant1_name),
|
'name' => $trim($this->participant1_name),
|
||||||
'club' => $trim($p['p1_club'] ?? null),
|
'club' => $trim($p['p1_club'] ?? null),
|
||||||
'flag' => $this->flagFor($p['p1_country'] ?? null),
|
'flag' => $this->flagFor($p['p1_country'] ?? null),
|
||||||
'club_logo' => $trim($m['p1_club_logo'] ?? null),
|
// Uploader form (sports-match modal) stores these under the
|
||||||
'headshot' => $trim($m['p1_headshot'] ?? null),
|
// canonical media.* keys — mirror them to headerData() so the
|
||||||
|
// scoreboard/VS partials get real image paths.
|
||||||
|
'club_logo' => $trim($m['club1_logo'] ?? $m['p1_club_logo'] ?? null),
|
||||||
|
'headshot' => $trim($m['participant1_photo'] ?? $m['p1_headshot'] ?? null),
|
||||||
],
|
],
|
||||||
'red' => [
|
'red' => [
|
||||||
'name' => $trim($this->participant2_name),
|
'name' => $trim($this->participant2_name),
|
||||||
'club' => $trim($p['p2_club'] ?? null),
|
'club' => $trim($p['p2_club'] ?? null),
|
||||||
'flag' => $this->flagFor($p['p2_country'] ?? null),
|
'flag' => $this->flagFor($p['p2_country'] ?? null),
|
||||||
'club_logo' => $trim($m['p2_club_logo'] ?? null),
|
'club_logo' => $trim($m['club2_logo'] ?? $m['p2_club_logo'] ?? null),
|
||||||
'headshot' => $trim($m['p2_headshot'] ?? null),
|
'headshot' => $trim($m['participant2_photo'] ?? $m['p2_headshot'] ?? null),
|
||||||
],
|
],
|
||||||
'weight_category' => $trim($p['weight_class'] ?? null),
|
'weight_category' => $trim($p['weight_class'] ?? null),
|
||||||
'division' => $trim($c['division'] ?? null),
|
'division' => $trim($c['division'] ?? null),
|
||||||
|
|||||||
@ -122,7 +122,12 @@
|
|||||||
|
|
||||||
/* ── Open / close ── */
|
/* ── Open / close ── */
|
||||||
function openModal() {
|
function openModal() {
|
||||||
document.getElementById('tcOverlay_' + id).classList.add('open');
|
var ov = document.getElementById('tcOverlay_' + id);
|
||||||
|
// Reparent to <body> at open time — a transformed ancestor (e.g. an
|
||||||
|
// opening Bootstrap modal) would otherwise constrain our position:fixed
|
||||||
|
// overlay to its own stacking context and clip Cropme's canvas.
|
||||||
|
if (ov && ov.parentNode !== document.body) document.body.appendChild(ov);
|
||||||
|
ov.classList.add('open');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
window['openCropperModal_' + id] = openModal;
|
window['openCropperModal_' + id] = openModal;
|
||||||
@ -135,42 +140,161 @@
|
|||||||
if (e.target === this) window.closeCropperModal(id);
|
if (e.target === this) window.closeCropperModal(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ── Preload a File into the cropper ── */
|
/* ── Preload a File into the cropper ─────────────────────────────────
|
||||||
|
* Uses URL.createObjectURL (fast, memory-friendly) instead of FileReader
|
||||||
|
* data URLs, which are slow / can silently fail with large images.
|
||||||
|
*/
|
||||||
function preloadFile(file) {
|
function preloadFile(file) {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
originalFile = file;
|
originalFile = file;
|
||||||
document.getElementById('tcFileName_' + id).textContent = file.name;
|
document.getElementById('tcFileName_' + id).textContent = file.name;
|
||||||
var reader = new FileReader();
|
var url = URL.createObjectURL(file);
|
||||||
reader.onload = function (e) { initCropper(e.target.result); };
|
initCropper(url);
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
}
|
||||||
window['tcPreload_' + id] = preloadFile;
|
window['tcPreload_' + id] = preloadFile;
|
||||||
|
|
||||||
function initCropper(dataUrl) {
|
/*
|
||||||
|
* ───────────────────────────────────────────────────────────────────
|
||||||
|
* DIY cropper — no library. Renders the loaded image in the canvas
|
||||||
|
* with a viewport frame of the desired vw:vh aspect (dashed red
|
||||||
|
* border, dark scrim outside via box-shadow). Pan by dragging the
|
||||||
|
* image, zoom via the slider (or wheel), rotate via the slider.
|
||||||
|
* Save clips the visible viewport region to an offscreen canvas.
|
||||||
|
* ───────────────────────────────────────────────────────────────────
|
||||||
|
*/
|
||||||
|
var _st = null; // cropper state — replaces Cropme's `cropperInst`
|
||||||
|
|
||||||
|
function initCropper(imageUrl) {
|
||||||
document.getElementById('tcPlaceholder_' + id).style.display = 'none';
|
document.getElementById('tcPlaceholder_' + id).style.display = 'none';
|
||||||
document.getElementById('tcSaveBtn_' + id).disabled = false;
|
document.getElementById('tcSaveBtn_' + id).disabled = false;
|
||||||
document.getElementById('tcAsIsBtn_' + id).disabled = false;
|
document.getElementById('tcAsIsBtn_' + id).disabled = false;
|
||||||
|
|
||||||
var canvas = document.getElementById('tcCanvas_' + id);
|
var canvas = document.getElementById('tcCanvas_' + id);
|
||||||
if (cropperInst) { cropperInst.destroy(); cropperInst = null; }
|
canvas.innerHTML = '';
|
||||||
|
canvas.className = 'tc-canvas tc-canvas-active';
|
||||||
|
|
||||||
cropperInst = new Cropme(canvas, {
|
// ── layout: image element + viewport frame (frame drawn via box-shadow scrim) ──
|
||||||
container: { width: '100%', height: 320 },
|
var img = document.createElement('img');
|
||||||
viewport: {
|
img.alt = '';
|
||||||
width: vw, height: vh,
|
img.style.cssText =
|
||||||
type: shape,
|
'position:absolute;top:50%;left:50%;transform-origin:center center;' +
|
||||||
border: { enable: true, width: 2, color: '#ef4444' }
|
'user-select:none;-webkit-user-drag:none;pointer-events:none;max-width:none;';
|
||||||
},
|
var frame = document.createElement('div');
|
||||||
transformOrigin: 'viewport',
|
frame.style.cssText =
|
||||||
zoom: { min: zoomMin, max: zoomMax, enable: true, mouseWheel: true, slider: false },
|
'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);' +
|
||||||
rotation: { enable: true, slider: false }
|
'border:2px dashed #ef4444;box-sizing:border-box;pointer-events:none;' +
|
||||||
});
|
'box-shadow:0 0 0 2000px rgba(0,0,0,.55);' +
|
||||||
cropperInst.bind({ url: dataUrl }).then(function () {
|
(shape === 'circle' ? 'border-radius:50%;' : '');
|
||||||
document.getElementById('tcZoom_' + id).value = 0;
|
canvas.appendChild(img);
|
||||||
document.getElementById('tcRot_' + id).value = 0;
|
canvas.appendChild(frame);
|
||||||
});
|
|
||||||
|
// Reset sliders
|
||||||
|
var zoomEl = document.getElementById('tcZoom_' + id);
|
||||||
|
var rotEl = document.getElementById('tcRot_' + id);
|
||||||
|
zoomEl.value = 0; rotEl.value = 0;
|
||||||
|
|
||||||
|
// Init state
|
||||||
|
_st = {
|
||||||
|
img: img, frame: frame, canvas: canvas,
|
||||||
|
imgW: 0, imgH: 0,
|
||||||
|
canvasW: 0, canvasH: 0,
|
||||||
|
vpW: 0, vpH: 0, // viewport display size (px inside canvas)
|
||||||
|
baseScale: 1, // scale that makes image "cover" the viewport
|
||||||
|
scale: 1, deg: 0, tx: 0, ty: 0,
|
||||||
|
ready: false,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onload = function () {
|
||||||
|
_st.imgW = img.naturalWidth || 1;
|
||||||
|
_st.imgH = img.naturalHeight || 1;
|
||||||
|
layoutCropper();
|
||||||
|
_st.ready = true;
|
||||||
|
};
|
||||||
|
img.onerror = function (e) {
|
||||||
|
console.error('[image-cropper]', id, 'image failed to load', e);
|
||||||
|
};
|
||||||
|
img.src = imageUrl;
|
||||||
|
// If already cached, onload may have fired synchronously — no-op if not.
|
||||||
|
if (img.complete && img.naturalWidth > 0) img.onload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fit the viewport inside the canvas, then fit the image to "cover" the viewport.
|
||||||
|
function layoutCropper() {
|
||||||
|
var s = _st; if (!s) return;
|
||||||
|
s.canvasW = s.canvas.clientWidth || 320;
|
||||||
|
s.canvasH = s.canvas.clientHeight || 320;
|
||||||
|
|
||||||
|
// Viewport: same aspect as vw:vh, fits inside canvas with ~10% padding.
|
||||||
|
var pad = 20;
|
||||||
|
var maxW = s.canvasW - pad * 2;
|
||||||
|
var maxH = s.canvasH - pad * 2;
|
||||||
|
var vAspect = vw / vh;
|
||||||
|
if (maxW / vAspect <= maxH) { s.vpW = maxW; s.vpH = Math.round(maxW / vAspect); }
|
||||||
|
else { s.vpH = maxH; s.vpW = Math.round(maxH * vAspect); }
|
||||||
|
s.frame.style.width = s.vpW + 'px';
|
||||||
|
s.frame.style.height = s.vpH + 'px';
|
||||||
|
|
||||||
|
// Base scale: image must at least cover the viewport.
|
||||||
|
s.baseScale = Math.max(s.vpW / s.imgW, s.vpH / s.imgH);
|
||||||
|
s.scale = s.baseScale;
|
||||||
|
s.tx = 0; s.ty = 0; s.deg = 0;
|
||||||
|
applyTransform();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTransform() {
|
||||||
|
var s = _st; if (!s) return;
|
||||||
|
// Image is centered via top/left 50% translate + own translate + scale + rotate.
|
||||||
|
// Set explicit width so scale multiplies pixels rather than object-fit.
|
||||||
|
s.img.style.width = s.imgW + 'px';
|
||||||
|
s.img.style.height = s.imgH + 'px';
|
||||||
|
s.img.style.marginLeft = (-s.imgW / 2) + 'px';
|
||||||
|
s.img.style.marginTop = (-s.imgH / 2) + 'px';
|
||||||
|
s.img.style.transform =
|
||||||
|
'translate(' + s.tx + 'px,' + s.ty + 'px)' +
|
||||||
|
' rotate(' + s.deg + 'deg)' +
|
||||||
|
' scale(' + s.scale + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag to pan (mouse + touch)
|
||||||
|
(function bindPan() {
|
||||||
|
var canvas = document.getElementById('tcCanvas_' + id);
|
||||||
|
var dragging = false, sx = 0, sy = 0, otx = 0, oty = 0;
|
||||||
|
function down(e) {
|
||||||
|
if (!_st || !_st.ready) return;
|
||||||
|
dragging = true;
|
||||||
|
var p = e.touches ? e.touches[0] : e;
|
||||||
|
sx = p.clientX; sy = p.clientY; otx = _st.tx; oty = _st.ty;
|
||||||
|
canvas.style.cursor = 'grabbing';
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
function move(e) {
|
||||||
|
if (!dragging || !_st) return;
|
||||||
|
var p = e.touches ? e.touches[0] : e;
|
||||||
|
_st.tx = otx + (p.clientX - sx);
|
||||||
|
_st.ty = oty + (p.clientY - sy);
|
||||||
|
applyTransform();
|
||||||
|
}
|
||||||
|
function up() { dragging = false; canvas.style.cursor = 'grab'; }
|
||||||
|
canvas.addEventListener('mousedown', down);
|
||||||
|
canvas.addEventListener('touchstart', down, { passive: false });
|
||||||
|
window.addEventListener('mousemove', move);
|
||||||
|
window.addEventListener('touchmove', move, { passive: false });
|
||||||
|
window.addEventListener('mouseup', up);
|
||||||
|
window.addEventListener('touchend', up);
|
||||||
|
// Wheel zoom
|
||||||
|
canvas.addEventListener('wheel', function (e) {
|
||||||
|
if (!_st || !_st.ready) return;
|
||||||
|
e.preventDefault();
|
||||||
|
var delta = e.deltaY < 0 ? 1.08 : 1 / 1.08;
|
||||||
|
_st.scale = Math.max(_st.baseScale, Math.min(_st.baseScale * 6, _st.scale * delta));
|
||||||
|
var pct = Math.round(((_st.scale - _st.baseScale) / (_st.baseScale * 5)) * 100);
|
||||||
|
document.getElementById('tcZoom_' + id).value = pct;
|
||||||
|
applyTransform();
|
||||||
|
}, { passive: false });
|
||||||
|
canvas.style.cursor = 'grab';
|
||||||
|
})();
|
||||||
|
|
||||||
/* ── Internal file input (the "Choose image" button inside the modal) ── */
|
/* ── Internal file input (the "Choose image" button inside the modal) ── */
|
||||||
document.getElementById('tcInput_' + id).addEventListener('change', function () {
|
document.getElementById('tcInput_' + id).addEventListener('change', function () {
|
||||||
if (this.files && this.files[0]) preloadFile(this.files[0]);
|
if (this.files && this.files[0]) preloadFile(this.files[0]);
|
||||||
@ -182,14 +306,16 @@
|
|||||||
|
|
||||||
/* ── Zoom / rotate sliders ── */
|
/* ── Zoom / rotate sliders ── */
|
||||||
document.getElementById('tcZoom_' + id).addEventListener('input', function () {
|
document.getElementById('tcZoom_' + id).addEventListener('input', function () {
|
||||||
if (!cropperInst || !cropperInst.properties.image) return;
|
if (!_st || !_st.ready) return;
|
||||||
|
// 0 → baseScale (fit), 100 → baseScale * 6 (max zoom-in)
|
||||||
var p = parseFloat(this.value) / 100;
|
var p = parseFloat(this.value) / 100;
|
||||||
cropperInst.properties.scale = zoomMin + (zoomMax - zoomMin) * p;
|
_st.scale = _st.baseScale + (_st.baseScale * 5) * p;
|
||||||
var s = cropperInst.properties;
|
applyTransform();
|
||||||
s.image.style.transform = 'translate3d(' + s.x + 'px,' + s.y + 'px,0) scale(' + s.scale + ') rotate(' + s.deg + 'deg)';
|
|
||||||
});
|
});
|
||||||
document.getElementById('tcRot_' + id).addEventListener('input', function () {
|
document.getElementById('tcRot_' + id).addEventListener('input', function () {
|
||||||
if (cropperInst) cropperInst.rotate(parseInt(this.value, 10));
|
if (!_st || !_st.ready) return;
|
||||||
|
_st.deg = parseInt(this.value, 10) || 0;
|
||||||
|
applyTransform();
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ── Helpers ── */
|
/* ── Helpers ── */
|
||||||
@ -250,43 +376,82 @@
|
|||||||
.catch(onFail);
|
.catch(onFail);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Crop & Save ── */
|
/* ── Crop & Save ─────────────────────────────────────────────────────
|
||||||
|
* Renders the visible viewport region to an offscreen canvas at the
|
||||||
|
* output size, then returns the result as a JPEG data URL.
|
||||||
|
* The image is transformed by (translate → rotate → scale) — the
|
||||||
|
* offscreen canvas replays those same transforms scaled up to the
|
||||||
|
* output resolution so nothing is lost.
|
||||||
|
*/
|
||||||
|
function renderCroppedBase64() {
|
||||||
|
var s = _st; if (!s || !s.ready) return null;
|
||||||
|
var outW = outputWidth > 0 ? outputWidth : vw;
|
||||||
|
var outH = Math.round(outW * (vh / vw));
|
||||||
|
|
||||||
|
var cv = document.createElement('canvas');
|
||||||
|
cv.width = outW;
|
||||||
|
cv.height = outH;
|
||||||
|
var ctx = cv.getContext('2d');
|
||||||
|
|
||||||
|
// Circle crop mask (matches the round viewport look on save)
|
||||||
|
if (shape === 'circle') {
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(outW / 2, outH / 2, Math.min(outW, outH) / 2, 0, Math.PI * 2);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.clip();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1 display px inside the viewport == (outW / s.vpW) output px
|
||||||
|
var pxRatio = outW / s.vpW;
|
||||||
|
|
||||||
|
ctx.translate(outW / 2, outH / 2);
|
||||||
|
ctx.scale(pxRatio, pxRatio);
|
||||||
|
ctx.translate(s.tx, s.ty);
|
||||||
|
ctx.rotate(s.deg * Math.PI / 180);
|
||||||
|
ctx.scale(s.scale, s.scale);
|
||||||
|
ctx.drawImage(s.img, -s.imgW / 2, -s.imgH / 2);
|
||||||
|
|
||||||
|
if (shape === 'circle') ctx.restore();
|
||||||
|
|
||||||
|
return cv.toDataURL('image/jpeg', 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
window['tcSave_' + id] = function () {
|
window['tcSave_' + id] = function () {
|
||||||
if (!cropperInst) return;
|
if (!_st || !_st.ready) return;
|
||||||
var btn = document.getElementById('tcSaveBtn_' + id);
|
var btn = document.getElementById('tcSaveBtn_' + id);
|
||||||
var txt = document.getElementById('tcSaveBtnText_' + id);
|
var txt = document.getElementById('tcSaveBtnText_' + id);
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
txt.textContent = 'Saving…';
|
txt.textContent = 'Saving…';
|
||||||
|
|
||||||
var cropOpts = outputWidth > 0 ? { type: 'base64', width: outputWidth } : { type: 'base64' };
|
var base64 = renderCroppedBase64();
|
||||||
|
if (!base64) { btn.disabled = false; txt.textContent = 'Crop & Save'; return; }
|
||||||
|
|
||||||
cropperInst.crop(cropOpts).then(function (base64) {
|
if (isCallbackMode) {
|
||||||
if (isCallbackMode) {
|
var cbName = originalFile ? originalFile.name : 'cropped.jpg';
|
||||||
var cbName = originalFile ? originalFile.name : 'cropped.png';
|
deliverResult(base64ToFile(base64, cbName));
|
||||||
deliverResult(base64ToFile(base64, cbName));
|
return;
|
||||||
return;
|
}
|
||||||
}
|
if (isFormMode) {
|
||||||
if (isFormMode) {
|
var fname = originalFile ? originalFile.name : 'cropped.jpg';
|
||||||
var fname = originalFile ? originalFile.name : 'cropped.png';
|
setOnTargetInput(base64ToFile(base64, fname));
|
||||||
setOnTargetInput(base64ToFile(base64, fname));
|
window.closeCropperModal(id);
|
||||||
|
if (typeof window.showToast === 'function') window.showToast('Image ready!', 'success');
|
||||||
|
btn.disabled = false;
|
||||||
|
txt.textContent = 'Crop & Save';
|
||||||
|
} else {
|
||||||
|
uploadToServer(base64, function (res) {
|
||||||
window.closeCropperModal(id);
|
window.closeCropperModal(id);
|
||||||
if (typeof window.showToast === 'function') window.showToast('Image ready!', 'success');
|
if (typeof window.showToast === 'function') window.showToast('Saved!', 'success');
|
||||||
|
if (callbackFn && typeof window[callbackFn] === 'function') window[callbackFn](res.url);
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
txt.textContent = 'Crop & Save';
|
txt.textContent = 'Crop & Save';
|
||||||
} else {
|
}, function (err) {
|
||||||
uploadToServer(base64, function (res) {
|
if (typeof window.showToast === 'function') window.showToast(err.message || 'Upload failed', 'error');
|
||||||
window.closeCropperModal(id);
|
btn.disabled = false;
|
||||||
if (typeof window.showToast === 'function') window.showToast('Saved!', 'success');
|
txt.textContent = 'Crop & Save';
|
||||||
if (callbackFn && typeof window[callbackFn] === 'function') window[callbackFn](res.url);
|
});
|
||||||
btn.disabled = false;
|
}
|
||||||
txt.textContent = 'Crop & Save';
|
|
||||||
}, function (err) {
|
|
||||||
if (typeof window.showToast === 'function') window.showToast(err.message || 'Upload failed', 'error');
|
|
||||||
btn.disabled = false;
|
|
||||||
txt.textContent = 'Crop & Save';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ── Upload as-is ── */
|
/* ── Upload as-is ── */
|
||||||
|
|||||||
@ -46,6 +46,7 @@ $sizeClasses = match($size) {
|
|||||||
'small' => 'yt-video-card-sm',
|
'small' => 'yt-video-card-sm',
|
||||||
default => '',
|
default => '',
|
||||||
};
|
};
|
||||||
|
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="yt-video-card {{ $sizeClasses }}" data-video-url="{{ $videoUrl }}">
|
<div class="yt-video-card {{ $sizeClasses }}" data-video-url="{{ $videoUrl }}">
|
||||||
|
|||||||
@ -417,8 +417,10 @@
|
|||||||
{{-- ── Image croppers (outside the form so their inner file inputs aren't submitted) ──
|
{{-- ── Image croppers (outside the form so their inner file inputs aren't submitted) ──
|
||||||
Six form-mode croppers write the cropped file straight onto each hidden input;
|
Six form-mode croppers write the cropped file straight onto each hidden input;
|
||||||
one callback-mode cropper serves all dynamic official rows. --}}
|
one callback-mode cropper serves all dynamic official rows. --}}
|
||||||
<x-image-cropper id="smc_media_participant1_photo" :width="380" :height="380" shape="square" output-width="600" title="Crop Participant 1 photo" target-input="sm-file-media_participant1_photo" preview-img="sm-prev-media_participant1_photo" />
|
{{-- Fighter photos: 3:4 portrait crop to match the VS card frame (228×304).
|
||||||
<x-image-cropper id="smc_media_participant2_photo" :width="380" :height="380" shape="square" output-width="600" title="Crop Participant 2 photo" target-input="sm-file-media_participant2_photo" preview-img="sm-prev-media_participant2_photo" />
|
Club logos + referee: 1:1 square. --}}
|
||||||
|
<x-image-cropper id="smc_media_participant1_photo" :width="285" :height="380" shape="square" output-width="600" title="Crop Participant 1 photo" target-input="sm-file-media_participant1_photo" preview-img="sm-prev-media_participant1_photo" />
|
||||||
|
<x-image-cropper id="smc_media_participant2_photo" :width="285" :height="380" shape="square" output-width="600" title="Crop Participant 2 photo" target-input="sm-file-media_participant2_photo" preview-img="sm-prev-media_participant2_photo" />
|
||||||
<x-image-cropper id="smc_media_referee_photo" :width="380" :height="380" shape="square" output-width="600" title="Crop Referee photo" target-input="sm-file-media_referee_photo" preview-img="sm-prev-media_referee_photo" />
|
<x-image-cropper id="smc_media_referee_photo" :width="380" :height="380" shape="square" output-width="600" title="Crop Referee photo" target-input="sm-file-media_referee_photo" preview-img="sm-prev-media_referee_photo" />
|
||||||
<x-image-cropper id="smc_media_club1_logo" :width="380" :height="380" shape="square" output-width="600" title="Crop Club / team 1 logo" target-input="sm-file-media_club1_logo" preview-img="sm-prev-media_club1_logo" />
|
<x-image-cropper id="smc_media_club1_logo" :width="380" :height="380" shape="square" output-width="600" title="Crop Club / team 1 logo" target-input="sm-file-media_club1_logo" preview-img="sm-prev-media_club1_logo" />
|
||||||
<x-image-cropper id="smc_media_club2_logo" :width="380" :height="380" shape="square" output-width="600" title="Crop Club / team 2 logo" target-input="sm-file-media_club2_logo" preview-img="sm-prev-media_club2_logo" />
|
<x-image-cropper id="smc_media_club2_logo" :width="380" :height="380" shape="square" output-width="600" title="Crop Club / team 2 logo" target-input="sm-file-media_club2_logo" preview-img="sm-prev-media_club2_logo" />
|
||||||
|
|||||||
@ -14,14 +14,6 @@
|
|||||||
!empty($hd['court']) ? ('Tatami '.$hd['court']) : null,
|
!empty($hd['court']) ? ('Tatami '.$hd['court']) : null,
|
||||||
], fn ($p) => is_string($p) && trim($p) !== '');
|
], fn ($p) => is_string($p) && trim($p) !== '');
|
||||||
|
|
||||||
/* ── Country display names (from Countries::all()) ───────────────── */
|
|
||||||
$countries = \App\Data\Countries::all();
|
|
||||||
$countryName = function (?string $iso2) use ($countries) {
|
|
||||||
if (!$iso2) return null;
|
|
||||||
$key = strtoupper($iso2);
|
|
||||||
return $countries[$key]['name'] ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ── Playback-time-driven data (rounds + points) ─────────────────── */
|
/* ── Playback-time-driven data (rounds + points) ─────────────────── */
|
||||||
$msbRounds = $video->matchRounds()
|
$msbRounds = $video->matchRounds()
|
||||||
->orderBy('round_number')
|
->orderBy('round_number')
|
||||||
@ -75,64 +67,10 @@
|
|||||||
'rounds' => $msbRounds ?: [['n' => 1, 'name' => '', 'start' => 0]],
|
'rounds' => $msbRounds ?: [['n' => 1, 'name' => '', 'start' => 0]],
|
||||||
'points' => $msbPoints,
|
'points' => $msbPoints,
|
||||||
'defaults' => is_array($hd['scoreboard_defaults'] ?? null) ? $hd['scoreboard_defaults'] : [],
|
'defaults' => is_array($hd['scoreboard_defaults'] ?? null) ? $hd['scoreboard_defaults'] : [],
|
||||||
'vs' => null, // populated below once $vs is built
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ── VS card — only render if there is anything to show ─────────── */
|
|
||||||
$vs = [
|
|
||||||
'event' => $hd['championship'] ?? null,
|
|
||||||
'division' => $hd['division'] ?? null,
|
|
||||||
'category' => $hd['weight_category'] ?? null,
|
|
||||||
'round' => null,
|
|
||||||
'red' => [
|
|
||||||
'name' => $hd['red']['name'] ?? null,
|
|
||||||
'flag' => $hd['red']['flag'] ?? null,
|
|
||||||
'country_name' => $countryName($hd['red']['flag'] ?? null),
|
|
||||||
'club' => $hd['red']['club'] ?? null,
|
|
||||||
'club_logo' => $hd['red']['club_logo'] ?? null,
|
|
||||||
'headshot' => $hd['red']['headshot'] ?? null,
|
|
||||||
],
|
|
||||||
'blue' => [
|
|
||||||
'name' => $hd['blue']['name'] ?? null,
|
|
||||||
'flag' => $hd['blue']['flag'] ?? null,
|
|
||||||
'country_name' => $countryName($hd['blue']['flag'] ?? null),
|
|
||||||
'club' => $hd['blue']['club'] ?? null,
|
|
||||||
'club_logo' => $hd['blue']['club_logo'] ?? null,
|
|
||||||
'headshot' => $hd['blue']['headshot'] ?? null,
|
|
||||||
],
|
|
||||||
'weight_category' => $hd['weight_category'] ?? null,
|
|
||||||
'format_line' => $hd['format'] ?? null,
|
|
||||||
'match_number' => $hd['match_number'] ?? null,
|
|
||||||
'court' => !empty($hd['court']) ? ('Tatami '.$hd['court']) : null,
|
|
||||||
'match_date' => $hd['match_date'] ?? null,
|
|
||||||
'venue_name' => $hd['venue']['name'] ?? null,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Enough metadata for a VS card? Need at least one athlete name or headshot.
|
|
||||||
$hasMatchMeta = ($vs['red']['name'] || $vs['red']['headshot'] ||
|
|
||||||
$vs['blue']['name'] || $vs['blue']['headshot']);
|
|
||||||
|
|
||||||
// Feed image paths (headshot/logo) already routed to media.thumbnail for the
|
|
||||||
// JS to consume — mirror them into $msbState.vs so setVsImg() can use them.
|
|
||||||
$msbState['vs'] = [
|
|
||||||
'red' => [
|
|
||||||
'name' => $vs['red']['name'] ?? null,
|
|
||||||
'flag' => $vs['red']['flag'] ?? null,
|
|
||||||
'club_logo' => !empty($vs['red']['club_logo']) ? route('media.thumbnail', $vs['red']['club_logo']) : null,
|
|
||||||
'headshot' => !empty($vs['red']['headshot']) ? route('media.thumbnail', $vs['red']['headshot']) : null,
|
|
||||||
],
|
|
||||||
'blue' => [
|
|
||||||
'name' => $vs['blue']['name'] ?? null,
|
|
||||||
'flag' => $vs['blue']['flag'] ?? null,
|
|
||||||
'club_logo' => !empty($vs['blue']['club_logo']) ? route('media.thumbnail', $vs['blue']['club_logo']) : null,
|
|
||||||
'headshot' => !empty($vs['blue']['headshot']) ? route('media.thumbnail', $vs['blue']['headshot']) : null,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@include('videos.partials.match.scoreboard.styles')
|
@include('videos.partials.match.scoreboard.styles')
|
||||||
@include('videos.partials.match.scoreboard.overlay')
|
@include('videos.partials.match.scoreboard.overlay')
|
||||||
@if ($hasMatchMeta)
|
@include('videos.partials.match.scoreboard.script', ['msbState' => $msbState])
|
||||||
@include('videos.partials.match.scoreboard.vs-intro', ['vs' => $vs])
|
|
||||||
@endif
|
|
||||||
@include('videos.partials.match.scoreboard.script', ['msbState' => $msbState, 'hasMatchMeta' => $hasMatchMeta])
|
|
||||||
|
|||||||
@ -13,10 +13,8 @@
|
|||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const MSB_STATE = @json($msbState);
|
const MSB_STATE = @json($msbState);
|
||||||
const HAS_VS = @json($hasMatchMeta);
|
|
||||||
const VIDEO_ID = @json($video->id);
|
const VIDEO_ID = @json($video->id);
|
||||||
const LS_KEY = 'msb_prefs';
|
const LS_KEY = 'msb_prefs';
|
||||||
const SS_VS_KEY = 'msb_vs_shown_v' + VIDEO_ID;
|
|
||||||
|
|
||||||
const RUN = function () {
|
const RUN = function () {
|
||||||
const root = document.getElementById('msbRoot');
|
const root = document.getElementById('msbRoot');
|
||||||
@ -130,31 +128,6 @@
|
|||||||
setFlag('blueFlag', MSB_STATE.blue.flag);
|
setFlag('blueFlag', MSB_STATE.blue.flag);
|
||||||
setImage('redClub', MSB_STATE.red.club_logo);
|
setImage('redClub', MSB_STATE.red.club_logo);
|
||||||
setImage('blueClub', MSB_STATE.blue.club_logo);
|
setImage('blueClub', MSB_STATE.blue.club_logo);
|
||||||
|
|
||||||
// ── VS card image slots (photo, crest, flag) ────────────────
|
|
||||||
// If data exists, swap the placeholder for a real image.
|
|
||||||
// If no data, hide the placeholder box entirely (never leave
|
|
||||||
// "FIGHTER PHOTO" / "CLUB LOGO" / "FLAG" text visible in prod).
|
|
||||||
const VS = (MSB_STATE.vs || {});
|
|
||||||
const setVsImg = (name, path, hideWhenEmpty = true) => {
|
|
||||||
const el = $(name); if (!el) return;
|
|
||||||
if (path) {
|
|
||||||
el.textContent = '';
|
|
||||||
el.style.background = '#0d0d10';
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = path; img.alt = '';
|
|
||||||
img.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block';
|
|
||||||
el.appendChild(img);
|
|
||||||
} else if (hideWhenEmpty) {
|
|
||||||
el.style.display = 'none';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
setVsImg('vsRedPhoto', VS.red && VS.red.headshot);
|
|
||||||
setVsImg('vsBluePhoto', VS.blue && VS.blue.headshot);
|
|
||||||
setVsImg('vsRedCrest', VS.red && VS.red.club_logo);
|
|
||||||
setVsImg('vsBlueCrest', VS.blue && VS.blue.club_logo);
|
|
||||||
setFlag('vsRedFlag', VS.red && VS.red.flag);
|
|
||||||
setFlag('vsBlueFlag', VS.blue && VS.blue.flag);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Playback-time-driven render (memoised) ────────────────────────
|
// ── Playback-time-driven render (memoised) ────────────────────────
|
||||||
@ -291,48 +264,6 @@
|
|||||||
}
|
}
|
||||||
tryInject(30);
|
tryInject(30);
|
||||||
|
|
||||||
// ── VS intro lifecycle ────────────────────────────────────────────
|
|
||||||
function initVsIntro() {
|
|
||||||
if (!HAS_VS) return;
|
|
||||||
const vs = document.getElementById('msbVs');
|
|
||||||
const v = player();
|
|
||||||
if (!vs || !v) return;
|
|
||||||
|
|
||||||
let alreadyShown = false;
|
|
||||||
try { alreadyShown = sessionStorage.getItem(SS_VS_KEY) === '1'; } catch (e) {}
|
|
||||||
if (alreadyShown) return;
|
|
||||||
|
|
||||||
let shown = false;
|
|
||||||
const show = () => {
|
|
||||||
if (shown) return;
|
|
||||||
shown = true;
|
|
||||||
try { sessionStorage.setItem(SS_VS_KEY, '1'); } catch (e) {}
|
|
||||||
vs.hidden = false;
|
|
||||||
vs.dataset.visible = 'true';
|
|
||||||
try { v.pause(); } catch (e) {}
|
|
||||||
|
|
||||||
const dismiss = (playToo = true) => {
|
|
||||||
if (!vs || vs.classList.contains('msb-vs-out')) return;
|
|
||||||
vs.classList.add('msb-vs-out');
|
|
||||||
if (playToo) { try { v.play(); } catch (e) {} }
|
|
||||||
setTimeout(() => { if (vs && vs.parentNode) vs.parentNode.removeChild(vs); }, 520);
|
|
||||||
document.removeEventListener('keydown', onKey);
|
|
||||||
};
|
|
||||||
vs.addEventListener('click', () => dismiss(true));
|
|
||||||
vs.addEventListener('touchstart', () => dismiss(true), { passive: true });
|
|
||||||
const onKey = () => dismiss(true);
|
|
||||||
document.addEventListener('keydown', onKey);
|
|
||||||
setTimeout(() => dismiss(true), 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Trigger on the first play attempt (autoplay-friendly).
|
|
||||||
const onPlay = () => { v.removeEventListener('play', onPlay); v.removeEventListener('playing', onPlay); show(); };
|
|
||||||
v.addEventListener('play', onPlay, { once: true });
|
|
||||||
v.addEventListener('playing', onPlay, { once: true });
|
|
||||||
// If autoplay is already firing when we arrive:
|
|
||||||
if (!v.paused && v.currentTime < 0.5) show();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Overlay scaling to a fixed 1150 px design canvas ──────────────
|
// ── Overlay scaling to a fixed 1150 px design canvas ──────────────
|
||||||
// Keeps the score bar, ticker, and identity at their pixel-perfect
|
// Keeps the score bar, ticker, and identity at their pixel-perfect
|
||||||
// proportions no matter how wide the actual player is.
|
// proportions no matter how wide the actual player is.
|
||||||
@ -372,7 +303,6 @@
|
|||||||
v.addEventListener('loadedmetadata', tick);
|
v.addEventListener('loadedmetadata', tick);
|
||||||
v.addEventListener('ratechange', tick);
|
v.addEventListener('ratechange', tick);
|
||||||
tick();
|
tick();
|
||||||
initVsIntro();
|
|
||||||
}
|
}
|
||||||
attachPlayer();
|
attachPlayer();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,16 +12,6 @@
|
|||||||
/* ────────────────────────────── keyframes ────────────────────────────── */
|
/* ────────────────────────────── keyframes ────────────────────────────── */
|
||||||
@keyframes msbRiseIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
@keyframes msbRiseIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
||||||
@keyframes msbPulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
|
@keyframes msbPulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
|
||||||
@keyframes msbFadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
||||||
@keyframes msbFadeOut { from { opacity: 1; } to { opacity: 0; } }
|
|
||||||
@keyframes msbDriftA { 0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
|
|
||||||
50% { transform: translate3d(60px, -28px, 0) scale(1.14); } }
|
|
||||||
@keyframes msbDriftB { 0%, 100% { transform: translate3d(0, 0, 0) scale(1.08); }
|
|
||||||
50% { transform: translate3d(-54px, 26px, 0) scale(1); } }
|
|
||||||
@keyframes msbSweep { 0% { transform: translateX(-60%) skewX(-14deg); opacity: 0; }
|
|
||||||
25% { opacity: .5; }
|
|
||||||
60% { opacity: 0; }
|
|
||||||
100% { transform: translateX(160%) skewX(-14deg); opacity: 0; } }
|
|
||||||
|
|
||||||
/* ────────────────────────────── root layer ─────────────────────────────
|
/* ────────────────────────────── root layer ─────────────────────────────
|
||||||
No z-index on the root: source order inside #ytpWrap places the video
|
No z-index on the root: source order inside #ytpWrap places the video
|
||||||
@ -128,19 +118,6 @@ body.msb-off-penalties [data-msb-part="penalties"] { display: none !important; }
|
|||||||
body.msb-small .msb-info,
|
body.msb-small .msb-info,
|
||||||
body.msb-small .msb-feed { display: none !important; }
|
body.msb-small .msb-feed { display: none !important; }
|
||||||
|
|
||||||
/*
|
|
||||||
* VS intro card — visuals are entirely inline styles on the verbatim markup
|
|
||||||
* from drafts/vs-screen.html. Only the outer wrapper + fade in/out lives
|
|
||||||
* here; keyframes (msbDriftA/B, msbSweep, msbFadeIn/Out) are up top.
|
|
||||||
*/
|
|
||||||
.msb-vs {
|
|
||||||
position: absolute; inset: 0; z-index: 12;
|
|
||||||
background: #08080a; overflow: hidden;
|
|
||||||
pointer-events: auto;
|
|
||||||
animation: msbFadeIn .3s ease both;
|
|
||||||
}
|
|
||||||
.msb-vs.msb-vs-out { animation: msbFadeOut .5s ease both; }
|
|
||||||
|
|
||||||
/* ────────────────────────────── injected gear rows ──────────────────── */
|
/* ────────────────────────────── injected gear rows ──────────────────── */
|
||||||
/* Matches the design prototype's custom look (26×14 track, 10 px bone knob),
|
/* Matches the design prototype's custom look (26×14 track, 10 px bone knob),
|
||||||
scoped so it never touches the platform's other rows. */
|
scoped so it never touches the platform's other rows. */
|
||||||
|
|||||||
@ -1,106 +0,0 @@
|
|||||||
{{--
|
|
||||||
VS SCREEN — verbatim markup from drafts/vs-screen.html. Only {{ }} holes
|
|
||||||
filled with Blade data. `data-msb` hooks on the photo, crest, and flag
|
|
||||||
boxes let the script swap in real images at runtime. Missing fields are
|
|
||||||
hidden by inline display:none (never substituted with placeholder text).
|
|
||||||
--}}
|
|
||||||
@php
|
|
||||||
$rc = strtoupper(trim($vs['red']['flag'] ?? ''));
|
|
||||||
$bc = strtoupper(trim($vs['blue']['flag'] ?? ''));
|
|
||||||
|
|
||||||
$eventName = $vs['event'] ?? '';
|
|
||||||
$category = $vs['category'] ?? '';
|
|
||||||
$division = $vs['division'] ?? '';
|
|
||||||
$roundName = $vs['round'] ?? '';
|
|
||||||
$redName = $vs['red']['name'] ?? '';
|
|
||||||
$redCountry = $vs['red']['country_name'] ?? '';
|
|
||||||
$redClub = $vs['red']['club'] ?? '';
|
|
||||||
$blueName = $vs['blue']['name'] ?? '';
|
|
||||||
$blueCountry= $vs['blue']['country_name'] ?? '';
|
|
||||||
$blueClub = $vs['blue']['club'] ?? '';
|
|
||||||
$weightClass= $vs['weight_category'] ?? '';
|
|
||||||
$format = $vs['format_line'] ?? '';
|
|
||||||
$bout = !empty($vs['match_number']) ? ('Bout '.$vs['match_number']) : '';
|
|
||||||
$tatami = $vs['court'] ?? '';
|
|
||||||
$dateVenue = trim(($vs['match_date'] ?? '').' '.($vs['venue_name'] ? '· '.$vs['venue_name'] : ''));
|
|
||||||
|
|
||||||
$hide = fn($v) => (is_string($v) && trim($v) === '') ? 'display:none;' : '';
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
<div class="msb-vs" id="msbVs" hidden data-visible="false">
|
|
||||||
<div style="position:relative;width:100%;height:100%;overflow:hidden;background:#08080a">
|
|
||||||
|
|
||||||
<div style="position:absolute;inset:-12%;width:70%;left:-6%;background:radial-gradient(closest-side, rgba(168,42,34,.62), rgba(8,8,10,0) 72%);animation:msbDriftA 19s ease-in-out infinite"></div>
|
|
||||||
<div style="position:absolute;inset:-12%;width:70%;left:36%;background:radial-gradient(closest-side, rgba(28,64,136,.62), rgba(8,8,10,0) 72%);animation:msbDriftB 23s ease-in-out infinite"></div>
|
|
||||||
<div style="position:absolute;top:0;bottom:0;left:0;width:26%;background:linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,.09), rgba(255,255,255,0));animation:msbSweep 11s linear infinite"></div>
|
|
||||||
<div style="position:absolute;top:0;bottom:0;left:0;width:54%;clip-path:polygon(0 0, 100% 0, 78% 100%, 0 100%);background:linear-gradient(120deg, rgba(122,26,22,.5), rgba(8,8,10,0) 76%)"></div>
|
|
||||||
<div style="position:absolute;top:0;bottom:0;right:0;width:54%;clip-path:polygon(22% 0, 100% 0, 100% 100%, 0 100%);background:linear-gradient(300deg, rgba(24,52,110,.5), rgba(8,8,10,0) 76%)"></div>
|
|
||||||
|
|
||||||
{{-- event header --}}
|
|
||||||
<div style="position:absolute;top:34px;left:0;right:0;display:flex;flex-direction:column;align-items:center;gap:8px">
|
|
||||||
<div style="font-family:'Zen Old Mincho',serif;font-size:24px;letter-spacing:.44em;color:#efe9e0;text-transform:uppercase;text-indent:.44em;{{ $hide($eventName) }}">{{ $eventName }}</div>
|
|
||||||
<div style="display:flex;align-items:center;gap:14px;font-size:12px;letter-spacing:.34em;color:#8f8a83;text-transform:uppercase">
|
|
||||||
<span style="{{ $hide($category) }}">{{ $category }}</span>
|
|
||||||
<span style="width:4px;height:4px;background:#e8534a;{{ $hide($category).$hide($division) }}"></span>
|
|
||||||
<span style="{{ $hide($division) }}">{{ $division }}</span>
|
|
||||||
<span style="width:4px;height:4px;background:#6aa6ff;{{ $hide($division).$hide($roundName) }}"></span>
|
|
||||||
<span style="{{ $hide($roundName) }}">{{ $roundName }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="position:absolute;inset:96px 0 54px;display:grid;grid-template-columns:1fr 200px 1fr;align-items:start">
|
|
||||||
|
|
||||||
{{-- RED fighter --}}
|
|
||||||
<div style="display:flex;flex-direction:column;align-items:center;padding:34px 24px 0;min-width:0">
|
|
||||||
<div style="position:relative;width:228px;height:304px">
|
|
||||||
<div data-msb="vsRedPhoto" style="position:absolute;inset:0;background:repeating-linear-gradient(135deg,rgba(255,255,255,.1) 0 6px,rgba(255,255,255,.03) 6px 12px);border:1px solid rgba(255,255,255,.16);display:flex;align-items:flex-end;justify-content:center;padding-bottom:12px;font-family:ui-monospace,monospace;font-size:10px;letter-spacing:.1em;color:rgba(255,255,255,.6)">FIGHTER PHOTO</div>
|
|
||||||
<div data-msb="vsRedCrest" style="position:absolute;bottom:-26px;right:-26px;width:104px;height:104px;border-radius:50%;background:#0d0d10;border:1px solid rgba(255,255,255,.28);box-shadow:0 10px 30px rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:9px;letter-spacing:.06em;color:rgba(255,255,255,.7);text-align:center;line-height:1.35">CLUB<br>LOGO</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:34px;padding-top:12px;border-top:3px solid #e8534a;display:flex;flex-direction:column;align-items:center;gap:8px">
|
|
||||||
<div style="font-size:42px;line-height:.95;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap">{{ $redName ?: 'RED' }}</div>
|
|
||||||
<div style="display:flex;align-items:center;gap:12px;{{ $hide($redCountry).($rc ? '' : $hide('')) }}">
|
|
||||||
<span data-msb="vsRedFlag" style="width:40px;height:27px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 5px,rgba(255,255,255,.1) 5px 10px);box-shadow:0 0 0 1px rgba(255,255,255,.32);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;color:rgba(255,255,255,.85)">FLAG</span>
|
|
||||||
<span style="font-size:14px;letter-spacing:.3em;color:#d3b3ad;text-transform:uppercase;{{ $hide($redCountry) }}">{{ $redCountry }}</span>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;align-items:center;gap:10px;margin-top:2px;{{ $hide($redClub) }}">
|
|
||||||
<span style="font-size:13px;letter-spacing:.18em;color:#9d9691;text-transform:uppercase">{{ $redClub }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- centre --}}
|
|
||||||
<div style="display:flex;flex-direction:column;align-items:center;gap:12px;padding-top:96px">
|
|
||||||
<span style="font-family:'Zen Old Mincho',serif;font-size:88px;line-height:.8;font-weight:700;color:#efe9e0;text-shadow:0 8px 34px rgba(0,0,0,.75)">VS</span>
|
|
||||||
<span style="padding:7px 16px;border:1px solid rgba(255,255,255,.3);font-size:14px;letter-spacing:.32em;color:#efe9e0;text-transform:uppercase;{{ $hide($weightClass) }}">{{ $weightClass }}</span>
|
|
||||||
<span style="font-size:11px;letter-spacing:.34em;color:#8f8a83;text-transform:uppercase;{{ $hide($format) }}">{{ $format }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- BLUE fighter — mirror --}}
|
|
||||||
<div style="display:flex;flex-direction:column;align-items:center;padding:34px 24px 0;min-width:0">
|
|
||||||
<div style="position:relative;width:228px;height:304px">
|
|
||||||
<div data-msb="vsBluePhoto" style="position:absolute;inset:0;background:repeating-linear-gradient(135deg,rgba(255,255,255,.1) 0 6px,rgba(255,255,255,.03) 6px 12px);border:1px solid rgba(255,255,255,.16);display:flex;align-items:flex-end;justify-content:center;padding-bottom:12px;font-family:ui-monospace,monospace;font-size:10px;letter-spacing:.1em;color:rgba(255,255,255,.6)">FIGHTER PHOTO</div>
|
|
||||||
<div data-msb="vsBlueCrest" style="position:absolute;bottom:-26px;left:-26px;width:104px;height:104px;border-radius:50%;background:#0d0d10;border:1px solid rgba(255,255,255,.28);box-shadow:0 10px 30px rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:9px;letter-spacing:.06em;color:rgba(255,255,255,.7);text-align:center;line-height:1.35">CLUB<br>LOGO</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top:34px;padding-top:12px;border-top:3px solid #6aa6ff;display:flex;flex-direction:column;align-items:center;gap:8px">
|
|
||||||
<div style="font-size:42px;line-height:.95;font-weight:800;color:#fff;text-transform:uppercase;white-space:nowrap">{{ $blueName ?: 'BLUE' }}</div>
|
|
||||||
<div style="display:flex;align-items:center;gap:12px;{{ $hide($blueCountry) }}">
|
|
||||||
<span style="font-size:14px;letter-spacing:.3em;color:#a8b6cf;text-transform:uppercase;{{ $hide($blueCountry) }}">{{ $blueCountry }}</span>
|
|
||||||
<span data-msb="vsBlueFlag" style="width:40px;height:27px;flex:none;background:repeating-linear-gradient(135deg,rgba(255,255,255,.28) 0 5px,rgba(255,255,255,.1) 5px 10px);box-shadow:0 0 0 1px rgba(255,255,255,.32);display:flex;align-items:center;justify-content:center;font-family:ui-monospace,monospace;font-size:8px;color:rgba(255,255,255,.85)">FLAG</span>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;align-items:center;gap:10px;margin-top:2px;{{ $hide($blueClub) }}">
|
|
||||||
<span style="font-size:13px;letter-spacing:.18em;color:#9d9691;text-transform:uppercase">{{ $blueClub }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- bout line --}}
|
|
||||||
<div style="position:absolute;bottom:26px;left:0;right:0;display:flex;align-items:center;justify-content:center;gap:16px;font-size:11px;letter-spacing:.32em;color:#8f8a83;text-transform:uppercase">
|
|
||||||
<span style="{{ $hide($bout) }}">{{ $bout }}</span>
|
|
||||||
<span style="width:4px;height:4px;background:#e8534a;{{ $hide($bout).$hide($tatami) }}"></span>
|
|
||||||
<span style="{{ $hide($tatami) }}">{{ $tatami }}</span>
|
|
||||||
<span style="width:4px;height:4px;background:#6aa6ff;{{ $hide($tatami).$hide($dateVenue) }}"></span>
|
|
||||||
<span style="{{ $hide($dateVenue) }}">{{ $dateVenue }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user