diff --git a/api/save_settings.php b/api/save_settings.php new file mode 100644 index 0000000..06ca8d4 --- /dev/null +++ b/api/save_settings.php @@ -0,0 +1,55 @@ +prepare("SELECT setting_value FROM settings WHERE setting_key = 'is_locked'"); + $stmt->execute(); + $isLocked = $stmt->fetchColumn(); + + if ($isLocked === '1') { + echo json_encode(['success' => false, 'error' => 'Settings are locked.']); + exit; + } + + $uploadDir = __DIR__ . '/../assets/images/uploads/'; + $fileName = 'valentine_' . time() . '_' . basename($_FILES['image']['name']); + $targetPath = $uploadDir . $fileName; + + if (move_uploaded_file($_FILES['image']['tmp_name'], $targetPath)) { + $webPath = 'assets/images/uploads/' . $fileName; + + $stmt = db()->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = 'valentine_image'"); + $stmt->execute([$webPath]); + + echo json_encode(['success' => true, 'path' => $webPath]); + } else { + echo json_encode(['success' => false, 'error' => 'Failed to move uploaded file.']); + } + } else { + echo json_encode(['success' => false, 'error' => 'No file uploaded or upload error.']); + } +} elseif ($action === 'toggle_lock') { + $lockValue = $_POST['lock'] === 'true' ? '1' : '0'; + $stmt = db()->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = 'is_locked'"); + $stmt->execute([$lockValue]); + echo json_encode(['success' => true, 'locked' => $lockValue === '1']); +} elseif ($action === 'reset') { + // We don't necessarily reset the image here unless specified, + // but the user said "reset the experience". + // Maybe it just clears the locked state and image? + // Usually "reset experience" for the user means restart the proposal. + // However, if they want to reset the setup, we can clear the image. + $stmt = db()->prepare("UPDATE settings SET setting_value = '' WHERE setting_key = 'valentine_image'"); + $stmt->execute(); + $stmt = db()->prepare("UPDATE settings SET setting_value = '0' WHERE setting_key = 'is_locked'"); + $stmt->execute(); + echo json_encode(['success' => true]); +} else { + echo json_encode(['success' => false, 'error' => 'Invalid action.']); +} diff --git a/assets/css/custom.css b/assets/css/custom.css index d215fbd..87fbc6d 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,9 +1,10 @@ :root { --primary-color: #e63946; - --bg-color: #ffffff; + --bg-color: #ffe4e6; /* Light Pink */ + --popup-bg: #ffccd5; /* Light Red */ --text-color: #2d3436; --secondary-text: #636e72; - --border-color: #eee; + --border-color: rgba(0,0,0,0.05); --font-family: 'Inter', system-ui, -apple-system, sans-serif; } @@ -19,16 +20,46 @@ body { overflow-x: hidden; } +.admin-controls { + position: fixed; + top: 1rem; + right: 1rem; + display: flex; + gap: 0.5rem; + z-index: 100; +} + +.admin-controls button { + background: rgba(255, 255, 255, 0.8); + border: none; + border-radius: 50%; + width: 36px; + height: 36px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + transition: all 0.2s ease; + color: var(--secondary-text); +} + +.admin-controls button:hover { + background: #fff; + color: var(--primary-color); + transform: scale(1.1); +} + .container { max-width: 500px; width: 90%; text-align: center; - padding: 2rem; - border: 1px solid var(--border-color); - border-radius: 12px; - background: #fff; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); + padding: 2.5rem 2rem; + border-radius: 20px; + background: var(--popup-bg); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); transition: all 0.3s ease; + position: relative; } h1 { @@ -36,14 +67,15 @@ h1 { font-weight: 700; margin-bottom: 1.5rem; letter-spacing: -0.5px; + color: var(--primary-color); } .image-preview-container { width: 100%; height: 250px; - background-color: #f8f9fa; - border: 2px dashed var(--border-color); - border-radius: 8px; + background-color: rgba(255, 255, 255, 0.5); + border: 2px dashed rgba(0,0,0,0.1); + border-radius: 12px; margin-bottom: 1.5rem; display: flex; justify-content: center; @@ -51,6 +83,12 @@ h1 { overflow: hidden; position: relative; cursor: pointer; + transition: all 0.2s ease; +} + +.state-locked .image-preview-container { + cursor: default; + border-style: solid; } .image-preview-container img { @@ -63,6 +101,7 @@ h1 { .image-preview-container .placeholder-text { color: var(--secondary-text); font-size: 0.9rem; + padding: 1rem; } .button-group { @@ -76,13 +115,13 @@ h1 { } .btn { - padding: 0.75rem 1.5rem; + padding: 0.75rem 1.75rem; font-size: 1rem; font-weight: 600; - border-radius: 6px; + border-radius: 10px; border: none; cursor: pointer; - transition: transform 0.1s ease, font-size 0.2s ease; + transition: transform 0.1s ease, font-size 0.2s ease, background-color 0.2s ease; white-space: nowrap; } @@ -90,12 +129,18 @@ h1 { background-color: var(--primary-color); color: white; z-index: 10; + box-shadow: 0 4px 12px rgba(230, 57, 70, 0.3); +} + +.btn-yes:hover { + background-color: #d62839; } .btn-no { - background-color: #f1f2f6; + background-color: #fff; color: var(--text-color); position: relative; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); } #success-message { @@ -112,15 +157,17 @@ h1 { font-size: 1.25rem; line-height: 1.6; color: var(--primary-color); - font-weight: 600; + font-weight: 700; + margin-bottom: 0.5rem; } .redirect-hint { - margin-top: 1rem; + margin-top: 2rem; font-size: 0.85rem; color: var(--secondary-text); + font-style: italic; } #image-input { display: none; -} \ No newline at end of file +} diff --git a/assets/js/main.js b/assets/js/main.js index 133ec46..9a9aeac 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -6,24 +6,85 @@ document.addEventListener('DOMContentLoaded', () => { const placeholderText = document.querySelector('.placeholder-text'); const proposalBox = document.getElementById('proposal-box'); const successBox = document.getElementById('success-message'); + const lockBtn = document.getElementById('lock-btn'); + const resetBtn = document.getElementById('reset-btn'); let yesScale = 1; + let isLocked = typeof IS_LOCKED !== 'undefined' ? IS_LOCKED : false; // Image Upload Logic document.querySelector('.image-preview-container').addEventListener('click', () => { + if (isLocked) return; imageInput.click(); }); imageInput.addEventListener('change', function() { const file = this.files[0]; if (file) { - const reader = new FileReader(); - reader.onload = function(e) { - imagePreview.src = e.target.result; - imagePreview.style.display = 'block'; - placeholderText.style.display = 'none'; + const formData = new FormData(); + formData.append('action', 'upload_image'); + formData.append('image', file); + + fetch('api/save_settings.php', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + imagePreview.src = data.path + '?t=' + new Date().getTime(); + imagePreview.style.display = 'block'; + placeholderText.style.display = 'none'; + document.querySelector('.image-preview-container').classList.add('has-image'); + } else { + alert(data.error || 'Failed to upload image'); + } + }) + .catch(error => { + console.error('Error:', error); + alert('An error occurred during upload.'); + }); + } + }); + + // Lock/Unlock Toggle + lockBtn.addEventListener('click', () => { + const newLockedState = !isLocked; + const formData = new FormData(); + formData.append('action', 'toggle_lock'); + formData.append('lock', newLockedState); + + fetch('api/save_settings.php', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + isLocked = data.locked; + document.body.classList.toggle('state-locked', isLocked); + // Update icon (re-fetch or just refresh page is easier, but let's just refresh) + location.reload(); } - reader.readAsDataURL(file); + }); + }); + + // Reset Experience + resetBtn.addEventListener('click', () => { + if (confirm('Reset everything? This will clear the image and unlock changes.')) { + const formData = new FormData(); + formData.append('action', 'reset'); + + fetch('api/save_settings.php', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + location.reload(); + } + }); } }); @@ -58,12 +119,13 @@ document.addEventListener('DOMContentLoaded', () => { } }); - // "No" Click Logic (if they somehow manage to click it) + // "No" Click Logic noBtn.addEventListener('click', (e) => { e.preventDefault(); yesScale += 0.15; yesBtn.style.transform = `scale(${yesScale})`; - yesBtn.style.fontSize = `${1 * yesScale}rem`; + // We don't change font-size via style attribute directly to avoid layout jumps if possible, + // but it's what was requested ("yes button gets slightly bigger") }); // "Yes" Click Logic @@ -71,6 +133,9 @@ document.addEventListener('DOMContentLoaded', () => { proposalBox.style.display = 'none'; successBox.style.display = 'block'; + // Hide controls during celebration + document.querySelector('.admin-controls').style.display = 'none'; + // Confetti effect const duration = 15 * 1000; const animationEnd = Date.now() + duration; @@ -97,4 +162,4 @@ document.addEventListener('DOMContentLoaded', () => { window.location.href = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; }, 15000); }); -}); +}); \ No newline at end of file diff --git a/db/migrations/001_create_settings.sql b/db/migrations/001_create_settings.sql new file mode 100644 index 0000000..a0c8471 --- /dev/null +++ b/db/migrations/001_create_settings.sql @@ -0,0 +1,2 @@ +CREATE TABLE IF NOT EXISTS settings (id INT AUTO_INCREMENT PRIMARY KEY, setting_key VARCHAR(255) UNIQUE NOT NULL, setting_value TEXT); +INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('valentine_image', ''), ('is_locked', '0'); diff --git a/index.php b/index.php index bb8bc88..48dc4ae 100644 --- a/index.php +++ b/index.php @@ -1,5 +1,14 @@ prepare("SELECT setting_key, setting_value FROM settings"); +$stmt->execute(); +$settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); + +$valentineImage = $settings['valentine_image'] ?? ''; +$isLocked = ($settings['is_locked'] ?? '0') === '1'; ?> @@ -9,7 +18,7 @@ declare(strict_types=1);