// app.js — wizard logic, markdown generation, live preview const STEPS = ["Identity", "About", "Stack", "Socials", "Add-ons"]; const LS_KEY = "ghprofile.v2"; const defaultState = () => ({ name: "", username: "", tagline: "", tagline2: "", greeting: "Hello! I'm", headlineColor: "#a371f7", bio: "", working: "", learning: "", collab: "", help: "", ask: "", pronouns: "", fun: "", tech: {}, // { category: [names] } socials: {}, // { key: value } factOrder: ["working", "learning", "collab", "help", "ask", "pronouns", "fun"], socialOrder: ["linkedin", "x", "instagram", "tiktok", "youtube", "pinterest", "devto", "website", "email"], addons: { stats: true, langs: true, activity: true, quote: true }, statsHost: "", badgeStyle: "for-the-badge", accent: "#2ea043", // NEW CUSTOMIZATIONS: headerType: "transparent", // e.g., transparent, wave, rect, slice, soft headerFont: "Auto", // e.g., Auto, Montserrat, Pacifico, Bungee animateHeader: false // Toggle animations if supported by render engine }); // About fact fields (drag-reorderable) const ABOUT_FIELDS = { working: { emoji: "\uD83D\uDD2D", label: "Currently working on", lead: "I'm currently working on", placeholder: "a real-time collaboration engine" }, learning: { emoji: "\uD83C\uDF31", label: "Currently learning", lead: "I'm currently learning", placeholder: "Rust and distributed systems" }, collab: { emoji: "\uD83D\uDC6F", label: "Looking to collaborate on", lead: "I'm looking to collaborate on", placeholder: "open-source developer tools" }, help: { emoji: "\uD83E\uDD14", label: "Looking for help with", lead: "I'm looking for help with", placeholder: "scaling WebSocket infrastructure" }, ask: { emoji: "\uD83D\uDCAC", label: "Ask me about", lead: "Ask me about", placeholder: "React, Node.js, API design" }, pronouns: { emoji: "\uD83D\uDE04", label: "Pronouns", lead: "Pronouns:", placeholder: "she/her" }, fun: { emoji: "\u26A1", label: "Fun fact", lead: "Fun fact:", placeholder: "I once debugged from a mountaintop" }, }; // ─── color themes (accent palette) ─── const THEME_COLORS = [ { name: "GitHub Green", hex: "#2ea043" }, { name: "Ocean", hex: "#2f81f7" }, { name: "Indigo", hex: "#4f46e5" }, { name: "Violet", hex: "#8957e5" }, { name: "Purple", hex: "#a855f7" }, { name: "Magenta", hex: "#db61a2" }, { name: "Rose", hex: "#f43f5e" }, { name: "Ruby", hex: "#e5484d" }, { name: "Orange", hex: "#e36209" }, { name: "Amber", hex: "#d29922" }, { name: "Lime", hex: "#65a30d" }, { name: "Teal", hex: "#1f9c8f" }, { name: "Cyan", hex: "#0891b2" }, { name: "Slate", hex: "#5b6573" }, ]; let state = load(); let step = 0; let previewMode = "preview"; function load() { try { const raw = JSON.parse(localStorage.getItem(LS_KEY)); if (raw && typeof raw === "object") { const s = Object.assign(defaultState(), raw); s.addons = Object.assign(defaultState().addons, raw.addons || {}); delete s.addons.views; delete s.addons.trophies; // removed features delete s.reach; // ensure order arrays are present & complete (migration) const def = defaultState(); s.factOrder = mergeOrder(raw.factOrder, def.factOrder); s.socialOrder = mergeOrder(raw.socialOrder, def.socialOrder); return s; } } catch (e) {} return defaultState(); } function mergeOrder(saved, def) { const a = Array.isArray(saved) ? saved.filter((k) => def.includes(k)) : []; def.forEach((k) => { if (!a.includes(k)) a.push(k); }); return a; } function persist() { try { localStorage.setItem(LS_KEY, JSON.stringify(state)); } catch (e) {} } // ─── output-safety helpers ─────────────────────────────────────────────────── // User input flows into generated HTML/markdown that is later parsed with // innerHTML, so every user-derived value must be escaped before it is emitted. // escapeHtml: full escape for HTML *attribute* contexts (href, alt, ...). function escapeHtml(str) { return String(str == null ? "" : str) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // escapeMdText: lighter escape for markdown body text (keeps raw output clean // while still neutralizing raw-HTML injection). function escapeMdText(str) { return String(str == null ? "" : str) .replace(/&/g, "&") .replace(//g, ">"); } // Build a safe github.com profile URL from a (possibly empty) username. function ghProfileUrl(user) { return "https://github.com/" + encodeURIComponent((user || "your-username").trim()); } // ─── theme ─── function applyThemeIcon() { const dark = document.documentElement.getAttribute("data-theme") === "dark"; document.getElementById("themeBtn").innerHTML = dark ? '' : ''; } function toggleTheme() { const cur = document.documentElement.getAttribute("data-theme"); const next = cur === "dark" ? "light" : "dark"; document.documentElement.setAttribute("data-theme", next); try { localStorage.setItem("ghprofile.theme", next); } catch (e) {} applyThemeIcon(); render(); } (function initTheme() { try { const saved = localStorage.getItem("ghprofile.theme"); if (saved) document.documentElement.setAttribute("data-theme", saved); } catch (e) {} })(); // ─── accent color ─── function setAccent(hex) { state.accent = hex; const r = document.documentElement; r.style.setProperty("--accent", hex); r.style.setProperty("--green", hex); r.style.setProperty("--green-emph", `color-mix(in oklab, ${hex}, #000 18%)`); r.style.setProperty("--green-hover", `color-mix(in oklab, ${hex}, #000 4%)`); document.querySelectorAll(".swatch").forEach((s) => s.classList.toggle("on", s.dataset.hex && s.dataset.hex.toLowerCase() === hex.toLowerCase())); const ci = document.getElementById("customColor"); if (ci) ci.value = hex; persist(); render(); } function buildPalette() { const wrap = document.getElementById("swatches"); if (!wrap) return; wrap.innerHTML = ""; THEME_COLORS.forEach((c) => { const b = document.createElement("button"); b.className = "swatch"; b.dataset.hex = c.hex; b.style.background = c.hex; b.title = c.name; b.setAttribute("aria-label", c.name); b.innerHTML = ''; b.onclick = () => setAccent(c.hex); wrap.appendChild(b); }); } function togglePalette(force) { const pop = document.getElementById("palettePop"); const open = force === false ? false : (force === true ? true : pop.classList.contains("open") ? false : true); pop.classList.toggle("open", open); } document.addEventListener("click", (e) => { const pop = document.getElementById("palettePop"); if (pop && pop.classList.contains("open") && !pop.contains(e.target) && !e.target.closest("#paletteBtn")) { pop.classList.remove("open"); } }); // ─── stepper ─── function buildStepper() { const el = document.getElementById("stepper"); el.innerHTML = ""; STEPS.forEach((name, i) => { const item = document.createElement("div"); item.className = "stepper-item" + (i === step ? " active" : "") + (i < step ? " done" : ""); item.onclick = () => goStep(i); const done = i < step; item.innerHTML = `
`);
L.push(` `);
L.push(` `);
L.push(` `);
L.push(`
`);
L.push(` `);
L.push(`
`);
L.push(allTech.map(([n, cfg]) => ` `).join("\n"));
L.push(`
`); L.push(socialBadges.join("\n")); L.push(`
`); L.push(""); } // ───────── STATS ───────── const cardColors = `&title_color=${ac}&icon_color=${ac}&hide_border=true&bg_color=00000000`; const sText = th === "default" ? "1f2328" : "c9d1d9"; const sDate = th === "default" ? "656d76" : "8b949e"; // Your own github-readme-stats instance (reliable for all users of this app). const statsHost = normalizeHost(s.statsHost) || "github-readme-stats-five-sigma-99.vercel.app"; if (user && (a.stats || a.langs)) { L.push("### 📊 GitHub Stats"); L.push(""); L.push(``);
if (a.stats) L.push(` `);
if (a.langs) L.push(`
`);
L.push(`
`);
L.push(` `);
L.push(`
`);
L.push(` `);
L.push(`
⭐️ From ${escapeMdText(user || "your-username")}
`); return L.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n"; } // ─── render preview ─── function render() { const md = generate(); document.getElementById("previewRaw").textContent = md; const rendered = document.getElementById("previewRendered"); const hasUser = (state.username || "").trim(); if (!hasUser) { rendered.innerHTML = `" + escapeHtml(md) + ""; } } // ─── preview tabs ─── function setPreviewMode(mode) { previewMode = mode; document.getElementById("tabPreview").classList.toggle("on", mode === "preview"); document.getElementById("tabRaw").classList.toggle("on", mode === "raw"); document.getElementById("previewRendered").hidden = mode !== "preview"; document.getElementById("previewRaw").hidden = mode !== "raw"; } // ─── actions ─── function flash(msg) { const old = document.querySelector(".toast"); if (old) old.remove(); const t = document.createElement("div"); t.className = "toast"; t.innerHTML = `${msg}`; document.body.appendChild(t); setTimeout(() => t.remove(), 2200); } function copyMd() { navigator.clipboard.writeText(generate()).then(() => flash("Markdown copied to clipboard")).catch(() => flash("Copy failed — select the Markdown tab")); } function downloadMd() { const blob = new Blob([generate()], { type: "text/markdown" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "README.md"; a.click(); URL.revokeObjectURL(a.href); flash("README.md downloaded"); } function resetAll() { if (!confirm("Clear all fields and start over?")) return; doReset(); flash("Cleared"); } function doReset() { const accent = state.accent; state = defaultState(); state.accent = accent; // keep chosen theme persist(); document.querySelectorAll("[data-key]").forEach((i) => (i.value = "")); document.querySelectorAll("#socials input").forEach((i) => (i.value = "")); const gEl = document.getElementById("f_greeting"); if (gEl) gEl.value = state.greeting; buildCatalog(); buildSocials(); buildAboutFields(); buildAddons(); buildHeadlineColors(); goStep(0); render(); } // ─── finish view (in the left pane) ─── function showFinish() { const u = (state.username || "your-username").trim(); document.getElementById("finishUser").textContent = u; document.getElementById("finishRepo").textContent = u; document.getElementById("wizardView").style.display = "none"; document.getElementById("finishView").classList.add("show"); const pane = document.querySelector(".pane-form"); if (pane) pane.scrollTo({ top: 0 }); } function closeFinish() { document.getElementById("finishView").classList.remove("show"); document.getElementById("wizardView").style.display = ""; } function createAnother() { closeFinish(); if (confirm("Start a fresh profile? This clears the current one.")) { doReset(); flash("New profile started"); } } async function shareProfile() { const md = generate(); if (navigator.share) { try { await navigator.share({ title: "My GitHub profile README", text: md }); return; } catch (e) {} } try { await navigator.clipboard.writeText(md); flash("README copied — paste it anywhere to share"); } catch (e) { flash("Select the Markdown tab to copy"); } } function toggleMobilePreview(force) { const on = force === true ? true : !document.body.classList.contains("show-preview"); document.body.classList.toggle("show-preview", on); document.getElementById("mobilePreviewBtn").innerHTML = on ? ' Edit' : ' Preview'; } // ─── wire up DOM events (no inline handlers — CSP-friendly) ─── function on(id, evt, fn) { const el = document.getElementById(id); if (el) el.addEventListener(evt, fn); } function wireEvents() { on("mobilePreviewBtn", "click", () => toggleMobilePreview()); on("paletteBtn", "click", () => togglePalette()); on("customColor", "input", (e) => setAccent(e.target.value)); on("themeBtn", "click", toggleTheme); on("resetBtn", "click", resetAll); on("backBtn", "click", prev); on("nextBtn", "click", next); on("tabPreview", "click", () => setPreviewMode("preview")); on("tabRaw", "click", () => setPreviewMode("raw")); on("copyBtn", "click", copyMd); on("downloadBtn", "click", downloadMd); on("finishDownloadBtn", "click", downloadMd); on("finishCopyBtn", "click", copyMd); on("finishShareBtn", "click", shareProfile); on("finishBackBtn", "click", closeFinish); on("finishAnotherBtn", "click", createAnother); // hero: greeting text + animate toggle const g = document.getElementById("f_greeting"); if (g) { g.value = state.greeting == null ? "Hello! I'm" : state.greeting; g.addEventListener("input", () => { state.greeting = g.value; persist(); render(); }); } buildHeadlineColors(); } // swatches to pick the (separate) color for the moving headline lines const HL_COLORS = ["#a371f7", "#2f81f7", "#f778ba", "#3fb950", "#e3b341", "#ff7b72", "#56d4dd", "#ff9bce"]; function buildHeadlineColors() { const wrap = document.getElementById("hlSwatches"); if (!wrap) return; wrap.innerHTML = ""; HL_COLORS.forEach((c) => { const b = document.createElement("button"); b.type = "button"; b.className = "hl-swatch" + (String(state.headlineColor).toLowerCase() === c ? " on" : ""); b.style.background = c; b.style.color = c; b.title = c; b.innerHTML = ''; b.addEventListener("click", () => { state.headlineColor = c; buildHeadlineColors(); persist(); render(); }); wrap.appendChild(b); }); } // ─── init ─── if (window.marked) marked.setOptions({ breaks: true, gfm: true }); wireEvents(); buildStepper(); bindInputs(); buildAboutFields(); buildCatalog(); buildSocials(); buildAddons(); buildPalette(); applyThemeIcon(); setAccent(state.accent || "#2ea043"); goStep(0); render();