New POST /videos/{video}/share/email route (auth + throttled) handled by
VideoController@shareByEmail, sending the VideoShared mailable rendered from
emails/video-shared.blade.php. Wired into the share modal and video-actions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
292 lines
15 KiB
PHP
292 lines
15 KiB
PHP
<!-- Share Modal -->
|
|
<div class="modal fade" id="shareModal" tabindex="-1" aria-labelledby="shareModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content" style="background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 16px;">
|
|
<div class="modal-header" style="border-bottom: 1px solid var(--border-color); padding: 20px 24px;">
|
|
<h5 class="modal-title" id="shareModalLabel" style="font-weight: 600; color: var(--text-primary);">
|
|
<i class="bi bi-share me-2"></i>Share
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body" style="padding: 24px;">
|
|
<p style="color: var(--text-secondary); margin-bottom: 16px;">Share this link with your friends:</p>
|
|
|
|
<div class="share-link-container" style="display: flex; gap: 8px; align-items: center;">
|
|
<input type="text" id="shareLinkInput" class="form-control" readonly
|
|
style="background: var(--bg-primary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 12px 16px; border-radius: 8px;">
|
|
<button type="button" id="copyLinkBtn" class="btn-copy action-btn action-btn-primary">
|
|
<i class="bi bi-clipboard"></i> <span>Copy</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div id="copySuccess" class="copy-success" style="display: none; margin-top: 12px; color: #4caf50; font-size: 14px; text-align: center;">
|
|
<i class="bi bi-check-circle-fill me-1"></i> Link copied to clipboard!
|
|
</div>
|
|
|
|
<div style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border-color);">
|
|
<p style="color: var(--text-secondary); font-size: 13px; margin-bottom: 12px;">Share on social media:</p>
|
|
<div style="display: flex; gap: 12px;">
|
|
<a href="#" id="shareFacebook" class="social-share-btn" target="_blank"
|
|
style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #1877f2; color: white; text-decoration: none;">
|
|
<i class="bi bi-facebook"></i>
|
|
</a>
|
|
<a href="#" id="shareTwitter" class="social-share-btn" target="_blank"
|
|
style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #1da1f2; color: white; text-decoration: none;">
|
|
<i class="bi bi-twitter-x"></i>
|
|
</a>
|
|
<a href="#" id="shareWhatsApp" class="social-share-btn" target="_blank"
|
|
style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #25d366; color: white; text-decoration: none;">
|
|
<i class="bi bi-whatsapp"></i>
|
|
</a>
|
|
<a href="#" id="shareTelegram" class="social-share-btn" target="_blank"
|
|
style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #0088cc; color: white; text-decoration: none;">
|
|
<i class="bi bi-telegram"></i>
|
|
</a>
|
|
<a href="#" id="shareEmailBtn" class="social-share-btn" role="button"
|
|
style="display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; border-radius: 50%; background: #6b7280; color: white; text-decoration: none;">
|
|
<i class="bi bi-envelope-fill"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Send by email --}}
|
|
<div id="shareEmailSection" style="display:none; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border-color);">
|
|
<p style="color: var(--text-secondary); font-size: 13px; margin-bottom: 12px;">
|
|
<i class="bi bi-envelope me-1"></i> Send to a friend by email:
|
|
</p>
|
|
<input type="email" id="shareEmailTo" class="form-control" placeholder="friend@example.com" autocomplete="off"
|
|
style="background: var(--bg-primary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 10px 14px; border-radius: 8px;">
|
|
<textarea id="shareEmailMsg" class="form-control" rows="2" maxlength="500" placeholder="Add a short message (optional)"
|
|
style="background: var(--bg-primary); border: 1px solid var(--border-color); color: var(--text-primary); padding: 10px 14px; border-radius: 8px; margin-top: 8px; resize: vertical;"></textarea>
|
|
<button type="button" id="shareEmailSend" class="action-btn action-btn-primary" style="width:100%; justify-content:center; margin-top:10px;">
|
|
<i class="bi bi-send"></i> <span>Send email</span>
|
|
</button>
|
|
<div id="shareEmailStatus" style="display:none; margin-top:10px; font-size:13px; text-align:center;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.social-share-btn {
|
|
transition: transform 0.2s, opacity 0.2s;
|
|
}
|
|
.social-share-btn:hover {
|
|
transform: scale(1.1);
|
|
opacity: 0.9;
|
|
}
|
|
.btn-copy.copied {
|
|
background: #4caf50 !important;
|
|
border-color: #4caf50 !important;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
function _getLatestCsrf() {
|
|
var match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);
|
|
if (match) return decodeURIComponent(match[1]);
|
|
return (typeof csrf !== 'undefined') ? csrf : '';
|
|
}
|
|
|
|
// Set per-open so the "Send by email" form knows the endpoint + which version to send.
|
|
var _shareEmailUrl = '';
|
|
var _shareTrack = '';
|
|
|
|
async function openShareModal(videoUrl, videoTitle, recordUrl, emailUrl) {
|
|
var csrfToken = _getLatestCsrf();
|
|
var shareUrl = videoUrl;
|
|
|
|
// Preserve the version selector (?track=) from the requested URL — the server's tracked
|
|
// share link replaces shareUrl below, so we re-attach it afterwards.
|
|
var trackParam = '';
|
|
try { trackParam = (new URL(videoUrl, window.location.origin)).searchParams.get('track') || ''; } catch (e) {}
|
|
_shareEmailUrl = emailUrl || '';
|
|
_shareTrack = trackParam;
|
|
|
|
// Obtain a unique tracked share link from the server
|
|
if (recordUrl) {
|
|
try {
|
|
var res = await fetch(recordUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-TOKEN': csrfToken,
|
|
'Accept': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
});
|
|
if (res.ok) {
|
|
var data = await res.json();
|
|
if (data.url) shareUrl = data.url;
|
|
}
|
|
} catch (e) { /* fallback to plain URL */ }
|
|
}
|
|
|
|
// Re-attach the version selector so the recipient opens the right language.
|
|
if (trackParam) {
|
|
shareUrl += (shareUrl.indexOf('?') === -1 ? '?' : '&') + 'track=' + encodeURIComponent(trackParam);
|
|
}
|
|
|
|
// Mobile: use native share sheet with the unique link
|
|
if (window.innerWidth <= 768 && navigator.share) {
|
|
navigator.share({ title: videoTitle, url: shareUrl }).catch(function() {});
|
|
return;
|
|
}
|
|
|
|
// Desktop: show modal
|
|
_populateShareModal(shareUrl, videoTitle);
|
|
var modal = new bootstrap.Modal(document.getElementById('shareModal'), { backdrop: true, keyboard: true });
|
|
modal.show();
|
|
}
|
|
|
|
function _populateShareModal(shareUrl, videoTitle) {
|
|
document.getElementById('shareLinkInput').value = shareUrl;
|
|
|
|
var encodedUrl = encodeURIComponent(shareUrl);
|
|
var encodedTitle = encodeURIComponent(videoTitle);
|
|
var waText = encodeURIComponent(videoTitle + '\n' + shareUrl);
|
|
|
|
document.getElementById('shareFacebook').href = 'https://www.facebook.com/sharer/sharer.php?u=' + encodedUrl;
|
|
document.getElementById('shareTwitter').href = 'https://twitter.com/intent/tweet?url=' + encodedUrl + '&text=' + encodedTitle;
|
|
document.getElementById('shareWhatsApp').href = 'https://wa.me/?text=' + waText;
|
|
document.getElementById('shareTelegram').href = 'https://t.me/share/url?url=' + encodedUrl + '&text=' + encodedTitle;
|
|
|
|
var copyBtn = document.getElementById('copyLinkBtn');
|
|
copyBtn.innerHTML = '<i class="bi bi-clipboard"></i> <span>Copy</span>';
|
|
copyBtn.classList.remove('copied');
|
|
document.getElementById('copySuccess').style.display = 'none';
|
|
|
|
// Reset the email form; the envelope button only works when an endpoint was provided.
|
|
var emailBtn = document.getElementById('shareEmailBtn');
|
|
var emailSec = document.getElementById('shareEmailSection');
|
|
if (emailBtn) emailBtn.style.display = _shareEmailUrl ? 'flex' : 'none';
|
|
if (emailSec) {
|
|
emailSec.style.display = 'none';
|
|
var to = document.getElementById('shareEmailTo'); if (to) to.value = '';
|
|
var msg = document.getElementById('shareEmailMsg'); if (msg) msg.value = '';
|
|
var st = document.getElementById('shareEmailStatus'); if (st) st.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function _copyToClipboard(text) {
|
|
// Prefer modern clipboard API (requires HTTPS)
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
return navigator.clipboard.writeText(text);
|
|
}
|
|
// Textarea fallback — works even inside Bootstrap modals
|
|
return new Promise(function(resolve, reject) {
|
|
var ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.setAttribute('readonly', '');
|
|
ta.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;';
|
|
document.body.appendChild(ta);
|
|
ta.focus();
|
|
ta.select();
|
|
var ok = false;
|
|
try { ok = document.execCommand('copy'); } catch(e) {}
|
|
document.body.removeChild(ta);
|
|
ok ? resolve() : reject();
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var copyBtn = document.getElementById('copyLinkBtn');
|
|
var shareInput = document.getElementById('shareLinkInput');
|
|
var copySuccess = document.getElementById('copySuccess');
|
|
|
|
if (!copyBtn || !shareInput) return;
|
|
|
|
copyBtn.addEventListener('click', function() {
|
|
_copyToClipboard(shareInput.value).then(function() {
|
|
copyBtn.innerHTML = '<i class="bi bi-check-lg"></i> <span>Copied!</span>';
|
|
copyBtn.classList.add('copied');
|
|
copySuccess.style.display = 'block';
|
|
setTimeout(function() {
|
|
copyBtn.innerHTML = '<i class="bi bi-clipboard"></i> <span>Copy</span>';
|
|
copyBtn.classList.remove('copied');
|
|
copySuccess.style.display = 'none';
|
|
}, 2500);
|
|
}).catch(function() {
|
|
showToast('Could not copy — please copy the link manually.', 'error');
|
|
});
|
|
});
|
|
|
|
// ── Send by email ──────────────────────────────────────────────
|
|
var emailBtn = document.getElementById('shareEmailBtn');
|
|
var emailSec = document.getElementById('shareEmailSection');
|
|
var emailTo = document.getElementById('shareEmailTo');
|
|
var emailMsg = document.getElementById('shareEmailMsg');
|
|
var emailSend = document.getElementById('shareEmailSend');
|
|
var emailStat = document.getElementById('shareEmailStatus');
|
|
|
|
if (emailBtn && emailSec) {
|
|
emailBtn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
emailSec.style.display = (emailSec.style.display === 'none' || !emailSec.style.display) ? 'block' : 'none';
|
|
if (emailSec.style.display === 'block' && emailTo) emailTo.focus();
|
|
});
|
|
}
|
|
|
|
function _showEmailStatus(msg, color) {
|
|
if (!emailStat) return;
|
|
emailStat.textContent = msg;
|
|
emailStat.style.color = color;
|
|
emailStat.style.display = 'block';
|
|
}
|
|
|
|
if (emailSend) {
|
|
emailSend.addEventListener('click', function() {
|
|
var to = (emailTo && emailTo.value || '').trim();
|
|
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(to)) {
|
|
_showEmailStatus('Please enter a valid email address.', '#ef4444');
|
|
if (emailTo) emailTo.focus();
|
|
return;
|
|
}
|
|
if (!_shareEmailUrl) { _showEmailStatus('Email sharing is unavailable here.', '#ef4444'); return; }
|
|
|
|
emailSend.disabled = true;
|
|
var _orig = emailSend.innerHTML;
|
|
emailSend.innerHTML = '<i class="bi bi-hourglass-split"></i> <span>Sending…</span>';
|
|
_showEmailStatus('Sending…', 'var(--text-secondary)');
|
|
|
|
var body = new URLSearchParams({
|
|
_token: '{{ csrf_token() }}',
|
|
email: to,
|
|
message: (emailMsg && emailMsg.value || ''),
|
|
track: _shareTrack || '0',
|
|
});
|
|
|
|
fetch(_shareEmailUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-TOKEN': _getLatestCsrf(),
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
},
|
|
body: body.toString(),
|
|
})
|
|
.then(function(r) { return r.json().then(function(d){ return { ok: r.ok, d: d }; }); })
|
|
.then(function(res) {
|
|
emailSend.disabled = false;
|
|
emailSend.innerHTML = _orig;
|
|
if (res.ok && res.d.success) {
|
|
_showEmailStatus('Sent! Your friend will get the email shortly.', '#4caf50');
|
|
if (emailTo) emailTo.value = '';
|
|
if (emailMsg) emailMsg.value = '';
|
|
setTimeout(function(){ if (emailSec) emailSec.style.display = 'none'; }, 1800);
|
|
} else {
|
|
_showEmailStatus((res.d && res.d.error) || 'Could not send the email. Please try again.', '#ef4444');
|
|
}
|
|
})
|
|
.catch(function() {
|
|
emailSend.disabled = false;
|
|
emailSend.innerHTML = _orig;
|
|
_showEmailStatus('Network error — please try again.', '#ef4444');
|
|
});
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
|