PageSourceSearch

https://emailmoney.ai/app.js

js emailmoney.ai collected 2026-10-05 14:21:41 UTC 11,388 bytes, 268 lines download raw bytes

1const state = {
2  status: null,
3  user: null,
4  project: null,
5  tools: [],
6  currentTool: null,
7  currentOutput: null
8};
9
10const $ = selector => document.querySelector(selector);
11const $$ = selector => [...document.querySelectorAll(selector)];
12
13async function api(url, options = {}) {
14  const response = await fetch(url, {
15    ...options,
16    headers: { "Content-Type": "application/json", ...(options.headers || {}) }
17  });
18  const data = await response.json().catch(() => ({}));
19  if (!response.ok) throw new Error(data.error || "Something went wrong.");
20  return data;
21}
22
23function toast(message, type = "success") {
24  const el = $("#toast");
25  el.textContent = message;
26  el.className = `toast show ${type}`;
27  clearTimeout(toast.timer);
28  toast.timer = setTimeout(() => el.className = "toast", 3200);
29}
30
31function escapeHtml(value) {
32  return String(value || "").replace(/[&<>'"]/g, char => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;" })[char]);
33}
34
35function formObject(form) {
36  return Object.fromEntries(new FormData(form).entries());
37}
38
39function openAuth(mode = "signup") {
40  $("#authModal").classList.remove("hidden");
41  $("#authModal").setAttribute("aria-hidden", "false");
42  switchAuth(mode);
43}
44
45function closeAuth() {
46  $("#authModal").classList.add("hidden");
47  $("#authModal").setAttribute("aria-hidden", "true");
48}
49
50function switchAuth(mode) {
51  $("#signupPanel").classList.toggle("hidden", mode !== "signup");
52  $("#loginPanel").classList.toggle("hidden", mode !== "login");
53}
54
55async function initialize() {
56  state.status = await api("/api/status");
57  $$("[data-checkout-link]").forEach(link => {
58    link.href = state.status.checkoutUrl || "#";
59  });
60  if (state.status.demoMode) $("#demoUpgrade").classList.remove("hidden");
61  try {
62    const data = await api("/api/me");
63    state.user = data.user;
64    state.project = data.project;
65    await enterApp();
66  } catch {
67    $("#marketing").classList.remove("hidden");
68  }
69}
70
71async function enterApp() {
72  const data = await api("/api/tools");
73  state.tools = data.tools;
74  $("#marketing").classList.add("hidden");
75  $("#authModal").classList.add("hidden");
76  $("#appShell").classList.remove("hidden");
77  $("#welcomeName").textContent = `Welcome, ${state.user.name.split(/\s+/)[0]}`;
78  $("#modeBadge").textContent = state.user.access === "ai_email_money" ? "AI Email Money" : "Free Builder";
79  updateUpgradeOffers();
80  renderNav();
81  updateProgress();
82  const firstIncomplete = state.tools.find(tool => tool.day && !state.project.completedDays.includes(tool.day)) || state.tools[0];
83  selectTool(firstIncomplete.id);
84}
85
86function updateUpgradeOffers() {
87  const hasPaidAccess = state.user?.access === "ai_email_money";
88  const hasCompletedDaySeven = state.project?.completedDays.includes(7);
89  $("#upgradeCard").classList.toggle("hidden", hasPaidAccess);
90  $("#completionUpgrade").classList.toggle("hidden", hasPaidAccess || !hasCompletedDaySeven);
91}
92
93function renderNav() {
94  const render = tier => state.tools.filter(tool => tool.tier === tier).map(tool => {
95    const complete = tool.day && state.project.completedDays.includes(tool.day);
96    const locked = tool.tier === "paid" && state.user.access !== "ai_email_money";
97    return `<button class="nav-tool ${complete ? "complete" : ""} ${locked ? "locked" : ""}" data-tool="${tool.id}">
98      <b>${escapeHtml(tool.icon)}</b><span>${escapeHtml(tool.title)}</span><i>${complete ? "✓" : locked ? "⌁" : ""}</i>
99    </button>`;
100  }).join("");
101  $("#freeNav").innerHTML = render("free");
102  $("#paidNav").innerHTML = render("paid");
103  $$("[data-tool]").forEach(button => button.addEventListener("click", () => selectTool(button.dataset.tool)));
104}
105
106function updateProgress() {
107  const complete = state.project.completedDays.length;
108  $("#progressText").textContent = `${complete} of 7 complete`;
109  $("#progressBar").style.width = `${complete / 7 * 100}%`;
110}
111
112function selectTool(id) {
113  const tool = state.tools.find(item => item.id === id);
114  if (!tool) return;
115  if (tool.tier === "paid" && state.user.access !== "ai_email_money") {
116    toast("This workspace unlocks with AI Email Money.", "error");
117    $("#upgradeCard").scrollIntoView({ behavior: "smooth" });
118    return;
119  }
120  state.currentTool = tool;
121  state.currentOutput = state.project.sections[tool.id] || null;
122  $$(".nav-tool").forEach(button => button.classList.toggle("active", button.dataset.tool === id));
123  $("#toolEyebrow").textContent = tool.day ? `DAY ${tool.day} · ${tool.eyebrow}` : tool.eyebrow;
124  $("#toolTitle").textContent = tool.title;
125  $("#toolDescription").textContent = tool.description;
126  $("#toolOutcome").textContent = tool.outcome;
127  $("#inputCount").textContent = `${tool.inputs.length} QUESTIONS`;
128  renderFields(tool);
129  renderResult(state.currentOutput);
130  $(".sidebar").classList.remove("open");
131  window.scrollTo({ top: 0, behavior: "smooth" });
132}
133
134function renderFields(tool) {
135  const savedContext = state.project.context?.[tool.id] || {};
136  $("#dynamicFields").innerHTML = tool.inputs.map(field => {
137    const saved = escapeHtml(savedContext[field.id] || "");
138    if (field.type === "textarea") return `<label>${escapeHtml(field.label)}<textarea name="${field.id}" placeholder="${escapeHtml(field.placeholder)}">${saved}</textarea></label>`;
139    if (field.type === "select") return `<label>${escapeHtml(field.label)}<select name="${field.id}">${field.options.map(option => `<option ${saved === escapeHtml(option) ? "selected" : ""}>${escapeHtml(option)}</option>`).join("")}</select></label>`;
140    return `<label>${escapeHtml(field.label)}<input name="${field.id}" value="${saved}" placeholder="${escapeHtml(field.placeholder || "")}"></label>`;
141  }).join("");
142}
143
144function renderResult(output) {
145  const panel = $("#resultPanel");
146  panel.classList.toggle("empty", !output);
147  $("#emptyResult").classList.toggle("hidden", Boolean(output));
148  $("#resultContent").classList.toggle("hidden", !output);
149  if (!output) return;
150  $("#resultTitle").textContent = output.title;
151  $("#resultSummary").textContent = output.summary;
152  $("#primaryOutput").value = output.primary_output;
153  $("#resultFields").innerHTML = output.fields.map(item => `<div class="result-field"><span>${escapeHtml(item.label)}</span><strong>${escapeHtml(item.value)}</strong></div>`).join("");
154  $("#alternatives").innerHTML = output.alternatives.map(item => `<li>${escapeHtml(item)}</li>`).join("");
155  $("#actionSteps").innerHTML = output.action_steps.map(item => `<li>${escapeHtml(item)}</li>`).join("");
156  const done = state.currentTool.day && state.project.completedDays.includes(state.currentTool.day);
157  $("#completeButton").classList.toggle("hidden", !state.currentTool.day);
158  $("#completeButton").innerHTML = done ? "Completed <span>✓</span>" : "Approve and Complete This Day <span>✓</span>";
159  $("#completeButton").disabled = Boolean(done);
160}
161
162async function handleGenerate(event) {
163  event.preventDefault();
164  const button = $("#generateButton");
165  button.disabled = true;
166  button.querySelector(".button-label").textContent = "Building your result…";
167  button.querySelector(".loader").classList.remove("hidden");
168  try {
169    const data = await api(`/api/generate/${state.currentTool.id}`, { method: "POST", body: JSON.stringify({ input: formObject(event.currentTarget) }) });
170    state.currentOutput = data.output;
171    state.project = data.project;
172    renderResult(data.output);
173    toast(`Result saved · ${data.usage.used} of ${data.usage.limit} generations used`);
174  } catch (error) {
175    toast(error.message, "error");
176  } finally {
177    button.disabled = false;
178    button.querySelector(".button-label").textContent = state.currentOutput ? "Generate a New Version" : "Generate My Result";
179    button.querySelector(".loader").classList.add("hidden");
180  }
181}
182
183async function completeDay() {
184  if (!state.currentTool.day) return;
185  try {
186    await saveEdits(false);
187    const data = await api(`/api/complete/${state.currentTool.day}`, { method: "POST" });
188    state.project = data.project;
189    updateProgress();
190    renderNav();
191    renderResult(state.currentOutput);
192    updateUpgradeOffers();
193    toast(state.currentTool.day === 7 ? "Your Newsletter Launch Package is ready." : `Day ${state.currentTool.day} complete.`);
194    if (state.currentTool.day === 7 && state.user.access !== "ai_email_money") {
195      $("#completionUpgrade").scrollIntoView({ behavior: "smooth", block: "start" });
196    }
197  } catch (error) {
198    toast(error.message, "error");
199  }
200}
201
202async function saveEdits(showConfirmation = true) {
203  if (!state.currentOutput) return;
204  const primaryOutput = $("#primaryOutput").value.trim();
205  if (primaryOutput === state.currentOutput.primary_output) return;
206  const data = await api(`/api/result/${state.currentTool.id}`, {
207    method: "PATCH",
208    body: JSON.stringify({ primary_output: primaryOutput })
209  });
210  state.currentOutput = data.output;
211  state.project = data.project;
212  if (showConfirmation) toast("Your changes were saved.");
213}
214
215async function submitAuth(event, mode) {
216  event.preventDefault();
217  const button = event.currentTarget.querySelector("button[type=submit]");
218  button.disabled = true;
219  try {
220    const data = await api(`/api/auth/${mode}`, { method: "POST", body: JSON.stringify(formObject(event.currentTarget)) });
221    state.user = data.user;
222    state.project = data.project;
223    await enterApp();
224  } catch (error) {
225    toast(error.message, "error");
226  } finally {
227    button.disabled = false;
228  }
229}
230
231$$('[data-open-auth]').forEach(button => button.addEventListener("click", () => openAuth(button.dataset.openAuth)));
232$$('[data-close-auth]').forEach(button => button.addEventListener("click", closeAuth));
233$$('[data-switch-auth]').forEach(button => button.addEventListener("click", () => switchAuth(button.dataset.switchAuth)));
234$("#signupForm").addEventListener("submit", event => submitAuth(event, "signup"));
235$("#loginForm").addEventListener("submit", event => submitAuth(event, "login"));
236$("#builderForm").addEventListener("submit", handleGenerate);
237$("#completeButton").addEventListener("click", completeDay);
238$("#copyResult").addEventListener("click", async () => {
239  await navigator.clipboard.writeText($("#primaryOutput").value || "");
240  toast("Copied to clipboard.");
241});
242$("#saveResult").addEventListener("click", () => saveEdits().catch(error => toast(error.message, "error")));
243$("#menuButton").addEventListener("click", () => $(".sidebar").classList.toggle("open"));
244$("#logoutButton").addEventListener("click", async () => { await api("/api/auth/logout", { method: "POST" }); location.reload(); });
245async function refreshAccess() {
246  try {
247    const data = await api("/api/access/refresh", { method: "POST" });
248    state.user = data.user;
249    if (data.accessFound) {
250      await enterApp();
251      toast(data.message);
252    } else {
253      updateUpgradeOffers();
254      toast(data.message, "error");
255    }
256  } catch (error) { toast(error.message, "error"); }
257}
258$$("[data-refresh-access]").forEach(button => button.addEventListener("click", refreshAccess));
259$("#demoUpgrade").addEventListener("click", async () => {
260  try {
261    const data = await api("/api/demo/upgrade", { method: "POST" });
262    state.user = data.user;
263    await enterApp();
264    toast("Paid workspace unlocked in demo mode.");
265  } catch (error) { toast(error.message, "error"); }
266});
267
268initialize().catch(error => toast(error.message, "error"));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.