mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-05-03 16:18:49 -05:00
585 lines
23 KiB
HTML
585 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Complete Profile</title>
|
|
<link rel="icon" href="assets/flame.svg" type="image/svg+xml">
|
|
<!-- Load Three.js -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<style>
|
|
/* Basic clean styling */
|
|
body {
|
|
margin: 0;
|
|
width: 100%;
|
|
min-height: 100vh;
|
|
overflow: hidden; /* Prevent body scroll, handle inside container */
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background-color: #f0f4f8;
|
|
font-family: 'Verdana', sans-serif;
|
|
color: #333;
|
|
}
|
|
|
|
#canvas-container {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100vh;
|
|
z-index: -1;
|
|
}
|
|
|
|
.container {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
backdrop-filter: blur(10px);
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
|
width: 100%;
|
|
max-width: 800px;
|
|
max-height: 85vh;
|
|
display: flex;
|
|
align-items: stretch;
|
|
overflow: hidden;
|
|
z-index: 1;
|
|
margin: 20px;
|
|
}
|
|
|
|
.form-content {
|
|
flex: 1;
|
|
padding: 40px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.candle-side {
|
|
flex: 0 0 250px;
|
|
background: rgba(252, 252, 252, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-left: 1px solid #f0f0f0;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#candleCanvas {
|
|
width: 160px;
|
|
height: auto;
|
|
aspect-ratio: 16/24;
|
|
image-rendering: pixelated;
|
|
filter: drop-shadow(4px 4px 0px rgba(0,0,0,0.05));
|
|
z-index: 2;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Mobile Responsive */
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
flex-direction: column;
|
|
max-height: 90vh;
|
|
}
|
|
.candle-side {
|
|
display: none; /* Hide candle side on mobile */
|
|
}
|
|
.form-content {
|
|
width: 100%;
|
|
padding: 25px;
|
|
}
|
|
}
|
|
|
|
h2 { margin-top: 0; color: #222; font-size: 1.5rem; margin-bottom: 8px; }
|
|
p.subtitle { color: #666; font-size: 0.9rem; margin-bottom: 25px; line-height: 1.4; }
|
|
|
|
.form-group { margin-bottom: 20px; }
|
|
|
|
label { display: block; font-weight: bold; font-size: 0.9rem; color: #444; margin-bottom: 8px; }
|
|
.optional { font-weight: normal; color: #888; font-size: 0.8rem; margin-left: 5px; }
|
|
|
|
input, textarea {
|
|
width: 100%; padding: 12px;
|
|
border: 1px solid #ddd; border-radius: 8px;
|
|
box-sizing: border-box; font-size: 1rem; font-family: inherit;
|
|
transition: border-color 0.2s;
|
|
}
|
|
input:focus, textarea:focus { border-color: #ff6600; outline: none; }
|
|
|
|
textarea { resize: vertical; min-height: 80px; }
|
|
|
|
button {
|
|
width: 100%; padding: 14px;
|
|
background: #ff6600; color: white;
|
|
border: none; border-radius: 8px;
|
|
font-weight: bold; font-size: 1.1rem;
|
|
cursor: pointer; transition: background 0.2s, transform 0.1s;
|
|
margin-top: 10px;
|
|
}
|
|
button:hover { background: #e65c00; transform: translateY(-1px); }
|
|
button:disabled { background: #ffcc99; cursor: not-allowed; transform: none; }
|
|
|
|
/* Autocomplete Dropdown */
|
|
.input-wrapper { position: relative; }
|
|
.suggestions {
|
|
position: absolute; top: 100%; left: 0; right: 0;
|
|
background: white; border: 1px solid #ccc; border-radius: 8px;
|
|
max-height: 150px; overflow-y: auto; display: none; z-index: 10;
|
|
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
|
margin-top: 4px;
|
|
}
|
|
.suggestion-item { padding: 12px; cursor: pointer; font-size: 0.9em; border-bottom: 1px solid #eee; }
|
|
.suggestion-item:last-child { border-bottom: none; }
|
|
.suggestion-item:hover { background: #fff5e6; color: #ff6600; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- 3D Background Container -->
|
|
<div id="canvas-container"></div>
|
|
|
|
<div class="container">
|
|
<div class="form-content">
|
|
<h2>🔥 Tiny 🔥 Torch: Complete Your Profile</h2>
|
|
<p class="subtitle">Tell us a bit about yourself to get started</p>
|
|
|
|
<form id="setupForm">
|
|
<div class="form-group">
|
|
<label>Display Name *</label>
|
|
<input type="text" id="displayName" required placeholder="e.g. Alex">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Full Name <span class="optional">(Optional)</span></label>
|
|
<input type="text" id="fullName" placeholder="e.g. Alex Smith">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Institution / Company *</label>
|
|
<input type="text" id="institution" required placeholder="e.g. University of Science" value="Independent">
|
|
<div style="font-size: 0.75rem; color: #888; margin-top: 4px;">Defaults to "Independent" if you are not affiliated.</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Role *</label>
|
|
<select id="roleInput" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; background: white;">
|
|
<option value="student" selected>Student</option>
|
|
<option value="educator">Educator</option>
|
|
<option value="industry">Industry</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Location (City) *</label>
|
|
<div class="input-wrapper">
|
|
<input type="text" id="locationInput" required placeholder="Start typing..." autocomplete="off">
|
|
<div id="suggestions" class="suggestions"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Summary / Bio <span class="optional">(Optional)</span></label>
|
|
<textarea id="summary" placeholder="A brief summary about yourself and your interests in AI..."></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Websites <span class="optional">(Optional, comma-separated)</span></label>
|
|
<input type="text" id="websites" placeholder="https://github.com/..., https://linkedin.com/...">
|
|
</div>
|
|
|
|
<div class="form-group" style="display: flex; align-items: center; gap: 10px;">
|
|
<input type="checkbox" id="mailingList" checked style="width: auto;">
|
|
<label for="mailingList" style="margin: 0; font-weight: normal;">Subscribe to our mailing list</label>
|
|
</div>
|
|
|
|
<input type="hidden" id="latParams">
|
|
<input type="hidden" id="lonParams">
|
|
|
|
<button type="submit" id="saveBtn">Complete Setup</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="candle-side">
|
|
<canvas id="candleCanvas" width="16" height="24"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sky Shader Vertex -->
|
|
<script id="sky-vs" type="x-shader/x-vertex">
|
|
varying vec2 vUv;
|
|
varying vec3 vWorldPosition;
|
|
void main() {
|
|
vUv = uv;
|
|
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
|
vWorldPosition = worldPosition.xyz;
|
|
gl_Position = projectionMatrix * viewMatrix * worldPosition;
|
|
}
|
|
</script>
|
|
|
|
<!-- Sky Shader Fragment -->
|
|
<script id="sky-fs" type="x-shader/x-fragment">
|
|
uniform float uTime;
|
|
uniform vec3 uColor;
|
|
uniform float uCloudDramatic;
|
|
varying vec2 vUv;
|
|
varying vec3 vWorldPosition;
|
|
|
|
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
|
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
|
|
vec4 permute(vec4 x) { return mod289(((x*34.0)+1.0)*x); }
|
|
vec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; }
|
|
|
|
float snoise(vec3 v){
|
|
const vec2 C = vec2(1.0/6.0, 1.0/3.0) ;
|
|
const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
|
|
vec3 i = floor(v + dot(v, C.yyy) );
|
|
vec3 x0 = v - i + dot(i, C.xxx) ;
|
|
vec3 g = step(x0.yzx, x0.xyz);
|
|
vec3 l = 1.0 - g;
|
|
vec3 i1 = min( g.xyz, l.zxy );
|
|
vec3 i2 = max( g.xyz, l.zxy );
|
|
vec3 x1 = x0 - i1 + C.xxx;
|
|
vec3 x2 = x0 - i2 + C.yyy;
|
|
vec3 x3 = x0 - D.yyy;
|
|
i = mod289(i);
|
|
vec4 p = permute( permute( permute(
|
|
i.z + vec4(0.0, i1.z, i2.z, 1.0 ))
|
|
+ i.y + vec4(0.0, i1.y, i2.y, 1.0 ))
|
|
+ i.x + vec4(0.0, i1.x, i2.x, 1.0 ));
|
|
float n_ = 0.142857142857;
|
|
vec3 ns = n_ * D.wyz - D.xzx;
|
|
vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
|
|
vec4 x_ = floor(j * ns.z);
|
|
vec4 y_ = floor(j - 7.0 * x_ );
|
|
vec4 x = x_ *ns.x + ns.yyyy;
|
|
vec4 y = y_ *ns.x + ns.yyyy;
|
|
vec4 h = 1.0 - abs(x) - abs(y);
|
|
vec4 b0 = vec4( x.xy, y.xy );
|
|
vec4 b1 = vec4( x.zw, y.zw );
|
|
vec4 s0 = floor(b0)*2.0 + 1.0;
|
|
vec4 s1 = floor(b1)*2.0 + 1.0;
|
|
vec4 sh = -step(h, vec4(0.0));
|
|
vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ;
|
|
vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ;
|
|
vec3 p0 = vec3(a0.xy,h.x);
|
|
vec3 p1 = vec3(a0.zw,h.y);
|
|
vec3 p2 = vec3(a1.xy,h.z);
|
|
vec3 p3 = vec3(a1.zw,h.w);
|
|
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
|
|
p0 *= norm.x;
|
|
p1 *= norm.y;
|
|
p2 *= norm.z;
|
|
p3 *= norm.w;
|
|
vec4 m = max(0.5 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
|
|
m = m * m;
|
|
return 105.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1),
|
|
dot(p2,x2), dot(p3,x3) ) );
|
|
}
|
|
|
|
float fbm(vec3 p) {
|
|
float value = 0.0;
|
|
float amplitude = 0.5;
|
|
for (int i = 0; i < 5; i++) {
|
|
value += amplitude * snoise(p);
|
|
p *= 2.0;
|
|
amplitude *= 0.5;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
void main() {
|
|
vec3 pos = normalize(vWorldPosition);
|
|
vec3 motion = vec3(uTime * 0.02, 0.0, 0.0);
|
|
vec3 warp = vec3(
|
|
fbm(pos * 2.0 + vec3(uTime * 0.05)),
|
|
fbm(pos * 2.0 + vec3(uTime * 0.05 + 10.0)),
|
|
fbm(pos * 2.0 + vec3(uTime * 0.05 + 20.0))
|
|
);
|
|
float n = fbm(pos * 3.0 + motion + warp * 0.5);
|
|
float cloudVal = (n + 1.0) * 0.5;
|
|
float dramFactor = (1.0 - uCloudDramatic) * 0.2;
|
|
cloudVal = smoothstep(0.45 - dramFactor, 0.65 + dramFactor, cloudVal);
|
|
vec3 baseColor = uColor;
|
|
float intensity = uCloudDramatic * uCloudDramatic;
|
|
vec3 cloudHighlight = baseColor + vec3(0.2, 0.2, 0.25) * intensity;
|
|
cloudHighlight = min(cloudHighlight, vec3(1.0));
|
|
vec3 finalColor = mix(baseColor, cloudHighlight, cloudVal);
|
|
gl_FragColor = vec4(finalColor, 1.0);
|
|
}
|
|
</script>
|
|
|
|
<script type="module">
|
|
import { SUPABASE_PROJECT_URL, SUPABASE_ANON_KEY, SUPABASE_URL, getBasePath } from './modules/config.js';
|
|
import { initCandle } from './modules/candle.js';
|
|
import { supabase } from './modules/state.js';
|
|
import './modules/guard.js'; // Guard handles access control and completeness redirects
|
|
|
|
// Initialize Candle
|
|
initCandle('candleCanvas');
|
|
|
|
// --- THREE.JS BACKGROUND ---
|
|
const Noise = (function() {
|
|
var p = [151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
|
|
190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,
|
|
68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
|
|
102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,
|
|
186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
|
|
223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,
|
|
178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,
|
|
214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,
|
|
195,78,66,215,61,156,180];
|
|
for (var i=0; i < 256 ; i++) p[256+i] = p[i];
|
|
function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
|
|
function lerp(t, a, b) { return a + t * (b - a); }
|
|
function grad(hash, x, y, z) {
|
|
var h = hash & 15;
|
|
var u = h<8 ? x : y, v = h<4 ? y : h==12||h==14 ? x : z;
|
|
return ((h&1) == 0 ? u : -u) + ((h&2) == 0 ? v : -v);
|
|
}
|
|
return {
|
|
perlin2: function(x, y) {
|
|
var X = Math.floor(x) & 255, Y = Math.floor(y) & 255;
|
|
x -= Math.floor(x); y -= Math.floor(y);
|
|
var u = fade(x), v = fade(y);
|
|
var A = p[X]+Y, AA = p[A], AB = p[A+1], B = p[X+1]+Y, BA = p[B], BB = p[B+1];
|
|
return lerp(v, lerp(u, grad(p[AA], x, y, 0), grad(p[BA], x-1, y, 0)),
|
|
lerp(u, grad(p[AB], x, y-1, 0), grad(p[BB], x-1, y-1, 0)));
|
|
}
|
|
};
|
|
})();
|
|
|
|
const container = document.getElementById('canvas-container');
|
|
const scene = new THREE.Scene();
|
|
const initialBgColor = new THREE.Color(0xf0f4f8);
|
|
scene.fog = new THREE.Fog(initialBgColor, 20, 90);
|
|
|
|
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 150);
|
|
camera.position.set(0, 6, 12);
|
|
camera.rotation.x = -Math.PI / 6;
|
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
container.appendChild(renderer.domElement);
|
|
|
|
const skyGeo = new THREE.SphereGeometry(100, 64, 64);
|
|
const skyMat = new THREE.ShaderMaterial({
|
|
vertexShader: document.getElementById('sky-vs').textContent,
|
|
fragmentShader: document.getElementById('sky-fs').textContent,
|
|
uniforms: {
|
|
uTime: { value: 0 },
|
|
uColor: { value: new THREE.Color(0xf0f4f8) },
|
|
uCloudDramatic: { value: 1.0 }
|
|
},
|
|
side: THREE.BackSide,
|
|
fog: false
|
|
});
|
|
const skyMesh = new THREE.Mesh(skyGeo, skyMat);
|
|
scene.add(skyMesh);
|
|
|
|
const width = 60;
|
|
const depth = 80;
|
|
const widthSegments = 80;
|
|
const depthSegments = 100;
|
|
const geometry = new THREE.PlaneGeometry(width, depth, widthSegments, depthSegments);
|
|
geometry.rotateX(-Math.PI / 2);
|
|
|
|
const terrainMat = new THREE.ShaderMaterial({
|
|
vertexShader: document.getElementById('terrain-vs').textContent,
|
|
fragmentShader: document.getElementById('terrain-fs').textContent,
|
|
uniforms: {
|
|
uTime: { value: 0 },
|
|
uNoiseScale: { value: 0.15 },
|
|
uHeightScale: { value: 4.0 }
|
|
},
|
|
wireframe: true,
|
|
transparent: true
|
|
});
|
|
|
|
const terrain = new THREE.Mesh(geometry, terrainMat);
|
|
scene.add(terrain);
|
|
|
|
// Solid base layer
|
|
const solidMat = new THREE.ShaderMaterial({
|
|
vertexShader: document.getElementById('terrain-vs').textContent,
|
|
fragmentShader: document.getElementById('terrain-fs').textContent,
|
|
uniforms: {
|
|
uTime: { value: 0 },
|
|
uNoiseScale: { value: 0.15 },
|
|
uHeightScale: { value: 4.0 }
|
|
},
|
|
polygonOffset: true,
|
|
polygonOffsetFactor: 1,
|
|
polygonOffsetUnits: 1
|
|
});
|
|
const terrainSolid = new THREE.Mesh(geometry, solidMat);
|
|
scene.add(terrainSolid);
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
const time = performance.now() * 0.001;
|
|
skyMesh.material.uniforms.uTime.value = time;
|
|
terrain.material.uniforms.uTime.value = time;
|
|
terrainSolid.material.uniforms.uTime.value = time;
|
|
renderer.render(scene, camera);
|
|
}
|
|
animate();
|
|
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
|
|
|
|
// --- AUTH & PROFILE LOGIC ---
|
|
// supabase is imported from state.js above
|
|
|
|
// --- 1. AUTOCOMPLETE LOGIC ---
|
|
const locInput = document.getElementById('locationInput');
|
|
const box = document.getElementById('suggestions');
|
|
let timer;
|
|
|
|
locInput.addEventListener('input', (e) => {
|
|
clearTimeout(timer);
|
|
document.getElementById('latParams').value = "";
|
|
document.getElementById('lonParams').value = "";
|
|
if(e.target.value.length < 3) { box.style.display = 'none'; return; }
|
|
timer = setTimeout(async () => {
|
|
try {
|
|
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(e.target.value)}`);
|
|
const data = await res.json();
|
|
box.innerHTML = '';
|
|
if(data.length) box.style.display = 'block';
|
|
data.slice(0,5).forEach(place => {
|
|
const div = document.createElement('div');
|
|
div.className = 'suggestion-item';
|
|
div.textContent = place.display_name;
|
|
div.onclick = () => {
|
|
locInput.value = place.display_name;
|
|
document.getElementById('latParams').value = place.lat;
|
|
document.getElementById('lonParams').value = place.lon;
|
|
box.style.display = 'none';
|
|
};
|
|
box.appendChild(div);
|
|
});
|
|
} catch(e) { console.error(e); }
|
|
}, 300);
|
|
});
|
|
|
|
// --- 2. PRE-FILL LOGIC ---
|
|
async function loadUser(user) {
|
|
if (!user) return;
|
|
|
|
// 1. Try to grab name from Google/GitHub metadata
|
|
if(user.user_metadata) {
|
|
if(user.user_metadata.full_name) {
|
|
document.getElementById('fullName').value = user.user_metadata.full_name;
|
|
const parts = user.user_metadata.full_name.split(' ');
|
|
if(parts.length > 0 && !document.getElementById('displayName').value) {
|
|
document.getElementById('displayName').value = parts[0];
|
|
}
|
|
}
|
|
if(user.user_metadata.name && !document.getElementById('fullName').value) {
|
|
document.getElementById('fullName').value = user.user_metadata.name;
|
|
}
|
|
}
|
|
|
|
// 2. Load Profile Data from DB
|
|
const { data: profile } = await supabase.from('profiles').select('*').eq('id', user.id).single();
|
|
if(profile) populateForm(profile);
|
|
}
|
|
|
|
function populateForm(profile) {
|
|
if(profile.display_name) document.getElementById('displayName').value = profile.display_name;
|
|
if(profile.full_name) document.getElementById('fullName').value = profile.full_name;
|
|
if(profile.location) document.getElementById('locationInput').value = profile.location;
|
|
if(profile.role) document.getElementById('roleInput').value = profile.role;
|
|
|
|
let inst = "Independent";
|
|
if(profile.institution) {
|
|
const dbInst = Array.isArray(profile.institution) ? profile.institution[0] : profile.institution;
|
|
if(dbInst && dbInst.trim() !== "") inst = dbInst;
|
|
}
|
|
document.getElementById('institution').value = inst;
|
|
if(profile.bio || profile.summary) document.getElementById('summary').value = profile.bio || profile.summary;
|
|
if(profile.website || profile.websites) {
|
|
const sites = profile.website || profile.websites;
|
|
document.getElementById('websites').value = Array.isArray(sites) ? sites.join(', ') : sites;
|
|
}
|
|
if(profile.mailing_list !== undefined && profile.mailing_list !== null) {
|
|
document.getElementById('mailingList').checked = profile.mailing_list;
|
|
}
|
|
}
|
|
|
|
// Listen for auth state to trigger load
|
|
supabase.auth.onAuthStateChange((event, session) => {
|
|
if (session && session.user) {
|
|
loadUser(session.user);
|
|
}
|
|
});
|
|
|
|
// Initial load check
|
|
supabase.auth.getUser().then(({ data: { user } }) => {
|
|
if (user) loadUser(user);
|
|
});
|
|
|
|
// --- 3. SUBMIT LOGIC ---
|
|
document.getElementById('setupForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('saveBtn');
|
|
btn.textContent = "Saving Profile...";
|
|
btn.disabled = true;
|
|
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
if (!session) {
|
|
alert("Session lost. Please log in again.");
|
|
window.location.href = getBasePath() + '/index.html?action=login';
|
|
return;
|
|
}
|
|
|
|
const instVal = document.getElementById('institution').value.trim();
|
|
const institutions = instVal ? [instVal] : ["Independent"];
|
|
const webVal = document.getElementById('websites').value;
|
|
const websites = webVal.split(',').map(s => s.trim()).filter(s => s);
|
|
|
|
try {
|
|
const res = await fetch(`${SUPABASE_URL}/update-profile`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${session.access_token}`
|
|
},
|
|
body: JSON.stringify({
|
|
display_name: document.getElementById('displayName').value,
|
|
full_name: document.getElementById('fullName').value,
|
|
institution: institutions,
|
|
role: document.getElementById('roleInput').value,
|
|
location: locInput.value,
|
|
bio: document.getElementById('summary').value,
|
|
website: websites,
|
|
mailing_list: document.getElementById('mailingList').checked,
|
|
latitude: document.getElementById('latParams').value ? parseFloat(document.getElementById('latParams').value) : undefined,
|
|
longitude: document.getElementById('lonParams').value ? parseFloat(document.getElementById('lonParams').value) : undefined
|
|
})
|
|
});
|
|
|
|
if(!res.ok) throw new Error("Save failed");
|
|
|
|
// Success! Guard will now see profile as complete and allow dashboard access
|
|
window.location.href = getBasePath() + '/dashboard.html';
|
|
|
|
} catch(err) {
|
|
console.error(err);
|
|
alert("Error saving. Please try again.");
|
|
btn.textContent = "Complete Setup";
|
|
btn.disabled = false;
|
|
}
|
|
});
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if(e.target !== locInput) box.style.display = 'none';
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|