Skip to content

index.js

40 lines · 15.1 KB · JavaScript
Raw
import { packageStorage } from "kody:runtime";
export default { async fetch(request) { if (new URL(request.url).pathname.endsWith("/api/complaints")) {
      if (request.method !== "POST") return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "content-type": "application/json", "allow": "POST" } });
      let data;
      try { data = await request.json(); } catch { return new Response(JSON.stringify({ error: "Please submit a valid form." }), { status: 400, headers: { "content-type": "application/json" } }); }
      const allowed = ["Bug or technical issue", "AI difficulty or gameplay", "Accessibility", "Suggestion", "Other"];
      const category = typeof data.category === "string" ? data.category.slice(0, 60) : "";
      const subject = typeof data.subject === "string" ? data.subject.trim().slice(0, 100) : "";
      const details = typeof data.details === "string" ? data.details.trim().slice(0, 2000) : "";
      const contact = typeof data.contact === "string" ? data.contact.trim().slice(0, 120) : "";
      if (!allowed.includes(category) || !subject || !details) return new Response(JSON.stringify({ error: "Please complete the topic, subject, and details." }), { status: 400, headers: { "content-type": "application/json" } });
      try {
        const db = packageStorage();
        const ticket = crypto.randomUUID();
        const createdAt = new Date().toISOString();
        const isBug = category === "Bug or technical issue";
        const reportText = (subject + " " + details).toLowerCase();
        const signals = [
          { tag: "gameplay", words: ["win", "draw", "turn", "square", "board", "move", "score"] },
          { tag: "ai", words: ["ai", "computer", "difficulty", "easy", "medium", "hard", "opponent"] },
          { tag: "interface", words: ["button", "screen", "layout", "mobile", "display", "click", "page"] },
          { tag: "performance", words: ["slow", "freeze", "hang", "load", "crash", "stuck"] }
        ];
        const matched = signals.filter(signal => signal.words.some(word => reportText.includes(word))).map(signal => signal.tag);
        const triage = isBug ? {
          state: "needs_code_review",
          labels: matched.length ? matched : ["uncategorized"],
          priority: /crash|security|private|data loss|can't submit|cannot submit/.test(reportText) ? "high" : "normal",
          nextStep: "Reproduce the report, prepare a minimal patch, run checks and regression tests, then request approval before publishing."
        } : { state: "not_a_bug_report", labels: [category.toLowerCase().replace(/[^a-z0-9]+/g, "-")], priority: "normal", nextStep: "Review feedback and decide whether to make a change." };
        const status = isBug ? "pending_review" : "received";
        const complaint = { ticket, category, subject, details, contact: contact || null, status, triage, createdAt };
        await db.set("complaint:" + ticket, JSON.stringify(complaint));
        await db.set("complaint-index:" + createdAt + ":" + ticket, ticket);
        return new Response(JSON.stringify({ ok: true, ticket, status, triage: { state: triage.state, labels: triage.labels, priority: triage.priority } }), { status: 201, headers: { "content-type": "application/json", "cache-control": "no-store" } });
      } catch (err) {
        return new Response(JSON.stringify({ error: "We couldn’t save your report right now. Please try again later." }), { status: 503, headers: { "content-type": "application/json", "cache-control": "no-store" } });
      }
    }
    return new Response("<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Tic-Tac-Toe</title><style>\n:root{color-scheme:dark;--bg:#10121a;--panel:#1a1d29;--line:#33384b;--text:#f4f5fb;--muted:#a4abc0;--x:#83b7ff;--o:#ffb86b;--accent:#a7f3d0}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:radial-gradient(ellipse at top,#252b42 0%,var(--bg) 60%);font-family:system-ui,sans-serif;color:var(--text)}main{width:min(100%,430px);text-align:center}.eyebrow{font-size:.75rem;letter-spacing:.22em;text-transform:uppercase;color:var(--accent);font-weight:800}h1{font-size:clamp(2.3rem,10vw,3.5rem);line-height:1.05;margin:10px 0 8px;letter-spacing:-.05em}.sub{margin:0 0 24px;color:var(--muted)}.modes{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px}.difficulty-row{display:grid;grid-template-columns:1fr 1fr;align-items:center;gap:10px;margin-bottom:14px;text-align:left;color:var(--muted);font-size:.9rem}.difficulty-row[hidden]{display:none}.difficulty-row select{width:100%;text-align:left}.mode-btn.active{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}.scoreboard{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}.score{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:12px}.score.active{border-color:var(--accent)}.score-label{font-size:.8rem;color:var(--muted)}.score-value{font-size:1.8rem;font-weight:800;margin-top:3px}.x{color:var(--x)}.o{color:var(--o)}.status{min-height:52px;display:grid;place-items:center;background:var(--panel);border:1px solid var(--line);border-radius:14px;margin-bottom:14px;font-weight:700}.board{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.cell{aspect-ratio:1;border:1px solid var(--line);border-radius:18px;background:linear-gradient(145deg,#202435,#171a25);color:var(--text);font-size:clamp(3rem,17vw,5rem);font-weight:800;cursor:pointer;transition:transform .12s,border-color .12s}.cell:hover:enabled{transform:translateY(-2px);border-color:#747e9c}.cell:focus-visible,button:focus-visible{outline:3px solid var(--accent);outline-offset:3px}.cell:disabled{cursor:default}.cell.win{background:#1c3a36;border-color:var(--accent)}.actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px}.action{padding:13px 10px;border-radius:12px;border:1px solid var(--line);background:var(--panel);color:var(--text);font-weight:700;cursor:pointer}.action.primary{background:var(--accent);color:#10211b;border-color:transparent}.hint{color:var(--muted);font-size:.8rem;margin-top:18px}.complaint-link{margin-top:10px;width:100%}.complaint-card{background:var(--panel);border:1px solid var(--line);border-radius:18px;padding:22px;text-align:left}.complaint-card h2{text-align:center;margin-top:0}.complaint-card label{display:block;color:var(--muted);font-size:.9rem;margin:14px 0 6px}.complaint-card input,.complaint-card select,.complaint-card textarea{width:100%;padding:12px;border-radius:10px;border:1px solid var(--line);background:#11141e;color:var(--text);font:inherit}.complaint-card textarea{min-height:140px;resize:vertical}.complaint-actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px}.form-note{font-size:.85rem;color:var(--muted);line-height:1.5}.success-note{padding:12px;border-radius:10px;background:#1c3a36;color:#d1fae5;margin-top:12px}</style></head><body><main><div class=\"eyebrow\">Classic strategy</div><h1>Tic-Tac-Toe</h1><p class=\"sub\">Three in a row wins the round.</p><div class=\"modes\" aria-label=\"Choose opponent\"><button class=\"action mode-btn active\" id=\"twoPlayer\" type=\"button\">2 Players</button><button class=\"action mode-btn\" id=\"vsAI\" type=\"button\">Play vs AI</button></div><div class=\\\"difficulty-row\\\" id=\\\"difficultyRow\\\" hidden><label for=\\\"difficulty\\\">AI difficulty</label><select id=\\\"difficulty\\\" class=\\\"action\\\"><option value=\\\"easy\\\">Easy</option><option value=\\\"medium\\\" selected>Medium</option><option value=\\\"hard\\\">Hard</option></select></div><section class=\"scoreboard\" aria-label=\"Scoreboard\"><div id=\"scoreX\" class=\"score active\"><div class=\"score-label\"><span class=\"x\">✕</span> Player X</div><div id=\"winsX\" class=\"score-value\">0</div></div><div id=\"scoreO\" class=\"score\"><div class=\"score-label\"><span class=\"o\">○</span> Player O</div><div id=\"winsO\" class=\"score-value\">0</div></div></section><div id=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\">Player X's turn</div><div id=\"board\" class=\"board\" role=\"group\" aria-label=\"Tic-Tac-Toe board\"></div><div class=\"actions\"><button class=\"action primary\" id=\"newRound\">New Round</button><button class=\"action\" id=\"reset\">Reset Scores</button></div><p class=\"hint\">Tap a square to make your move. The computer plays as O in AI mode.</p><button class=\"action complaint-link\" id=\"openComplaints\" type=\"button\">Complaints &amp; Feedback</button></main><main id=\"complaintPage\" hidden><div class=\"eyebrow\">Help us improve</div><h1>Complaints</h1><p class=\"sub\">Tell us what went wrong or how we can improve.</p><section class=\"complaint-card\"><form id=\"complaintForm\"><label for=\"complaintType\">What is this about?</label><select id=\"complaintType\" required><option value=\"\">Choose a topic</option><option>Bug or technical issue</option><option>AI difficulty or gameplay</option><option>Accessibility</option><option>Suggestion</option><option>Other</option></select><label for=\"complaintSubject\">Subject</label><input id=\"complaintSubject\" maxlength=\"100\" placeholder=\"Brief summary\" required><label for=\"complaintMessage\">Details</label><textarea id=\"complaintMessage\" maxlength=\"2000\" placeholder=\"Describe the issue or suggestion…\" required></textarea><label for=\"complaintContact\">Contact info (optional)</label><input id=\"complaintContact\" maxlength=\"120\" placeholder=\"Email, if you want a reply\"><p class=\"form-note\">Privacy note: reports are saved for review. Bug reports are automatically labeled using simple keyword matching, but code is never changed automatically from public comments. Avoid entering private or sensitive information.</p><div class=\"complaint-actions\"><button class=\"action\" id=\"backToGame\" type=\"button\">Back to Game</button><button class=\"action primary\" type=\"submit\">Review Complaint</button></div><div id=\"complaintResult\" role=\"status\" aria-live=\"polite\"></div></form></section></main><script>\nconst boardEl=document.querySelector('#board'),statusEl=document.querySelector('#status'),scoreXEl=document.querySelector('#scoreX'),scoreOEl=document.querySelector('#scoreO'),winsXEl=document.querySelector('#winsX'),winsOEl=document.querySelector('#winsO');const wins=[[0,1,2],[3,4,5],[6,7,8],[0,3,6],[1,4,7],[2,5,8],[0,4,8],[2,4,6]];let board=Array(9).fill(''),current='X',finished=false,scores={X:0,O:0},winning=[],mode='two',difficulty='medium';\nfunction render(){boardEl.innerHTML='';board.forEach((mark,i)=>{const b=document.createElement('button');b.className='cell'+(winning.includes(i)?' win':'')+(mark==='X'?' x':mark==='O'?' o':'');b.type='button';b.textContent=mark==='X'?'×':mark==='O'?'○':'';b.setAttribute('aria-label',mark?'Square '+(i+1)+': '+mark:'Square '+(i+1)+', empty');b.disabled=finished||!!mark;b.addEventListener('click',()=>play(i));boardEl.appendChild(b)});scoreXEl.classList.toggle('active',!finished&&current==='X');scoreOEl.classList.toggle('active',!finished&&current==='O')}\nfunction play(i){if(finished||board[i])return;board[i]=current;const line=wins.find(line=>line.every(n=>board[n]===current));if(line){finished=true;winning=line;scores[current]++;statusEl.textContent=(mode==='ai'&&current==='O'?'Computer':'Player '+current)+' wins! 🎉'}else if(board.every(Boolean)){finished=true;statusEl.textContent='Draw game! 🤝'}else{current=current==='X'?'O':'X';statusEl.textContent=mode==='ai'&&current==='O'?'Computer is thinking…':'Player '+current+String.fromCharCode(39)+'s turn'}winsXEl.textContent=scores.X;winsOEl.textContent=scores.O;render();if(mode==='ai'&&!finished&&current==='O')aiMove()}\nfunction aiMove(){let choice=-1;if(difficulty==='easy'||(difficulty==='medium'&&Math.random()<0.45)){const empty=board.map((v,i)=>v?-1:i).filter(i=>i>=0);choice=empty[Math.floor(Math.random()*empty.length)]}else{let best=-Infinity;for(let i=0;i<9;i++){if(board[i])continue;board[i]='O';const score=minimax(0,false);board[i]='';if(score>best){best=score;choice=i}}}if(choice>=0)play(choice)}\\nfunction minimax(depth,maximizing){const line=wins.find(line=>line.every(n=>board[n]&&board[n]===board[line[0]]));if(line)return board[line[0]]==='O'?10-depth:depth-10;if(board.every(Boolean))return 0;let best=maximizing?-Infinity:Infinity;for(let i=0;i<9;i++){if(board[i])continue;board[i]=maximizing?'O':'X';const score=minimax(depth+1,!maximizing);board[i]='';best=maximizing?Math.max(best,score):Math.min(best,score)}return best}\nfunction setMode(next){mode=next;scores={X:0,O:0};winsXEl.textContent='0';winsOEl.textContent='0';document.querySelector('#twoPlayer').classList.toggle('active',mode==='two');document.querySelector('#vsAI').classList.toggle('active',mode==='ai');document.querySelector('#difficultyRow').hidden=mode!=='ai';scoreOEl.querySelector('.score-label').textContent=mode==='ai'?'○ Computer':'○ Player O';newRound()}\nfunction newRound(){board=Array(9).fill('');current='X';finished=false;winning=[];statusEl.textContent='Player X turn';render()}\ndocument.querySelector('#twoPlayer').addEventListener('click',()=>setMode('two'));document.querySelector('#vsAI').addEventListener('click',()=>setMode('ai'));document.querySelector('#difficulty').addEventListener('change',e=>{difficulty=e.target.value;newRound()});document.querySelector('#openComplaints').addEventListener('click',()=>{document.querySelector('body > main').hidden=true;document.querySelector('#complaintPage').hidden=false;});document.querySelector('#backToGame').addEventListener('click',()=>{document.querySelector('#complaintPage').hidden=true;document.querySelector('body > main').hidden=false;});document.querySelector('#complaintForm').addEventListener('submit',async e=>{e.preventDefault();const result=document.querySelector('#complaintResult');const submit=e.currentTarget.querySelector('button[type=submit]');submit.disabled=true;result.className='form-note';result.textContent='Submitting…';try{const response=await fetch(location.pathname.replace(/\/$/,'')+'/api/complaints',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({category:document.querySelector('#complaintType').value,subject:document.querySelector('#complaintSubject').value,details:document.querySelector('#complaintMessage').value,contact:document.querySelector('#complaintContact').value})});const data=await response.json();if(!response.ok)throw new Error(data.error||'Submission failed');result.className='success-note';result.textContent='Thanks! Your report was saved'+(data.ticket?' as ticket '+data.ticket:'')+'. '+(data.status==='pending_review'?'It has been queued for bug review'+(data.triage&&data.triage.labels&&data.triage.labels.length?' (labels: '+data.triage.labels.join(', ')+')':'')+'.':'Feedback received.');e.currentTarget.reset()}catch(err){result.className='form-note';result.textContent=err.message||'Could not submit. Please try again.'}finally{submit.disabled=false}});document.querySelector('#newRound').addEventListener('click',newRound);document.querySelector('#reset').addEventListener('click',()=>{scores={X:0,O:0};winsXEl.textContent='0';winsOEl.textContent='0';newRound()});render();</script></body></html>", { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } }); } };