chartboard/app/Views/share/modal.php
2026-03-30 09:51:33 +05:30

248 lines
13 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
$csrfName = csrf_token();
$csrfHash = csrf_hash();
?>
<div class="modal fade" id="modalShare" tabindex="-1" aria-hidden="true" data-csrf-name="<?= esc($csrfName) ?>" data-csrf-hash="<?= esc($csrfHash) ?>">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-0">
<h2 class="modal-title h5 fw-bold">Share</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<ul class="nav nav-tabs mb-3" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabShareLink" type="button">Public link</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabShareEmbed" type="button">Embed</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="tabShareLink">
<form id="cbShareGenerateForm" class="row g-2 mb-3">
<input type="hidden" name="<?= esc($csrfName) ?>" value="<?= esc($csrfHash) ?>" class="cb-share-csrf">
<input type="hidden" name="type" id="cbShareType" value="chart">
<input type="hidden" name="resource_id" id="cbShareResourceId" value="">
<div class="col-md-6">
<label class="form-label small">Optional password</label>
<input type="password" name="password" class="form-control form-control-sm" autocomplete="new-password" placeholder="Leave empty for open access">
</div>
<div class="col-md-6">
<label class="form-label small">Optional expiry</label>
<input type="datetime-local" name="expires_at" class="form-control form-control-sm">
</div>
<div class="col-12">
<button type="submit" class="btn btn-dark btn-sm">Generate new link</button>
</div>
</form>
<p class="text-danger small d-none" id="cbShareErr"></p>
<div id="cbShareLinksWrap" class="small">
<p class="text-muted mb-0">Open Share to load existing links.</p>
</div>
</div>
<div class="tab-pane fade" id="tabShareEmbed">
<label class="form-label small">Preset size</label>
<select class="form-select form-select-sm mb-2" id="cbEmbedPreset">
<option value="640x480">640 × 480</option>
<option value="800x600">800 × 600</option>
<option value="1200x800" selected>1200 × 800</option>
<option value="custom">Custom</option>
</select>
<div class="row g-2 mb-2 d-none" id="cbEmbedCustomDims">
<div class="col-6">
<input type="number" class="form-control form-control-sm" id="cbEmbedW" value="1200" min="200" max="2400">
</div>
<div class="col-6">
<input type="number" class="form-control form-control-sm" id="cbEmbedH" value="800" min="200" max="1600">
</div>
</div>
<label class="form-label small">Iframe HTML</label>
<textarea class="form-control font-monospace small" id="cbEmbedCode" rows="4" readonly></textarea>
<button type="button" class="btn btn-outline-secondary btn-sm mt-2" id="cbEmbedCopy">Copy embed code</button>
<p class="text-muted small mt-2 mb-0">Uses <code>?embed=1</code> and allows embedding in external pages.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
(function () {
const modal = document.getElementById('modalShare');
if (!modal) return;
const urlGen = <?= json_encode(rtrim(base_url(), '/') . '/sharing/generate') ?>;
const urlList = <?= json_encode(rtrim(base_url(), '/') . '/sharing/links') ?>;
const urlRevoke = <?= json_encode(rtrim(base_url(), '/') . '/sharing/revoke/') ?>;
function getCsrf() {
return { name: modal.getAttribute('data-csrf-name') || '', hash: modal.querySelector('.cb-share-csrf')?.value || '' };
}
function setEmbedSnippet(url) {
const preset = document.getElementById('cbEmbedPreset')?.value || '1200x800';
let w = 1200, h = 800;
if (preset === 'custom') {
w = parseInt(document.getElementById('cbEmbedW')?.value || '1200', 10);
h = parseInt(document.getElementById('cbEmbedH')?.value || '800', 10);
} else {
const p = preset.split('x');
w = parseInt(p[0], 10) || 1200;
h = parseInt(p[1], 10) || 800;
}
const u = (url || '').split('#')[0];
const src = u.indexOf('?') >= 0 ? u + '&embed=1' : u + '?embed=1';
const code = '<iframe src="' + src.replace(/"/g, '&quot;') + '" width="' + w + '" height="' + h + '" style="border:0;" loading="lazy" title="Chart-Board"></iframe>';
const ta = document.getElementById('cbEmbedCode');
if (ta) ta.value = code;
}
async function loadLinks() {
const type = document.getElementById('cbShareType')?.value || 'chart';
const rid = document.getElementById('cbShareResourceId')?.value || '';
const wrap = document.getElementById('cbShareLinksWrap');
const err = document.getElementById('cbShareErr');
if (!wrap || !rid) return;
err.classList.add('d-none');
wrap.innerHTML = '<p class="text-muted mb-0">Loading…</p>';
try {
const r = await fetch(urlList + '?type=' + encodeURIComponent(type) + '&resource_id=' + encodeURIComponent(rid), {
credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
});
const j = await r.json();
if (!j.success || !j.links) {
wrap.innerHTML = '<p class="text-muted mb-0">No links yet.</p>';
return;
}
if (j.links.length === 0) {
wrap.innerHTML = '<p class="text-muted mb-0">No active links. Generate one above.</p>';
setEmbedSnippet('');
return;
}
let html = '<ul class="list-group list-group-flush">';
j.links.forEach((L) => {
const qr = 'https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=' + encodeURIComponent(L.url);
html += '<li class="list-group-item px-0">' +
'<div class="d-flex flex-wrap gap-2 align-items-start">' +
'<div class="flex-grow-1 min-w-0">' +
'<div class="input-group input-group-sm mb-1">' +
'<input type="text" class="form-control font-monospace small" readonly value="' + String(L.url).replace(/"/g, '&quot;') + '">' +
'<button type="button" class="btn btn-outline-secondary cb-copy-url">Copy</button></div>' +
'<div class="text-muted" style="font-size:11px;">Views: ' + (L.view_count ?? 0) +
(L.expires_at ? ' · Expires: ' + L.expires_at : '') +
(L.has_password ? ' · Password protected' : '') + '</div>' +
'<form class="d-inline mt-1 cb-revoke-form" data-id="' + L.id + '">' +
'<button type="submit" class="btn btn-link btn-sm text-danger p-0">Revoke</button></form>' +
'</div><div><img src="' + qr + '" width="120" height="120" alt="QR code"></div></div></li>';
});
html += '</ul>';
wrap.innerHTML = html;
setEmbedSnippet(j.links[0].url);
wrap.querySelectorAll('.cb-copy-url').forEach((btn) => {
btn.addEventListener('click', () => {
const inp = btn.closest('.input-group')?.querySelector('input');
if (!inp) return;
inp.select();
document.execCommand('copy');
});
});
wrap.querySelectorAll('.cb-revoke-form').forEach((f) => {
f.addEventListener('submit', async (ev) => {
ev.preventDefault();
const id = f.getAttribute('data-id');
const csrf = getCsrf();
const body = new URLSearchParams();
body.append(csrf.name, csrf.hash);
const res = await fetch(urlRevoke + id, {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
body,
});
const jj = await res.json();
if (jj.csrf) {
modal.setAttribute('data-csrf-name', jj.csrf.name);
const hi = modal.querySelector('.cb-share-csrf');
if (hi) { hi.name = jj.csrf.name; hi.value = jj.csrf.hash; }
}
loadLinks();
});
});
} catch (e) {
wrap.innerHTML = '<p class="text-danger mb-0">Could not load links.</p>';
}
}
modal.addEventListener('show.bs.modal', (ev) => {
const t = ev.relatedTarget;
const type = t?.getAttribute('data-share-type') || 'chart';
const rid = t?.getAttribute('data-resource-id') || '';
const st = document.getElementById('cbShareType');
const sr = document.getElementById('cbShareResourceId');
if (st) st.value = type;
if (sr) sr.value = rid;
loadLinks();
});
document.getElementById('cbShareGenerateForm')?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const err = document.getElementById('cbShareErr');
err.classList.add('d-none');
const form = ev.target;
const fd = new FormData(form);
try {
const r = await fetch(urlGen, {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' },
body: new URLSearchParams(fd),
});
const j = await r.json();
if (j.csrf) {
const hi = modal.querySelector('.cb-share-csrf');
if (hi) { hi.name = j.csrf.name; hi.value = j.csrf.hash; }
}
if (!j.success) {
err.textContent = j.message || (j.errors ? Object.values(j.errors).flat().join(' ') : 'Failed');
err.classList.remove('d-none');
return;
}
if (j.csrf) {
const hi = modal.querySelector('.cb-share-csrf');
if (hi) { hi.name = j.csrf.name; hi.value = j.csrf.hash; }
modal.setAttribute('data-csrf-name', j.csrf.name);
}
form.querySelector('[name="password"]').value = '';
form.querySelector('[name="expires_at"]').value = '';
loadLinks();
} catch (e) {
err.textContent = 'Request failed.';
err.classList.remove('d-none');
}
});
document.getElementById('cbEmbedPreset')?.addEventListener('change', () => {
const c = document.getElementById('cbEmbedCustomDims');
if (c) c.classList.toggle('d-none', document.getElementById('cbEmbedPreset').value !== 'custom');
const first = document.querySelector('#cbShareLinksWrap input[readonly]');
setEmbedSnippet(first ? first.value : '');
});
document.getElementById('cbEmbedW')?.addEventListener('input', () => {
const first = document.querySelector('#cbShareLinksWrap input[readonly]');
setEmbedSnippet(first ? first.value : '');
});
document.getElementById('cbEmbedH')?.addEventListener('input', () => {
const first = document.querySelector('#cbShareLinksWrap input[readonly]');
setEmbedSnippet(first ? first.value : '');
});
document.getElementById('cbEmbedCopy')?.addEventListener('click', () => {
const ta = document.getElementById('cbEmbedCode');
if (!ta) return;
ta.select();
document.execCommand('copy');
});
})();
</script>