Skip to content

Commit eaf2c22

Browse files
committed
feat: guest mode — anonymous play with soft signup nudges
- Auto-init guest session on first visit (no forced onboarding wall) - Guest data stored under breakaway-guest localStorage key - All challenges accessible without signup - Post-challenge toast nudge: 'Create an account to save permanently' - Export/QR/verification gated: redirects to signup with context toast - Profile page shows Guest Challenger card with credential count + XP - Nav avatar shows 'G' for guests, clickable to onboard - On signup: migrateGuestData() merges all guest credentials/XP/streak into new account - saveState() routes to guest or user storage based on APP.isGuest
1 parent f9c2c0f commit eaf2c22

6 files changed

Lines changed: 80 additions & 13 deletions

File tree

js/app.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
document.addEventListener('DOMContentLoaded',()=>{
2-
loadState();initParticles();initGrid();
2+
loadState();
3+
if(!APP.user){
4+
loadGuestState();
5+
if(!APP.isGuest) initGuestSession();
6+
}
7+
initParticles();initGrid();
38
document.getElementById('stat-challenges').textContent=CHALLENGES.length;
49
document.getElementById('stat-domains').textContent=DOMAINS.length;
510
document.getElementById('stat-verified').textContent=APP.credentials.length;
611
document.getElementById('btn-get-started').addEventListener('click',()=>{
7-
if(APP.user)navigate('#/dashboard');else navigate('#/onboard');
12+
if(APP.user||APP.isGuest)navigate('#/dashboard');else navigate('#/onboard');
813
});
914
document.getElementById('btn-learn-more').addEventListener('click',()=>{document.getElementById('how-it-works').scrollIntoView({behavior:'smooth'});});
1015
// Domain chips
@@ -84,6 +89,7 @@ document.addEventListener('DOMContentLoaded',()=>{
8489
if (authMode === 'signup') {
8590
const salt = generateSalt();
8691
const hash = await pbkdf2Hash(password, salt);
92+
migrateGuestData();
8793
APP.user = {
8894
email: email,
8995
salt: salt,
@@ -111,7 +117,15 @@ document.addEventListener('DOMContentLoaded',()=>{
111117
}
112118
}
113119
});
114-
if(APP.user&&APP.user.name&&APP.user.name.length>0){document.getElementById('nav-avatar').textContent=APP.user.name[0].toUpperCase();}
120+
const navAvatar=document.getElementById('nav-avatar');
121+
if(APP.user&&APP.user.name&&APP.user.name.length>0){
122+
navAvatar.textContent=APP.user.name[0].toUpperCase();
123+
}else if(APP.isGuest){
124+
navAvatar.textContent='G';
125+
navAvatar.title='Guest — click to sign up';
126+
navAvatar.style.cursor='pointer';
127+
navAvatar.onclick=()=>navigate('#/onboard');
128+
}
115129
// ── Keyboard Shortcuts ──
116130
document.addEventListener('keydown',function(e){
117131
// Ctrl+Enter: submit code or prompt

js/challenge-engine.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ async function submitQuiz(){
162162
const hash=await sha256(JSON.stringify({id:c.id,answers:APP.quizAnswers,score,time:elapsed,aiMode:APP.aiMode,pasteCount:APP.pasteCount,tabSwitches:APP.tabSwitchCount,ts:Date.now()}));
163163
const integrity=computeIntegrity(APP.pasteCount,APP.tabSwitchCount,APP.aiMode);
164164
const cred={id:hash.slice(0,16),challengeId:c.id,domain:c.domain,title:c.title,score,time:elapsed,hash,aiMode:APP.aiMode,pasteCount:APP.pasteCount,tabSwitches:APP.tabSwitchCount,integrity:integrity.label,timestamp:new Date().toISOString()};
165-
APP.credentials.push(cred);saveState();showResults(cred,correctCount+'/'+c.questions.length+' correct');
165+
APP.credentials.push(cred);saveState();showResults(cred,correctCount+'/'+c.questions.length+' correct');maybeShowSignupNudge();
166166
}
167167
function runTests(){
168168
const c=APP.activeChallenge;const code=document.getElementById('code-input').value;
@@ -183,7 +183,7 @@ async function submitCode(){
183183
const hash=await sha256(JSON.stringify({id:c.id,code,score,time:elapsed,aiMode:APP.aiMode,pasteCount:APP.pasteCount,tabSwitches:APP.tabSwitchCount,ts:Date.now()}));
184184
const integrity=computeIntegrity(APP.pasteCount,APP.tabSwitchCount,APP.aiMode);
185185
const cred={id:hash.slice(0,16),challengeId:c.id,domain:c.domain,title:c.title,score,time:elapsed,hash,aiMode:APP.aiMode,pasteCount:APP.pasteCount,tabSwitches:APP.tabSwitchCount,integrity:integrity.label,timestamp:new Date().toISOString()};
186-
APP.credentials.push(cred);saveState();showResults(cred,passed+'/'+c.tests.length+' tests passed');
186+
APP.credentials.push(cred);saveState();showResults(cred,passed+'/'+c.tests.length+' tests passed');maybeShowSignupNudge();
187187
}
188188
async function submitValidate(){
189189
clearInterval(timerInterval);const c=APP.activeChallenge;if(!c)return;
@@ -210,7 +210,7 @@ async function submitValidate(){
210210
const hash=await sha256(JSON.stringify({id:c.id,answers,score,time:elapsed,aiMode:APP.aiMode,pasteCount:APP.pasteCount,tabSwitches:APP.tabSwitchCount,ts:Date.now()}));
211211
const integrity=computeIntegrity(APP.pasteCount,APP.tabSwitchCount,APP.aiMode);
212212
const cred={id:hash.slice(0,16),challengeId:c.id,domain:c.domain,title:c.title,score,time:elapsed,hash,aiMode:APP.aiMode,pasteCount:APP.pasteCount,tabSwitches:APP.tabSwitchCount,integrity:integrity.label,timestamp:new Date().toISOString()};
213-
APP.credentials.push(cred);saveState();showResults(cred,found+'/'+c.bugCount+' bugs found');
213+
APP.credentials.push(cred);saveState();showResults(cred,found+'/'+c.bugCount+' bugs found');maybeShowSignupNudge();
214214
}
215215
async function submitPrompt(){
216216
clearInterval(timerInterval);const c=APP.activeChallenge;
@@ -227,7 +227,7 @@ async function submitPrompt(){
227227
const hash=await sha256(JSON.stringify({id:c.id,prompt,score,time:elapsed,aiMode:APP.aiMode,tabSwitches:APP.tabSwitchCount,ts:Date.now()}));
228228
const integrity=computeIntegrity(0,APP.tabSwitchCount,APP.aiMode);
229229
const cred={id:hash.slice(0,16),challengeId:c.id,domain:c.domain,title:c.title,score,time:elapsed,hash,aiMode:'native',pasteCount:0,tabSwitches:APP.tabSwitchCount,integrity:integrity.label,timestamp:new Date().toISOString()};
230-
APP.credentials.push(cred);saveState();showResults(cred,'Prompt scored '+score+'/100');
230+
APP.credentials.push(cred);saveState();showResults(cred,'Prompt scored '+score+'/100');maybeShowSignupNudge();
231231
}
232232
// ═══════════════════════════════════════
233233
// STROOP CHALLENGE ENGINE
@@ -285,7 +285,7 @@ async function finishStroop(){
285285
const cred={id:hash.slice(0,16),challengeId:c.id,domain:c.domain,title:c.title,score,time:elapsed,hash,aiMode:APP.aiMode,pasteCount:0,tabSwitches:APP.tabSwitchCount,integrity:integrity.label,timestamp:new Date().toISOString(),
286286
meta:{accuracy,avgReactionMs:avgTime,speedBonus,correct:APP.stroopCorrect,total:APP.stroopTotal}};
287287
APP.credentials.push(cred);saveState();
288-
showResults(cred,APP.stroopCorrect+'/'+APP.stroopTotal+' correct — avg '+avgTime+'ms');
288+
showResults(cred,APP.stroopCorrect+'/'+APP.stroopTotal+' correct — avg '+avgTime+'ms');maybeShowSignupNudge();
289289
}
290290
// ═══════════════════════════════════════
291291
// SPEED PARSE CHALLENGE ENGINE
@@ -327,7 +327,7 @@ async function submitSpeedParse(){
327327
const cred={id:hash.slice(0,16),challengeId:c.id,domain:c.domain,title:c.title,score,time:elapsed,hash,aiMode:APP.aiMode,pasteCount:0,tabSwitches:APP.tabSwitchCount,integrity:integrity.label,timestamp:new Date().toISOString(),
328328
meta:{correct,total:snippet.questions.length,recallRate:score}};
329329
APP.credentials.push(cred);saveState();
330-
showResults(cred,correct+'/'+snippet.questions.length+' recalled correctly');
330+
showResults(cred,correct+'/'+snippet.questions.length+' recalled correctly');maybeShowSignupNudge();
331331
}
332332
// ═══════════════════════════════════════
333333
// AI VALIDATE (ADVANCED) ENGINE
@@ -367,5 +367,5 @@ async function submitAIValidate(){
367367
const cred={id:hash.slice(0,16),challengeId:c.id,domain:c.domain,title:c.title,score,time:elapsed,hash,aiMode:APP.aiMode,pasteCount:APP.pasteCount,tabSwitches:APP.tabSwitchCount,integrity:integrity.label,timestamp:new Date().toISOString(),
368368
meta:{bugsFound:found,bugCount:c.bugCount,detailBonus,matchDetails}};
369369
APP.credentials.push(cred);saveState();
370-
showResults(cred,found+'/'+c.bugCount+' bugs found'+(detailBonus?' (+'+detailBonus+' detail bonus)':''));
370+
showResults(cred,found+'/'+c.bugCount+' bugs found'+(detailBonus?' (+'+detailBonus+' detail bonus)':''));maybeShowSignupNudge();
371371
}

js/challenges.js

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

js/config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Breakaway App - Core
2-
const APP = { user: null, credentials: [], view: 'landing', aiMode: null, pasteCount: 0, tabSwitchCount: 0, xp: 0, streak: 0, lastCompleted: null, stroopRound: 0, stroopCorrect: 0, stroopTotal: 0, speedparsePhase: 'flash', speedparseAnswers: [] };
2+
const APP = { user: null, credentials: [], view: 'landing', aiMode: null, pasteCount: 0, tabSwitchCount: 0, xp: 0, streak: 0, lastCompleted: null, stroopRound: 0, stroopCorrect: 0, stroopTotal: 0, speedparsePhase: 'flash', speedparseAnswers: [], isGuest: false, hasShownSignupNudge: false };
33
// ── Session Integrity System ──
44
// Measures session continuity during solo mode via paste events and tab switches.
55
// Produces a transparent focus label on each credential — informational, not judgmental.

js/ui.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,10 @@ function drawRadarChart(){
225225
function renderProfile(){
226226
const pw=document.getElementById('profile-wrapper');
227227
if(!APP.user){
228-
pw.innerHTML='<div class="profile-empty"><div class="profile-empty-icon"><svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="var(--gold)" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 1 0-16 0"/></svg></div><h2>Create Your Profile</h2><p>Set up your identity to start earning verified credentials.</p><button class="btn btn-primary" onclick="navigate(\'#/onboard\')">⚡ Get Started</button></div>';
228+
const credsCount=APP.credentials.length;
229+
const guestCard='<div class="profile-card" style="margin-bottom:16px"><div class="profile-card-top"><div class="profile-avatar" style="background:rgba(201,168,76,0.15);color:var(--gold)">G</div><div class="profile-identity"><div class="profile-name">Guest Challenger</div><div class="profile-title">'+credsCount+' credential'+(credsCount!==1?'s':'')+' earned anonymously</div></div></div><div class="profile-xp-bar"><div class="profile-xp-label"><span>⭐ '+APP.xp+' XP</span><span>🔥 '+APP.streak+' streak</span></div><div class="profile-xp-track"><div class="profile-xp-fill" style="width:'+Math.min(100,(APP.xp/1000)*100)+'%"></div></div></div><div style="margin-top:16px"><button class="btn btn-primary" onclick="navigate(\'#/onboard\')">⚡ Sign Up to Save Progress</button></div></div>';
230+
const skillsMsg='<div class="card"><div class="card-header"><h3>Skill Map</h3></div><div class="profile-empty-skills"><p>Your guest credentials are stored on this device only. Sign up to make them permanent and portable.</p></div></div>';
231+
pw.innerHTML=guestCard+skillsMsg;
229232
return;
230233
}
231234
const domains={};APP.credentials.forEach(c=>{if(!domains[c.domain])domains[c.domain]={count:0,total:0};domains[c.domain].count++;domains[c.domain].total+=c.score;});
@@ -247,6 +250,7 @@ function renderCredentials(){
247250
}).join('');
248251
}
249252
function exportCredentials(){
253+
if(APP.isGuest){showToast('Sign up to export and share your credentials','info',4000);navigate('#/onboard');return;}
250254
const data={exportVersion:'1.0',exportedAt:new Date().toISOString(),user:{name:APP.user?.name,email:APP.user?.email},credentials:APP.credentials};
251255
const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'});
252256
const url=URL.createObjectURL(blob);
@@ -268,6 +272,7 @@ function downloadQR(){
268272
const a=document.createElement('a');a.href=_qrDataUrl;a.download='breakaway-credential-qr.png';a.click();
269273
}
270274
function downloadVerificationPage(hash){
275+
if(APP.isGuest){showToast('Sign up to make your credentials portable and shareable','info',4000);navigate('#/onboard');return;}
271276
const cred=APP.credentials.find(c=>c.hash===hash);if(!cred)return;
272277
const verifyHtml='<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Breakaway Credential Verification</title><style>body{font-family:Inter,system-ui,sans-serif;background:#0a0a0f;color:#e8e8f0;max-width:600px;margin:40px auto;padding:24px;line-height:1.6;}h1{color:#C9A84C;font-size:1.4rem;margin-bottom:4px;}h2{color:#65657a;font-size:0.85rem;font-weight:400;margin-bottom:24px;}.card{background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:24px;margin-bottom:16px;}.label{color:#65657a;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px;}.value{color:#e8e8f0;font-size:1rem;font-weight:600;}.hash{font-family:JetBrains Mono,monospace;font-size:0.7rem;color:#65657a;word-break:break-all;margin-top:8px;padding:12px;background:rgba(0,0,0,0.3);border-radius:6px;}.badge{display:inline-block;padding:4px 10px;border-radius:4px;font-size:0.75rem;font-weight:700;margin-top:8px;}.verified{background:rgba(52,211,153,0.12);color:#34d399;}.footer{margin-top:32px;font-size:0.75rem;color:#65657a;text-align:center;}</style></head><body><h1>◈ Breakaway Credential</h1><h2>SHA-256 Verified Skill Proof</h2><div class="card"><div class="label">Challenge</div><div class="value">'+escapeHtml(cred.title)+'</div><div class="label">Domain</div><div class="value">'+escapeHtml(cred.domain)+'</div><div class="label">Score</div><div class="value">'+cred.score+'%</div><div class="label">Mode</div><div class="value">'+escapeHtml(cred.aiMode||'solo')+'</div><div class="label">Completed</div><div class="value">'+new Date(cred.timestamp).toLocaleString()+'</div><div class="label">Integrity</div><div class="value">'+escapeHtml(cred.integrity||'N/A')+'</div></div><div class="card"><div class="label">SHA-256 Hash</div><div class="hash">'+cred.hash+'</div><div class="badge verified">✓ Tamper-Evident</div></div><div class="footer">Verified by Breakaway — zero-dependency credentialing engine.<br>This file is self-contained and does not require an internet connection.</div></body></html>';
273278
const blob=new Blob([verifyHtml],{type:'text/html'});

js/utils.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,48 @@ function drainToast(){
2020
}
2121
async function sha256(msg){const d=new TextEncoder().encode(msg);const h=await crypto.subtle.digest('SHA-256',d);return[...new Uint8Array(h)].map(b=>b.toString(16).padStart(2,'0')).join('');}
2222
function escapeHtml(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'}[c]));}
23+
24+
// ── Guest Session Helpers ──
25+
function initGuestSession(){
26+
APP.isGuest=true;
27+
APP.hasShownSignupNudge=false;
28+
try{localStorage.setItem('breakaway-guest',JSON.stringify({credentials:APP.credentials,xp:APP.xp,streak:APP.streak,lastCompleted:APP.lastCompleted}));}catch(e){}
29+
}
30+
function saveGuestState(){
31+
if(!APP.isGuest)return;
32+
try{localStorage.setItem('breakaway-guest',JSON.stringify({credentials:APP.credentials,xp:APP.xp,streak:APP.streak,lastCompleted:APP.lastCompleted}));}catch(e){}
33+
}
34+
function loadGuestState(){
35+
try{
36+
const d=JSON.parse(localStorage.getItem('breakaway-guest'));
37+
if(d){
38+
APP.isGuest=true;
39+
APP.credentials=d.credentials||[];
40+
APP.xp=d.xp||0;
41+
APP.streak=d.streak||0;
42+
APP.lastCompleted=d.lastCompleted||null;
43+
}
44+
}catch(e){}
45+
}
46+
function migrateGuestData(){
47+
try{
48+
const guest=JSON.parse(localStorage.getItem('breakaway-guest'));
49+
if(guest){
50+
APP.credentials=guest.credentials||APP.credentials;
51+
APP.xp=(guest.xp||0)+APP.xp;
52+
APP.streak=Math.max(guest.streak||0,APP.streak);
53+
if(guest.lastCompleted)APP.lastCompleted=guest.lastCompleted;
54+
localStorage.removeItem('breakaway-guest');
55+
}
56+
}catch(e){}
57+
APP.isGuest=false;
58+
APP.hasShownSignupNudge=false;
59+
}
60+
function maybeShowSignupNudge(){
61+
if(!APP.isGuest||APP.hasShownSignupNudge)return;
62+
APP.hasShownSignupNudge=true;
63+
saveGuestState();
64+
setTimeout(()=>{
65+
showToast('💾 Create an account to save your credentials permanently','info',5000);
66+
},1200);
67+
}

0 commit comments

Comments
 (0)