Files
cs249r_book/tinytorch/site/extra/community/modules/profile.js
2026-02-27 17:06:38 -05:00

466 lines
18 KiB
JavaScript

import { SUPABASE_URL, NETLIFY_URL, getBasePath } from './config.js';
import { forceLogin, getSession, clearSession } from './state.js';
import { initCandle } from './candle.js';
export async function geocodeAndSetCoordinates(location) {
const latInput = document.getElementById('profileLatitude');
const lonInput = document.getElementById('profileLongitude');
if (!location) {
if(latInput) latInput.value = '';
if(lonInput) lonInput.value = '';
return;
}
try {
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(location)}&limit=1`);
if (!response.ok) {
throw new Error('Geocoding search failed');
}
const data = await response.json();
if (data && data.length > 0) {
const { lat, lon } = data[0];
if(latInput) latInput.value = lat;
if(lonInput) lonInput.value = lon;
} else {
console.warn(`Could not geocode '${location}'`);
if(latInput) latInput.value = '';
if(lonInput) lonInput.value = '';
}
} catch (error) {
console.error('Geocoding error:', error);
if(latInput) latInput.value = '';
if(lonInput) lonInput.value = '';
}
}
let candleInitialized = false;
export function openProfileModal() {
const profileOverlay = document.getElementById('profileOverlay');
profileOverlay.classList.add('active');
// Reset delete section
const deleteBtn = document.getElementById('profileDeleteBtn');
const deleteConfirmSection = document.getElementById('deleteConfirmSection');
const deleteConfirmInput = document.getElementById('profileDeleteConfirmInput');
const deleteFinalBtn = document.getElementById('profileDeleteFinalBtn');
if (deleteBtn) deleteBtn.classList.remove('hidden');
if (deleteConfirmSection) deleteConfirmSection.classList.add('hidden');
if (deleteConfirmInput) deleteConfirmInput.value = '';
if (deleteFinalBtn) {
deleteFinalBtn.disabled = true;
deleteFinalBtn.style.opacity = '0.5';
deleteFinalBtn.style.cursor = 'not-allowed';
}
fetchUserProfile();
if (!candleInitialized) {
setTimeout(() => {
initCandle('profileCandleCanvas');
candleInitialized = true;
}, 100);
}
}
export function setupProfileDeleteEvents() {
const deleteBtn = document.getElementById('profileDeleteBtn');
const deleteConfirmSection = document.getElementById('deleteConfirmSection');
const deleteConfirmName = document.getElementById('deleteConfirmName');
const deleteConfirmInput = document.getElementById('profileDeleteConfirmInput');
const deleteFinalBtn = document.getElementById('profileDeleteFinalBtn');
const profileDisplayNameInput = document.getElementById('profileDisplayName');
if (!deleteBtn) return;
deleteBtn.addEventListener('click', () => {
let displayName = profileDisplayNameInput.value.trim();
if (!displayName) {
const { email } = getSession();
displayName = email || 'DELETE';
}
deleteConfirmName.textContent = displayName;
deleteConfirmSection.classList.remove('hidden');
deleteBtn.classList.add('hidden');
});
deleteConfirmInput.addEventListener('input', () => {
const displayName = deleteConfirmName.textContent.trim();
if (deleteConfirmInput.value.trim() === displayName) {
deleteFinalBtn.disabled = false;
deleteFinalBtn.style.opacity = '1';
deleteFinalBtn.style.cursor = 'pointer';
} else {
deleteFinalBtn.disabled = true;
deleteFinalBtn.style.opacity = '0.5';
deleteFinalBtn.style.cursor = 'not-allowed';
}
});
deleteFinalBtn.addEventListener('click', async () => {
const confirmResult = confirm("Are you absolutely sure? This will delete all your data and access.");
if (!confirmResult) return;
deleteFinalBtn.disabled = true;
deleteFinalBtn.textContent = 'Deleting...';
const token = localStorage.getItem("tinytorch_token");
try {
const response = await fetch(`${SUPABASE_URL}/delete-account`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
let errData = {};
try {
errData = await response.json();
} catch(e) {}
throw new Error(errData.error || 'Failed to delete account');
}
alert("Your account has been deleted.");
await clearSession();
window.location.href = getBasePath() + '/index.html';
} catch (error) {
console.error("Delete account error:", error);
alert("Error deleting account: " + error.message);
deleteFinalBtn.disabled = false;
deleteFinalBtn.textContent = 'Permanently Delete My Account';
}
});
}
export function closeProfileModal() {
const profileOverlay = document.getElementById('profileOverlay');
profileOverlay.classList.remove('active');
}
export async function getProfileData(token) {
if (!token) return null;
let retryCount = 0;
const MAX_RETRIES = 1;
do {
try {
const response = await fetch(`${SUPABASE_URL}/get-profile-details`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
let needsRefresh = response.status === 401;
let errorData = null;
if (response.status === 400) {
try {
errorData = await response.clone().json();
if (errorData && errorData.error && errorData.error.includes("Invalid Token")) {
needsRefresh = true;
}
} catch(e) { }
}
if (needsRefresh && retryCount === 0) {
console.log("Token expired or invalid (400/401). Attempting refresh...");
const refreshToken = localStorage.getItem("tinytorch_refresh_token");
if (!refreshToken) return null;
const refreshRes = await fetch(`${NETLIFY_URL}/api/auth/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken })
});
if (!refreshRes.ok) return null;
const refreshData = await refreshRes.json();
const session = refreshData.session || refreshData;
if (session && session.access_token) {
token = session.access_token;
localStorage.setItem("tinytorch_token", token);
if (session.refresh_token) {
localStorage.setItem("tinytorch_refresh_token", session.refresh_token);
}
retryCount++;
continue;
} else {
return null;
}
}
if (!response.ok) {
throw new Error(`Failed to fetch profile data: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Error fetching user profile:", error);
if (retryCount >= MAX_RETRIES) return null;
}
} while (retryCount < MAX_RETRIES);
return null;
}
export async function fetchUserProfile() {
let token = localStorage.getItem("tinytorch_token");
if (!token) {
console.error("No token found for fetching profile.");
forceLogin();
return;
}
const profileData = await getProfileData(token);
if (profileData) {
populateProfileForm(profileData.profile);
} else {
alert("Failed to load profile data. Please try again.");
closeProfileModal();
}
}
async function updateProfileLocation(token, updates) {
try {
const response = await fetch(`${SUPABASE_URL}/update-profile`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
});
if (response.ok) {
console.log("Location auto-updated successfully");
} else {
console.warn("Failed to auto-update location");
}
} catch (e) {
console.error("Error auto-updating location:", e);
}
}
export async function checkAndAutoUpdateLocation() {
const token = localStorage.getItem("tinytorch_token");
if (!token) return;
// Check if we already checked in this session to avoid spamming
if (sessionStorage.getItem("tinytorch_location_checked")) return;
try {
const data = await getProfileData(token);
if (!data || !data.profile) return;
const p = data.profile;
const isPlaceholder = p.location && (p.location.includes("Lost at Sea") || p.location.includes("🌊"));
// If location is missing or placeholder, try to find it
if (isPlaceholder || (!p.location && (!p.latitude || !p.longitude))) {
console.log("Location missing or placeholder, attempting auto-detection...");
// Try HTTPS first (ipapi.co)
let found = false;
try {
const res = await fetch('https://ipapi.co/json/');
if (res.ok) {
const geo = await res.json();
if (geo.city && geo.latitude && geo.longitude) {
const locationStr = `${geo.city}, ${geo.country_name}`;
await updateProfileLocation(token, {
location: locationStr,
latitude: geo.latitude,
longitude: geo.longitude
});
found = true;
}
}
} catch (e) {
console.warn("Primary geo lookup failed:", e);
}
if (!found) {
// Fallback (might fail on HTTPS sites due to mixed content if using http)
try {
const res = await fetch('http://ip-api.com/json/');
if (res.ok) {
const geo = await res.json();
if (geo.city && geo.lat && geo.lon) {
const locationStr = `${geo.city}, ${geo.country}`;
await updateProfileLocation(token, {
location: locationStr,
latitude: geo.lat,
longitude: geo.lon
});
}
}
} catch (e) {
console.warn("Secondary geo lookup failed:", e);
}
}
}
// Mark as checked for this session
sessionStorage.setItem("tinytorch_location_checked", "true");
} catch (e) {
console.error("Auto-location check failed:", e);
}
}
function populateProfileForm(data) {
const profileDisplayNameInput = document.getElementById('profileDisplayName');
const profileIsPublicCheckbox = document.getElementById('profileIsPublic');
const profileFullNameInput = document.getElementById('profileFullName');
const profileSummaryTextarea = document.getElementById('profileSummary');
const profileLocationInput = document.getElementById('profileLocation');
const profileInstitutionInput = document.getElementById('profileInstitution');
const profileRoleInput = document.getElementById('profileRole');
const profileWebsitesInput = document.getElementById('profileWebsites');
const profileMailingList = document.getElementById('profileMailingList');
const profileLatitude = document.getElementById('profileLatitude');
const profileLongitude = document.getElementById('profileLongitude');
profileDisplayNameInput.value = data.display_name || '';
profileFullNameInput.value = data.full_name || '';
profileSummaryTextarea.value = data.bio || data.summary || '';
profileLocationInput.value = data.location || '';
if (profileLatitude) profileLatitude.value = data.latitude || '';
if (profileLongitude) profileLongitude.value = data.longitude || '';
if (profileRoleInput) {
profileRoleInput.value = data.role || 'student';
}
profileInstitutionInput.value = Array.isArray(data.institution) ? data.institution.join(', ') : (data.institution || '');
const sites = data.website || data.websites;
profileWebsitesInput.value = Array.isArray(sites) ? sites.join(', ') : (sites || '');
if (profileMailingList && data.mailing_list !== undefined && data.mailing_list !== null) {
profileMailingList.checked = data.mailing_list;
}
}
export async function handleProfileUpdate(e) {
e.preventDefault();
let token = localStorage.getItem("tinytorch_token");
if (!token) {
console.error("No token found for updating profile.");
forceLogin();
return;
}
const profileDisplayNameInput = document.getElementById('profileDisplayName');
const profileFullNameInput = document.getElementById('profileFullName');
const profileSummaryTextarea = document.getElementById('profileSummary');
const profileLocationInput = document.getElementById('profileLocation');
const profileRoleInput = document.getElementById('profileRole');
const profileInstitutionInput = document.getElementById('profileInstitution');
const profileWebsitesInput = document.getElementById('profileWebsites');
const profileMailingList = document.getElementById('profileMailingList');
const profileLatitude = document.getElementById('profileLatitude');
const profileLongitude = document.getElementById('profileLongitude');
const updatedProfile = {
display_name: profileDisplayNameInput.value,
full_name: profileFullNameInput.value,
summary: profileSummaryTextarea.value,
role: profileRoleInput ? profileRoleInput.value : 'student',
location: profileLocationInput.value,
institution: profileInstitutionInput.value.split(',').map(s => s.trim()).filter(s => s),
website: profileWebsitesInput.value.split(',').map(s => s.trim()).filter(s => s),
mailing_list: profileMailingList ? profileMailingList.checked : true,
latitude: profileLatitude && profileLatitude.value ? parseFloat(profileLatitude.value) : null,
longitude: profileLongitude && profileLongitude.value ? parseFloat(profileLongitude.value) : null,
};
let retryCount = 0;
const MAX_RETRIES = 1;
do {
try {
const response = await fetch(`${SUPABASE_URL}/update-profile`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(updatedProfile)
});
let needsRefresh = response.status === 401;
let errorData = null;
if (response.status === 400) {
try {
errorData = await response.clone().json();
if (errorData && errorData.error && errorData.error.includes("Invalid Token")) {
needsRefresh = true;
}
} catch(e) {}
}
if (needsRefresh && retryCount === 0) {
console.log("Token expired or invalid during update (400/401). Attempting refresh...");
const refreshToken = localStorage.getItem("tinytorch_refresh_token");
if (!refreshToken) { forceLogin(); return; }
const refreshRes = await fetch(`${NETLIFY_URL}/api/auth/refresh`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken })
});
if (!refreshRes.ok) { forceLogin(); return; }
const refreshData = await refreshRes.json();
const session = refreshData.session || refreshData;
if (session && session.access_token) {
token = session.access_token;
localStorage.setItem("tinytorch_token", token);
if (session.refresh_token) {
localStorage.setItem("tinytorch_refresh_token", session.refresh_token);
}
retryCount++;
continue;
} else {
console.warn("Refresh failed: No access token in response", refreshData);
forceLogin();
return;
}
}
if (!response.ok) {
if (!errorData) {
try { errorData = await response.json(); } catch(e) {}
}
throw new Error(errorData?.error || 'Failed to update profile');
}
alert('Profile updated successfully!');
closeProfileModal();
// Check if 'community' param is present in the URL
const params = new URLSearchParams(window.location.search);
if (params.get('community')) {
window.location.href = getBasePath() + '/community.html';
}
return;
} catch (error) {
console.error("Error updating user profile:", error);
alert("Failed to update profile: " + error.message);
return;
}
} while (retryCount < MAX_RETRIES);
}