VALENTINE 1
This commit is contained in:
parent
1cb89e7ef8
commit
978955ec5b
55
api/save_settings.php
Normal file
55
api/save_settings.php
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'upload_image') {
|
||||||
|
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
// Check if locked
|
||||||
|
$stmt = db()->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.']);
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
:root {
|
:root {
|
||||||
--primary-color: #e63946;
|
--primary-color: #e63946;
|
||||||
--bg-color: #ffffff;
|
--bg-color: #ffe4e6; /* Light Pink */
|
||||||
|
--popup-bg: #ffccd5; /* Light Red */
|
||||||
--text-color: #2d3436;
|
--text-color: #2d3436;
|
||||||
--secondary-text: #636e72;
|
--secondary-text: #636e72;
|
||||||
--border-color: #eee;
|
--border-color: rgba(0,0,0,0.05);
|
||||||
--font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
--font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,16 +20,46 @@ body {
|
|||||||
overflow-x: hidden;
|
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 {
|
.container {
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2.5rem 2rem;
|
||||||
border: 1px solid var(--border-color);
|
border-radius: 20px;
|
||||||
border-radius: 12px;
|
background: var(--popup-bg);
|
||||||
background: #fff;
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@ -36,14 +67,15 @@ h1 {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
letter-spacing: -0.5px;
|
letter-spacing: -0.5px;
|
||||||
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-preview-container {
|
.image-preview-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 250px;
|
height: 250px;
|
||||||
background-color: #f8f9fa;
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
border: 2px dashed var(--border-color);
|
border: 2px dashed rgba(0,0,0,0.1);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -51,6 +83,12 @@ h1 {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-locked .image-preview-container {
|
||||||
|
cursor: default;
|
||||||
|
border-style: solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-preview-container img {
|
.image-preview-container img {
|
||||||
@ -63,6 +101,7 @@ h1 {
|
|||||||
.image-preview-container .placeholder-text {
|
.image-preview-container .placeholder-text {
|
||||||
color: var(--secondary-text);
|
color: var(--secondary-text);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
@ -76,13 +115,13 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.75rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border-radius: 6px;
|
border-radius: 10px;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
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;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,12 +129,18 @@ h1 {
|
|||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
color: white;
|
color: white;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
box-shadow: 0 4px 12px rgba(230, 57, 70, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-yes:hover {
|
||||||
|
background-color: #d62839;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-no {
|
.btn-no {
|
||||||
background-color: #f1f2f6;
|
background-color: #fff;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
#success-message {
|
#success-message {
|
||||||
@ -112,15 +157,17 @@ h1 {
|
|||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.redirect-hint {
|
.redirect-hint {
|
||||||
margin-top: 1rem;
|
margin-top: 2rem;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--secondary-text);
|
color: var(--secondary-text);
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
#image-input {
|
#image-input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,24 +6,85 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const placeholderText = document.querySelector('.placeholder-text');
|
const placeholderText = document.querySelector('.placeholder-text');
|
||||||
const proposalBox = document.getElementById('proposal-box');
|
const proposalBox = document.getElementById('proposal-box');
|
||||||
const successBox = document.getElementById('success-message');
|
const successBox = document.getElementById('success-message');
|
||||||
|
const lockBtn = document.getElementById('lock-btn');
|
||||||
|
const resetBtn = document.getElementById('reset-btn');
|
||||||
|
|
||||||
let yesScale = 1;
|
let yesScale = 1;
|
||||||
|
let isLocked = typeof IS_LOCKED !== 'undefined' ? IS_LOCKED : false;
|
||||||
|
|
||||||
// Image Upload Logic
|
// Image Upload Logic
|
||||||
document.querySelector('.image-preview-container').addEventListener('click', () => {
|
document.querySelector('.image-preview-container').addEventListener('click', () => {
|
||||||
|
if (isLocked) return;
|
||||||
imageInput.click();
|
imageInput.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
imageInput.addEventListener('change', function() {
|
imageInput.addEventListener('change', function() {
|
||||||
const file = this.files[0];
|
const file = this.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
const reader = new FileReader();
|
const formData = new FormData();
|
||||||
reader.onload = function(e) {
|
formData.append('action', 'upload_image');
|
||||||
imagePreview.src = e.target.result;
|
formData.append('image', file);
|
||||||
imagePreview.style.display = 'block';
|
|
||||||
placeholderText.style.display = 'none';
|
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) => {
|
noBtn.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
yesScale += 0.15;
|
yesScale += 0.15;
|
||||||
yesBtn.style.transform = `scale(${yesScale})`;
|
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
|
// "Yes" Click Logic
|
||||||
@ -71,6 +133,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
proposalBox.style.display = 'none';
|
proposalBox.style.display = 'none';
|
||||||
successBox.style.display = 'block';
|
successBox.style.display = 'block';
|
||||||
|
|
||||||
|
// Hide controls during celebration
|
||||||
|
document.querySelector('.admin-controls').style.display = 'none';
|
||||||
|
|
||||||
// Confetti effect
|
// Confetti effect
|
||||||
const duration = 15 * 1000;
|
const duration = 15 * 1000;
|
||||||
const animationEnd = Date.now() + duration;
|
const animationEnd = Date.now() + duration;
|
||||||
@ -97,4 +162,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
window.location.href = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
|
window.location.href = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
|
||||||
}, 15000);
|
}, 15000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
2
db/migrations/001_create_settings.sql
Normal file
2
db/migrations/001_create_settings.sql
Normal file
@ -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');
|
||||||
35
index.php
35
index.php
@ -1,5 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
// Fetch settings
|
||||||
|
$stmt = db()->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';
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@ -9,7 +18,7 @@ declare(strict_types=1);
|
|||||||
<title>Gvantsa, would you be my valentine?</title>
|
<title>Gvantsa, would you be my valentine?</title>
|
||||||
<?php
|
<?php
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'A special valentine proposal for Gvantsa.';
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'A special valentine proposal for Gvantsa.';
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ($valentineImage ?: '');
|
||||||
?>
|
?>
|
||||||
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||||
@ -24,15 +33,28 @@ declare(strict_types=1);
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="<?= $isLocked ? 'state-locked' : '' ?>">
|
||||||
|
|
||||||
|
<div class="admin-controls">
|
||||||
|
<button id="lock-btn" title="<?= $isLocked ? 'Unlock' : 'Lock' ?> changes">
|
||||||
|
<?php if ($isLocked): ?>
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
|
||||||
|
<?php else: ?>
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 9.9-1"></path></svg>
|
||||||
|
<?php endif; ?>
|
||||||
|
</button>
|
||||||
|
<button id="reset-btn" title="Reset Experience">
|
||||||
|
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="proposal-box">
|
<div id="proposal-box">
|
||||||
<h1>Gvantsa, would you be my valentine?</h1>
|
<h1>Gvantsa, would you be my valentine?</h1>
|
||||||
|
|
||||||
<div class="image-preview-container">
|
<div class="image-preview-container <?= $valentineImage ? 'has-image' : '' ?>">
|
||||||
<span class="placeholder-text">Click to upload our photo ❤️</span>
|
<span class="placeholder-text" style="<?= $valentineImage ? 'display:none' : '' ?>">Click to upload our photo ❤️</span>
|
||||||
<img id="preview-img" src="" alt="Valentine Image">
|
<img id="preview-img" src="<?= htmlspecialchars($valentineImage) ?>" alt="Valentine Image" style="<?= $valentineImage ? 'display:block' : '' ?>">
|
||||||
</div>
|
</div>
|
||||||
<input type="file" id="image-input" accept="image/*">
|
<input type="file" id="image-input" accept="image/*">
|
||||||
|
|
||||||
@ -50,6 +72,9 @@ declare(strict_types=1);
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const IS_LOCKED = <?= $isLocked ? 'true' : 'false' ?>;
|
||||||
|
</script>
|
||||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user