From 41111c794f6a4c5aacf752920a0512978d63d167 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 21 Nov 2025 18:15:18 +0000 Subject: [PATCH] tick and clock --- api/alarms.php | 101 +++++++++ assets/css/custom.css | 80 +++++++ assets/js/main.js | 229 +++++++++++++++++++ db/config.php | 31 +++ db/migrations/001_create_notes_table.sql | 7 + db/migrations/002_create_alarms_table.sql | 8 + index.php | 260 ++++++++++------------ note.php | 84 +++++++ 8 files changed, 660 insertions(+), 140 deletions(-) create mode 100644 api/alarms.php create mode 100644 assets/css/custom.css create mode 100644 assets/js/main.js create mode 100644 db/migrations/001_create_notes_table.sql create mode 100644 db/migrations/002_create_alarms_table.sql create mode 100644 note.php diff --git a/api/alarms.php b/api/alarms.php new file mode 100644 index 0000000..386c89f --- /dev/null +++ b/api/alarms.php @@ -0,0 +1,101 @@ + false, 'message' => 'Invalid request']; +$pdo = db(); + +if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action'])) { + if ($_GET['action'] === 'check') { + try { + $pdo->beginTransaction(); + + // Find active alarms that are due and lock the rows + $stmt = $pdo->prepare("SELECT * FROM alarms WHERE alarm_time <= CURTIME() AND is_active = 1 FOR UPDATE"); + $stmt->execute(); + $alarms = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if ($alarms) { + // Deactivate the found alarms to prevent them from ringing again + $alarmIds = array_map(function($alarm) { + return $alarm['id']; + }, $alarms); + + if (!empty($alarmIds)) { + $placeholders = implode(',', array_fill(0, count($alarmIds), '?')); + $updateStmt = $pdo->prepare("UPDATE alarms SET is_active = 0 WHERE id IN ($placeholders)"); + $updateStmt->execute($alarmIds); + } + + $response = ['success' => true, 'alarms' => $alarms]; + } else { + $response = ['success' => true, 'alarms' => []]; + } + + $pdo->commit(); + } catch (PDOException $e) { + $pdo->rollBack(); + $response['message'] = 'Database error: ' . $e->getMessage(); + } + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + // Simple routing based on a POST field + $action = $_POST['action'] ?? ''; + + if ($action === 'create') { + $alarm_time = $_POST['alarm_time'] ?? null; + $label = $_POST['label'] ?? ''; + + if ($alarm_time) { + try { + $stmt = $pdo->prepare("INSERT INTO alarms (alarm_time, label) VALUES (?, ?)"); + $stmt->execute([$alarm_time, $label]); + $response = ['success' => true, 'message' => 'Alarm created successfully.', 'id' => $pdo->lastInsertId()]; + } catch (PDOException $e) { + $response['message'] = 'Database error: ' . $e->getMessage(); + } + } else { + $response['message'] = 'Alarm time is required.'; + } + } elseif ($action === 'delete') { + $alarm_id = $_POST['alarm_id'] ?? null; + + if ($alarm_id) { + try { + $stmt = $pdo->prepare("DELETE FROM alarms WHERE id = ?"); + $stmt->execute([$alarm_id]); + if ($stmt->rowCount()) { + $response = ['success' => true, 'message' => 'Alarm deleted successfully.']; + } else { + $response['message'] = 'Alarm not found.'; + } + } catch (PDOException $e) { + $response['message'] = 'Database error: ' . $e->getMessage(); + } + } else { + $response['message'] = 'Alarm ID is required.'; + } + } elseif ($action === 'toggle') { + $alarm_id = $_POST['alarm_id'] ?? null; + $is_active = $_POST['is_active'] ?? null; + + if ($alarm_id && $is_active !== null) { + try { + $stmt = $pdo->prepare("UPDATE alarms SET is_active = ? WHERE id = ?"); + $stmt->execute([$is_active, $alarm_id]); + if ($stmt->rowCount()) { + $response = ['success' => true, 'message' => 'Alarm status updated.']; + } else { + $response['message'] = 'Alarm not found or status unchanged.'; + } + } catch (PDOException $e) { + $response['message'] = 'Database error: ' . $e->getMessage(); + } + } else { + $response['message'] = 'Alarm ID and active status are required.'; + } + } +} + +echo json_encode($response); diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..44b7841 --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,80 @@ +/* General Body Styles */ +body { + background-color: #F4F7F6; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + color: #333; +} + +/* Header Styles */ +.header-gradient { + background: linear-gradient(90deg, #4A90E2, #50E3C2); + color: white; + padding: 1rem 0; +} + +/* Card Styles */ +.card { + border-radius: 0.5rem; + border: none; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); +} + +/* Button Styles */ +.btn-primary { + background-color: #4A90E2; + border-color: #4A90E2; + border-radius: 0.5rem; + padding: 0.75rem 1.5rem; + font-weight: 500; +} + +.btn-primary:hover { + background-color: #3a7ac8; + border-color: #3a7ac8; +} + +/* Form Styles */ +.form-control { + border-radius: 0.5rem; +} + +.form-control:focus { + border-color: #4A90E2; + box-shadow: 0 0 0 0.2rem rgba(74, 144, 226, 0.25); +} + +/* Toast Notification */ +.toast-container { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 1055; +} + +/* Alarm List Styles */ +#alarmList .list-group-item { + transition: background-color 0.2s ease-in-out; +} + +#alarmList .list-group-item:hover { + background-color: #f8f9fa; +} + +#alarmList .btn-outline-danger { + border: none; +} + +/* Alarm Modal Styles */ +#alarmModal .modal-content { + border-radius: 1rem; +} + +#alarmModal .feather-lg { + width: 80px; + height: 80px; + stroke-width: 1.5; +} + +.modal-backdrop.show { + opacity: 0.8; +} diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..ac580b2 --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,229 @@ +document.addEventListener('DOMContentLoaded', function () { + // --- ELEMENTS --- + const createAlarmForm = document.getElementById('createAlarmForm'); + const alarmList = document.getElementById('alarmList'); + const noAlarmsMessage = document.getElementById('noAlarmsMessage'); + const alarmModalEl = document.getElementById('alarmModal'); + const alarmModal = new bootstrap.Modal(alarmModalEl); + const dismissAlarmBtn = document.getElementById('dismissAlarmBtn'); + const alarmSound = document.getElementById('alarmSound'); + const alarmModalMessage = document.getElementById('alarmModalMessage'); + + // --- STATE --- + let isAlarmModalShown = false; + + // --- FUNCTIONS --- + + /** + * Handles the submission of the create alarm form. + */ + const handleCreateAlarm = async (e) => { + e.preventDefault(); + const timeInput = document.getElementById('alarmTime'); + const labelInput = document.getElementById('alarmLabel'); + + const formData = new FormData(); + formData.append('action', 'create'); + formData.append('alarm_time', timeInput.value); + formData.append('label', labelInput.value); + + try { + const response = await fetch('api/alarms.php', { + method: 'POST', + body: formData + }); + const result = await response.json(); + + if (result.success) { + addAlarmToList(result.id, timeInput.value, labelInput.value); + timeInput.value = ''; + labelInput.value = ''; + if (noAlarmsMessage) { + noAlarmsMessage.style.display = 'none'; + } + } else { + alert('Error: ' + result.message); + } + } catch (error) { + console.error('Failed to create alarm:', error); + alert('An error occurred while creating the alarm.'); + } + }; + + /** + * Handles the click on a delete alarm form. + */ + const handleDeleteAlarm = async (e) => { + if (!e.target.closest('.delete-alarm-form')) return; + e.preventDefault(); + + const form = e.target.closest('.delete-alarm-form'); + const alarmId = form.querySelector('input[name="alarm_id"]').value; + + if (!confirm('Are you sure you want to delete this alarm?')) return; + + const formData = new FormData(form); + + try { + const response = await fetch('api/alarms.php', { + method: 'POST', + body: formData + }); + const result = await response.json(); + + if (result.success) { + const listItem = form.closest('li'); + listItem.remove(); + if (!alarmList.querySelector('li')) { + if (noAlarmsMessage) { + noAlarmsMessage.style.display = 'block'; + } + } + } else { + alert('Error: ' + result.message); + } + } catch (error) { + console.error('Failed to delete alarm:', error); + alert('An error occurred while deleting the alarm.'); + } + }; + + /** + * Adds a new alarm item to the DOM. + */ + const addAlarmToList = (id, time, label, isActive = true) => { + const date = new Date(`1970-01-01T${time}`); + const formattedTime = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); + + const li = document.createElement('li'); + li.className = 'list-group-item d-flex justify-content-between align-items-center'; + li.dataset.id = id; + li.innerHTML = ` +
+
+ + +
+
+ ${formattedTime} + ${escapeHTML(label)} +
+
+
+ + + +
+ `; + alarmList.appendChild(li); + feather.replace(); + }; + + /** + * Handles toggling the active state of an alarm. + */ + const handleToggleAlarm = async (e) => { + if (!e.target.classList.contains('toggle-alarm-switch')) return; + + const switchEl = e.target; + const listItem = switchEl.closest('li'); + const alarmId = listItem.dataset.id; + const isActive = switchEl.checked ? 1 : 0; + + const formData = new FormData(); + formData.append('action', 'toggle'); + formData.append('alarm_id', alarmId); + formData.append('is_active', isActive); + + try { + const response = await fetch('api/alarms.php', { + method: 'POST', + body: formData + }); + const result = await response.json(); + + if (!result.success) { + alert('Error: ' + result.message); + // Revert the switch on failure + switchEl.checked = !switchEl.checked; + } + } catch (error) { + console.error('Failed to toggle alarm:', error); + alert('An error occurred while updating the alarm.'); + // Revert the switch on failure + switchEl.checked = !switchEl.checked; + } + }; + + /** + * Checks the server for any due alarms. + */ + const checkAlarms = async () => { + if (isAlarmModalShown) return; // Don't check if an alarm is already ringing + + try { + const response = await fetch('api/alarms.php?action=check'); + const result = await response.json(); + + if (result.success && result.alarms.length > 0) { + const alarm = result.alarms[0]; + triggerAlarm(alarm); + } + } catch (error) { + console.error('Error checking alarms:', error); + } + }; + + /** + * Triggers the visual and audible alarm. + */ + const triggerAlarm = (alarm) => { + isAlarmModalShown = true; + if (alarm.label) { + alarmModalMessage.textContent = alarm.label; + } else { + alarmModalMessage.textContent = 'Time to write your notes.'; + } + alarmModal.show(); + alarmSound.play().catch(e => console.error("Audio play failed:", e)); + }; + + /** + * Dismisses the alarm and redirects to the note page. + */ + const dismissAlarm = () => { + alarmSound.pause(); + alarmSound.currentTime = 0; + alarmModal.hide(); + isAlarmModalShown = false; + + const today = new Date(); + const dateString = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0'); + window.location.href = `note.php?date=${dateString}`; + }; + + const escapeHTML = (str) => { + const p = document.createElement('p'); + p.appendChild(document.createTextNode(str)); + return p.innerHTML; + } + + // --- EVENT LISTENERS --- + if (createAlarmForm) { + createAlarmForm.addEventListener('submit', handleCreateAlarm); + } + + if (alarmList) { + alarmList.addEventListener('click', handleDeleteAlarm); + alarmList.addEventListener('change', handleToggleAlarm); + } + + if (dismissAlarmBtn) { + dismissAlarmBtn.addEventListener('click', dismissAlarm); + } + + // --- INITIALIZATION --- + setInterval(checkAlarms, 5000); // Check for alarms every 5 seconds +}); \ No newline at end of file diff --git a/db/config.php b/db/config.php index 43208bc..d19bf1f 100644 --- a/db/config.php +++ b/db/config.php @@ -15,3 +15,34 @@ function db() { } return $pdo; } + +function run_migrations() { + $pdo = db(); + // Create migrations table if it doesn't exist + $pdo->exec("CREATE TABLE IF NOT EXISTS migrations ( + id INT AUTO_INCREMENT PRIMARY KEY, + migration VARCHAR(255) NOT NULL UNIQUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); + + // Get all run migrations + $run_migrations_stmt = $pdo->query("SELECT migration FROM migrations"); + $run_migrations = $run_migrations_stmt->fetchAll(PDO::FETCH_COLUMN); + + // Get all migration files + $migration_files = glob(__DIR__ . '/migrations/*.sql'); + + foreach ($migration_files as $file) { + $migration_name = basename($file); + if (!in_array($migration_name, $run_migrations)) { + $sql = file_get_contents($file); + $pdo->exec($sql); + $stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?)"); + $stmt->execute([$migration_name]); + } + } +} + +// Run migrations on every request for simplicity in this context. +// For production, this should be a separate script. +run_migrations(); diff --git a/db/migrations/001_create_notes_table.sql b/db/migrations/001_create_notes_table.sql new file mode 100644 index 0000000..8026ca4 --- /dev/null +++ b/db/migrations/001_create_notes_table.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS notes ( + id INT AUTO_INCREMENT PRIMARY KEY, + note_date DATE NOT NULL UNIQUE, + content TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/migrations/002_create_alarms_table.sql b/db/migrations/002_create_alarms_table.sql new file mode 100644 index 0000000..01ccd8b --- /dev/null +++ b/db/migrations/002_create_alarms_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS alarms ( + id INT AUTO_INCREMENT PRIMARY KEY, + alarm_time TIME NOT NULL, + label VARCHAR(255) NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); diff --git a/index.php b/index.php index 7205f3d..2dda883 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,130 @@ query("SELECT * FROM alarms ORDER BY alarm_time ASC"); + $alarms = $stmt->fetchAll(); +} catch (PDOException $e) { + // Handle error gracefully + $alarms = []; + error_log("Error fetching alarms: " . $e->getMessage()); +} ?> - + - - - New Style - - - - - - - - - - - - - - - - - - - + + + Alarm & Note App + + + + + + -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+ +
+
+

Alarm Dashboard

+

Set your alarms. Write your notes.

+
+
+ +
+
+
+ +
+
+
Create a New Alarm
+
+
+ + + +
+
+
+
+ + +
+
+ Your Alarms +
+
    + +
  • + No alarms set yet. +
  • + + +
  • +
    +
    + > + +
    +
    + + +
    +
    +
    + + + +
    +
  • + + +
+
+
+
+ Recent Notes +
+ +
+
+
+
+ + + -
- + + + + + + + + diff --git a/note.php b/note.php new file mode 100644 index 0000000..96155c6 --- /dev/null +++ b/note.php @@ -0,0 +1,84 @@ +prepare( + "INSERT INTO notes (note_date, content) VALUES (?, ?) + ON DUPLICATE KEY UPDATE content = VALUES(content)" + ); + $stmt->execute([$date_to_save, $content]); + $message = 'Note saved successfully!'; + } catch (PDOException $e) { + // In a real app, log this error instead of showing it to the user + $message = 'Error saving note: ' . $e->getMessage(); + } +} + +// Fetch existing note for the date +$stmt = $pdo->prepare("SELECT content FROM notes WHERE note_date = ?"); +$stmt->execute([$note_date]); +$note = $stmt->fetch(); +if ($note) { + $note_content = $note['content']; +} + +$formatted_date = date("l, F j, Y", strtotime($note_date)); + +?> + + + + + + Note for <?= htmlspecialchars($formatted_date) ?> + + + + + + + + +
+
+

My Diary

+
+
+ +
+
+

+ + + + + +
+ +
+ +
+
+ Back to Dashboard + +
+
+
+
+ + + +