diff --git a/app/page.tsx b/app/page.tsx
index 3c7303b..a8dd538 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,119 +1,12 @@
import type { Metadata } from "next";
-import { promises as fs } from "node:fs";
-import path from "node:path";
+import { redirect } from "next/navigation";
export const metadata: Metadata = {
- title: "AdConsole",
- description: "Amazon PPC teaching simulator",
+ title: "AdConsole - Amazon PPC Simulator",
+ description: "Amazon PPC teaching simulator - Migrating to React",
};
-async function getLegacyStats() {
- try {
- const filePath = path.join(process.cwd(), "public", "adconsole.html");
- const stat = await fs.stat(filePath);
- return {
- bytes: stat.size,
- kb: Math.round(stat.size / 1024),
- modified: stat.mtime.toLocaleString("sv-SE"),
- };
- } catch (e) {
- if (process.env.NODE_ENV !== "production") {
- console.warn("adconsole.html not found:", e);
- }
- return null;
- }
-}
-
export default async function Home() {
- const stats = await getLegacyStats();
-
- return (
-
-
-
-
-
-
-
-
- Next.js · App Router · TypeScript · Tailwind v4
-
- Run npm run dev to start
-
-
-
- );
+ // Redirect directly to the legacy tool
+ redirect("/adconsole.html");
}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 05e726d..f23fd1e 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -12,6 +12,8 @@ const eslintConfig = defineConfig([
"out/**",
"build/**",
"next-env.d.ts",
+ // Project-specific ignores:
+ ".ijfw/**",
]),
]);
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..1655fec 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,40 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ async headers() {
+ return [
+ {
+ source: "/(.*)",
+ headers: [
+ {
+ key: "Content-Security-Policy",
+ value: [
+ "default-src 'self'",
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net",
+ "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com",
+ "font-src 'self' https://cdn.jsdelivr.net https://fonts.gstatic.com",
+ "img-src 'self' data: https:",
+ "connect-src 'self'",
+ "frame-ancestors 'none'",
+ "form-action 'self'",
+ ].join("; "),
+ },
+ {
+ key: "X-Content-Type-Options",
+ value: "nosniff",
+ },
+ {
+ key: "X-Frame-Options",
+ value: "DENY",
+ },
+ {
+ key: "Referrer-Policy",
+ value: "strict-origin-when-cross-origin",
+ },
+ ],
+ },
+ ];
+ },
};
export default nextConfig;
diff --git a/public/adconsole.html b/public/adconsole.html
index 4c2de51..1bbac37 100644
--- a/public/adconsole.html
+++ b/public/adconsole.html
@@ -45,6 +45,7 @@
.tb-left{display:flex;align-items:center;gap:12px;min-width:0}
.tb-icon{width:38px;height:38px;border-radius:6px;display:grid;place-items:center;color:#D5D9D9;border:1px solid transparent;transition:.15s;position:relative;flex:none}
.tb-icon:hover{border-color:#485769;color:#fff;background:rgba(255,255,255,.05)}
+.tb-icon:focus-visible{outline:2px solid #FF9900;outline-offset:2px}
.logo{display:flex;align-items:center;gap:2px;padding:6px 8px;border-radius:6px;border:1px solid transparent;cursor:pointer;transition:.15s;text-decoration:none;flex:none}
.logo:hover{border-color:#485769}
.logo-t{color:#fff;font-family:Archivo;font-weight:700;font-size:19px;letter-spacing:-.3px;line-height:1;white-space:nowrap}
@@ -75,6 +76,7 @@
.nv-sec{font-family:Archivo;font-size:10.5px;font-weight:600;letter-spacing:1.1px;color:var(--faint);text-transform:uppercase;padding:14px 18px 6px}
.nv{display:flex;align-items:center;gap:11px;width:calc(100% - 14px);margin:1px 7px;padding:9.5px 11px;border-radius:6px;color:#3B4149;font-size:13.5px;font-weight:400;position:relative;transition:.13s;text-align:left;border-left:3px solid transparent}
.nv:hover{background:#F2F4F4;color:var(--ink)}
+.nv:focus-visible{outline:2px solid #FF9900;outline-offset:-2px}
.nv.on{background:#FEF3E7;border-left-color:var(--orange);font-weight:700;color:var(--ink)}
.nv svg{flex:none;color:#5B6570}
.nv.on svg{color:var(--orange-d)}
@@ -118,6 +120,7 @@
/* ---------- BUTTONS ---------- */
.btn{display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 14px;border-radius:7px;border:1px solid var(--border);background:#fff;color:var(--ink);font-size:13px;font-weight:700;font-family:Archivo;transition:.15s;white-space:nowrap}
.btn:hover{background:#F7FAFA;box-shadow:0 1px 3px rgba(15,17,17,.12)}
+.btn:focus-visible{outline:2px solid #FF9900;outline-offset:2px}
.btn:active{transform:translateY(1px)}
.btn-p{background:linear-gradient(180deg,#FFAD33,var(--orange));border-color:#E47911;color:var(--navy);position:relative;overflow:hidden}
.btn-p:hover{background:linear-gradient(180deg,#FFB84D,var(--orange-h))}
@@ -130,8 +133,8 @@
/* ---------- FORMS ---------- */
.inp,.sel,.tarea{height:34px;border:1px solid #8A939B;border-radius:7px;padding:0 10px;background:#fff;outline:none;transition:.13s;width:100%;min-width:0}
+.inp:focus,.sel:focus,.tarea:focus,.rsel:focus{outline:2px solid #FF9900;outline-offset:1px;border-color:var(--orange);box-shadow:0 0 0 3px rgba(255,153,0,.28)}
.tarea{height:auto;padding:8px 10px;resize:vertical}
-.inp:focus,.sel:focus,.tarea:focus{border-color:var(--orange);box-shadow:0 0 0 3px rgba(255,153,0,.28)}
.sel{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23565959' stroke-width='1.6' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}
label.fld{display:block;font-family:Archivo;font-size:12px;font-weight:600;color:#3B4149;margin:0 0 5px}
.fhint{font-size:11.5px;color:var(--faint);margin-top:4px}
@@ -546,6 +549,11 @@
+
+
@@ -577,7 +585,7 @@
-
+
Advertising
Dashboard
Campaigns 0
@@ -1081,10 +1089,20 @@ All ${LESSONS.length} missions compl
function perfChart(id,campId){
const cv=document.getElementById(id);if(!cv)return;
const b=rangeBounds();const s=seriesFor(campId,b);
+ // Helper: create segment colorizer for day-over-day comparison
+ const segColor = (ctx, colorUp, colorDown) => {
+ const p0 = ctx.p0.parsed.y;
+ const p1 = ctx.p1.parsed.y;
+ if (p0 == null || p1 == null) return colorUp;
+ return p1 >= p0 ? colorUp : colorDown;
+ };
const ds=[
- {label:'Sales',data:s.sal,borderColor:'#067D62',backgroundColor:'rgba(6,125,98,.10)',yAxisID:'y',fill:true},
- {label:'Spend',data:s.sp,borderColor:'#E47911',backgroundColor:'rgba(228,121,17,.08)',yAxisID:'y'},
- {label:'ACOS %',data:s.acos,borderColor:'#007185',borderDash:[5,4],yAxisID:'y1',pointStyle:'line'},
+ {label:'Sales',data:s.sal,borderColor:'#067D62',backgroundColor:'rgba(6,125,98,.10)',yAxisID:'y',fill:true,
+ segment:{borderColor:ctx=>segColor(ctx,'#067D62','#B12704')}},
+ {label:'Spend',data:s.sp,borderColor:'#E47911',backgroundColor:'rgba(228,121,17,.08)',yAxisID:'y',
+ segment:{borderColor:ctx=>segColor(ctx,'#E47911','#B12704')}},
+ {label:'ACOS %',data:s.acos,borderColor:'#007185',borderDash:[5,4],yAxisID:'y1',pointStyle:'line',
+ segment:{borderColor:ctx=>segColor(ctx,'#007185','#B12704')}},
];
charts[id]=new Chart(cv,{type:'line',data:{labels:s.labels,datasets:ds.map(d=>({...d,borderWidth:2,pointRadius:0,pointHitRadius:8,tension:.35,spanGaps:true}))},
options:{responsive:true,maintainAspectRatio:false,animation:{duration:250},interaction:{mode:'index',intersect:false},