| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 'use strict'; |
|
|
| |
| |
| |
| |
| const ErrorHandler = (() => { |
| const isDevelopment = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; |
|
|
| |
| |
| |
| |
| |
| const logError = (context, error) => { |
| if (isDevelopment) { |
| console.error(`[Valentine App Error - ${context}]:`, error); |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| const handleError = (context, error, fallback = null) => { |
| logError(context, error); |
|
|
| if (fallback && typeof fallback === 'function') { |
| try { |
| fallback(); |
| } catch (fallbackError) { |
| logError(`${context} - Fallback`, fallbackError); |
| } |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| const safeExecute = (fn, context) => { |
| return (...args) => { |
| try { |
| return fn(...args); |
| } catch (error) { |
| handleError(context, error); |
| return null; |
| } |
| }; |
| }; |
|
|
| return { |
| logError, |
| handleError, |
| safeExecute |
| }; |
| })(); |
|
|
| |
| |
| |
| |
| const PerformanceMonitor = (() => { |
| const metrics = { |
| animationFrames: 0, |
| droppedFrames: 0, |
| lastFrameTime: 0 |
| }; |
|
|
| let isMonitoring = false; |
| let monitoringId = null; |
|
|
| |
| |
| |
| const monitorFrameRate = () => { |
| if (!isMonitoring) return; |
|
|
| const now = performance.now(); |
|
|
| if (metrics.lastFrameTime > 0) { |
| const delta = now - metrics.lastFrameTime; |
| const fps = 1000 / delta; |
|
|
| |
| if (fps < 55) { |
| metrics.droppedFrames++; |
| } |
|
|
| metrics.animationFrames++; |
| } |
|
|
| metrics.lastFrameTime = now; |
| monitoringId = requestAnimationFrame(monitorFrameRate); |
| }; |
|
|
| |
| |
| |
| const startMonitoring = () => { |
| if (isMonitoring) return; |
| isMonitoring = true; |
| monitorFrameRate(); |
| }; |
|
|
| |
| |
| |
| const stopMonitoring = () => { |
| isMonitoring = false; |
| if (monitoringId) { |
| cancelAnimationFrame(monitoringId); |
| monitoringId = null; |
| } |
| }; |
|
|
| |
| |
| |
| |
| const getMetrics = () => { |
| return { |
| ...metrics, |
| averageFPS: metrics.animationFrames > 0 |
| ? Math.round((metrics.animationFrames - metrics.droppedFrames) / metrics.animationFrames * 60) |
| : 60 |
| }; |
| }; |
|
|
| |
| |
| |
| |
| const supportsHardwareAcceleration = () => { |
| const canvas = document.createElement('canvas'); |
| const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); |
| return !!gl; |
| }; |
|
|
| return { |
| startMonitoring, |
| stopMonitoring, |
| getMetrics, |
| supportsHardwareAcceleration |
| }; |
| })(); |
|
|
| |
| |
| |
| |
| const BrowserCompatibility = (() => { |
| |
| |
| |
| |
| const checkFeatures = () => { |
| return { |
| canvas: !!document.createElement('canvas').getContext, |
| requestAnimationFrame: typeof requestAnimationFrame !== 'undefined', |
| transform3d: (() => { |
| const el = document.createElement('div'); |
| const transforms = { |
| 'transform': 'transform', |
| 'WebkitTransform': '-webkit-transform', |
| 'MozTransform': '-moz-transform', |
| 'msTransform': '-ms-transform' |
| }; |
|
|
| for (let t in transforms) { |
| if (el.style[t] !== undefined) { |
| return true; |
| } |
| } |
| return false; |
| })(), |
| backdropFilter: CSS.supports('backdrop-filter', 'blur(10px)') || |
| CSS.supports('-webkit-backdrop-filter', 'blur(10px)'), |
| touchEvents: 'ontouchstart' in window || navigator.maxTouchPoints > 0, |
| passiveEvents: (() => { |
| let supportsPassive = false; |
| try { |
| const opts = Object.defineProperty({}, 'passive', { |
| get: () => { supportsPassive = true; } |
| }); |
| window.addEventListener('testPassive', null, opts); |
| window.removeEventListener('testPassive', null, opts); |
| } catch (e) { } |
| return supportsPassive; |
| })() |
| }; |
| }; |
|
|
| |
| |
| |
| |
| const applyFallbacks = (features) => { |
| |
| if (!features.requestAnimationFrame) { |
| window.requestAnimationFrame = (callback) => { |
| return setTimeout(callback, 1000 / 60); |
| }; |
| window.cancelAnimationFrame = (id) => { |
| clearTimeout(id); |
| }; |
| } |
|
|
| |
| if (!features.transform3d) { |
| document.documentElement.classList.add('no-transforms'); |
| } |
|
|
| |
| if (!features.backdropFilter) { |
| document.documentElement.classList.add('no-backdrop-filter'); |
| } |
| }; |
|
|
| |
| |
| |
| const init = () => { |
| const features = checkFeatures(); |
| applyFallbacks(features); |
| return features; |
| }; |
|
|
| return { |
| init, |
| checkFeatures |
| }; |
| })(); |
|
|
| |
| |
| |
| |
| const AnimationController = (() => { |
| |
| let canvas = null; |
| let ctx = null; |
| let particles = []; |
| let animationId = null; |
| let isConfettiActive = false; |
|
|
| |
| |
| |
| |
| class HeartParticle { |
| constructor(x, y) { |
| this.x = x; |
| this.y = y; |
| this.size = Math.random() * 20 + 10; |
| this.speedX = (Math.random() - 0.5) * 8; |
| this.speedY = Math.random() * -12 - 5; |
| this.gravity = 0.3; |
| this.rotation = Math.random() * Math.PI * 2; |
| this.rotationSpeed = (Math.random() - 0.5) * 0.2; |
| this.opacity = 1; |
| this.fadeSpeed = 0.008 + Math.random() * 0.005; |
|
|
| |
| const colors = ['#8b0000', '#ff6b81', '#ffb6c1', '#d4af37', '#ff4757']; |
| this.color = colors[Math.floor(Math.random() * colors.length)]; |
| } |
|
|
| |
| |
| |
| update() { |
| this.speedY += this.gravity; |
| this.x += this.speedX; |
| this.y += this.speedY; |
| this.rotation += this.rotationSpeed; |
| this.opacity -= this.fadeSpeed; |
|
|
| |
| this.x += Math.sin(this.rotation) * 0.5; |
| } |
|
|
| |
| |
| |
| |
| draw(ctx) { |
| if (this.opacity <= 0) return; |
|
|
| ctx.save(); |
| ctx.translate(this.x, this.y); |
| ctx.rotate(this.rotation); |
| ctx.globalAlpha = this.opacity; |
| ctx.fillStyle = this.color; |
|
|
| |
| ctx.beginPath(); |
| const s = this.size / 15; |
| ctx.moveTo(0, s * 3); |
| ctx.bezierCurveTo(-s * 5, -s * 2, -s * 5, -s * 7, 0, -s * 5); |
| ctx.bezierCurveTo(s * 5, -s * 7, s * 5, -s * 2, 0, s * 3); |
| ctx.fill(); |
|
|
| ctx.restore(); |
| } |
|
|
| |
| |
| |
| |
| isAlive() { |
| return this.opacity > 0; |
| } |
| } |
|
|
| |
| |
| |
| const initCanvas = ErrorHandler.safeExecute(() => { |
| canvas = document.getElementById('confetti-canvas'); |
| if (!canvas) { |
| throw new Error('Confetti canvas element not found'); |
| } |
|
|
| |
| if (!canvas.getContext) { |
| ErrorHandler.logError('Canvas', new Error('Canvas not supported')); |
| return; |
| } |
|
|
| ctx = canvas.getContext('2d', { alpha: true }); |
| resizeCanvas(); |
|
|
| |
| window.addEventListener('resize', resizeCanvas, { passive: true }); |
| }, 'Canvas Initialization'); |
|
|
| |
| |
| |
| const resizeCanvas = ErrorHandler.safeExecute(() => { |
| if (!canvas) return; |
|
|
| |
| const dpr = window.devicePixelRatio || 1; |
| const rect = canvas.getBoundingClientRect(); |
|
|
| canvas.width = rect.width * dpr; |
| canvas.height = rect.height * dpr; |
|
|
| |
| if (ctx) { |
| ctx.scale(dpr, dpr); |
| } |
|
|
| |
| canvas.style.width = `${rect.width}px`; |
| canvas.style.height = `${rect.height}px`; |
| }, 'Canvas Resize'); |
|
|
| |
| |
| |
| |
| |
| |
| const createConfettiBurst = ErrorHandler.safeExecute((x, y, count = 50) => { |
| for (let i = 0; i < count; i++) { |
| particles.push(new HeartParticle(x, y)); |
| } |
| }, 'Confetti Burst Creation'); |
|
|
| |
| |
| |
| const animateConfetti = ErrorHandler.safeExecute(() => { |
| if (!ctx || !canvas) return; |
|
|
| |
| const rect = canvas.getBoundingClientRect(); |
| ctx.clearRect(0, 0, rect.width, rect.height); |
|
|
| |
| particles = particles.filter(particle => { |
| particle.update(); |
| particle.draw(ctx); |
| return particle.isAlive(); |
| }); |
|
|
| |
| if (particles.length > 0 || isConfettiActive) { |
| animationId = requestAnimationFrame(animateConfetti); |
| } else { |
| PerformanceMonitor.stopMonitoring(); |
| } |
| }, 'Confetti Animation'); |
|
|
| |
| |
| |
| const startConfetti = ErrorHandler.safeExecute(() => { |
| if (!canvas || !ctx) initCanvas(); |
|
|
| isConfettiActive = true; |
| PerformanceMonitor.startMonitoring(); |
|
|
| |
| const centerX = window.innerWidth / 2; |
| const centerY = window.innerHeight / 2; |
| createConfettiBurst(centerX, centerY, 80); |
|
|
| |
| const burstPositions = [ |
| { x: window.innerWidth * 0.2, y: window.innerHeight * 0.3, count: 40, delay: 200 }, |
| { x: window.innerWidth * 0.8, y: window.innerHeight * 0.3, count: 40, delay: 400 }, |
| { x: centerX, y: centerY - 100, count: 60, delay: 600 } |
| ]; |
|
|
| burstPositions.forEach(({ x, y, count, delay }) => { |
| setTimeout(() => createConfettiBurst(x, y, count), delay); |
| }); |
|
|
| |
| let burstCount = 0; |
| const burstInterval = setInterval(() => { |
| if (burstCount >= 8) { |
| clearInterval(burstInterval); |
| isConfettiActive = false; |
| return; |
| } |
| const randomX = Math.random() * window.innerWidth; |
| const randomY = Math.random() * window.innerHeight * 0.5; |
| createConfettiBurst(randomX, randomY, 20); |
| burstCount++; |
| }, 300); |
|
|
| |
| if (!animationId) { |
| animateConfetti(); |
| } |
| }, 'Start Confetti'); |
|
|
| |
| |
| |
| const stopConfetti = ErrorHandler.safeExecute(() => { |
| isConfettiActive = false; |
| if (animationId) { |
| cancelAnimationFrame(animationId); |
| animationId = null; |
| } |
| particles = []; |
| PerformanceMonitor.stopMonitoring(); |
| }, 'Stop Confetti'); |
|
|
| |
| |
| |
| |
| |
| |
| const showSection = (sectionId, delay = 0) => { |
| return new Promise(resolve => { |
| setTimeout(() => { |
| const section = document.getElementById(sectionId); |
| if (section) { |
| section.classList.remove('hidden'); |
| |
| section.setAttribute('aria-hidden', 'false'); |
| } |
| resolve(); |
| }, delay); |
| }); |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| const hideSection = (sectionId, delay = 0) => { |
| return new Promise(resolve => { |
| setTimeout(() => { |
| const section = document.getElementById(sectionId); |
| if (section) { |
| section.classList.add('hidden'); |
| |
| section.setAttribute('aria-hidden', 'true'); |
| } |
| resolve(); |
| }, delay); |
| }); |
| }; |
|
|
| |
| |
| |
| |
| const openEnvelope = () => { |
| return new Promise(resolve => { |
| const envelope = document.getElementById('envelope'); |
| if (envelope) { |
| envelope.classList.add('open'); |
| |
| const announcement = document.createElement('div'); |
| announcement.setAttribute('role', 'status'); |
| announcement.setAttribute('aria-live', 'polite'); |
| announcement.className = 'sr-only'; |
| announcement.textContent = 'Envelope opened, revealing a love letter'; |
| document.body.appendChild(announcement); |
| setTimeout(() => announcement.remove(), 2000); |
| } |
| |
| setTimeout(resolve, 1200); |
| }); |
| }; |
|
|
| |
| return { |
| init: initCanvas, |
| startConfetti, |
| stopConfetti, |
| showSection, |
| hideSection, |
| openEnvelope |
| }; |
| })(); |
|
|
|
|
| |
| |
| |
| |
| const UIController = (() => { |
| |
| let notesRemoved = 0; |
| const totalNotes = 5; |
| let noClickCount = 0; |
| let yesScale = 1; |
| let currentZIndex = 20; |
| let isEvading = false; |
| let noButtonActivated = false; |
|
|
| |
| const isMobileDevice = () => { |
| return ('ontouchstart' in window || navigator.maxTouchPoints > 0) && |
| window.matchMedia('(max-width: 768px)').matches; |
| }; |
|
|
| |
| const elements = { |
| btnOpen: null, |
| btnYes: null, |
| btnNo: null, |
| buttonsWrapper: null, |
| notes: null |
| }; |
|
|
| |
| const cacheElements = () => { |
| elements.btnOpen = document.getElementById('btn-open'); |
| elements.btnYes = document.getElementById('btn-yes'); |
| elements.btnNo = document.getElementById('btn-no'); |
| elements.buttonsWrapper = document.getElementById('buttons-wrapper'); |
| elements.notes = document.querySelectorAll('.draggable-note'); |
| }; |
|
|
| |
| const setupEventListeners = () => { |
| |
| if (elements.btnOpen) { |
| elements.btnOpen.addEventListener('click', handleOpenClick); |
| } |
|
|
| |
| if (elements.btnYes) { |
| elements.btnYes.addEventListener('click', handleYesClick); |
| } |
|
|
| |
| if (elements.btnNo) { |
| |
| elements.btnNo.addEventListener('click', handleNoClick); |
|
|
| |
| if (!isMobileDevice()) { |
| elements.btnNo.addEventListener('mouseenter', handleNoHover); |
|
|
| |
| document.addEventListener('mousemove', (e) => { |
| if (!elements.btnNo || !noButtonActivated || isMobileDevice()) return; |
|
|
| const btn = elements.btnNo; |
| const btnRect = btn.getBoundingClientRect(); |
| const mouseX = e.clientX; |
| const mouseY = e.clientY; |
|
|
| |
| const buffer = 30; |
| const isNearButton = mouseX >= btnRect.left - buffer && |
| mouseX <= btnRect.right + buffer && |
| mouseY >= btnRect.top - buffer && |
| mouseY <= btnRect.bottom + buffer; |
|
|
| if (isNearButton && !isEvading) { |
| evadeNoButton(); |
| } |
| }); |
| } |
| } |
|
|
| |
| if (elements.notes) { |
| elements.notes.forEach(note => { |
| setupDrag(note); |
| }); |
| } |
| }; |
|
|
| |
| const handleOpenClick = async () => { |
| |
| elements.btnOpen.disabled = true; |
| elements.btnOpen.style.opacity = '0.7'; |
|
|
| |
| await AnimationController.openEnvelope(); |
|
|
| |
| await AnimationController.hideSection('envelope-section'); |
| await AnimationController.showSection('notes-section', 300); |
| }; |
|
|
| |
| const setupDrag = (note) => { |
| let isDragging = false; |
| let startX, startY; |
| let initialX, initialY; |
| let currentX = 0, currentY = 0; |
| let rafId = null; |
|
|
| |
| const rect = note.getBoundingClientRect(); |
| const parentRect = note.parentElement.getBoundingClientRect(); |
| initialX = rect.left - parentRect.left; |
| initialY = rect.top - parentRect.top; |
|
|
| const onStart = (e) => { |
| isDragging = true; |
| document.body.classList.add('dragging'); |
| note.classList.add('dragging'); |
|
|
| |
| currentZIndex++; |
| note.style.zIndex = currentZIndex; |
|
|
| |
| if (e.type === 'touchstart') { |
| startX = e.touches[0].clientX - currentX; |
| startY = e.touches[0].clientY - currentY; |
| } else { |
| startX = e.clientX - currentX; |
| startY = e.clientY - currentY; |
| } |
| }; |
|
|
| const onMove = (e) => { |
| if (!isDragging) return; |
|
|
| |
| if (e.type === 'touchmove') { |
| e.preventDefault(); |
| } |
|
|
| let clientX, clientY; |
| if (e.type === 'touchmove') { |
| clientX = e.touches[0].clientX; |
| clientY = e.touches[0].clientY; |
| } else { |
| clientX = e.clientX; |
| clientY = e.clientY; |
| } |
|
|
| currentX = clientX - startX; |
| currentY = clientY - startY; |
|
|
| |
| if (rafId) { |
| cancelAnimationFrame(rafId); |
| } |
|
|
| rafId = requestAnimationFrame(() => { |
| |
| note.style.transform = `translate3d(${currentX}px, ${currentY}px, 0)`; |
| }); |
| }; |
|
|
| const onEnd = () => { |
| if (!isDragging) return; |
|
|
| isDragging = false; |
| document.body.classList.remove('dragging'); |
| note.classList.remove('dragging'); |
|
|
| if (rafId) { |
| cancelAnimationFrame(rafId); |
| rafId = null; |
| } |
|
|
| |
| const threshold = 150; |
| const distance = Math.sqrt(currentX * currentX + currentY * currentY); |
|
|
| if (distance > threshold && !note.dataset.removed) { |
| note.dataset.removed = 'true'; |
| notesRemoved++; |
|
|
| |
| note.style.opacity = '0'; |
| note.style.pointerEvents = 'none'; |
|
|
| |
| const announcement = document.createElement('div'); |
| announcement.setAttribute('role', 'status'); |
| announcement.setAttribute('aria-live', 'polite'); |
| announcement.className = 'sr-only'; |
| announcement.textContent = `Note ${notesRemoved} of ${totalNotes} removed`; |
| document.body.appendChild(announcement); |
| setTimeout(() => announcement.remove(), 1000); |
|
|
| |
| checkAllNotesRemoved(); |
| } |
| }; |
|
|
| |
| note.addEventListener('mousedown', onStart); |
| document.addEventListener('mousemove', onMove); |
| document.addEventListener('mouseup', onEnd); |
|
|
| |
| note.addEventListener('touchstart', onStart, { passive: true }); |
| document.addEventListener('touchmove', onMove, { passive: false }); |
| document.addEventListener('touchend', onEnd, { passive: true }); |
|
|
| |
| note._dragCleanup = () => { |
| note.removeEventListener('mousedown', onStart); |
| document.removeEventListener('mousemove', onMove); |
| document.removeEventListener('mouseup', onEnd); |
| note.removeEventListener('touchstart', onStart); |
| document.removeEventListener('touchmove', onMove); |
| document.removeEventListener('touchend', onEnd); |
| if (rafId) { |
| cancelAnimationFrame(rafId); |
| } |
| }; |
| }; |
|
|
| |
| const checkAllNotesRemoved = async () => { |
| if (notesRemoved >= totalNotes) { |
| |
| const hint = document.getElementById('hint-text'); |
| if (hint) hint.style.opacity = '0'; |
|
|
| |
| await AnimationController.hideSection('notes-section', 500); |
| await AnimationController.showSection('question-section', 300); |
| } |
| }; |
|
|
| |
| const handleYesClick = async () => { |
| |
| AnimationController.startConfetti(); |
|
|
| |
| await AnimationController.hideSection('question-section'); |
| await AnimationController.showSection('success-section', 300); |
| }; |
|
|
| |
| const handleNoClick = (e) => { |
| e.preventDefault(); |
| e.stopPropagation(); |
|
|
| noClickCount++; |
| noButtonActivated = true; |
|
|
| |
| evadeNoButton(); |
|
|
| |
| if (noClickCount >= 15) { |
| handleYesClick(); |
| } |
| }; |
|
|
| |
| const handleNoHover = () => { |
| |
| if (!isMobileDevice()) { |
| noButtonActivated = true; |
| evadeNoButton(); |
| } |
| }; |
|
|
| |
| const evadeNoButton = () => { |
| if (isEvading) return; |
| isEvading = true; |
|
|
| const btn = elements.btnNo; |
| const questionContainer = document.querySelector('.question-container'); |
|
|
| if (!btn || !questionContainer) { |
| isEvading = false; |
| return; |
| } |
|
|
| |
| btn.classList.add('evading'); |
|
|
| |
| const containerRect = questionContainer.getBoundingClientRect(); |
|
|
| |
| const btnRect = btn.getBoundingClientRect(); |
| const btnWidth = btnRect.width || 100; |
| const btnHeight = btnRect.height || 48; |
|
|
| |
| const padding = 20; |
| const minX = containerRect.left + padding; |
| const minY = containerRect.top + padding; |
| const maxX = containerRect.right - btnWidth - padding; |
| const maxY = containerRect.bottom - btnHeight - padding; |
|
|
| |
| if (maxX <= minX || maxY <= minY) { |
| |
| btn.style.position = 'fixed'; |
| btn.style.left = `${containerRect.left + containerRect.width / 2 - btnWidth / 2}px`; |
| btn.style.top = `${containerRect.top + containerRect.height / 2}px`; |
| btn.style.transform = 'scale(1)'; |
| isEvading = false; |
| return; |
| } |
|
|
| |
| let newX, newY; |
| let attempts = 0; |
| const maxAttempts = 20; |
|
|
| do { |
| newX = minX + Math.random() * (maxX - minX); |
| newY = minY + Math.random() * (maxY - minY); |
| attempts++; |
|
|
| |
| if (elements.btnYes) { |
| const yesRect = elements.btnYes.getBoundingClientRect(); |
|
|
| |
| const safetyBuffer = 50; |
|
|
| |
| const noLeft = newX; |
| const noRight = newX + btnWidth; |
| const noTop = newY; |
| const noBottom = newY + btnHeight; |
|
|
| |
| const yesLeft = yesRect.left - safetyBuffer; |
| const yesRight = yesRect.right + safetyBuffer; |
| const yesTop = yesRect.top - safetyBuffer; |
| const yesBottom = yesRect.bottom + safetyBuffer; |
|
|
| |
| const isInExclusionZone = (noRight > yesLeft && |
| noLeft < yesRight && |
| noBottom > yesTop && |
| noTop < yesBottom); |
|
|
| |
| if (!isInExclusionZone) { |
| break; |
| } |
| } else { |
| break; |
| } |
| } while (attempts < maxAttempts); |
|
|
| |
| |
| if (attempts >= maxAttempts && elements.btnYes) { |
| const yesRect = elements.btnYes.getBoundingClientRect(); |
| |
| if (yesRect.left < containerRect.left + containerRect.width / 2) { |
| |
| newX = maxX; |
| } else { |
| |
| newX = minX; |
| } |
| if (yesRect.top < containerRect.top + containerRect.height / 2) { |
| |
| newY = maxY; |
| } else { |
| |
| newY = minY; |
| } |
| } |
|
|
| |
| const finalX = Math.max(minX, Math.min(maxX, newX)); |
| const finalY = Math.max(minY, Math.min(maxY, newY)); |
|
|
| |
| btn.style.position = 'fixed'; |
| btn.style.left = `${finalX}px`; |
| btn.style.top = `${finalY}px`; |
| btn.style.margin = '0'; |
| btn.style.right = 'auto'; |
| btn.style.bottom = 'auto'; |
|
|
| |
| if (noClickCount >= 3) { |
| const shrinkScale = Math.max(0.6, 1 - (noClickCount - 2) * 0.08); |
| btn.style.transform = `scale(${shrinkScale})`; |
| } else { |
| btn.style.transform = 'scale(1)'; |
| } |
|
|
| |
| btn.style.opacity = '1'; |
| btn.style.visibility = 'visible'; |
| btn.style.display = 'inline-flex'; |
| btn.style.pointerEvents = 'auto'; |
| btn.style.zIndex = '100'; |
|
|
| setTimeout(() => { |
| isEvading = false; |
| }, 150); |
| }; |
|
|
| |
| const cleanup = () => { |
| |
| if (elements.btnOpen) { |
| elements.btnOpen.removeEventListener('click', handleOpenClick); |
| } |
| if (elements.btnYes) { |
| elements.btnYes.removeEventListener('click', handleYesClick); |
| } |
| if (elements.btnNo) { |
| elements.btnNo.removeEventListener('click', handleNoClick); |
| elements.btnNo.removeEventListener('mouseenter', handleNoHover); |
| elements.btnNo.removeEventListener('touchstart', handleNoTouch); |
| } |
|
|
| |
| if (elements.notes) { |
| elements.notes.forEach(note => { |
| if (note._dragCleanup) { |
| note._dragCleanup(); |
| } |
| }); |
| } |
|
|
| }; |
|
|
| |
| const init = () => { |
| cacheElements(); |
| setupEventListeners(); |
| AnimationController.init(); |
| }; |
|
|
| |
| return { |
| init, |
| cleanup |
| }; |
| })(); |
|
|
|
|
| |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| const features = BrowserCompatibility.init(); |
|
|
| |
| if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { |
| console.log('π¨ Valentine App Initialized'); |
| console.log('π Browser Features:', features); |
| console.log('π Hardware Acceleration:', PerformanceMonitor.supportsHardwareAcceleration()); |
| } |
|
|
| |
| try { |
| UIController.init(); |
| } catch (error) { |
| ErrorHandler.handleError('Application Initialization', error, () => { |
| |
| document.body.innerHTML = ` |
| <div style="display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 2rem; text-align: center; font-family: sans-serif;"> |
| <div> |
| <h1 style="color: #8b0000; margin-bottom: 1rem;">π</h1> |
| <p style="color: #666;">Something went wrong, but the love is still there!</p> |
| <p style="color: #999; font-size: 0.9rem; margin-top: 1rem;">Please try refreshing the page.</p> |
| </div> |
| </div> |
| `; |
| }); |
| } |
|
|
| |
| window.addEventListener('beforeunload', () => { |
| try { |
| UIController.cleanup(); |
| AnimationController.stopConfetti(); |
| PerformanceMonitor.stopMonitoring(); |
| } catch (error) { |
| ErrorHandler.logError('Cleanup', error); |
| } |
| }); |
|
|
| |
| document.addEventListener('visibilitychange', () => { |
| if (document.hidden) { |
| PerformanceMonitor.stopMonitoring(); |
| } else { |
| |
| const canvas = document.getElementById('confetti-canvas'); |
| if (canvas && canvas.style.display !== 'none') { |
| PerformanceMonitor.startMonitoring(); |
| } |
| } |
| }); |
| }); |
|
|
| |
| let lastTouchEnd = 0; |
| document.addEventListener('touchend', (e) => { |
| const now = Date.now(); |
| if (now - lastTouchEnd < 300) { |
| e.preventDefault(); |
| } |
| lastTouchEnd = now; |
| }, { passive: false }); |
|
|
| |
| window.addEventListener('error', (event) => { |
| ErrorHandler.logError('Global Error', event.error); |
| |
| if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') { |
| event.preventDefault(); |
| } |
| }); |
|
|
| |
| window.addEventListener('unhandledrejection', (event) => { |
| ErrorHandler.logError('Unhandled Promise Rejection', event.reason); |
| |
| if (window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') { |
| event.preventDefault(); |
| } |
| }); |
|
|
| |
| const style = document.createElement('style'); |
| style.textContent = ` |
| .sr-only { |
| position: absolute; |
| width: 1px; |
| height: 1px; |
| padding: 0; |
| margin: -1px; |
| overflow: hidden; |
| clip: rect(0, 0, 0, 0); |
| white-space: nowrap; |
| border-width: 0; |
| } |
| |
| /* Fallback styles for browsers without transform support */ |
| .no-transforms .draggable-note { |
| position: absolute; |
| transition: left 0.3s ease, top 0.3s ease; |
| } |
| |
| /* Fallback for backdrop-filter */ |
| .no-backdrop-filter .glass-container { |
| background: rgba(255, 255, 255, 0.95); |
| box-shadow: 0 12px 48px rgba(139, 0, 0, 0.2); |
| } |
| `; |
| document.head.appendChild(style); |
|
|