tags before injecting into the iframe. function fixScripts(html) { return html.split('<\\/script>').join(''); } var PAGE_ADMIN = fixScripts(document.getElementById('tpl-admin').textContent); var PAGE_VIEWER = fixScripts(document.getElementById('tpl-viewer').textContent); /* ============== THEME + LOWER-END DEVICE STATE ============== */ var root = document.documentElement; var lite = root.classList.contains('lite'); var lightTarget = root.getAttribute('data-theme') === 'light' ? 1 : 0, lightNow = lightTarget; function remember(k, v){ try{ window.localStorage.setItem(k, v); }catch(e){} } /* ============== WEBGL BACKGROUND ============== */ var canvas = document.getElementById('webgl-bg'); var gl = null, glTried = false; var raf = 0, glRunning = false; var mouseX = window.innerWidth / 2, mouseY = window.innerHeight / 2; var drawFrame = null; function initGL(){ if(glTried) return; glTried = true; gl = canvas.getContext('webgl'); if(!gl) return; var resize = function(){ canvas.width = window.innerWidth; canvas.height = window.innerHeight; gl.viewport(0, 0, canvas.width, canvas.height); }; window.addEventListener('resize', resize); resize(); var vsSource = ` attribute vec2 a_position; void main() { gl_Position = vec4(a_position, 0.0, 1.0); } `; var fsSource = ` precision highp float; uniform vec2 u_resolution; uniform float u_time; uniform vec2 u_mouse; uniform float u_light; float random(vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); } float noise(vec2 st) { vec2 i = floor(st); vec2 f = fract(st); float a = random(i); float b = random(i + vec2(1.0, 0.0)); float c = random(i + vec2(0.0, 1.0)); float d = random(i + vec2(1.0, 1.0)); vec2 u = f * f * (3.0 - 2.0 * f); return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; } float fbm(vec2 st) { float value = 0.0; float amplitude = 0.5; for (int i = 0; i < 5; i++) { value += amplitude * noise(st); st *= 2.0; amplitude *= 0.5; } return value; } void main() { vec2 st = gl_FragCoord.xy / u_resolution.xy; st.x *= u_resolution.x / u_resolution.y; vec2 mouse = u_mouse / u_resolution; mouse.x *= u_resolution.x / u_resolution.y; float t = u_time * 0.03; vec2 dist = st - mouse; float mouseInfluence = exp(-dot(dist, dist) * 3.0) * 0.5; vec2 q = vec2(fbm(st + t), fbm(st - t + vec2(5.2, 1.3))); vec2 r = vec2(fbm(st + 4.0*q + vec2(1.7,9.2) + t*1.5), fbm(st + 4.0*q + vec2(8.3,2.8) - t*1.5)); float f = fbm(st + 4.0*r + mouseInfluence); vec3 color1 = mix(vec3(0.07, 0.05, 0.04), vec3(0.96, 0.93, 0.90), u_light); vec3 color2 = mix(vec3(0.30, 0.08, 0.10), vec3(0.90, 0.76, 0.74), u_light); vec3 color3 = mix(vec3(0.69, 0.22, 0.25), vec3(0.74, 0.34, 0.36), u_light); vec3 color = mix(color1, color2, f); color = mix(color, color3, smoothstep(0.4, 0.8, f) * smoothstep(0.0, 0.5, length(q))); color += vec3(0.15, 0.05, 0.05) * mouseInfluence * (1.0 - 0.6 * u_light); float vignette = smoothstep(1.5, 0.2, distance(st, vec2(0.5, 0.5))); vec3 darkMix = color * (0.4 + vignette * 0.6); vec3 lightMix = mix(color, vec3(0.91, 0.85, 0.80), (1.0 - vignette) * 0.55); color = mix(darkMix, lightMix, u_light); gl_FragColor = vec4(color, 1.0); } `; var compileShader = function(type, source){ var s = gl.createShader(type); gl.shaderSource(s, source); gl.compileShader(s); return s; }; var program = gl.createProgram(); gl.attachShader(program, compileShader(gl.VERTEX_SHADER, vsSource)); gl.attachShader(program, compileShader(gl.FRAGMENT_SHADER, fsSource)); gl.linkProgram(program); gl.useProgram(program); var buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW); var aPos = gl.getAttribLocation(program, 'a_position'); gl.enableVertexAttribArray(aPos); gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0); var uRes = gl.getUniformLocation(program, 'u_resolution'); var uTime = gl.getUniformLocation(program, 'u_time'); var uMouse = gl.getUniformLocation(program, 'u_mouse'); var uLight = gl.getUniformLocation(program, 'u_light'); var t0 = performance.now(); window.addEventListener('mousemove', function(e){ mouseX = e.clientX; mouseY = window.innerHeight - e.clientY; }); drawFrame = function(){ gl.uniform2f(uRes, canvas.width, canvas.height); gl.uniform1f(uTime, (performance.now() - t0) / 1000); gl.uniform2f(uMouse, mouseX, mouseY); lightNow += (lightTarget - lightNow) * 0.08; gl.uniform1f(uLight, lightNow); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); if(glRunning) raf = requestAnimationFrame(drawFrame); }; } if(!lite) initGL(); function startGL(){ if(lite) return; initGL(); if(drawFrame && !glRunning){ glRunning = true; drawFrame(); } } function stopGL(){ glRunning = false; cancelAnimationFrame(raf); } /* ============== ELEMENTS ============== */ var $ = function(id){ return document.getElementById(id); }; var authEl = $('auth'), appEl = $('app'), frame = $('frame'); var tabIn = $('tab-in'), tabUp = $('tab-up'), form = $('form'); var email = $('email'), pass = $('pass'), pass2 = $('pass2'), confirmReveal = $('confirm-reveal'); var cardEl = document.querySelector('.card'), tabsEl = document.querySelector('.tabs'); var calm = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); var EASE = 'cubic-bezier(.22,.61,.36,1)'; function swap(el, dy){ if(calm || !el.animate) return; el.animate([{ opacity:0, transform:'translateY(' + dy + 'px)' }, { opacity:1, transform:'translateY(0)' }], { duration:340, easing:EASE }); } var msg = $('msg'), go = $('go'), sub = $('sub'), foot = $('foot'), eye = $('eye'); var mode = 'in', busy = false; var unameReveal = $('uname-reveal'), unameEl = $('uname-status'); var unameTimer = 0, unameReq = 0, unameState = '', unameChecked = ''; /* ============== THEME + LOWER-END DEVICE CONTROLS ============== */ var themeBtn = $('theme-toggle'), liteBtn = $('lite-toggle'); function syncThemeUI(){ var light = lightTarget === 1; var tip = light ? 'Switch to dark mode' : 'Switch to light mode'; themeBtn.setAttribute('aria-label', tip); themeBtn.setAttribute('data-tip', tip); } function syncLiteUI(){ liteBtn.setAttribute('aria-pressed', String(lite)); liteBtn.setAttribute('data-tip', 'Lower-end device mode: ' + (lite ? 'On' : 'Off')); } themeBtn.addEventListener('click', function(){ var light = lightTarget === 0; lightTarget = light ? 1 : 0; if(!glRunning) lightNow = lightTarget; if(light) root.setAttribute('data-theme', 'light'); else root.removeAttribute('data-theme'); remember('vanilla-theme', light ? 'light' : 'dark'); syncThemeUI(); }); liteBtn.addEventListener('click', function(){ lite = !lite; root.classList.toggle('lite', lite); remember('vanilla-lite', lite ? '1' : '0'); if(lite) stopGL(); else startGL(); syncLiteUI(); }); syncThemeUI(); syncLiteUI(); function say(text, kind){ msg.textContent = text || ''; msg.className = 'msg' + (kind ? ' ' + kind : ''); if(!text || calm || !msg.animate) return; msg.animate([{ opacity:0, transform:'translateY(-4px)' }, { opacity:1, transform:'translateY(0)' }], { duration:260, easing:EASE }); if(kind === 'err') cardEl.animate([{ transform:'translateX(0)' }, { transform:'translateX(-7px)' }, { transform:'translateX(6px)' }, { transform:'translateX(-3px)' }, { transform:'translateX(0)' }], { duration:380, easing:'ease-out' }); } function setMode(m){ var changed = m !== mode; mode = m; var up = m === 'up'; tabIn.classList.toggle('on', !up); tabUp.classList.toggle('on', up); tabIn.setAttribute('aria-selected', String(!up)); tabUp.setAttribute('aria-selected', String(up)); tabsEl.classList.toggle('up', up); confirmReveal.classList.toggle('open', up); unameReveal.classList.toggle('open', up); pass.autocomplete = up ? 'new-password' : 'current-password'; go.textContent = up ? 'Create account' : 'Sign in'; sub.textContent = up ? 'Create an account to view the résumé.' : 'Sign in to continue.'; foot.textContent = up ? 'Already registered? Switch to Sign in.' : 'New here? Create an account to view the résumé.'; if(changed){ swap(sub, 6); swap(foot, 6); swap(go, 0); } say(''); if(up) scheduleUnameCheck(); else unameReset(); } tabIn.addEventListener('click', function(){ setMode('in'); }); tabUp.addEventListener('click', function(){ setMode('up'); }); eye.addEventListener('click', function(){ var show = pass.type === 'password'; pass.type = show ? 'text' : 'password'; pass2.type = pass.type; eye.textContent = show ? 'Hide' : 'Show'; eye.setAttribute('aria-label', show ? 'Hide password' : 'Show password'); }); /* ============== USERNAME HELPERS ============== */ var LOCAL_DOMAIN = '@local.resume'; function normalizeUsername(u){ return (u || '').trim().toLowerCase(); } function usernameToEmail(u){ return normalizeUsername(u) + LOCAL_DOMAIN; } function validUsername(u){ return /^[a-z0-9._-]{3,24}$/.test(normalizeUsername(u)); } /* ============== USERNAME AVAILABILITY (create account tab) ============== */ var UNAME_TEXT = { checking:'Checking…', available:'✓ available', taken:'✗ already taken' }; var UNAME_CLASS = { checking:' checking', available:' ok', taken:' err' }; function unameSet(state){ unameState = state; unameEl.textContent = state ? UNAME_TEXT[state] : ''; unameEl.className = 'msg uname' + (state ? UNAME_CLASS[state] : ''); } function unameReset(){ clearTimeout(unameTimer); unameReq++; unameChecked = ''; unameSet(''); } async function checkUsername(uname){ var mine = ++unameReq; unameSet('checking'); try{ var r = await sb.from('profiles').select('id').eq('username', uname).maybeSingle(); if(mine !== unameReq || mode !== 'up' || normalizeUsername(email.value) !== uname) return; if(r.error){ console.warn('[username check]', r.error); unameSet(''); return; } unameChecked = uname; unameSet(r.data ? 'taken' : 'available'); }catch(err){ console.warn('[username check]', err); if(mine === unameReq) unameSet(''); } } function scheduleUnameCheck(){ unameReset(); if(mode !== 'up') return; var uname = normalizeUsername(email.value); if(!uname || !validUsername(uname)) return; unameTimer = setTimeout(function(){ checkUsername(uname); }, 400); } email.addEventListener('input', scheduleUnameCheck); function friendly(err){ var m = (err && err.message || '').toLowerCase(); if(m.indexOf('invalid login') > -1) return 'Wrong username or password.'; if(m.indexOf('email not confirmed') > -1) return 'Confirm your email first, then sign in.'; if(m.indexOf('already registered') > -1 || m.indexOf('already been registered') > -1) return 'That username is already taken. Pick another.'; if(m.indexOf('password') > -1 && m.indexOf('least') > -1) return 'Password must be at least 8 characters.'; if(m.indexOf('rate limit') > -1 || m.indexOf('too many') > -1) return 'Too many attempts. Wait a minute and try again.'; if(m.indexOf('fetch') > -1 || m.indexOf('network') > -1) return 'Network error. Check your connection.'; return (err && err.message) || 'Something went wrong. Try again.'; } function setBusy(b){ busy = b; go.disabled = b; go.textContent = b ? (mode === 'up' ? 'Creating…' : 'Signing in…') : (mode === 'up' ? 'Create account' : 'Sign in'); } form.addEventListener('submit', async function(e){ e.preventDefault(); if(busy) return; var raw = normalizeUsername(email.value), pw = pass.value; if(!validUsername(raw)){ say('Username must be 3–24 chars: a–z, 0–9, . _ -', 'err'); email.focus(); return; } if(pw.length < 8){ say('Password must be at least 8 characters.', 'err'); pass.focus(); return; } if(mode === 'up' && pw !== pass2.value){ say('Passwords do not match.', 'err'); pass2.focus(); return; } if(mode === 'up' && unameState === 'taken' && unameChecked === raw){ say('That username is already taken. Pick another.', 'err'); email.focus(); return; } setBusy(true); say(''); try{ if(mode === 'up'){ var c = await sb.from('profiles').select('id').eq('username', raw).maybeSingle(); if(c.error){ console.warn('[username check]', c.error); } else if(c.data){ unameChecked = raw; unameSet('taken'); say('That username is already taken. Pick another.', 'err'); return; } var r = await sb.auth.signUp({ email: usernameToEmail(raw), password: pw }); if(r.error){ say(friendly(r.error), 'err'); return; } if(r.data && r.data.user && r.data.user.identities && r.data.user.identities.length === 0){ say('That username is already taken. Pick another.', 'err'); return; } if(!r.data.session){ pass.value = ''; pass2.value = ''; setMode('in'); say('Account created. You can sign in now.', 'ok'); return; } } else { var s = await sb.auth.signInWithPassword({ email: usernameToEmail(raw), password: pw }); if(s.error){ say(friendly(s.error), 'err'); return; } } }catch(err){ say(friendly(err), 'err'); }finally{ setBusy(false); } }); /* ============== ROLE + ROUTING ============== */ async function fetchRole(user){ try{ var a = await sb.rpc('is_admin'); if(!a.error && typeof a.data === 'boolean') return a.data ? 'admin' : 'viewer'; if(a.error) console.warn('[is_admin]', a.error); }catch(err){ console.warn('[is_admin]', err); } try{ var r = await sb.from('profiles').select('role').eq('id', user.id).maybeSingle(); if(r.error){ console.warn('[profiles]', r.error); return 'viewer'; } return (r.data && r.data.role === 'admin') ? 'admin' : 'viewer'; }catch(err){ console.warn('[profiles]', err); return 'viewer'; } } var mountedFor = null; async function mount(session){ if(mountedFor === session.user.id) return; mountedFor = session.user.id; var role = await fetchRole(session.user); $('who').textContent = (session.user.email || '').split('@')[0]; $('role').textContent = role === 'admin' ? 'Admin' : 'Viewer'; frame.srcdoc = role === 'admin' ? PAGE_ADMIN : PAGE_VIEWER; appEl.hidden = false; authEl.classList.add('leaving'); requestAnimationFrame(function(){ requestAnimationFrame(function(){ appEl.classList.add('in'); }); }); setTimeout(function(){ if(mountedFor !== session.user.id) return; authEl.hidden = true; stopGL(); }, 550); } function showAuth(){ mountedFor = null; try{ sb.removeAllChannels(); }catch(err){ console.warn(err); } appEl.classList.remove('in'); appEl.hidden = true; frame.removeAttribute('srcdoc'); frame.src = 'about:blank'; authEl.hidden = false; authEl.classList.remove('leaving'); pass.value = ''; pass2.value = ''; startGL(); } $('out').addEventListener('click', async function(){ try{ await sb.auth.signOut(); }catch(err){ console.warn(err); } showAuth(); say('Signed out.', 'ok'); }); sb.auth.onAuthStateChange(function(event, session){ if(event === 'SIGNED_OUT'){ if(!appEl.hidden) showAuth(); return; } if(session && (event === 'SIGNED_IN' || event === 'INITIAL_SESSION')){ setTimeout(function(){ mount(session); }, 0); } }); (async function init(){ startGL(); var r = await sb.auth.getSession(); if(r.data && r.data.session){ mount(r.data.session); } else { authEl.hidden = false; } })(); })();