Résumé
Resumé/CV holder
Sign in to continue.
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; }
})();
})();