js
Created: March 20, 2026 at 7:40 am
Expires: Never
Syntax: Plain Text
Permalink:
http://gingerbook.com/pastebin/index.php?id=u2cze8Zp
// ...existing code...
document.addEventListener('DOMContentLoaded', function () {
const gearBtns = document.querySelectorAll('.create-top-gear');
gearBtns.forEach(function(gearBtn) {
gearBtn.addEventListener('click', async function () {
try {
await window.enablePushNotifications();
showToast('Notifications enabled!');
} catch (err) {
showToast('Notification permission denied or error', true);
}
});
});
});
let selectedPlatform = 'instagram';
let currentGuideStep = 0;
let isInstagramGuide = false;
async function apiRequest(url, options = {}) {
const response = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
...options,
});
const payload = await response.json().catch(() => ({ ok: false, message: 'Invalid server response' }));
if (!response.ok || !payload.ok) {
throw new Error(payload.message || 'Request failed');
}
return payload.data;
// FIX: closing brace was missing — return was outside the function body
}
function updatePrimaryButton() {
const btn = document.getElementById('sharePresetBtn');
if (!btn) return;
btn.innerText = 'Share';
}
function showToast(message, isError = false) {
const toast = document.getElementById('toast');
if (!toast) return;
if (window.__toastHideTimer) {
clearTimeout(window.__toastHideTimer);
window.__toastHideTimer = null;
}
toast.textContent = message;
toast.classList.remove('hidden', 'toast-from-left', 'toast-from-right');
void toast.offsetWidth;
toast.classList.add(isError ? 'toast-from-left' : 'toast-from-right');
toast.classList.toggle('border-emerald-500/30', !isError);
toast.classList.toggle('bg-emerald-500/10', !isError);
toast.classList.toggle('text-emerald-200', !isError);
toast.classList.toggle('border-red-500/30', isError);
toast.classList.toggle('bg-red-500/10', isError);
toast.classList.toggle('text-red-200', isError);
window.__toastHideTimer = setTimeout(() => {
toast.classList.add('hidden');
toast.classList.remove('toast-from-left', 'toast-from-right');
window.__toastHideTimer = null;
}, 3200);
}
const ONBOARDING_AVATAR_STORAGE_KEY = 'ngl_onboarding_avatar_data_url';
const READ_VIEW_IDS_STORAGE_KEY = 'ngl_read_view_ids';
const FCM_TOKEN_STORAGE_KEY = 'ngl_fcm_token';
// FIX: Centralised avatar data-URL validator — prevents XSS via crafted data URLs
// injected into CSS / canvas through localStorage.
function isValidImageDataUrl(value) {
return typeof value === 'string' && /^data:image\/(png|jpeg|jpg|gif|webp);base64,/.test(value);
}
// FIX: blobToBase64 moved to module scope so it is accessible everywhere
// (was previously defined inside an event-handler, causing ReferenceError on reuse)
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
async function initFirebasePush() {
const firebaseConfig = window.APP_CONFIG?.firebase;
const viewerId = Number(window.APP_CONFIG?.viewerId || 0);
if (!firebaseConfig?.enabled || viewerId <= 0) return;
if (!('serviceWorker' in navigator) || !('Notification' in window)) return;
const hasRequiredWebConfig = [
firebaseConfig?.web?.apiKey,
firebaseConfig?.web?.projectId,
firebaseConfig?.web?.messagingSenderId,
firebaseConfig?.web?.appId,
firebaseConfig?.vapidPublicKey,
].every((value) => String(value || '').trim() !== '');
if (!hasRequiredWebConfig) return;
let firebaseAppModule;
let firebaseMessagingModule;
const loadFirebaseModules = async () => {
if (firebaseAppModule && firebaseMessagingModule) {
return { firebaseAppModule, firebaseMessagingModule };
}
[firebaseAppModule, firebaseMessagingModule] = await Promise.all([
import('https://www.gstatic.com/firebasejs/10.13.2/firebase-app.js'),
import('https://www.gstatic.com/firebasejs/10.13.2/firebase-messaging.js'),
]);
return { firebaseAppModule, firebaseMessagingModule };
};
const registerTokenWithBackend = async (token) => {
await apiRequest(`${window.APP_CONFIG.baseUrl}/api/push/register-token`, {
method: 'POST',
body: JSON.stringify({ token, platform: 'web_twa_android' }),
});
};
let foregroundHandlerBound = false;
const syncFcmToken = async ({ requestPermission = false } = {}) => {
const currentPermission = Notification.permission;
if (requestPermission && currentPermission === 'default') {
const nextPermission = await Notification.requestPermission();
if (nextPermission !== 'granted') throw new Error('Notification permission not granted');
} else if (currentPermission !== 'granted') {
throw new Error('Notification permission not granted');
}
const base = String(window.APP_CONFIG?.baseUrl || '').replace(/\/$/, '');
const swUrl = `${base}/sw.js`;
const swScope = `${base}/`;
let swRegistration = await navigator.serviceWorker.getRegistration(swScope);
if (!swRegistration) {
swRegistration = await navigator.serviceWorker.register(swUrl, { scope: swScope });
}
const { firebaseAppModule: appMod, firebaseMessagingModule: msgMod } = await loadFirebaseModules();
const app = appMod.getApps().some((item) => item.name === 'ngl-firebase-app')
? appMod.getApp('ngl-firebase-app')
: appMod.initializeApp(firebaseConfig.web, 'ngl-firebase-app');
const messaging = msgMod.getMessaging(app);
if (!foregroundHandlerBound) {
msgMod.onMessage(messaging, (payload) => {
const title = payload?.notification?.title || 'New anonymous message';
const body = payload?.notification?.body || 'Someone sent you a message';
showToast(title, false);
if (document.visibilityState !== 'visible' && Notification.permission === 'granted') {
new Notification(title, {
body,
icon: `${window.APP_CONFIG.baseUrl}/assets/icons/icon-192.png`,
});
}
});
foregroundHandlerBound = true;
}
const token = await msgMod.getToken(messaging, {
vapidKey: firebaseConfig.vapidPublicKey,
serviceWorkerRegistration: swRegistration,
});
if (!token) throw new Error('Unable to obtain FCM token');
const previousToken = localStorage.getItem(FCM_TOKEN_STORAGE_KEY) || '';
if (previousToken !== token) {
await registerTokenWithBackend(token);
localStorage.setItem(FCM_TOKEN_STORAGE_KEY, token);
}
return token;
};
window.enablePushNotifications = async () => {
const token = await syncFcmToken({ requestPermission: true });
showToast('Notifications enabled');
return token;
};
if (Notification.permission === 'granted') {
(async () => {
try {
await syncFcmToken({ requestPermission: false });
} catch {
// Keep app usable even if background token sync fails
}
})();
}
}
function readLocalArray(key) {
try {
const value = JSON.parse(localStorage.getItem(key) || '[]');
return Array.isArray(value) ? value : [];
} catch {
return [];
}
}
function setDotVisibility(dotElements, visible) {
dotElements.forEach((dot) => dot.classList.toggle('is-visible', visible));
}
function markViewAsRead(viewId) {
const normalizedId = String(viewId || '').trim();
if (!normalizedId) return;
const readIds = readLocalArray(READ_VIEW_IDS_STORAGE_KEY).map((id) => String(id));
if (!readIds.includes(normalizedId)) {
readIds.push(normalizedId);
localStorage.setItem(READ_VIEW_IDS_STORAGE_KEY, JSON.stringify(readIds));
}
}
function applyRecentViewReadState() {
const readSet = new Set(readLocalArray(READ_VIEW_IDS_STORAGE_KEY).map((id) => String(id)));
const viewItems = document.querySelectorAll('.recent-view-item[data-view-id]');
viewItems.forEach((item) => {
const viewId = String(item.dataset.viewId || '').trim();
const label = item.querySelector('[data-view-label]');
const badge = item.querySelector('[data-view-badge]');
const isRead = viewId !== '' && readSet.has(viewId);
item.classList.toggle('is-read', isRead);
if (badge) badge.classList.toggle('is-read', isRead);
if (label) label.textContent = isRead ? 'Seen' : 'New view!';
if (!item.dataset.readBound) {
item.addEventListener('click', () => markViewAsRead(viewId));
item.dataset.readBound = '1';
}
});
const viewDetailShell = document.querySelector('.view-detail-shell[data-view-id]');
if (viewDetailShell) {
markViewAsRead(String(viewDetailShell.dataset.viewId || '').trim());
syncHeaderNotificationDots();
}
}
function formatRelativeTimeFromTimestamp(timestampSeconds) {
const ts = Number(timestampSeconds || 0);
if (!Number.isFinite(ts) || ts <= 0) return '';
const nowSeconds = Math.floor(Date.now() / 1000);
const diff = Math.max(0, nowSeconds - ts);
if (diff < 60) return 'just now';
if (diff < 3600) return `${Math.max(1, Math.floor(diff / 60))} min ago`;
if (diff < 86400) return `${Math.max(1, Math.floor(diff / 3600))} hours ago`;
return `${Math.max(1, Math.floor(diff / 86400))} days ago`;
}
function parseSqlDateTimeToUnix(datetimeValue) {
const raw = String(datetimeValue || '').trim();
if (!raw) return null;
const match = raw.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})(?::(\d{2}))?$/);
if (!match) return null;
const date = new Date(
Number(match[1]),
Number(match[2]) - 1,
Number(match[3]),
Number(match[4]),
Number(match[5]),
Number(match[6] || 0)
);
const timeMs = date.getTime();
return Number.isFinite(timeMs) ? Math.floor(timeMs / 1000) : null;
}
function startRecentViewTimeTicker() {
const timeNodes = Array.from(document.querySelectorAll('.recent-view-item[data-created-ts] [data-view-time]'));
if (!timeNodes.length) return;
const render = () => {
timeNodes.forEach((timeNode) => {
const item = timeNode.closest('.recent-view-item[data-created-ts]');
if (!item) return;
const fromRawDate = parseSqlDateTimeToUnix(item.dataset.createdAt);
const baseTs = fromRawDate ?? Number(item.dataset.createdTs || 0);
const nextLabel = formatRelativeTimeFromTimestamp(baseTs);
if (nextLabel) timeNode.textContent = nextLabel;
});
};
render();
if (window.__recentViewTimeTicker) clearInterval(window.__recentViewTimeTicker);
window.__recentViewTimeTicker = setInterval(render, 60000);
}
function startInboxMessageTimeTicker() {
const timeNodes = Array.from(document.querySelectorAll('.inbox-feed-item[data-created-at] [data-msg-time]'));
if (!timeNodes.length) return;
const render = () => {
timeNodes.forEach((timeNode) => {
const item = timeNode.closest('.inbox-feed-item[data-created-at]');
if (!item) return;
const parsedTs = parseSqlDateTimeToUnix(item.dataset.createdAt);
const nextLabel = formatRelativeTimeFromTimestamp(parsedTs ?? 0);
if (nextLabel) timeNode.textContent = nextLabel;
});
};
render();
if (window.__inboxMessageTimeTicker) clearInterval(window.__inboxMessageTimeTicker);
window.__inboxMessageTimeTicker = setInterval(render, 60000);
}
async function syncHeaderNotificationDots() {
const inboxDots = Array.from(document.querySelectorAll('[data-header-inbox-dot]'));
const eyeDots = Array.from(document.querySelectorAll('[data-header-eye-dot]'));
if (!inboxDots.length && !eyeDots.length) return;
let hasUnreadInbox = false;
let hasUnseenViews = false;
if (inboxDots.length) {
try {
const inboxData = await apiRequest(`${window.APP_CONFIG.baseUrl}/api/inbox`);
const readSet = new Set(readLocalArray('ngl_read_msgs').map((id) => String(id)));
const items = Array.isArray(inboxData)
? inboxData
: (Array.isArray(inboxData?.items) ? inboxData.items : []);
hasUnreadInbox = items.some((item) => !readSet.has(String(item?.id ?? '')));
} catch {
hasUnreadInbox = false;
}
}
if (eyeDots.length) {
try {
const analyticsData = await apiRequest(`${window.APP_CONFIG.baseUrl}/api/analytics`);
const totalViews = Number(analyticsData?.views ?? 0);
const safeTotalViews = Number.isFinite(totalViews) && totalViews >= 0 ? totalViews : 0;
const readViewCount = new Set(readLocalArray(READ_VIEW_IDS_STORAGE_KEY).map((id) => String(id))).size;
hasUnseenViews = safeTotalViews > readViewCount;
} catch {
hasUnseenViews = false;
}
}
setDotVisibility(inboxDots, hasUnreadInbox);
setDotVisibility(eyeDots, hasUnseenViews);
}
syncHeaderNotificationDots();
applyRecentViewReadState();
startRecentViewTimeTicker();
startInboxMessageTimeTicker();
initFirebasePush();
const twaOnboarding = document.getElementById('twaOnboarding');
if (twaOnboarding) {
const stepUsername = document.getElementById('twaStepUsername');
const stepAvatar = document.getElementById('twaStepAvatar');
const skipBtn = document.getElementById('twaSkipBtn');
const usernameForm = document.getElementById('twaCreateUserForm');
const usernameInput = usernameForm?.querySelector('input[name="username"]');
const avatarInput = document.getElementById('twaAvatarInput');
const avatarPickBtn = document.getElementById('twaAvatarPickBtn');
const avatarPreview = document.getElementById('twaAvatarPreview');
const avatarPlaceholder = document.getElementById('twaAvatarPlaceholder');
const avatarContinueBtn = document.getElementById('twaAvatarContinueBtn');
let activeStep = 'username';
let selectedAvatarDataUrl = '';
const setStep = (step) => {
activeStep = step;
if (stepUsername) stepUsername.classList.toggle('hidden', step !== 'username');
if (stepAvatar) stepAvatar.classList.toggle('hidden', step !== 'avatar');
if (skipBtn) skipBtn.classList.toggle('hidden', step !== 'avatar');
};
const goToCreateQuestion = () => {
// FIX: validate data URL before writing to localStorage to prevent XSS
if (selectedAvatarDataUrl && isValidImageDataUrl(selectedAvatarDataUrl)) {
localStorage.setItem(ONBOARDING_AVATAR_STORAGE_KEY, selectedAvatarDataUrl);
} else {
localStorage.removeItem(ONBOARDING_AVATAR_STORAGE_KEY);
}
location.href = `${window.APP_CONFIG.baseUrl}/create-question`;
};
if (usernameInput) {
usernameInput.addEventListener('input', () => {
const normalized = String(usernameInput.value || '').toLowerCase().replace(/\s+/g, '');
if (usernameInput.value !== normalized) usernameInput.value = normalized;
});
}
if (usernameForm) {
usernameForm.addEventListener('submit', async (event) => {
event.preventDefault();
const formData = new FormData(usernameForm);
const normalizedUsername = String(formData.get('username') || '').toLowerCase().trim();
if (usernameInput) usernameInput.value = normalizedUsername;
try {
await apiRequest(`${window.APP_CONFIG.baseUrl}/api/users`, {
method: 'POST',
body: JSON.stringify({ username: normalizedUsername }),
});
setStep('avatar');
} catch (error) {
showToast(error.message, true);
}
});
}
if (avatarPickBtn && avatarInput) {
avatarPickBtn.addEventListener('click', () => avatarInput.click());
}
if (avatarInput) {
avatarInput.addEventListener('change', () => {
const file = avatarInput.files && avatarInput.files[0];
if (!file) return;
// FIX: validate MIME type before reading to prevent non-image files being stored
if (!file.type.startsWith('image/')) {
showToast('Please select a valid image file', true);
return;
}
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result !== 'string') return;
selectedAvatarDataUrl = reader.result;
if (avatarPreview) {
avatarPreview.src = selectedAvatarDataUrl;
avatarPreview.classList.remove('hidden');
}
if (avatarPlaceholder) avatarPlaceholder.classList.add('hidden');
};
reader.readAsDataURL(file);
avatarInput.value = '';
});
}
if (avatarContinueBtn) {
avatarContinueBtn.addEventListener('click', () => goToCreateQuestion());
}
if (skipBtn) {
skipBtn.addEventListener('click', () => {
selectedAvatarDataUrl = '';
goToCreateQuestion();
});
}
setStep('username');
}
const createUserForm = document.getElementById('createUserForm');
if (createUserForm) {
const usernameInput = createUserForm.querySelector('input[name="username"]');
if (usernameInput) {
usernameInput.addEventListener('input', () => {
const normalized = String(usernameInput.value || '').toLowerCase().replace(/\s+/g, '');
if (usernameInput.value !== normalized) usernameInput.value = normalized;
});
}
createUserForm.addEventListener('submit', async (event) => {
event.preventDefault();
const formData = new FormData(createUserForm);
const normalizedUsername = String(formData.get('username') || '').toLowerCase().trim();
if (usernameInput) usernameInput.value = normalizedUsername;
try {
await apiRequest(`${window.APP_CONFIG.baseUrl}/api/users`, {
method: 'POST',
body: JSON.stringify({ username: normalizedUsername }),
});
location.href = `${window.APP_CONFIG.baseUrl}/create-question`;
} catch (error) {
showToast(error.message, true);
}
});
}
const createQuestionForm = document.getElementById('createQuestionForm');
// ================= INSTAGRAM GUIDE =================
const instaSteps = [
{ text: 'Click the sticker button', html: `<img src="/assets/insta-step1.png" style="width:100%;border-radius:12px;">` },
{ text: 'Tap on LINK sticker', html: `<img src="/assets/insta-step2.png" style="width:100%;border-radius:12px;">` },
{ text: 'Paste your copied link', html: `<img src="/assets/insta-step3.jpeg" style="width:100%;border-radius:12px;">` },
{ text: 'Share your story 🚀', html: `<img src="/assets/insta-step4.png" style="width:100%;border-radius:12px;">` },
];
function renderGuideStep() {
const step = instaSteps[currentGuideStep];
document.getElementById('guideText').innerText = step.text;
document.getElementById('guideVisual').innerHTML = step.html;
document.querySelectorAll('#shareGuideSteps .step').forEach((el, i) => {
el.classList.toggle('active', i === currentGuideStep);
});
}
function renderPlatformGuide(platform) {
const titleEl = document.querySelector('#shareGuideModal .share-guide-title');
const visualEl = document.getElementById('guideVisual');
const textEl = document.getElementById('guideText');
const primaryBtn = document.getElementById('shareGuideWhatsappBtn');
const stepsEl = document.getElementById('shareGuideSteps');
if (!visualEl || !textEl || !primaryBtn) return;
if (platform === 'instagram') {
titleEl.textContent = 'How to add the Link to your story';
stepsEl?.classList.remove('hidden');
renderGuideStep();
primaryBtn.innerText = currentGuideStep < instaSteps.length - 1 ? 'Next' : 'Share on Instagram';
updatePrimaryButton();
return;
}
stepsEl?.classList.add('hidden');
if (platform === 'snapchat') {
titleEl.textContent = 'Share to Snapchat';
visualEl.innerHTML = `
<div class="share-guide-visual-topbar">
<span>Stories</span>
<span class="share-guide-visual-search">Snapchat</span>
</div>
<div class="share-guide-visual-card">
<div class="share-guide-status-icon">
<span style="font-size: 26px;">👻</span>
</div>
<div class="share-guide-status-copy">
<strong>My Story</strong>
<span>Share the story image first</span>
</div>
<div class="share-guide-status-dots">•••</div>
</div>
<div class="share-guide-hand">☟</div>
`;
textEl.innerHTML = 'Open Snapchat and post the story image, then add or paste your copied link in the snap.';
primaryBtn.innerText = 'Share on Snapchat';
} else {
titleEl.textContent = 'Share to WhatsApp Status';
visualEl.innerHTML = `
<div class="share-guide-visual-topbar">
<span>Cancel</span>
<span class="share-guide-visual-search">Search</span>
</div>
<div class="share-guide-visual-card">
<div class="share-guide-status-icon">
<span class="share-guide-status-clock"></span>
</div>
<div class="share-guide-status-copy">
<strong>My Status</strong>
<span>My contacts</span>
</div>
<div class="share-guide-status-dots">•••</div>
</div>
<div class="share-guide-hand">☟</div>
`;
textEl.innerHTML = 'Choose <strong>My Status</strong> inside WhatsApp, then add the copied link on your story.';
primaryBtn.innerText = 'Share on WhatsApp!';
}
updatePrimaryButton();
}
function setSelectedSharePlatform(platform) {
selectedPlatform = ['instagram', 'snapchat', 'whatsapp'].includes(platform) ? platform : 'instagram';
isInstagramGuide = selectedPlatform === 'instagram';
currentGuideStep = 0;
document.querySelectorAll('#shareGuideModal .platform-tab').forEach((tab) => {
tab.classList.toggle('active', tab.dataset.platform === selectedPlatform);
});
renderPlatformGuide(selectedPlatform);
}
function resetGuideModalUI() {
setSelectedSharePlatform('instagram');
}
// ================= SHARED CANVAS HELPERS =================
// FIX: deduplicated — was defined twice with slightly different signatures
/**
* Draws a rounded rectangle path without filling/stroking.
* Call ctx.fill() / ctx.stroke() after as needed.
*/
function drawRoundedRectPath(ctx, x, y, w, h, r) {
const rr = Math.min(r, w / 2, h / 2);
ctx.beginPath();
ctx.moveTo(x + rr, y);
ctx.arcTo(x + w, y, x + w, y + h, rr);
ctx.arcTo(x + w, y + h, x, y + h, rr);
ctx.arcTo(x, y + h, x, y, rr);
ctx.arcTo(x, y, x + w, y, rr);
ctx.closePath();
}
function drawCoverImage(ctx, image, x, y, width, height) {
const scale = Math.max(width / image.width, height / image.height);
const drawWidth = image.width * scale;
const drawHeight = image.height * scale;
const offsetX = x + (width - drawWidth) / 2;
const offsetY = y + (height - drawHeight) / 2;
ctx.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
}
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const words = text.split(' ');
let line = '';
const lines = [];
words.forEach((word) => {
const testLine = line + word + ' ';
if (ctx.measureText(testLine).width > maxWidth) {
lines.push(line);
line = word + ' ';
} else {
line = testLine;
}
});
lines.push(line);
lines.forEach((l, i) => ctx.fillText(l.trim(), x, y + i * lineHeight));
}
function loadImageFromSource(src) {
return new Promise((resolve, reject) => {
if (!src) { reject(new Error('Missing image source')); return; }
const image = new Image();
image.crossOrigin = 'anonymous';
image.onload = () => resolve(image);
image.onerror = () => reject(new Error('Unable to load image'));
image.src = src;
});
}
// ================= CREATE QUESTION PAGE =================
if (createQuestionForm) {
const templateButtons = document.querySelectorAll('.template-card');
const templateRail = document.getElementById('templateCards');
const presetScrollPrev = document.getElementById('presetScrollPrev');
const presetScrollNext = document.getElementById('presetScrollNext');
const questionText = document.getElementById('questionText');
const templateKey = document.getElementById('templateKey');
const presetActions = document.getElementById('presetActions');
const customTemplateCard = document.getElementById('customTemplateCard');
const customCardQuestionInput = document.getElementById('customCardQuestionInput');
const customCardAvatar = document.getElementById('customCardAvatar');
const customPromptDice = document.getElementById('customPromptDice');
const avatarUploadInput = document.getElementById('avatarUploadInput');
const avatarSlots = document.querySelectorAll('[data-avatar-key]');
const copyPresetLinkBtn = document.getElementById('copyPresetLinkBtn');
const sharePresetBtn = document.getElementById('sharePresetBtn');
const shareGuideModal = document.getElementById('shareGuideModal');
const shareGuideWhatsappBtn = document.getElementById('shareGuideWhatsappBtn');
const shareGuideCloseEls = document.querySelectorAll('[data-share-guide-close]');
const sharePlatformTabs = document.querySelectorAll('#shareGuideModal .platform-tab');
const shareGuideSteps = document.querySelectorAll('#shareGuideSteps .step');
const presetPublicLinkText = document.getElementById('presetPublicLinkText');
let selectedTemplateValue = '';
let selectedIsCustom = true;
let generatedForCurrentTemplate = null;
let pendingShareGuideData = null;
const welcomeBotTriggeredQuestionIds = new Set();
const welcomeBotPendingQuestionIds = new Set();
// FIX: debounce guard for copy/share buttons to prevent duplicate API calls on rapid clicks
let actionInFlight = false;
let autoLinkRequestToken = 0;
let customDraftValue = '';
let pendingAvatarKey = 'custom';
const avatarDataByKey = {};
const predefinedPromptTexts = Array.from(templateButtons)
.filter((button) => button.dataset.custom !== '1')
.map((button) => String(button.dataset.value || '').trim())
.filter((text) => text.length > 0);
let customPromptIndex = predefinedPromptTexts.length > 0
? Math.floor(Math.random() * predefinedPromptTexts.length)
: 0;
let customPromptRotateTimer = null;
let customPromptFadeTimer = null;
const setCustomPromptPlaceholder = (withFade = false) => {
if (!customCardQuestionInput || predefinedPromptTexts.length === 0) return;
const nextPlaceholder = predefinedPromptTexts[customPromptIndex];
if (!withFade || String(customCardQuestionInput.value || '').trim() !== '') {
customCardQuestionInput.placeholder = nextPlaceholder;
return;
}
if (customPromptFadeTimer) { clearTimeout(customPromptFadeTimer); customPromptFadeTimer = null; }
customCardQuestionInput.classList.add('is-placeholder-fading');
customPromptFadeTimer = setTimeout(() => {
customCardQuestionInput.placeholder = nextPlaceholder;
customCardQuestionInput.classList.remove('is-placeholder-fading');
customPromptFadeTimer = null;
}, 140);
};
const rotateCustomPromptPlaceholder = () => {
if (!customCardQuestionInput || predefinedPromptTexts.length === 0) return;
if (String(customCardQuestionInput.value || '').trim() !== '') return;
customPromptIndex = (customPromptIndex + 1) % predefinedPromptTexts.length;
setCustomPromptPlaceholder(true);
};
const startCustomPromptRotation = () => {
if (!customCardQuestionInput || predefinedPromptTexts.length === 0) return;
if (customPromptRotateTimer) clearInterval(customPromptRotateTimer);
customPromptRotateTimer = setInterval(rotateCustomPromptPlaceholder, 2000);
customCardQuestionInput.classList.add('is-rotating');
};
const stopCustomPromptRotation = () => {
if (customPromptRotateTimer) { clearInterval(customPromptRotateTimer); customPromptRotateTimer = null; }
if (customPromptFadeTimer) { clearTimeout(customPromptFadeTimer); customPromptFadeTimer = null; }
if (customCardQuestionInput) {
customCardQuestionInput.classList.remove('is-placeholder-fading', 'is-rotating');
}
};
const applyAvatarToSlot = (key, dataUrl) => {
if (!key || !dataUrl) return;
document.querySelectorAll(`[data-avatar-key="${key}"]`).forEach((slot) => {
const img = slot.querySelector('[data-avatar-image]');
if (!img) return;
img.src = dataUrl;
slot.classList.add('has-image');
});
};
const updateCustomQuestion = (value) => {
if (!customCardQuestionInput) return;
const safeValue = String(value ?? '');
if (customCardQuestionInput.value !== safeValue) customCardQuestionInput.value = safeValue;
if (questionText) questionText.value = safeValue;
};
const selectCustomTemplate = () => {
selectedIsCustom = true;
if (templateKey) templateKey.value = '';
updateCustomQuestion(customDraftValue);
templateButtons.forEach((item) => item.classList.remove('is-selected'));
if (customTemplateCard) {
customTemplateCard.classList.add('is-selected');
updatePresetActionsTheme(customTemplateCard);
}
selectedTemplateValue = questionText?.value || '';
generatedForCurrentTemplate = null;
showPresetActions();
};
const hidePresetActions = () => {
if (presetActions) presetActions.classList.add('hidden');
if (presetPublicLinkText) presetPublicLinkText.textContent = 'Link will appear after generation';
};
const showPresetActions = () => {
if (presetActions) presetActions.classList.remove('hidden');
};
const updatePresetActionsTheme = (button) => {
if (!presetActions || !button) return;
const computed = getComputedStyle(button);
const start = computed.getPropertyValue('--card-start').trim() || '#14a7ff';
const mid = computed.getPropertyValue('--card-mid').trim() || start;
const end = computed.getPropertyValue('--card-end').trim() || '#5647f8';
presetActions.style.setProperty('--preset-card-start', start);
presetActions.style.setProperty('--preset-card-mid', mid);
presetActions.style.setProperty('--preset-card-end', end);
};
const templateSignature = () => `${templateKey?.value || ''}::${questionText?.value || ''}`;
const openShareGuide = (data) => {
if (!shareGuideModal) return;
resetGuideModalUI();
pendingShareGuideData = data;
shareGuideModal.classList.remove('hidden');
shareGuideModal.setAttribute('aria-hidden', 'false');
document.body.classList.add('share-guide-open');
};
const closeShareGuide = () => {
if (!shareGuideModal) return;
shareGuideModal.classList.add('hidden');
shareGuideModal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('share-guide-open');
};
async function shareInstagramStory(data) {
try {
await copyToClipboard(data.public_url);
await new Promise((resolve) => setTimeout(resolve, 300));
closeShareGuide();
const blob = await buildInstagramStoryBlob(data);
if (window.Capacitor?.Plugins?.InstagramShare) {
try {
await syncSpecificStoryImageToServer(data, blob, 'instagram-story.png');
await window.Capacitor.Plugins.InstagramShare.shareToStory({
imageUrl: data.story_image_url || '',
contentUrl: data.public_url || 'https://pingxo.com/app',
});
showToast('Instagram Story share started!');
} catch (error) {
console.error('InstagramShare plugin error:', error);
showToast('Instagram share failed: ' + (error?.message || 'Unknown error'), true);
// Fallback: show download prompt, do NOT redirect
if (blob) {
// Show modal or download image
showDownloadModal(blob, data.public_url);
}
}
} else {
// Fallback for web
const url = URL.createObjectURL(blob);
window.open(url, '_blank');
showToast('Download image -> open Instagram -> add to story');
}
setTimeout(() => showToast('Link copied - tap sticker -> paste'), 800);
triggerWelcomeBotOnce(data.question_id);
} catch (error) {
console.error('shareInstagramStory error:', error);
showToast('Something went wrong: ' + (error?.message || 'Unknown error'), true);
}
}
const refreshLinkForSelection = ({ autoGeneratePreset = true } = {}) => {
const questionValue = String(questionText?.value || '').trim();
if (!presetPublicLinkText) return;
if (!questionValue) {
presetPublicLinkText.textContent = 'Type your own question';
return;
}
if (selectedIsCustom) {
const signature = templateSignature();
presetPublicLinkText.textContent =
generatedForCurrentTemplate && generatedForCurrentTemplate.signature === signature
? generatedForCurrentTemplate.data.public_url
: 'Tap Copy Link to generate';
return;
}
if (!autoGeneratePreset) {
presetPublicLinkText.textContent = 'Tap Copy Link to generate';
return;
}
const token = ++autoLinkRequestToken;
presetPublicLinkText.textContent = 'Updating link...';
ensureQuestionGenerated()
.then((data) => { if (token === autoLinkRequestToken) presetPublicLinkText.textContent = data.public_url; })
.catch(() => { if (token === autoLinkRequestToken) presetPublicLinkText.textContent = 'Tap Copy Link to generate'; });
};
const clearTemplateSelection = () => {
selectedIsCustom = false;
if (templateKey) templateKey.value = '';
templateButtons.forEach((item) => item.classList.remove('is-selected'));
generatedForCurrentTemplate = null;
showPresetActions();
};
if (customTemplateCard) {
customTemplateCard.addEventListener('click', () => {
selectCustomTemplate();
refreshLinkForSelection();
if (customCardQuestionInput) customCardQuestionInput.focus();
});
}
templateButtons.forEach((button) => {
button.addEventListener('click', () => {
if (button.dataset.custom === '1') {
selectCustomTemplate();
refreshLinkForSelection();
if (customCardQuestionInput) customCardQuestionInput.focus();
return;
}
selectPresetTemplate(button, { autoGenerateLink: true });
if (questionText) questionText.focus();
});
});
const selectPresetTemplate = (button, { autoGenerateLink = true } = {}) => {
if (!button || button.dataset.custom === '1') return;
customDraftValue = customCardQuestionInput?.value || customDraftValue;
questionText.value = button.dataset.value || '';
selectedTemplateValue = button.dataset.value || '';
selectedIsCustom = false;
if (templateKey) templateKey.value = button.dataset.key || '';
templateButtons.forEach((item) => item.classList.remove('is-selected'));
button.classList.add('is-selected');
updatePresetActionsTheme(button);
generatedForCurrentTemplate = null;
showPresetActions();
refreshLinkForSelection({ autoGeneratePreset: autoGenerateLink });
};
if (questionText) {
questionText.addEventListener('input', () => {
if (selectedIsCustom) {
selectedTemplateValue = questionText.value;
updateCustomQuestion(questionText.value);
return;
}
if (questionText.value !== selectedTemplateValue) clearTemplateSelection();
});
}
if (customCardQuestionInput) {
setCustomPromptPlaceholder();
startCustomPromptRotation();
customCardQuestionInput.addEventListener('focus', () => {
selectCustomTemplate();
refreshLinkForSelection();
});
customCardQuestionInput.addEventListener('input', () => {
const text = customCardQuestionInput.value || '';
if (String(text).trim() !== '') stopCustomPromptRotation();
else startCustomPromptRotation();
const capped = text.length > 255 ? text.slice(0, 255) : text;
if (capped !== text) customCardQuestionInput.value = capped;
if (questionText) questionText.value = capped;
customDraftValue = capped;
selectedTemplateValue = capped;
selectCustomTemplate();
refreshLinkForSelection();
});
}
if (customPromptDice && customCardQuestionInput) {
customPromptDice.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (predefinedPromptTexts.length === 0) return;
customPromptIndex = (customPromptIndex + 1) % predefinedPromptTexts.length;
const nextPrompt = predefinedPromptTexts[customPromptIndex];
customCardQuestionInput.value = nextPrompt;
customCardQuestionInput.dispatchEvent(new Event('input', { bubbles: true }));
customCardQuestionInput.focus();
stopCustomPromptRotation();
});
}
avatarSlots.forEach((slot) => {
slot.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
pendingAvatarKey = slot.getAttribute('data-avatar-key') || 'custom';
const parentCard = slot.closest('.template-card');
if (parentCard) parentCard.click();
if (avatarUploadInput) avatarUploadInput.click();
});
});
if (avatarUploadInput) {
avatarUploadInput.addEventListener('change', () => {
const file = avatarUploadInput.files && avatarUploadInput.files[0];
if (!file) return;
// FIX: validate MIME type before reading
if (!file.type.startsWith('image/')) {
showToast('Please select a valid image file', true);
return;
}
const reader = new FileReader();
reader.onload = () => {
if (typeof reader.result !== 'string') return;
const uploadedAvatarDataUrl = reader.result;
// FIX: validate data URL before applying to CSS / canvas
if (!isValidImageDataUrl(uploadedAvatarDataUrl)) return;
const allAvatarKeys = new Set(
Array.from(document.querySelectorAll('[data-avatar-key]'))
.map((el) => el.getAttribute('data-avatar-key') || '')
.filter(Boolean)
);
allAvatarKeys.forEach((key) => {
avatarDataByKey[key] = uploadedAvatarDataUrl;
applyAvatarToSlot(key, uploadedAvatarDataUrl);
});
localStorage.setItem(ONBOARDING_AVATAR_STORAGE_KEY, uploadedAvatarDataUrl);
generatedForCurrentTemplate = null;
if (customCardAvatar) customCardAvatar.src = uploadedAvatarDataUrl;
if (customTemplateCard) {
customTemplateCard.style.setProperty('--custom-avatar-bg', `url('${uploadedAvatarDataUrl}')`);
customTemplateCard.classList.add('has-avatar-bg');
}
pendingAvatarKey = 'custom';
selectCustomTemplate();
refreshLinkForSelection();
};
reader.readAsDataURL(file);
avatarUploadInput.value = '';
});
}
const onboardingAvatarDataUrl = localStorage.getItem(ONBOARDING_AVATAR_STORAGE_KEY) || '';
// FIX: validate before applying stored avatar to CSS / canvas
if (isValidImageDataUrl(onboardingAvatarDataUrl)) {
const allAvatarKeys = new Set(
Array.from(document.querySelectorAll('[data-avatar-key]'))
.map((el) => el.getAttribute('data-avatar-key') || '')
.filter(Boolean)
);
allAvatarKeys.forEach((key) => {
avatarDataByKey[key] = onboardingAvatarDataUrl;
applyAvatarToSlot(key, onboardingAvatarDataUrl);
});
if (customCardAvatar) customCardAvatar.src = onboardingAvatarDataUrl;
if (customTemplateCard) {
customTemplateCard.style.setProperty('--custom-avatar-bg', `url('${onboardingAvatarDataUrl}')`);
customTemplateCard.classList.add('has-avatar-bg');
}
pendingAvatarKey = 'custom';
selectCustomTemplate();
refreshLinkForSelection();
}
showPresetActions();
customDraftValue = customCardQuestionInput?.value || '';
updateCustomQuestion(questionText?.value || '');
refreshLinkForSelection();
if (templateRail) {
const step = 260;
const templateSnapDebounceMs = 70;
const templateSnapSmoothLockMs = 170;
let templateScrollSyncTimer = null;
let isSnappingTemplateRail = false;
const getClosestTemplateToCenter = () => {
if (!templateButtons.length) return null;
const railRect = templateRail.getBoundingClientRect();
const railCenterX = railRect.left + railRect.width / 2;
let closest = null;
let minDistance = Number.POSITIVE_INFINITY;
templateButtons.forEach((button) => {
const rect = button.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const distance = Math.abs(centerX - railCenterX);
if (distance < minDistance) { minDistance = distance; closest = button; }
});
return closest;
};
const syncSelectionWithRail = () => {
const closestButton = getClosestTemplateToCenter();
if (!closestButton || closestButton.classList.contains('is-selected')) return;
if (closestButton.dataset.custom === '1') {
selectCustomTemplate();
refreshLinkForSelection();
} else {
selectPresetTemplate(closestButton, { autoGenerateLink: true });
}
};
const snapRailToButton = (button, behavior = 'smooth') => {
if (!button) return;
const railRect = templateRail.getBoundingClientRect();
const buttonRect = button.getBoundingClientRect();
const delta = (buttonRect.left + buttonRect.width / 2) - (railRect.left + railRect.width / 2);
if (Math.abs(delta) < 1) return;
isSnappingTemplateRail = true;
templateRail.scrollTo({ left: templateRail.scrollLeft + delta, behavior });
setTimeout(() => { isSnappingTemplateRail = false; }, behavior === 'smooth' ? templateSnapSmoothLockMs : 0);
};
const scheduleRailSelectionSync = () => {
if (templateScrollSyncTimer) clearTimeout(templateScrollSyncTimer);
templateScrollSyncTimer = setTimeout(() => {
templateScrollSyncTimer = null;
snapRailToButton(getClosestTemplateToCenter(), 'smooth');
syncSelectionWithRail();
}, templateSnapDebounceMs);
};
const handleRailScroll = () => {
if (isSnappingTemplateRail) return;
if (isMobileRail() && isTouchingRail) return;
syncSelectionWithRail();
scheduleRailSelectionSync();
};
if (presetScrollPrev) {
presetScrollPrev.addEventListener('click', () => {
templateRail.scrollBy({ left: -step, behavior: 'smooth' });
scheduleRailSelectionSync();
});
}
if (presetScrollNext) {
presetScrollNext.addEventListener('click', () => {
templateRail.scrollBy({ left: step, behavior: 'smooth' });
scheduleRailSelectionSync();
});
}
const isMobileRail = () => window.matchMedia('(max-width: 768px)').matches;
const swipeProgressThreshold = 0.38;
const swipeVelocityThreshold = 0.65;
let touchStartX = 0;
let touchEndX = 0;
let touchStartTime = 0;
let touchStartIndex = 0;
let isTouchingRail = false;
const getSelectedCardIndex = () => {
const idx = Array.from(templateButtons).findIndex((b) => b.classList.contains('is-selected'));
return idx >= 0 ? idx : 0;
};
const getCenteredCardIndex = () => {
const closest = getClosestTemplateToCenter();
if (!closest) return getSelectedCardIndex();
const idx = Array.from(templateButtons).indexOf(closest);
return idx >= 0 ? idx : getSelectedCardIndex();
};
const scrollToCardByIndex = (index, behavior = 'smooth') => {
if (!templateButtons.length) return;
const safeIndex = Math.max(0, Math.min(templateButtons.length - 1, index));
const targetCard = templateButtons[safeIndex];
if (!targetCard) return;
snapRailToButton(targetCard, behavior);
if (targetCard.dataset.custom === '1') {
selectCustomTemplate();
refreshLinkForSelection();
} else {
selectPresetTemplate(targetCard, { autoGenerateLink: true });
}
};
const handleTouchStart = (event) => {
if (!isMobileRail() || !event.touches || event.touches.length === 0) return;
isTouchingRail = true;
touchStartX = event.touches[0].clientX;
touchEndX = touchStartX;
touchStartTime = performance.now();
touchStartIndex = getCenteredCardIndex();
};
const handleTouchMove = (event) => {
if (!isMobileRail() || !event.touches || event.touches.length === 0) return;
touchEndX = event.touches[0].clientX;
};
const handleTouchEnd = () => {
isTouchingRail = false;
if (!isMobileRail()) { scheduleRailSelectionSync(); return; }
const deltaX = touchEndX - touchStartX;
const dragDistance = Math.abs(deltaX);
const elapsedMs = Math.max(1, performance.now() - touchStartTime);
const swipeVelocity = dragDistance / elapsedMs;
const startCard = templateButtons[touchStartIndex] || null;
const cardWidth = startCard
? startCard.getBoundingClientRect().width
: Math.max(templateRail.clientWidth * 0.85, 1);
const requiredDistance = Math.max(54, cardWidth * swipeProgressThreshold);
const isIntentionalSwipe = dragDistance >= requiredDistance || swipeVelocity >= swipeVelocityThreshold;
if (!isIntentionalSwipe) { scrollToCardByIndex(touchStartIndex, 'smooth'); return; }
scrollToCardByIndex(deltaX < 0 ? touchStartIndex + 1 : touchStartIndex - 1);
};
templateRail.addEventListener('touchstart', handleTouchStart, { passive: true });
templateRail.addEventListener('touchmove', handleTouchMove, { passive: true });
templateRail.addEventListener('touchend', handleTouchEnd, { passive: true });
templateRail.addEventListener('touchcancel', handleTouchEnd, { passive: true });
templateRail.addEventListener('scroll', handleRailScroll, { passive: true });
if (isMobileRail()) scheduleRailSelectionSync();
}
async function createQuestionRequest() {
const formData = new FormData(createQuestionForm);
const questionValue = String(formData.get('question_text') || '').trim();
if (!questionValue) throw new Error('Type your question in the custom card');
const selectedAvatarKey = selectedIsCustom ? 'custom' : (templateKey?.value || '');
const selectedAvatarDataUrl = avatarDataByKey[selectedAvatarKey] || '';
const data = await apiRequest(`${window.APP_CONFIG.baseUrl}/api/questions`, {
method: 'POST',
body: JSON.stringify({
question_text: questionValue,
template_key: formData.get('template_key') || '',
avatar_data_url: selectedAvatarDataUrl,
}),
});
generatedForCurrentTemplate = { signature: templateSignature(), data };
if (presetPublicLinkText) presetPublicLinkText.textContent = data.public_url;
return data;
}
async function syncCanvasImageToServer(data) {
if (generatedForCurrentTemplate?.uploadedStoryImage) return;
try {
const blob = await buildTemplateShareBlob(data);
if (!blob) return;
const uploadForm = new FormData();
uploadForm.append('image', blob, 'story.png');
uploadForm.append('question_id', data.question_id);
const res = await fetch(`${window.APP_CONFIG.baseUrl}/api/questions/update-story-image`, {
method: 'POST',
body: uploadForm,
});
const result = await res.json();
if (result.ok && result.data?.story_image_url) {
data.story_image_url = result.data.story_image_url;
if (generatedForCurrentTemplate) {
generatedForCurrentTemplate.data.story_image_url = result.data.story_image_url;
generatedForCurrentTemplate.uploadedStoryImage = true;
}
}
} catch {
// Non-critical
}
}
async function syncSpecificStoryImageToServer(data, blob, filename = 'story.png') {
if (!blob) return '';
const uploadForm = new FormData();
uploadForm.append('image', blob, filename);
uploadForm.append('question_id', data.question_id);
const res = await fetch(`${window.APP_CONFIG.baseUrl}/api/questions/update-story-image`, {
method: 'POST',
body: uploadForm,
});
const result = await res.json().catch(() => ({ ok: false, message: 'Invalid server response' }));
if (!result.ok || !result.data?.story_image_url) {
throw new Error(result.message || 'Unable to upload story image');
}
data.story_image_url = result.data.story_image_url;
if (generatedForCurrentTemplate) {
generatedForCurrentTemplate.data.story_image_url = result.data.story_image_url;
}
return result.data.story_image_url;
}
createQuestionForm.addEventListener('submit', async (event) => {
event.preventDefault();
try {
await createQuestionRequest();
localStorage.removeItem(ONBOARDING_AVATAR_STORAGE_KEY);
showToast('Story question generated');
} catch (error) {
showToast(error.message, true);
}
});
async function ensureQuestionGenerated() {
const signature = templateSignature();
if (generatedForCurrentTemplate && generatedForCurrentTemplate.signature === signature) {
return generatedForCurrentTemplate.data;
}
return createQuestionRequest();
}
async function triggerWelcomeBotOnce(questionId) {
const normalizedQuestionId = Number(questionId || 0);
if (normalizedQuestionId <= 0) return;
if (welcomeBotTriggeredQuestionIds.has(normalizedQuestionId)) return;
if (welcomeBotPendingQuestionIds.has(normalizedQuestionId)) return;
welcomeBotPendingQuestionIds.add(normalizedQuestionId);
try {
const data = await apiRequest(`${window.APP_CONFIG.baseUrl}/api/bot/welcome-trigger`, {
method: 'POST',
body: JSON.stringify({ question_id: normalizedQuestionId }),
});
welcomeBotTriggeredQuestionIds.add(normalizedQuestionId);
if (data?.sent === true) showToast('Pingo sent a welcome message');
} catch {
// Silent — must not interrupt copy/share flow
} finally {
welcomeBotPendingQuestionIds.delete(normalizedQuestionId);
}
}
// FIX: share/copy buttons are guarded by actionInFlight to prevent duplicate API calls
if (copyPresetLinkBtn) {
copyPresetLinkBtn.addEventListener('click', async () => {
if (actionInFlight) return;
actionInFlight = true;
try {
const data = await ensureQuestionGenerated();
await syncCanvasImageToServer(data);
await copyToClipboard(data.public_url);
showToast('Link copied');
triggerWelcomeBotOnce(data.question_id);
} catch (error) {
showToast(error.message, true);
} finally {
actionInFlight = false;
}
});
}
if (sharePresetBtn) {
sharePresetBtn.addEventListener('click', async () => {
if (actionInFlight) return;
actionInFlight = true;
try {
const data = await ensureQuestionGenerated();
await syncCanvasImageToServer(data);
if (shareGuideModal) {
openShareGuide(data);
return;
}
await shareCreateQuestionOutput(data);
showToast('Share sheet opened');
triggerWelcomeBotOnce(data.question_id);
} catch (error) {
showToast(error.message, true);
} finally {
actionInFlight = false;
}
});
}
shareGuideCloseEls.forEach((element) => {
element.addEventListener('click', () => closeShareGuide());
});
sharePlatformTabs.forEach((tab) => {
tab.addEventListener('click', () => {
setSelectedSharePlatform(tab.dataset.platform || 'instagram');
});
});
shareGuideSteps.forEach((stepEl, index) => {
stepEl.style.cursor = 'pointer';
stepEl.addEventListener('click', () => {
if (selectedPlatform !== 'instagram') return;
currentGuideStep = index;
isInstagramGuide = true;
renderPlatformGuide(selectedPlatform);
});
});
if (shareGuideWhatsappBtn) {
shareGuideWhatsappBtn.addEventListener('click', async () => {
if (selectedPlatform === 'instagram') {
if (currentGuideStep < instaSteps.length - 1) {
currentGuideStep++;
renderPlatformGuide(selectedPlatform);
return;
}
if (!pendingShareGuideData) { closeShareGuide(); return; }
const data = pendingShareGuideData;
try {
await shareInstagramStory(data);
} catch (error) {
showToast(error.message, true);
}
return;
}
if (!pendingShareGuideData) { closeShareGuide(); return; }
const shareData = pendingShareGuideData;
try {
closeShareGuide();
if (selectedPlatform === 'whatsapp') {
try {
// Call your native plugin for WhatsApp image sharing
await WhatsAppShare.shareImage({
imageUrl: shareData.image_url, // Ensure this is a valid file or URL
caption: shareData.caption || '', // Optional caption
});
showToast('WhatsApp share opened');
triggerWelcomeBotOnce(shareData.question_id);
} catch (error) {
showToast(error.message, true);
}
} else {
await shareCreateQuestionOutput(shareData, selectedPlatform);
showToast('Share sheet opened');
triggerWelcomeBotOnce(shareData.question_id);
}
} catch (error) {
showToast(error.message, true);
}
});
}
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && shareGuideModal && !shareGuideModal.classList.contains('hidden')) {
closeShareGuide();
}
});
function getSelectedTemplateButton() {
return Array.from(templateButtons).find((button) => button.classList.contains('is-selected')) || null;
}
function showDownloadModal(blob, publicUrl) {
const url = URL.createObjectURL(blob);
const modalDiv = document.createElement('div');
modalDiv.innerHTML = `
<div style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;">
<div style="background:white;border-radius:16px;padding:24px;max-width:400px;width:100%;text-align:center;">
<h3 style="margin-bottom:16px;">Download Story Image</h3>
<img src="${url}" style="max-width:100%;border-radius:12px;margin-bottom:16px;" alt="Instagram Story"/>
<a href="${url}" download="instagram-story.png" style="display:block;margin-bottom:16px;" class="btn-primary">⬇️ Download Image</a>
<p style="font-size:13px;color:#555;margin-bottom:16px;">
After downloading, open Instagram → Your Story → Add Image.<br>
Tap the LINK sticker and paste your link:<br>
<span style="font-family:monospace;color:#6366f1;">${publicUrl}</span>
</p>
<button onclick="this.closest('div').parentNode.remove()" style="margin-top:8px;">Close</button>
</div>
</div>
`;
document.body.appendChild(modalDiv);
}
async function buildTemplateShareBlob(data, options = {}) {
const { forInstagram = false } = options;
const selectedButton = getSelectedTemplateButton();
if (!selectedButton) return null;
const selectedKey = selectedButton.dataset.custom === '1'
? 'custom'
: (templateKey?.value || selectedButton.dataset.key || '');
const uploadedAvatar = avatarDataByKey[selectedKey] || '';
const slotImage = selectedButton.querySelector('[data-avatar-image]');
const fallbackAvatar = slotImage?.getAttribute('src') || '';
const avatarSrc = uploadedAvatar || fallbackAvatar;
if (!avatarSrc) return null;
const canvas = document.createElement('canvas');
canvas.width = 1080;
canvas.height = 1920;
const ctx = canvas.getContext('2d');
if (!ctx) return null;
const cardX = 120, cardY = 455, cardW = 840, cardH = 585;
const topH = 300, cardRadius = 56, avatarSize = 156;
const avatarX = (canvas.width - avatarSize) / 2;
const avatarY = cardY - avatarSize / 2;
let avatarImage;
try {
avatarImage = await loadImageFromSource(avatarSrc);
} catch {
return null;
}
ctx.save();
ctx.filter = 'blur(26px)';
drawCoverImage(ctx, avatarImage, -80, -80, canvas.width + 160, canvas.height + 160);
ctx.restore();
const bgOverlay = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
bgOverlay.addColorStop(0, 'rgba(22, 24, 34, 0.20)');
bgOverlay.addColorStop(1, 'rgba(22, 24, 34, 0.34)');
ctx.fillStyle = bgOverlay;
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawRoundedRectPath(ctx, cardX, cardY, cardW, cardH, cardRadius);
ctx.fillStyle = '#ffffff';
ctx.fill();
ctx.save();
drawRoundedRectPath(ctx, cardX, cardY, cardW, cardH, cardRadius);
ctx.clip();
const start = getComputedStyle(selectedButton).getPropertyValue('--card-start').trim() || '#30b4ff';
const end = getComputedStyle(selectedButton).getPropertyValue('--card-end').trim() || '#5b4eff';
const topGradient = ctx.createLinearGradient(cardX, cardY, cardX + cardW, cardY + topH);
topGradient.addColorStop(0, start);
topGradient.addColorStop(1, end);
ctx.fillStyle = topGradient;
ctx.fillRect(cardX, cardY, cardW, topH);
ctx.restore();
const avatarRing = ctx.createLinearGradient(avatarX, avatarY, avatarX + avatarSize, avatarY + avatarSize);
avatarRing.addColorStop(0, '#49b6ff');
avatarRing.addColorStop(1, '#5961ff');
ctx.beginPath();
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2, 0, Math.PI * 2);
ctx.fillStyle = avatarRing;
ctx.fill();
ctx.save();
ctx.beginPath();
ctx.arc(avatarX + avatarSize / 2, avatarY + avatarSize / 2, avatarSize / 2 - 6, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
drawCoverImage(ctx, avatarImage, avatarX + 6, avatarY + 6, avatarSize - 12, avatarSize - 12);
ctx.restore();
const rawTitle = selectedButton.querySelector('.template-card-title')?.textContent || '';
const title = String(rawTitle).trim() || 'Confessions';
const rawQuestion = (data?.question_text || questionText?.value || selectedTemplateValue || selectedButton.dataset.value || '').trim();
const bodyText = rawQuestion || 'send me anonymous confessions';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '900 78px "Plus Jakarta Sans", "Arial Black", sans-serif';
ctx.lineJoin = 'round';
ctx.lineWidth = 12;
ctx.strokeStyle = 'rgba(15, 23, 42, 0.38)';
ctx.strokeText(title, canvas.width / 2, cardY + 195);
ctx.lineWidth = 7;
ctx.strokeStyle = '#ffffff';
ctx.strokeText(title, canvas.width / 2, cardY + 195);
ctx.fillStyle = '#f8fbff';
ctx.fillText(title, canvas.width / 2, cardY + 195);
ctx.fillStyle = '#0b0f1a';
ctx.font = '700 68px "Plus Jakarta Sans", "Inter", sans-serif';
const maxWidth = cardW - 110;
const words = bodyText.split(/\s+/).filter(Boolean);
const lines = [];
let current = '';
words.forEach((word) => {
const test = current ? `${current} ${word}` : word;
if (ctx.measureText(test).width <= maxWidth || current === '') {
current = test;
} else {
lines.push(current);
current = word;
}
});
if (current) lines.push(current);
const visibleLines = lines.slice(0, 2);
const lineHeight = 78;
const textStartY = cardY + topH + 120;
visibleLines.forEach((line, index) => ctx.fillText(line, canvas.width / 2, textStartY + index * lineHeight));
ctx.strokeStyle = 'rgba(255,255,255,0.92)';
ctx.lineWidth = 15;
ctx.lineCap = 'round';
const arrowY = 1560;
const drawArrow = (cx) => {
ctx.beginPath();
ctx.moveTo(cx, arrowY - 70);
ctx.lineTo(cx, arrowY + 38);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx - 38, arrowY + 6);
ctx.lineTo(cx, arrowY + 48);
ctx.lineTo(cx + 38, arrowY + 6);
ctx.stroke();
};
if (!forInstagram) {
drawArrow(330);
drawArrow(540);
drawArrow(750);
}
return new Promise((resolve) => canvas.toBlob((blob) => resolve(blob || null), 'image/png', 0.98));
}
async function buildInstagramStoryBlob(data) {
// 1. Get SAME base template (WhatsApp design)
const baseBlob = await buildTemplateShareBlob(data, { forInstagram: true });
if (!baseBlob) return null;
const baseImg = await loadImageFromSource(URL.createObjectURL(baseBlob));
const canvas = document.createElement('canvas');
canvas.width = 1080;
canvas.height = 1920;
const ctx = canvas.getContext('2d');
// Draw base image
ctx.drawImage(baseImg, 0, 0, canvas.width, canvas.height);
// ================= OVERLAY =================
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 🔵 LINK BOX
const boxWidth = 520;
const boxHeight = 120;
const boxX = (canvas.width - boxWidth) / 2;
const boxY = 1150;
ctx.fillStyle = '#ffffff';
drawRoundedRectPath(ctx, boxX, boxY, boxWidth, boxHeight, 30);
ctx.fill();
// Text
ctx.fillStyle = '#000';
ctx.font = 'bold 36px Arial';
ctx.fillText('Paste your PingXO.com', canvas.width / 2, boxY + 40);
ctx.fillStyle = '#007AFF';
ctx.font = 'bold 44px Arial';
ctx.fillText('link here', canvas.width / 2, boxY + 85);
// 🔵 ARROWS
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 12;
ctx.lineCap = 'round';
const drawPremiumArrowUp = (x, y, i = 0) => {
// slight stagger for visual "motion feel"
const offset = (i % 2 === 0) ? -6 : 6;
ctx.save();
// Glow
ctx.shadowColor = 'rgba(255,255,255,0.9)';
ctx.shadowBlur = 25;
// Main line
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 14;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(x, y + offset);
ctx.lineTo(x, y - 85 + offset);
ctx.stroke();
// Arrow head (rounded + bold)
ctx.beginPath();
ctx.moveTo(x - 26, y - 60 + offset);
ctx.lineTo(x, y - 95 + offset);
ctx.lineTo(x + 26, y - 60 + offset);
ctx.stroke();
ctx.restore();
};
drawPremiumArrowUp(330, 1350, 0);
drawPremiumArrowUp(540, 1350, 1);
drawPremiumArrowUp(750, 1350, 2);
return new Promise((resolve) =>
canvas.toBlob((blob) => resolve(blob), 'image/png', 1)
);
}
async function shareCreateQuestionOutput(data, platform = 'whatsapp') {
const composedBlob = await buildTemplateShareBlob(data);
if (composedBlob) {
await shareBlobOrLink(composedBlob, data.public_url, platform);
return;
}
await shareImageOrLink(data.story_image_url, data.public_url, platform);
}
}
// ================= MESSAGE FORM =================
const messageForm = document.getElementById('messageForm');
if (messageForm) {
if (window.APP_CONFIG.requireCaptcha) loadCaptchaQuestion();
messageForm.addEventListener('submit', async (event) => {
event.preventDefault();
const formData = new FormData(messageForm);
try {
await apiRequest(`${window.APP_CONFIG.baseUrl}/api/messages`, {
method: 'POST',
body: JSON.stringify({
question_id: Number(formData.get('question_id')),
message: formData.get('message'),
captcha_answer: formData.get('captcha_answer') || '',
}),
});
messageForm.reset();
showToast('Reply sent anonymously');
if (window.APP_CONFIG.requireCaptcha) loadCaptchaQuestion();
} catch (error) {
showToast(error.message, true);
if (window.APP_CONFIG.requireCaptcha) loadCaptchaQuestion();
}
});
}
// ================= SHARE BUTTONS =================
document.querySelectorAll('[data-share-image]').forEach((button) => {
button.addEventListener('click', async () => {
const imageUrl = button.getAttribute('data-share-image');
if (!imageUrl) return;
await shareImageOrLink(imageUrl, imageUrl);
});
});
async function loadCaptchaQuestion() {
const target = document.getElementById('captchaQuestion');
if (!target) return;
try {
const data = await apiRequest(`${window.APP_CONFIG.baseUrl}/api/captcha`);
target.textContent = data.question;
} catch {
target.textContent = 'Captcha unavailable';
}
}
async function shareImageOrLink(imageUrl, fallbackUrl, platform = 'whatsapp') {
try {
const response = await fetch(imageUrl, { cache: 'no-store' });
const blob = await response.blob();
const file = new File([blob], 'story.png', { type: blob.type || 'image/png' });
if (navigator.canSh