const API = "https://app.essaypro.com/api"; const orderId = crypto.randomUUID(); const authForms = new Map(); const quizzes = new Map(); const orderParams = {}; const attachedFiles = new Map(); const MAX_FILE_SIZE = 50 * 1024 * 1024; const funnel = { price_offer_variant: "first_sale", ai_form: false, input_start: false, auth_source: "", entry_placement: "", click_text: "", }; // frontId: order id of the quiz, the global orderId by default async function sendOrder(data, frontId = orderId) { if (Object.keys(data).length < 1) return false; try { const response = await fetch(`${API}/order/order-form/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ front_id: frontId, ...data }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${await response.text()}`); } const encodedCookie = encodeURIComponent( JSON.stringify({ context: "order-form", payload: { ...(funnel.ai_form && { type: "ai_form", auto_submit: true }), funnel: funnel.ai_form ? "hero_input" : "cta", front_id: frontId, }, }), ); document.cookie = `landing_context=${encodedCookie}; domain=.essaypro.com; path=/;`; return true; } catch (error) { console.error("Error", error.message); return false; } } async function uploadFile(file, frontID) { const createRes = await fetch( `${API}/unregistered/file/${orderId}/create-upload-link/`, { method: "POST", headers: { "Content-Type": "application/json", accept: "application/json", }, body: JSON.stringify({ content_type: file.type, file_name: file.name, front_id: frontID, }), }, ); if (!createRes.ok) { throw new Error(`Failed to create upload: ${createRes.status}`); } const { url, data } = await createRes.json(); const formData = new FormData(); Object.entries(data).forEach(([key, value]) => formData.append(key, value)); formData.append("file", file); const uploadRes = await fetch(url, { method: "POST", body: formData }); if (!uploadRes.ok) { throw new Error(`Failed to upload file: ${uploadRes.status}`); } } async function deleteFile(frontID) { const response = await fetch( `${API}/unregistered/file/${orderId}/${frontID}/`, { method: "DELETE", headers: { accept: "application/json" } }, ); if (!response.ok) { throw new Error(`Failed to delete file: ${response.status}`); } } function animateValue(from, to, duration, onUpdate) { from = Number(from); to = Number(to); return new Promise((resolve) => { const start = performance.now(); function update(currentTime) { const progress = Math.min((currentTime - start) / duration, 1); const ease = 1 - Math.pow(1 - progress, 3); onUpdate(from + (to - from) * ease); if (progress < 1) requestAnimationFrame(update); else resolve(); } requestAnimationFrame(update); }); } function formatFileSize(bytes) { if (bytes === 0) return "0 Bytes"; const units = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`; } function parseDuration(timeStr) { const [amountStr, unit = ""] = timeStr.trim().split(/\s+/); return { amount: parseInt(amountStr, 10), unit: unit.toLowerCase().replace(/s$/, ""), }; } function parseHours(timeStr) { const { amount, unit } = parseDuration(timeStr); switch (unit) { case "hour": return amount <= 3 ? 3 : amount; case "day": return amount * 24; case "week": return amount * 7 * 24; case "month": return amount * 30 * 24; default: return 3; } } function parseTimeString(timeStr) { const { amount, unit } = parseDuration(timeStr); const result = new Date(); switch (unit) { case "hour": if (amount > 3) { result.setHours(result.getHours() + amount); break; } default: result.setHours(result.getHours() + 3, result.getMinutes() + 10); break; case "day": result.setDate(result.getDate() + amount); break; case "week": result.setDate(result.getDate() + amount * 7); break; case "month": result.setMonth(result.getMonth() + amount); break; } return result.toISOString(); } function formatDeadlineName(hours, locale = "en-US") { const now = new Date(); const future = new Date(now.getTime() + hours * 3600e3); if (future.toDateString() !== now.toDateString()) { return future.toLocaleDateString(locale, { weekday: "short", month: "short", day: "numeric", }); } const timeString = future.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit", hour12: true, }); return `Today, ${timeString}`; } function getHoursUntil(dateString) { const targetDate = new Date(dateString); const now = new Date(); return Math.ceil((targetDate - now) / (1000 * 60 * 60)); } document.addEventListener("DOMContentLoaded", () => { document.querySelectorAll("[data-auth-form]").forEach((root, index) => { const instance = initAuthForm(root); const key = instance.name === "default" ? `default-${index}` : instance.name; authForms.set(key, instance); const quiz = initQuiz(root, { aiForm: key === "ai-quiz" }); if (quiz) quizzes.set(key, quiz); }); const initSlider = setupSlider(); function openForm(name, tab) { const form = authForms.get(name); if (!form) return; form.open(tab); if (name === "slider") initSlider(); else quizzes.get(name)?.open(); } // Auth triggers if (!getCookie("extra_metadata")) { document .querySelectorAll("a[href*='app.essaypro.com/auth/']") .forEach((button) => { button.addEventListener("click", (e) => { e.preventDefault(); loadRecaptcha(); const buttonText = button.textContent.trim().toLowerCase(); if (buttonText === "log in" || buttonText === "sign up") { funnel.ai_form = true; funnel.auth_source = "header"; openForm("slider", buttonText === "log in" ? 0 : 1); if (buttonText === "sign up") { const encodedCookie = encodeURIComponent( JSON.stringify({ context: "order-form", payload: { funnel: "sign_up" }, }), ); document.cookie = `landing_context=${encodedCookie}; domain=.essaypro.com; path=/;`; } window.dataLayer?.push({ event: "auth_modal_open", click_text: button.textContent, auth_source: funnel.auth_source, }); } else { funnel.ai_form = false; funnel.auth_source = "homepage_cta"; funnel.entry_placement = button.dataset.event; funnel.click_text = button.textContent; funnel.writer = button.dataset.writer; delete funnel.files_count; delete funnel.text_length; openForm("quiz", 1); } const inviteWriter = new URL(button.href).searchParams.get( "invite_writer", ); if (inviteWriter) orderParams.writer_id = parseInt(inviteWriter, 10); }); }); } setupHero(() => { funnel.ai_form = true; openForm("ai-quiz", 1); }); if (isMobileDevice()) { document.querySelectorAll(".r-pro__list").forEach((list) => { const clones = [...list.children].map((item) => { const clone = item.cloneNode(true); clone.setAttribute("aria-hidden", "true"); return clone; }); list.append(...clones); }); } }); // Hero input with file attachments function setupHero(onSubmit) { const smartBtn = document.querySelector(".smart__btn"); const smartInput = document.querySelector(".smart__input"); const smartError = document.querySelector(".smart__error"); const fileInput = document.querySelector(".smart input[type='file']"); const fileList = document.querySelector(".smart__list"); const fileTemplate = document.querySelector(".smart__file-template"); if (!smartBtn || !smartInput || !smartError) return; const FILES_ERROR = "One or more files weren’t uploaded. Supported: PDF, DOC(X), PPT(X), XLS(X), JPG, PNG, ZIP · up to 50 MB each."; function showError(text, type, details = {}) { smartError.textContent = text; smartError.style.display = "flex"; if (type) trackError(text, type, details); } function trackError(text, type, details = {}) { window.dataLayer?.push({ event: "hero_input_error", click_text: text.slice(0, 100), error_type: type, ...details, }); } function checkErrorFiles() { const hasErrorFiles = [...attachedFiles.values()].some( (entry) => entry.status === "error", ); if (hasErrorFiles) { showError(FILES_ERROR); } else { smartError.style.display = "none"; } return hasErrorFiles; } async function addFile(file) { const frontID = crypto.randomUUID().slice(0, 8); const tooLarge = file.size > MAX_FILE_SIZE; const entry = { status: "loading" }; attachedFiles.set(frontID, entry); const fileItem = fileTemplate.cloneNode(true); fileItem.classList.remove("smart__file-template"); fileItem.dataset.frontId = frontID; fileList.appendChild(fileItem); fileList.style.display = "flex"; const fileSize = fileItem.querySelector(".smart__file-weight"); const fileError = fileItem.querySelector(".smart__file-error"); const progress = fileItem.querySelector(".smart__progress"); const progressBar = fileItem.querySelector(".smart__progress-bar"); fileItem.querySelector(".smart__file-name").textContent = file.name; progress.style.display = "flex"; const upload = tooLarge ? Promise.resolve(false) : uploadFile(file, frontID).then( () => true, (error) => { console.error(error); return false; }, ); const [uploaded] = await Promise.all([ upload, animateValue(0, 100, 1000, (value) => { progressBar.style.width = `${value}%`; fileSize.textContent = `${Math.round(value)}%`; }), ]); if (!attachedFiles.has(frontID)) { if (uploaded) deleteFile(frontID).catch(console.error); return; } progress.style.display = "none"; fileSize.textContent = formatFileSize(file.size); if (uploaded) { entry.status = "loaded"; fileItem.classList.add("smart__file--loaded"); } else { entry.status = "error"; fileError.textContent = tooLarge ? "Upload failed — file is larger than 50 MB" : "Upload failed — please try again"; fileError.style.display = "block"; fileItem.classList.add("smart__file--error"); if (tooLarge) { trackError(FILES_ERROR, "file_too_large", { file_extension: file.name.includes(".") ? file.name.split(".").pop().toLowerCase() : "", file_size_mb: Math.round((file.size / 1024 / 1024) * 10) / 10, }); } else { trackError(FILES_ERROR, "upload_failed"); } } checkErrorFiles(); } smartInput.addEventListener("focus", () => { smartError.style.display = "none"; smartInput.classList.remove("smart__input--error"); }); smartInput.addEventListener("input", () => { if (!funnel.input_start) { window.dataLayer?.push({ event: "hero_input_started", placement: "hero_input", }); } funnel.input_start = true; }); fileInput?.addEventListener("change", async () => { const uploads = [...fileInput.files].map(addFile); if (!uploads.length) return; fileInput.value = ""; smartInput.classList.remove("smart__input--error"); await Promise.allSettled(uploads); }); fileList?.addEventListener("click", async (event) => { const deleteBtn = event.target.closest(".smart__file-clear"); if (!deleteBtn) return; const fileItem = deleteBtn.closest(".smart__file"); const frontID = fileItem.dataset.frontId; const entry = attachedFiles.get(frontID); attachedFiles.delete(frontID); fileItem.remove(); if (attachedFiles.size === 0) fileList.style.display = "none"; checkErrorFiles(); if (entry?.status === "loaded") { deleteFile(frontID).catch(console.error); } }); smartBtn.addEventListener("click", () => { funnel.auth_source = "hero_input"; funnel.entry_placement = smartBtn.dataset.event; funnel.click_text = "Write my paper"; delete funnel.writer; funnel.files_count = attachedFiles.size; funnel.text_length = smartInput.value.length; if (checkErrorFiles()) return; const instructions = smartInput.value.trim(); if (attachedFiles.size > 0) orderParams.has_file = true; else delete orderParams.has_file; if (instructions) orderParams.instructions = instructions; else delete orderParams.instructions; if (!instructions && !orderParams.has_file) { showError( "Add instructions or attach files so your writer knows what to do.", "empty_input", ); smartInput.classList.add("smart__input--error"); return; } onSubmit(); }); } // Quiz inside an auth form root function initQuiz(root, { aiForm = false } = {}) { const el = (selector) => root.querySelector(selector); const els = (selector) => root.querySelectorAll(selector); const quizCards = els(".ask__quiz-card"); if (!quizCards.length) return null; const quizNext = els(".ask__quiz-btn"); const quizPrev = els(".ask__quiz-nav"); const quizSmart = el(".ask__smart"); const quizText = el(".ask__smart-text"); const quizFiles = el(".ask__smart-count"); const quizFilesWrap = el(".ask__smart-files"); const quizServices = els(".ask__quiz-link"); const quizServiceName = el('[order="service"]'); const quizDeadline = els(".ask__quiz-date"); const quizDeadlineName = el('[order="deadline"]'); const quizOrderText = els(".ask__steps-text"); const quizOrderCalc = el(".ask__matching-calc"); const quizCustom = el(".ask__quiz-custom"); const quizCustomName = el(".ask__quiz-custom-label"); const quizCustomTime = el(".ask__quiz-time"); const quizCustomIcon = el(".ask__quiz-edit"); const pickr = el(".pickr"); // Answers of this quiz: service, deadline const params = {}; // AI quiz keeps the global orderId (hero files are uploaded with it), // the regular quiz gets a new one on every opening let quizOrderId = orderId; let orderSent = false; let customDate = null; let calendar = null; // Initial markup state, restored by resetQuiz() const initialState = { serviceName: quizServiceName?.textContent, deadlineName: quizDeadlineName?.textContent, customName: quizCustomName?.textContent, customTime: quizCustomTime?.textContent, customIcon: quizCustomIcon?.style.display, }; function resetQuiz() { setStep(0); delete params.service; delete params.deadline; customDate = null; calendar?.clear(); if (quizServiceName) quizServiceName.textContent = initialState.serviceName; if (quizDeadlineName) { quizDeadlineName.textContent = initialState.deadlineName; } if (quizCustomName) quizCustomName.textContent = initialState.customName; if (quizCustomTime) quizCustomTime.textContent = initialState.customTime; if (quizCustomIcon) quizCustomIcon.style.display = initialState.customIcon; quizDeadline.forEach((deadline) => deadline.classList.remove("active")); quizCustom?.classList.remove("active"); if (pickr) pickr.style.display = "none"; quizServices[0]?.click(); } function animateFound(found) { animateValue(0, found, 1500, (value) => { quizOrderCalc.textContent = Math.round(value); }); } function setStep(index) { quizCards.forEach((card, i) => card.classList.toggle("active", i === index), ); quizOrderText.forEach((text, i) => text.classList.toggle("active", i === index), ); } quizNext.forEach((btn, index) => { btn.addEventListener("click", () => { setStep(index + 1); if (index === 0) { params.service ??= 1; if (!params.deadline) { quizDeadlineName.textContent = "14 days"; quizDeadline[4]?.classList.add("active"); } animateFound(173); window.dataLayer?.push({ event: "landing_quiz_service_type", click_text: quizServiceName.textContent, }); } else if (index === 1) { params.deadline ??= parseTimeString("14 days"); submitOrder(); const deadlineHours = getHoursUntil(params.deadline); const eligibleFor8USD = quizServiceName.textContent === "Writing" && deadlineHours >= 336 && funnel.price_offer_variant === "first_sale"; window.dataLayer?.push({ event: "landing_quiz_deadline", click_text: quizDeadlineName.textContent, deadline_hours: deadlineHours, is_8usd_eligible: eligibleFor8USD, }); window.dataLayer?.push({ event: "auth_modal_open", click_text: "Quiz", auth_source: funnel.auth_source, }); } }); }); quizPrev.forEach((prev) => { prev.querySelectorAll(".ask__quiz-step.active").forEach((btn, index) => { btn.addEventListener("click", () => setStep(index)); }); }); quizServices.forEach((service) => { service.addEventListener("click", () => { params.service = parseInt(service.getAttribute("service"), 10); quizServiceName.textContent = service.textContent; animateFound(service.getAttribute("found")); }); }); quizDeadline.forEach((deadline) => { const deadlineLabel = deadline.querySelector(".ask__quiz-label").textContent; deadline.querySelector(".ask__quiz-value").textContent = formatDeadlineName( parseHours(deadlineLabel), ); deadline.addEventListener("click", () => { quizDeadline.forEach((d) => d.classList.remove("active")); quizCustom?.classList.remove("active"); deadline.classList.add("active"); quizDeadlineName.textContent = deadlineLabel; params.deadline = parseTimeString(deadlineLabel); }); }); // Custom deadline picker if (quizCustom && pickr) { calendar = flatpickr(pickr.querySelector(".pickr__top"), { inline: true, enableTime: true, minDate: new Date(Date.now() + 6 * 3600e3), // ≥ 6h from now minuteIncrement: 15, time_24hr: false, monthSelectorType: "dropdown", locale: { firstDayOfWeek: 1 }, disableMobile: true, onChange: (d) => (customDate = d[0]), }); // State before opening the picker, restored if it's closed without applying let prevActive = null; let prevCustomDate = null; quizCustom.addEventListener("click", () => { prevActive = [...quizDeadline, quizCustom].find((d) => d.classList.contains("active"), ); prevCustomDate = customDate; pickr.style.display = "flex"; quizDeadline.forEach((d) => d.classList.remove("active")); quizCustom.classList.add("active"); }); const resetCustomPickr = () => { pickr.style.display = "none"; customDate = prevCustomDate; if (customDate) calendar.setDate(customDate, false); else calendar.clear(false); quizCustom.classList.remove("active"); prevActive?.classList.add("active"); }; pickr .querySelectorAll(".pickr__close, .pickr__cancel") .forEach((btn) => btn.addEventListener("click", resetCustomPickr)); pickr.querySelector(".pickr__apply").addEventListener("click", () => { pickr.style.display = "none"; customDate ??= calendar.config.minDate; // Thu, Oct 8 quizCustomName.textContent = customDate.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", }); // 4:00 PM quizCustomTime.textContent = customDate.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, }); quizCustomIcon.style.display = "inline"; // Oct 8, 4:00 PM quizDeadlineName.textContent = customDate.toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit", hour12: true, }); params.deadline = customDate.toISOString(); }); } // Hero input data goes only with the AI quiz function submitOrder() { orderSent = true; const { instructions, has_file, ...shared } = orderParams; sendOrder({ ...(aiForm ? orderParams : shared), ...params }, quizOrderId); } function open() { if (!aiForm) { quizOrderId = crypto.randomUUID(); orderSent = false; resetQuiz(); } else if (orderSent) { submitOrder(); } if (!params.service && quizServices.length) { animateFound(quizServices[0].getAttribute("found")); } if (quizSmart) quizSmart.style.display = aiForm ? "flex" : "none"; if (quizText) { quizText.textContent = orderParams.instructions || ""; quizText.style.display = orderParams.instructions ? "-webkit-box" : "none"; } if (quizFiles) { const count = attachedFiles.size; quizFiles.textContent = `${count} file${count !== 1 ? "s" : ""} attached`; quizFilesWrap.style.display = count ? "flex" : "none"; } window.dataLayer?.push({ event: "landing_quiz_open", click_text: funnel.click_text, auth_source: funnel.auth_source, entry_placement: funnel.entry_placement, ...(funnel.writer ? { writer_expert: funnel.writer } : {}), ...(funnel.files_count ? { files_count: funnel.files_count } : {}), ...(funnel.text_length ? { text_length: funnel.text_length } : {}), }); } return { name: root.dataset.authForm, open }; } // Auth slider function setupSlider() { const slider = document.querySelector(".modal__slider"); const slides = [...document.querySelectorAll(".modal__slider-slide")]; const dots = [...document.querySelectorAll(".modal__slider-btn")]; const progresses = dots.map((dot) => dot.querySelector(".modal__slider-progress"), ); const DURATION = 4000; const SWIPE_DISTANCE = 50; let controller = null; let timer = null; function resetProgress(progress) { if (!progress) return; progress.style.transition = "none"; progress.style.width = "0%"; } function destroy() { controller?.abort(); clearTimeout(timer); slides.forEach((slide) => slide.classList.remove("active")); dots.forEach((dot) => dot.classList.remove("active")); progresses.forEach(resetProgress); } return function initSlider() { destroy(); if (!slides.length || !dots.length || !slider) return; controller = new AbortController(); const { signal } = controller; let current = 0; let autoplay = !window.matchMedia("(prefers-reduced-motion: reduce)") .matches; function showSlide(index) { slides.forEach((slide, i) => slide.classList.toggle("active", i === index), ); dots.forEach((dot, i) => dot.classList.toggle("active", i === index)); progresses.forEach(resetProgress); const progress = progresses[index]; if (progress) { if (autoplay) { requestAnimationFrame(() => { requestAnimationFrame(() => { progress.style.transition = `width ${DURATION}ms linear`; progress.style.width = "100%"; }); }); } else { progress.style.width = "100%"; } } current = index; } function schedule() { clearTimeout(timer); if (autoplay) timer = setTimeout(next, DURATION); } function next() { showSlide((current + 1) % slides.length); schedule(); } function prev() { showSlide((current - 1 + slides.length) % slides.length); schedule(); } function stopAutoplay() { autoplay = false; clearTimeout(timer); } dots.forEach((dot, index) => { dot.addEventListener( "click", () => { stopAutoplay(); showSlide(index); }, { signal }, ); }); if (window.matchMedia("(pointer: coarse)").matches) { let startX = 0; let startY = 0; let isSwiping = false; slider.addEventListener( "pointerdown", (e) => { if (e.pointerType !== "touch") return; slider.setPointerCapture(e.pointerId); startX = e.clientX; startY = e.clientY; isSwiping = true; }, { signal }, ); slider.addEventListener( "pointerup", (e) => { if (!isSwiping || e.pointerType !== "touch") return; isSwiping = false; const diffX = e.clientX - startX; const diffY = e.clientY - startY; if (Math.abs(diffY) > Math.abs(diffX)) return; if (Math.abs(diffX) < SWIPE_DISTANCE) return; stopAutoplay(); if (diffX < 0) next(); else prev(); }, { signal }, ); slider.addEventListener("pointercancel", () => (isSwiping = false), { signal, }); } showSlide(0); schedule(); }; }