V2 with UI

This commit is contained in:
Gurpreet Singh
2026-01-16 17:28:43 -05:00
parent 5cc4a3f917
commit 6df0dd70ed

View File

@@ -3,69 +3,110 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Release Notes Formatter</title> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@600;700&display=swap" rel="stylesheet">
<title>Release Notes Formatter</title>
<style> <style>
:root {
--bg: #f5f7fa;
--bg2: #eef2f7;
--card: #ffffff;
--text: #0f172a;
--muted: #64748b;
--border: rgba(2,6,23,0.10);
--shadow: 0 10px 26px rgba(2,6,23,0.07);
/* Palette */
--primary: #0D9488; /* teal */
--primary2: #146C94; /* deep blue */
--accent: #FF6B6B; /* coral */
--successBg: #eafff6;
}
[data-theme="dark"] {
--bg: #0b1220;
--bg2: #0f172a;
--card: #0f172a;
--text: #e5e7eb;
--muted: #94a3b8;
--border: rgba(226,232,240,0.14);
--shadow: 0 12px 34px rgba(0,0,0,0.40);
--successBg: rgba(13,148,136,0.16);
}
body { body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
margin: 0; margin: 0;
line-height: 1.35; line-height: 1.45;
background: #f6f7f9; background: radial-gradient(1200px 600px at 10% 0%, var(--bg2), var(--bg));
color: #111; color: var(--text);
} }
.container {
max-width: 1100px; .container {
width: min(92vw, 1500px);
max-width: 1500px;
margin: 0 auto; margin: 0 auto;
padding: 28px 28px 48px; padding: 26px 18px 56px;
} }
.row { display: grid; grid-template-columns: 1fr; gap: 14px; } .row { display: grid; grid-template-columns: 1fr; gap: 16px; }
.card { .card {
border: 1px solid rgba(0,0,0,0.08); border: 1px solid var(--border);
border-radius: 12px; border-radius: 14px;
padding: 14px; padding: 16px;
background: #fff; background: var(--card);
box-shadow: 0 1px 2px rgba(0,0,0,0.06); box-shadow: var(--shadow);
} }
.muted { color: #666; font-size: 14px; } .muted { color: var(--muted); font-size: 14px; }
.btn { .btn {
padding: 10px 12px; padding: 9px 12px;
border-radius: 10px; border-radius: 12px;
border: 1px solid rgba(0,0,0,0.15); border: 1px solid var(--border);
background: #fff; background: color-mix(in srgb, var(--card) 92%, var(--primary) 8%);
color: var(--text);
cursor: pointer; cursor: pointer;
transition: transform 140ms ease, background 140ms ease, border-color 140ms ease;
}
.btn:hover {
background: color-mix(in srgb, var(--card) 86%, var(--primary) 14%);
border-color: color-mix(in srgb, var(--border) 60%, var(--primary) 40%);
} }
.btn:hover { background: #f2f4f7; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; } .btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn:active { transform: translateY(1px); } .btn:active { transform: scale(0.98); }
textarea { textarea {
width: 100%; width: 100%;
min-height: 260px; min-height: 260px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 13px; font-size: 13px;
padding: 10px; padding: 12px;
border-radius: 10px; border-radius: 12px;
border: 1px solid rgba(0,0,0,0.15); border: 1px solid var(--border);
background: #fbfcfe; background: color-mix(in srgb, var(--card) 92%, var(--bg) 8%);
color: var(--text);
} }
.grid2 { display: grid; grid-template-columns: 1fr; gap: 12px; } .grid2 { display: grid; grid-template-columns: 1fr; gap: 14px; }
@media (min-width: 980px) { .grid2 { grid-template-columns: 1fr 1fr; } }
.topbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } .topbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.navRow { display: flex; flex-wrap: wrap; gap: 10px; } .navRow { display: flex; flex-wrap: wrap; gap: 10px; }
.navBtn { .navBtn {
padding: 8px 10px; padding: 8px 10px;
border-radius: 999px; border-radius: 999px;
border: 1px solid rgba(0,0,0,0.12); border: 1px solid var(--border);
background: #fff; background: color-mix(in srgb, var(--card) 92%, var(--primary2) 8%);
color: var(--text);
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 13px;
transition: transform 140ms ease, background 140ms ease, border-color 140ms ease;
} }
.navBtn:hover { background: #f2f4f7; } .navBtn:hover {
.ok { color: #0a7; } background: color-mix(in srgb, var(--card) 86%, var(--primary2) 14%);
.err { color: #b00; white-space: pre-wrap; } border-color: color-mix(in srgb, var(--border) 60%, var(--primary2) 40%);
.small { font-size: 12px; color: #666; } }
.ok { color: var(--primary); }
.err { color: #ef4444; white-space: pre-wrap; }
.small { font-size: 12px; color: var(--muted); }
label { font-weight: 600; } label { font-weight: 600; }
input[type="file"] { padding: 6px; } input[type="file"] { padding: 6px; }
.ascGrid { display: grid; grid-template-columns: 1fr; gap: 12px; margin-top: 10px; } .ascGrid { display: grid; grid-template-columns: 1fr; gap: 14px; margin-top: 10px; }
@media (min-width: 980px) { .ascGrid { grid-template-columns: 1fr 1fr; } }
.singleCol { grid-template-columns: 1fr !important; } .singleCol { grid-template-columns: 1fr !important; }
.ascBoxHeader { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom: 8px; } .ascBoxHeader { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom: 8px; }
.ascLocale { font-weight: 700; } .ascLocale { font-weight: 700; }
@@ -77,7 +118,7 @@
@keyframes uploadFlash { @keyframes uploadFlash {
0% { background-color: #eafff6; } 0% { background-color: #eafff6; }
100% { background-color: #fff; } 100% { background-color: var(--card); }
} }
#uploadCard.flash { #uploadCard.flash {
animation: uploadFlash 5s ease; animation: uploadFlash 5s ease;
@@ -91,7 +132,7 @@
} }
.btn.copied { .btn.copied {
border-color: rgba(0,170,119,0.6); border-color: rgba(0,170,119,0.6);
background: #eafff6; background: var(--successBg);
} }
/* Section highlight pulse animation */ /* Section highlight pulse animation */
@@ -104,20 +145,79 @@
.sectionHighlight { .sectionHighlight {
animation: sectionPulse 1.4s ease; animation: sectionPulse 1.4s ease;
} }
h2 { font-family: Poppins, Inter, system-ui, sans-serif; letter-spacing: -0.02em; }
h3 { letter-spacing: -0.01em; }
.dropZone {
border: 1.5px dashed color-mix(in srgb, var(--border) 60%, var(--primary) 40%);
border-radius: 14px;
padding: 12px;
background: color-mix(in srgb, var(--card) 92%, var(--bg) 8%);
transition: box-shadow 160ms ease, border-color 160ms ease, transform 160ms ease;
}
.dropZone.dragOver {
border-color: var(--primary);
box-shadow: 0 0 0 6px color-mix(in srgb, var(--primary) 20%, transparent 80%);
transform: translateY(-1px);
}
.hintPill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid var(--border);
background: color-mix(in srgb, var(--card) 92%, var(--primary2) 8%);
font-size: 12px;
color: var(--muted);
}
.toastHost {
position: fixed;
top: 16px;
right: 16px;
display: grid;
gap: 10px;
z-index: 9999;
pointer-events: none;
}
.toast {
pointer-events: none;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid var(--border);
background: color-mix(in srgb, var(--card) 88%, var(--bg) 12%);
color: var(--text);
box-shadow: var(--shadow);
opacity: 0;
transform: translateY(-6px);
animation: toastIn 180ms ease forwards;
max-width: min(380px, 88vw);
font-size: 13px;
}
.toast.ok { border-color: color-mix(in srgb, var(--border) 40%, var(--primary) 60%); }
.toast.err { border-color: color-mix(in srgb, var(--border) 40%, #ef4444 60%); }
@keyframes toastIn { to { opacity: 1; transform: translateY(0); } }
@keyframes toastOut { to { opacity: 0; transform: translateY(-6px); } }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h2 style="margin: 0 0 6px 0;">Release Notes Formatter</h2> <h2 style="margin: 0 0 6px 0;">🧾 Release Notes Formatter</h2>
<p class="muted" style="margin: 0 0 14px 0;">Upload a Gridly CSV export.</p> <p class="muted" style="margin: 0 0 14px 0;">Upload a Gridly CSV export.</p>
<div class="row"> <div class="row">
<div class="card" id="uploadCard"> <div class="card" id="uploadCard">
<div class="topbar"> <div class="topbar">
<div> <div class="dropZone" id="dropZone" title="Drag & drop a CSV here">
<label for="file">CSV file</label><br/> <label for="file">CSV file</label><br/>
<input id="file" type="file" accept=".csv,text/csv" /> <input id="file" type="file" accept=".csv,text/csv" />
<div id="fileInfo" class="small"></div> <div class="topbar" style="margin-top:8px; gap: 8px;">
<span class="hintPill">⬇️ Drag & drop CSV</span>
<span class="hintPill">or click to browse</span>
</div>
<div id="fileInfo" class="small" style="margin-top:8px;"></div>
</div> </div>
<div> <div>
<label for="gameVersion">Game version</label><br/> <label for="gameVersion">Game version</label><br/>
@@ -126,10 +226,12 @@
</div> </div>
<div style="flex:1"></div> <div style="flex:1"></div>
<button class="btn" id="btnRefresh" disabled>Refresh</button> <button class="btn" id="btnRefresh" disabled>Refresh</button>
<button class="btn" id="btnTheme" type="button" title="Toggle dark mode">🌙</button>
</div> </div>
</div> </div>
<div class="card" style="padding: 12px;"> <div class="card" style="padding: 12px;">
<div class="small muted" style="margin-bottom: 6px; font-weight: 600;">Jump to</div>
<div class="navRow"> <div class="navRow">
<button class="navBtn" type="button" data-scroll="googleSection">Google</button> <button class="navBtn" type="button" data-scroll="googleSection">Google</button>
<button class="navBtn" type="button" data-scroll="appleSection">Apple</button> <button class="navBtn" type="button" data-scroll="appleSection">Apple</button>
@@ -142,7 +244,7 @@
<div class="card" id="googleSection"> <div class="card" id="googleSection">
<div class="topbar" style="justify-content: space-between;"> <div class="topbar" style="justify-content: space-between;">
<div> <div>
<h3 style="margin:0 0 6px 0;">Google Play output</h3> <h3 style="margin:0 0 6px 0;">📦 Google Play output</h3>
<div class="muted">Tagged format like &lt;en-US&gt;...&lt;/en-US&gt;</div> <div class="muted">Tagged format like &lt;en-US&gt;...&lt;/en-US&gt;</div>
</div> </div>
<button class="btn" id="btnCopyGoogle" disabled><span class="btnLabel">Copy</span></button> <button class="btn" id="btnCopyGoogle" disabled><span class="btnLabel">Copy</span></button>
@@ -151,12 +253,12 @@
</div> </div>
<div class="card" id="appleSection"> <div class="card" id="appleSection">
<h3 style="margin:0 0 6px 0;">App Store Connect output</h3> <h3 style="margin:0 0 6px 0;">🍎 App Store Connect output</h3>
<div id="ascBoxes" class="ascGrid"></div> <div id="ascBoxes" class="ascGrid"></div>
</div> </div>
<div class="card" id="playfabAdcapSection"> <div class="card" id="playfabAdcapSection">
<h3 style="margin:0 0 6px 0;">PlayFab News output (AdCap only)</h3> <h3 style="margin:0 0 6px 0;">📰 PlayFab News output (AdCap only)</h3>
<div class="muted" style="margin-bottom: 10px;">Add this JSON to: AdCap &gt; TitleData &gt; Primary Title Data &gt; <code>newsfeed_mobile</code> (PlayFab).</div> <div class="muted" style="margin-bottom: 10px;">Add this JSON to: AdCap &gt; TitleData &gt; Primary Title Data &gt; <code>newsfeed_mobile</code> (PlayFab).</div>
<div id="adcapHelp" class="small muted" style="margin-bottom: 10px;">Requires a valid game version above. Uses English text only.</div> <div id="adcapHelp" class="small muted" style="margin-bottom: 10px;">Requires a valid game version above. Uses English text only.</div>
<div class="topbar" style="justify-content: space-between;"> <div class="topbar" style="justify-content: space-between;">
@@ -167,7 +269,7 @@
</div> </div>
<div class="card" id="playfabAdcomSection"> <div class="card" id="playfabAdcomSection">
<h3 style="margin:0 0 6px 0;">PlayFab News output (AdCom/AdAges)</h3> <h3 style="margin:0 0 6px 0;">🗞️ PlayFab News output (AdCom/AdAges)</h3>
<div class="muted">Per-language Title + Body. Requires a valid game version above.</div> <div class="muted">Per-language Title + Body. Requires a valid game version above.</div>
<div id="playfabBoxes" class="ascGrid singleCol"></div> <div id="playfabBoxes" class="ascGrid singleCol"></div>
</div> </div>
@@ -189,6 +291,8 @@
</div> </div>
</div> </div>
<div id="toastHost" class="toastHost"></div>
<script> <script>
// --- Config: map Gridly column headers -> output tags (Google). // --- Config: map Gridly column headers -> output tags (Google).
// Use array-of-pairs so the same Gridly column can be emitted multiple times. // Use array-of-pairs so the same Gridly column can be emitted multiple times.
@@ -261,6 +365,9 @@
const ascBoxes = document.getElementById("ascBoxes"); const ascBoxes = document.getElementById("ascBoxes");
const btnCopyGoogle = document.getElementById("btnCopyGoogle"); const btnCopyGoogle = document.getElementById("btnCopyGoogle");
const btnRefresh = document.getElementById("btnRefresh"); const btnRefresh = document.getElementById("btnRefresh");
const dropZone = document.getElementById("dropZone");
const btnTheme = document.getElementById("btnTheme");
const toastHost = document.getElementById("toastHost");
const skippedLocalesEl = document.getElementById("skippedLocales"); const skippedLocalesEl = document.getElementById("skippedLocales");
const gameVersionInput = document.getElementById("gameVersion"); const gameVersionInput = document.getElementById("gameVersion");
const versionHelpEl = document.getElementById("versionHelp"); const versionHelpEl = document.getElementById("versionHelp");
@@ -269,6 +376,33 @@
const btnCopyAdcap = document.getElementById("btnCopyAdcap"); const btnCopyAdcap = document.getElementById("btnCopyAdcap");
const adcapHelpEl = document.getElementById("adcapHelp"); const adcapHelpEl = document.getElementById("adcapHelp");
clearAdcap(); clearAdcap();
// Restore saved settings
(function initPrefs() {
try {
const savedVersion = localStorage.getItem("rn_gameVersion");
if (savedVersion && !gameVersionInput.value) gameVersionInput.value = savedVersion;
const theme = localStorage.getItem("rn_theme") || "light";
document.documentElement.setAttribute("data-theme", theme);
if (btnTheme) btnTheme.textContent = theme === "dark" ? "☀️" : "🌙";
} catch {}
})();
gameVersionInput.addEventListener("input", () => {
try { localStorage.setItem("rn_gameVersion", gameVersionInput.value || ""); } catch {}
});
if (btnTheme) {
btnTheme.addEventListener("click", () => {
const cur = document.documentElement.getAttribute("data-theme") || "light";
const next = cur === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", next);
btnTheme.textContent = next === "dark" ? "☀️" : "🌙";
try { localStorage.setItem("rn_theme", next); } catch {}
});
}
function clearAdcap() { function clearAdcap() {
outAdcap.value = ""; outAdcap.value = "";
btnCopyAdcap.disabled = true; btnCopyAdcap.disabled = true;
@@ -402,8 +536,8 @@
const ta = document.createElement("textarea"); const ta = document.createElement("textarea");
ta.readOnly = true; ta.readOnly = true;
ta.value = title; ta.value = title;
ta.rows = 1; ta.rows = 3;
ta.style.minHeight = "40px"; ta.style.minHeight = "72px";
wrap.className = "fieldBox"; wrap.className = "fieldBox";
wrap.appendChild(headerRow); wrap.appendChild(headerRow);
@@ -458,11 +592,23 @@
} }
} }
function setStatus(_msg, _type="") { function showToast(msg, type = "ok") {
// Status UI removed; keep function so existing calls don't error. if (!toastHost) return;
const t = document.createElement("div");
t.className = `toast ${type === "err" ? "err" : "ok"}`;
t.textContent = msg;
toastHost.appendChild(t);
window.setTimeout(() => {
t.style.animation = "toastOut 220ms ease forwards";
window.setTimeout(() => t.remove(), 240);
}, 2200);
} }
function isCharCountCol(name) { function setStatus(msg, type = "ok") {
// Back-compat: map to toast
showToast(String(msg || ""), type || "ok");
}function isCharCountCol(name) {
const c = (name || "").trim().toLowerCase(); const c = (name || "").trim().toLowerCase();
return c.endsWith(" chars") || c.endsWith(" chrs"); return c.endsWith(" chars") || c.endsWith(" chrs");
} }
@@ -776,8 +922,10 @@
setStatus(`✅ Generated output${recordId ? ` (Record ID: ${recordId})` : ""}`, "ok"); setStatus(`✅ Generated output${recordId ? ` (Record ID: ${recordId})` : ""}`, "ok");
} }
fileInput.addEventListener("change", async () => {
const file = fileInput.files?.[0]; let __uploadCount = 0;
async function processFile(file) {
outGoogle.value = ""; outGoogle.value = "";
ascBoxes.innerHTML = ""; ascBoxes.innerHTML = "";
btnCopyGoogle.disabled = true; btnCopyGoogle.disabled = true;
@@ -789,7 +937,7 @@
if (!file) return; if (!file) return;
fileInfo.textContent = `${file.name} (${Math.round(file.size/1024)} KB)`; fileInfo.textContent = `${file.name} (${Math.round(file.size/1024)} KB)`;
setStatus("Reading CSV...", "small"); setStatus("Parsing CSV", "ok");
try { try {
const text = await file.text(); const text = await file.text();
@@ -802,7 +950,6 @@
const headers = grid[0].map(h => String(h ?? "").trim()); const headers = grid[0].map(h => String(h ?? "").trim());
const dataRows = grid.slice(1); const dataRows = grid.slice(1);
// Convert to objects
const objects = dataRows const objects = dataRows
.filter(r => r.some(v => String(v ?? "").trim() !== "")) .filter(r => r.some(v => String(v ?? "").trim() !== ""))
.map(r => { .map(r => {
@@ -823,12 +970,10 @@
btnCopyGoogle.disabled = !google; btnCopyGoogle.disabled = !google;
// ---- PlayFab output ----
const version = (gameVersionInput.value || "").trim(); const version = (gameVersionInput.value || "").trim();
renderPlayfabBoxes(rowObj, headers, version); renderPlayfabBoxes(rowObj, headers, version);
renderAdcap(rowObj, headers, version); renderAdcap(rowObj, headers, version);
// Update help text styling (non-blocking)
if (version && !isValidVersion(version)) { if (version && !isValidVersion(version)) {
versionHelpEl.textContent = "Invalid version format. Use x.xx.x (e.g. 6.51.0). PlayFab output hidden."; versionHelpEl.textContent = "Invalid version format. Use x.xx.x (e.g. 6.51.0). PlayFab output hidden.";
versionHelpEl.classList.remove("muted"); versionHelpEl.classList.remove("muted");
@@ -843,14 +988,44 @@
btnRefresh.disabled = false; btnRefresh.disabled = false;
flashUploadCard(); flashUploadCard();
flashUploadCard(); __uploadCount += 1;
if (__uploadCount === 6) showToast("You rock 🤘", "ok");
setStatus(`✅ Generated output${recordId ? ` (Record ID: ${recordId})` : ""}`, "ok"); setStatus("✅ Outputs generated", "ok");
} catch (e) { } catch (e) {
setStatus(`${e.message || e}`, "err"); setStatus(`${e.message || e}`, "err");
} }
}
fileInput.addEventListener("change", async () => {
const file = fileInput.files?.[0];
await processFile(file);
}); });
// Drag & drop CSV
if (dropZone) {
const openPicker = () => fileInput && fileInput.click();
dropZone.addEventListener("click", openPicker);
dropZone.addEventListener("dragenter", (e) => {
e.preventDefault();
dropZone.classList.add("dragOver");
});
dropZone.addEventListener("dragover", (e) => {
e.preventDefault();
dropZone.classList.add("dragOver");
});
dropZone.addEventListener("dragleave", () => dropZone.classList.remove("dragOver"));
dropZone.addEventListener("drop", async (e) => {
e.preventDefault();
dropZone.classList.remove("dragOver");
const f = e.dataTransfer?.files?.[0];
if (!f) return;
await processFile(f);
});
}
btnCopyGoogle.addEventListener("click", async () => { btnCopyGoogle.addEventListener("click", async () => {
try { try {
await copyToClipboard(outGoogle.value); await copyToClipboard(outGoogle.value);