V2 with UI
This commit is contained in:
289
index.html
289
index.html
@@ -3,69 +3,110 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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>
|
||||
: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 {
|
||||
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;
|
||||
line-height: 1.35;
|
||||
background: #f6f7f9;
|
||||
color: #111;
|
||||
line-height: 1.45;
|
||||
background: radial-gradient(1200px 600px at 10% 0%, var(--bg2), var(--bg));
|
||||
color: var(--text);
|
||||
}
|
||||
.container {
|
||||
max-width: 1100px;
|
||||
|
||||
.container {
|
||||
width: min(92vw, 1500px);
|
||||
max-width: 1500px;
|
||||
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 {
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.06);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
background: var(--card);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.muted { color: #666; font-size: 14px; }
|
||||
.muted { color: var(--muted); font-size: 14px; }
|
||||
.btn {
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,0.15);
|
||||
background: #fff;
|
||||
padding: 9px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--card) 92%, var(--primary) 8%);
|
||||
color: var(--text);
|
||||
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:active { transform: translateY(1px); }
|
||||
.btn:active { transform: scale(0.98); }
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 260px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,0.15);
|
||||
background: #fbfcfe;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--card) 92%, var(--bg) 8%);
|
||||
color: var(--text);
|
||||
}
|
||||
.grid2 { display: grid; grid-template-columns: 1fr; gap: 12px; }
|
||||
@media (min-width: 980px) { .grid2 { grid-template-columns: 1fr 1fr; } }
|
||||
.grid2 { display: grid; grid-template-columns: 1fr; gap: 14px; }
|
||||
.topbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
||||
.navRow { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.navBtn {
|
||||
padding: 8px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(0,0,0,0.12);
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--card) 92%, var(--primary2) 8%);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: transform 140ms ease, background 140ms ease, border-color 140ms ease;
|
||||
}
|
||||
.navBtn:hover { background: #f2f4f7; }
|
||||
.ok { color: #0a7; }
|
||||
.err { color: #b00; white-space: pre-wrap; }
|
||||
.small { font-size: 12px; color: #666; }
|
||||
.navBtn:hover {
|
||||
background: color-mix(in srgb, var(--card) 86%, var(--primary2) 14%);
|
||||
border-color: color-mix(in srgb, var(--border) 60%, var(--primary2) 40%);
|
||||
}
|
||||
.ok { color: var(--primary); }
|
||||
.err { color: #ef4444; white-space: pre-wrap; }
|
||||
.small { font-size: 12px; color: var(--muted); }
|
||||
label { font-weight: 600; }
|
||||
input[type="file"] { padding: 6px; }
|
||||
.ascGrid { display: grid; grid-template-columns: 1fr; gap: 12px; margin-top: 10px; }
|
||||
@media (min-width: 980px) { .ascGrid { grid-template-columns: 1fr 1fr; } }
|
||||
.ascGrid { display: grid; grid-template-columns: 1fr; gap: 14px; margin-top: 10px; }
|
||||
.singleCol { grid-template-columns: 1fr !important; }
|
||||
.ascBoxHeader { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom: 8px; }
|
||||
.ascLocale { font-weight: 700; }
|
||||
@@ -77,7 +118,7 @@
|
||||
|
||||
@keyframes uploadFlash {
|
||||
0% { background-color: #eafff6; }
|
||||
100% { background-color: #fff; }
|
||||
100% { background-color: var(--card); }
|
||||
}
|
||||
#uploadCard.flash {
|
||||
animation: uploadFlash 5s ease;
|
||||
@@ -91,7 +132,7 @@
|
||||
}
|
||||
.btn.copied {
|
||||
border-color: rgba(0,170,119,0.6);
|
||||
background: #eafff6;
|
||||
background: var(--successBg);
|
||||
}
|
||||
|
||||
/* Section highlight pulse animation */
|
||||
@@ -104,20 +145,79 @@
|
||||
.sectionHighlight {
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
|
||||
<div class="row">
|
||||
<div class="card" id="uploadCard">
|
||||
<div class="topbar">
|
||||
<div>
|
||||
<div class="dropZone" id="dropZone" title="Drag & drop a CSV here">
|
||||
<label for="file">CSV file</label><br/>
|
||||
<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>
|
||||
<label for="gameVersion">Game version</label><br/>
|
||||
@@ -126,10 +226,12 @@
|
||||
</div>
|
||||
<div style="flex:1"></div>
|
||||
<button class="btn" id="btnRefresh" disabled>Refresh</button>
|
||||
<button class="btn" id="btnTheme" type="button" title="Toggle dark mode">🌙</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding: 12px;">
|
||||
<div class="small muted" style="margin-bottom: 6px; font-weight: 600;">Jump to</div>
|
||||
<div class="navRow">
|
||||
<button class="navBtn" type="button" data-scroll="googleSection">Google</button>
|
||||
<button class="navBtn" type="button" data-scroll="appleSection">Apple</button>
|
||||
@@ -142,7 +244,7 @@
|
||||
<div class="card" id="googleSection">
|
||||
<div class="topbar" style="justify-content: space-between;">
|
||||
<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 <en-US>...</en-US></div>
|
||||
</div>
|
||||
<button class="btn" id="btnCopyGoogle" disabled><span class="btnLabel">Copy</span></button>
|
||||
@@ -151,12 +253,12 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<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 > TitleData > Primary Title Data > <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 class="topbar" style="justify-content: space-between;">
|
||||
@@ -167,7 +269,7 @@
|
||||
</div>
|
||||
|
||||
<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 id="playfabBoxes" class="ascGrid singleCol"></div>
|
||||
</div>
|
||||
@@ -189,6 +291,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toastHost" class="toastHost"></div>
|
||||
|
||||
<script>
|
||||
// --- Config: map Gridly column headers -> output tags (Google).
|
||||
// Use array-of-pairs so the same Gridly column can be emitted multiple times.
|
||||
@@ -261,6 +365,9 @@
|
||||
const ascBoxes = document.getElementById("ascBoxes");
|
||||
const btnCopyGoogle = document.getElementById("btnCopyGoogle");
|
||||
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 gameVersionInput = document.getElementById("gameVersion");
|
||||
const versionHelpEl = document.getElementById("versionHelp");
|
||||
@@ -269,6 +376,33 @@
|
||||
const btnCopyAdcap = document.getElementById("btnCopyAdcap");
|
||||
const adcapHelpEl = document.getElementById("adcapHelp");
|
||||
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() {
|
||||
outAdcap.value = "";
|
||||
btnCopyAdcap.disabled = true;
|
||||
@@ -402,8 +536,8 @@
|
||||
const ta = document.createElement("textarea");
|
||||
ta.readOnly = true;
|
||||
ta.value = title;
|
||||
ta.rows = 1;
|
||||
ta.style.minHeight = "40px";
|
||||
ta.rows = 3;
|
||||
ta.style.minHeight = "72px";
|
||||
|
||||
wrap.className = "fieldBox";
|
||||
wrap.appendChild(headerRow);
|
||||
@@ -458,11 +592,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(_msg, _type="") {
|
||||
// Status UI removed; keep function so existing calls don't error.
|
||||
function showToast(msg, type = "ok") {
|
||||
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();
|
||||
return c.endsWith(" chars") || c.endsWith(" chrs");
|
||||
}
|
||||
@@ -776,8 +922,10 @@
|
||||
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 = "";
|
||||
ascBoxes.innerHTML = "";
|
||||
btnCopyGoogle.disabled = true;
|
||||
@@ -789,7 +937,7 @@
|
||||
if (!file) return;
|
||||
|
||||
fileInfo.textContent = `${file.name} (${Math.round(file.size/1024)} KB)`;
|
||||
setStatus("Reading CSV...", "small");
|
||||
setStatus("Parsing CSV…", "ok");
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
@@ -802,7 +950,6 @@
|
||||
const headers = grid[0].map(h => String(h ?? "").trim());
|
||||
const dataRows = grid.slice(1);
|
||||
|
||||
// Convert to objects
|
||||
const objects = dataRows
|
||||
.filter(r => r.some(v => String(v ?? "").trim() !== ""))
|
||||
.map(r => {
|
||||
@@ -823,12 +970,10 @@
|
||||
|
||||
btnCopyGoogle.disabled = !google;
|
||||
|
||||
// ---- PlayFab output ----
|
||||
const version = (gameVersionInput.value || "").trim();
|
||||
renderPlayfabBoxes(rowObj, headers, version);
|
||||
renderAdcap(rowObj, headers, version);
|
||||
|
||||
// Update help text styling (non-blocking)
|
||||
if (version && !isValidVersion(version)) {
|
||||
versionHelpEl.textContent = "Invalid version format. Use x.xx.x (e.g. 6.51.0). PlayFab output hidden.";
|
||||
versionHelpEl.classList.remove("muted");
|
||||
@@ -843,14 +988,44 @@
|
||||
btnRefresh.disabled = false;
|
||||
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) {
|
||||
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 () => {
|
||||
try {
|
||||
await copyToClipboard(outGoogle.value);
|
||||
|
||||
Reference in New Issue
Block a user