diff --git a/tinytorch/site/extra/community/app.js b/tinytorch/site/extra/community/app.js index 00f0a0298..9b55e7e61 100644 --- a/tinytorch/site/extra/community/app.js +++ b/tinytorch/site/extra/community/app.js @@ -2,7 +2,7 @@ import { injectStyles } from './modules/styles.js'; import { renderLayout, updateNavState } from './modules/ui.js?v=3'; import { getSession } from './modules/state.js?v=2'; import { openModal, closeModal, handleToggle, handleAuth, handleLogout, setMode, verifySession, signInWithSocial, supabase } from './modules/auth.js?v=3'; -import { openProfileModal, closeProfileModal, handleProfileUpdate, geocodeAndSetCoordinates, checkAndAutoUpdateLocation } from './modules/profile.js'; +import { openProfileModal, closeProfileModal, handleProfileUpdate, geocodeAndSetCoordinates, checkAndAutoUpdateLocation, setupProfileDeleteEvents } from './modules/profile.js'; import { setupCameraEvents } from './modules/camera.js'; import { getBasePath } from './modules/config.js'; @@ -32,6 +32,9 @@ import { getBasePath } from './modules/config.js'; }); } + // Initialize profile events + setupProfileDeleteEvents(); + // 2.6 Check for Supabase Session & Verify const checkProfile = async (session) => { if (!session || window.location.pathname.includes('profile_setup')) return; diff --git a/tinytorch/site/extra/community/events.html b/tinytorch/site/extra/community/events.html index f04fdd1b1..cbd9ec8dd 100644 --- a/tinytorch/site/extra/community/events.html +++ b/tinytorch/site/extra/community/events.html @@ -27,7 +27,7 @@ margin-bottom: 50px; width: 95%; max-width: 1200px; - background: rgba(255, 255, 255, 0.95); + background: #fff; /* Solid white background */ border: 1px solid #333; box-shadow: 8px 8px 0px rgba(0,0,0,0.1); padding: 40px; @@ -150,9 +150,11 @@ .legend { margin-top: 20px; display: flex; - gap: 20px; + flex-direction: column; + gap: 15px; font-size: 0.8rem; font-family: 'Verdana', sans-serif; + align-items: flex-start; } .legend-item { @@ -167,6 +169,104 @@ border-radius: 2px; } + /* Modal Styles */ + .modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + display: none; + justify-content: center; + align-items: center; + z-index: 2000; + backdrop-filter: blur(4px); + } + + .modal-content { + background: #fff; + width: 90%; + max-width: 500px; + border: 2px solid #333; + box-shadow: 10px 10px 0px #ff6600; + padding: 30px; + position: relative; + animation: modalSlide 0.3s ease-out; + } + + @keyframes modalSlide { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } + } + + .modal-close { + position: absolute; + top: 10px; + right: 15px; + font-size: 24px; + cursor: pointer; + font-weight: bold; + } + + .modal-title { + font-size: 1.4rem; + margin-bottom: 15px; + color: #ff6600; + border-bottom: 2px dashed #eee; + padding-bottom: 10px; + text-transform: uppercase; + } + + .modal-details { + font-family: 'Verdana', sans-serif; + font-size: 0.9rem; + line-height: 1.6; + } + + .modal-row { + margin-bottom: 12px; + } + + .modal-label { + font-weight: bold; + color: #555; + display: block; + font-size: 0.75rem; + text-transform: uppercase; + } + + .show-more { + font-size: 0.7rem; + color: #ff6600; + cursor: pointer; + font-weight: bold; + text-align: center; + display: block; + margin-top: 2px; + text-decoration: underline; + } + + .btn-google { + display: block; + width: 100%; + text-align: center; + background: #ff6600; + color: white !important; + padding: 12px; + text-decoration: none; + font-weight: bold; + margin-top: 25px; + border: 2px solid #333; + box-shadow: 4px 4px 0px #333; + transition: all 0.1s; + } + + .btn-google:hover { + transform: translate(-2px, -2px); + box-shadow: 6px 6px 0px #333; + } + /* Responsive */ @media (max-width: 700px) { .day { min-height: 60px; } @@ -202,17 +302,45 @@
-
-
- General Events +
+
+
+ General Events +
+
+
+ Historical Dates +
+
+
+ Community +
-
-
- Historical Dates -
- +
+ + + @@ -222,49 +350,111 @@ const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; - const allEvents = [ - // December 2025 events (based on user's current context) + // Static historical or community events + const staticEvents = [ { date: new Date(2025, 11, 1), description: 'Tiny Torch Community is born', type: 'community' }, { date: new Date(2025, 11, 4), description: 'NeuralIPS 2025 Kickoff', type: 'general' }, { date: new Date(2025, 11, 8), description: 'Tiny Torch merged with MLSYSBOOK.ai', type: 'community' }, { date: new Date(2025, 11, 9), description: 'Tiny Torch official launch', type: 'community' }, { date: new Date(2025, 11, 10), description: 'Ada Lovelace Birthday (1815)', type: 'historical' }, - { date: new Date(2025, 11, 17), description: 'MLSYSBOOK 10k GitHub Stars Celebration via Edge AI Foundation', type: 'community' }, + { date: new Date(2025, 11, 17), description: 'MLSYSBOOK 10k GitHub Stars', type: 'community' }, { date: new Date(2025, 11, 25), description: 'AI Winter Solstice', type: 'general' }, - // Additional AI known dates (historical or general for other months) - { date: new Date(2022, 10, 30), description: 'ChatGPT Launched', type: 'historical' }, // November 30, 2022 - { date: new Date(1956, 7, 1), description: 'Dartmouth Conference (Birth of AI)', type: 'historical' }, // August 1, 1956 - { date: new Date(1997, 4, 11), description: 'Deep Blue vs Garry Kasparov', type: 'historical' }, // May 11, 1997 - { date: new Date(2016, 2, 9), description: 'AlphaGo vs Lee Sedol', type: 'historical' }, // March 9, 2016 - { date: new Date(2025, 0, 15), description: 'Future AI Summit', type: 'general' }, // January 15, 2025 - { date: new Date(2025, 1, 20), description: 'Quantum AI Workshop', type: 'community' }, // February 20, 2025 - { date: new Date(2025, 8, 1), description: 'Generative AI Conference', type: 'general' }, // Sep 1, 2025 - { date: new Date(2025, 9, 26), description: 'AI Ethics Panel', type: 'community' } // Oct 26, 2025 + { date: new Date(2022, 10, 30), description: 'ChatGPT Launched', type: 'historical' }, + { date: new Date(1956, 7, 1), description: 'Dartmouth Conference', type: 'historical' }, + { date: new Date(1997, 4, 11), description: 'Deep Blue vs Kasparov', type: 'historical' }, + { date: new Date(2016, 2, 9), description: 'AlphaGo vs Lee Sedol', type: 'historical' } ]; - let currentYear = 2025; // Initial year (from context: Dec 1, 2025) - let currentMonth = 11; // Initial month (December is 11) + let fetchedEvents = []; + + let today = new Date(); + if (today.getFullYear() < 2026) { + today = new Date(2026, 1, 27); + } + + let currentYear = today.getFullYear(); + let currentMonth = today.getMonth(); + + async function fetchExternalEvents(year, month) { + const timeMin = new Date(year, month, 1).toISOString(); + const timeMax = new Date(year, month + 1, 1).toISOString(); + const url = `https://ai-engineering-home.vercel.app/api/calendar?timeMin=${timeMin}&timeMax=${timeMax}`; + + try { + const response = await fetch(url); + if (!response.ok) throw new Error('API request failed'); + const data = await response.json(); + return (data.events || []).map(event => ({ + date: new Date(event.start), + endDate: new Date(event.end), + description: event.summary, + fullDescription: event.description, + location: event.location, + type: 'general', + link: event.htmlLink + })); + } catch (error) { + console.error('Error fetching calendar events:', error); + return []; + } + } + + function openEventModal(event) { + const modal = document.getElementById('eventModal'); + document.getElementById('modalTitle').textContent = event.description; + + const startStr = event.date.toLocaleString([], { dateStyle: 'long', timeStyle: 'short' }); + const endStr = event.endDate ? ' to ' + event.endDate.toLocaleTimeString([], { timeStyle: 'short' }) : ''; + document.getElementById('modalWhen').textContent = startStr + endStr; + + const locRow = document.getElementById('locationRow'); + if (event.location) { + locRow.style.display = 'block'; + document.getElementById('modalWhere').textContent = event.location; + } else { + locRow.style.display = 'none'; + } + + const descRow = document.getElementById('descriptionRow'); + if (event.fullDescription) { + descRow.style.display = 'block'; + document.getElementById('modalDescription').innerHTML = event.fullDescription.replace(/\n/g, '
'); + } else { + descRow.style.display = 'none'; + } + + const linkBtn = document.getElementById('modalLink'); + if (event.link) { + linkBtn.style.display = 'inline-block'; + linkBtn.href = event.link; + } else { + linkBtn.style.display = 'none'; + } + + modal.style.display = 'flex'; + } function getDaysInMonth(year, month) { return new Date(year, month + 1, 0).getDate(); } function getFirstDayOfMonth(year, month) { - // 0 = Sunday, 1 = Monday, etc. return new Date(year, month, 1).getDay(); } - function renderCalendar() { + async function renderCalendar() { const currentMonthYearSpan = document.getElementById('currentMonthYear'); const calendarBody = document.getElementById('calendar-body'); currentMonthYearSpan.textContent = `${monthNames[currentMonth]} ${currentYear}`; - calendarBody.innerHTML = ''; // Clear previous days + + fetchedEvents = await fetchExternalEvents(currentYear, currentMonth); + + calendarBody.innerHTML = ''; const daysInMonth = getDaysInMonth(currentYear, currentMonth); - const firstDay = getFirstDayOfMonth(currentYear, currentMonth); // 0 for Sunday, 1 for Monday + const firstDay = getFirstDayOfMonth(currentYear, currentMonth); - // Fill leading empty days (from previous month) const prevMonthDays = getDaysInMonth(currentYear, currentMonth - 1); for (let i = 0; i < firstDay; i++) { const dayDiv = document.createElement('div'); @@ -273,35 +463,51 @@ calendarBody.appendChild(dayDiv); } - // Fill current month days for (let i = 1; i <= daysInMonth; i++) { const dayDiv = document.createElement('div'); dayDiv.classList.add('day'); dayDiv.innerHTML = `
${i}
`; - // Add events for this day - const dayEvents = allEvents.filter(event => + const combinedEvents = [...staticEvents, ...fetchedEvents]; + const dayEvents = combinedEvents.filter(event => event.date.getFullYear() === currentYear && event.date.getMonth() === currentMonth && event.date.getDate() === i ); - dayEvents.forEach(event => { + const maxVisible = 2; + dayEvents.slice(0, maxVisible).forEach(event => { const eventMarker = document.createElement('div'); eventMarker.classList.add('event-marker'); eventMarker.textContent = event.description; - if (event.type) { - eventMarker.classList.add(event.type); - } + if (event.type) eventMarker.classList.add(event.type); + eventMarker.onclick = (e) => { + e.stopPropagation(); + openEventModal(event); + }; dayDiv.appendChild(eventMarker); }); + if (dayEvents.length > maxVisible) { + const more = document.createElement('div'); + more.classList.add('show-more'); + more.textContent = `+ ${dayEvents.length - maxVisible} more`; + more.onclick = (e) => { + // For simplicity, just show the third event in the modal + // or could list them all. Here we'll just open the modal for the first one + // and let the user navigate or list them in the modal. + // Better: open the modal with a list if clicked "more". + // For now, let's just make the whole day clickable if there's more. + openEventModal(dayEvents[maxVisible]); + }; + dayDiv.appendChild(more); + } + calendarBody.appendChild(dayDiv); } - // Fill trailing empty days (from next month) const totalDaysRendered = firstDay + daysInMonth; - const remainingCells = 42 - totalDaysRendered; // Max 6 rows * 7 days = 42 cells + const remainingCells = 42 - totalDaysRendered; for (let i = 1; i <= remainingCells; i++) { const dayDiv = document.createElement('div'); dayDiv.classList.add('day', 'other-month'); @@ -310,7 +516,7 @@ } } - function changeMonth(delta) { + async function changeMonth(delta) { currentMonth += delta; if (currentMonth < 0) { currentMonth = 11; @@ -319,16 +525,20 @@ currentMonth = 0; currentYear++; } - renderCalendar(); + await renderCalendar(); } - // Initial render renderCalendar(); - // Event listeners for navigation buttons document.getElementById('prevMonth').addEventListener('click', () => changeMonth(-1)); document.getElementById('nextMonth').addEventListener('click', () => changeMonth(1)); - + document.getElementById('modalClose').addEventListener('click', () => { + document.getElementById('eventModal').style.display = 'none'; + }); + window.addEventListener('click', (e) => { + const modal = document.getElementById('eventModal'); + if (e.target === modal) modal.style.display = 'none'; + }); diff --git a/tinytorch/site/extra/community/modules/profile.js b/tinytorch/site/extra/community/modules/profile.js index 1ddfe1683..ae7e0c611 100644 --- a/tinytorch/site/extra/community/modules/profile.js +++ b/tinytorch/site/extra/community/modules/profile.js @@ -1,5 +1,5 @@ import { SUPABASE_URL, NETLIFY_URL, getBasePath } from './config.js'; -import { forceLogin } from './state.js?v=2'; +import { forceLogin, getSession } from './state.js?v=2'; import { initCandle } from './candle.js'; export async function geocodeAndSetCoordinates(location) { @@ -39,6 +39,21 @@ 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) { @@ -49,6 +64,79 @@ export function openProfileModal() { } } +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."); + localStorage.removeItem("tinytorch_token"); + localStorage.removeItem("tinytorch_refresh_token"); + localStorage.removeItem("tinytorch_user"); + 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'); diff --git a/tinytorch/site/extra/community/modules/styles.js b/tinytorch/site/extra/community/modules/styles.js index 8c3856d11..a71108c26 100644 --- a/tinytorch/site/extra/community/modules/styles.js +++ b/tinytorch/site/extra/community/modules/styles.js @@ -441,6 +441,33 @@ export function injectStyles() { background: #b71c1c; } + .profile-delete-btn { + width: 100%; + padding: 12px; + background: transparent; + color: #d32f2f; + border: 1px solid #d32f2f; + border-radius: 10px; + font-size: 1rem; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; + } + + .profile-delete-btn:hover:not(:disabled) { + background: #fff5f5; + } + + #profileDeleteFinalBtn { + background: #d32f2f; + color: white; + border: none; + } + + #profileDeleteFinalBtn:hover:not(:disabled) { + background: #b71c1c; + } + /* --- Mobile Optimizations --- */ @media (max-width: 768px) { /* Sidebar becomes a bottom sheet */ diff --git a/tinytorch/site/extra/community/modules/ui.js b/tinytorch/site/extra/community/modules/ui.js index 18c5479b6..7d5e6fe1f 100644 --- a/tinytorch/site/extra/community/modules/ui.js +++ b/tinytorch/site/extra/community/modules/ui.js @@ -132,15 +132,15 @@ export function renderLayout() {
-
-
-

Your Profile

+
+
+

Your Profile

- +
@@ -180,12 +180,26 @@ export function renderLayout() { + +
+

Danger Zone

+

Deleting your account is permanent and cannot be undone.

+ + + +
-
- +
+
+ +