diff --git a/public/css/default.css b/public/css/default.css
index ca54f5b..738995b 100644
--- a/public/css/default.css
+++ b/public/css/default.css
@@ -376,6 +376,20 @@ time {
text-decoration: none;
}
+.delete-tick-form {
+ display: inline;
+ margin: 0;
+}
+
+.delete-tick-button {
+ background: none;
+ border: none;
+ padding: 0;
+ margin: 0;
+ cursor: pointer;
+ font-size: inherit;
+}
+
/* Mood selection page */
.mood-option input {
position: absolute;
diff --git a/src/Controller/TickController/TickController.php b/src/Controller/TickController/TickController.php
index fb8d17a..f33aa24 100644
--- a/src/Controller/TickController/TickController.php
+++ b/src/Controller/TickController/TickController.php
@@ -27,4 +27,35 @@ class TickController extends Controller{
echo '
500 - Internal Server Error
';
}
}
+
+ public function handleDelete(string $id){
+ global $app;
+
+ $id = (int) $id;
+ Log::debug("Attempting to delete tick with ID: {$id}");
+
+ try {
+ $tickModel = new TickModel($app['db'], $app['settings']);
+
+ // TickModel->delete() handles validation and sets flash messages:
+ // - "Tick not found" if tick doesn't exist
+ // - "Tick is too old to delete" if outside deletion window
+ // - "Deleted: '{content}'" on success
+ $success = $tickModel->delete($id);
+
+ if ($success) {
+ Log::info("Successfully deleted tick {$id}");
+ } else {
+ Log::warning("Failed to delete tick {$id}");
+ }
+
+ } catch (Exception $e) {
+ Log::error("Exception while deleting tick {$id}: " . $e->getMessage());
+ Session::setFlashMessage('error', 'An error occurred while deleting the tick');
+ }
+
+ // Redirect back to homepage
+ header('Location: ' . Util::buildRelativeUrl($app['settings']->basePath, ''));
+ exit();
+ }
}
\ No newline at end of file
diff --git a/src/Framework/Router/Router.php b/src/Framework/Router/Router.php
index f0bb6e0..66f5f29 100644
--- a/src/Framework/Router/Router.php
+++ b/src/Framework/Router/Router.php
@@ -25,6 +25,7 @@ class Router {
['tkr-setup', 'AdminController@showSetup'],
['tkr-setup', 'AdminController@handleSetup', ['POST']],
['tick/{id}', 'TickController'],
+ ['tick/{id}/delete', 'TickController@handleDelete', ['POST']],
['css/custom/{filename}.css', 'CssController@serveCustomCss'],
];
diff --git a/src/Model/SettingsModel/SettingsModel.php b/src/Model/SettingsModel/SettingsModel.php
index 539332d..a9b714b 100644
--- a/src/Model/SettingsModel/SettingsModel.php
+++ b/src/Model/SettingsModel/SettingsModel.php
@@ -12,6 +12,8 @@ class SettingsModel {
public ?int $cssId = null;
public bool $strictAccessibility = true;
public ?int $logLevel = null;
+ // not currently configurable
+ public int $tickDeleteHours = 1;
public function __construct(private PDO $db) {}
diff --git a/src/Model/TickModel/TickModel.php b/src/Model/TickModel/TickModel.php
index 4896fbc..74bab9f 100644
--- a/src/Model/TickModel/TickModel.php
+++ b/src/Model/TickModel/TickModel.php
@@ -7,8 +7,17 @@ class TickModel {
public function getPage(int $limit, int $offset = 0): array {
$stmt = $this->db->prepare("SELECT id, timestamp, tick FROM tick ORDER BY timestamp DESC LIMIT ? OFFSET ?");
$stmt->execute([$limit, $offset]);
-
- return $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ $ticks = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ return array_map(function($tick) {
+ $tickTime = new DateTimeImmutable($tick['timestamp'], new DateTimeZone('UTC'));
+ $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
+ $hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600;
+
+ $tick['can_delete'] = $hoursSinceCreation <= $this->settings->tickDeleteHours;
+ return $tick;
+ }, $ticks);
}
public function insert(string $tick, ?DateTimeImmutable $datetime = null): void {
@@ -35,4 +44,33 @@ class TickModel {
'settings' => $this->settings,
];
}
+
+ public function delete(int $id): bool {
+ // Get tick and validate
+ $stmt = $this->db->prepare("SELECT tick, timestamp FROM tick WHERE id=?");
+ $stmt->execute([$id]);
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($row === false || empty($row)) {
+ Session::setFlashMessage('error', 'Tick not found');
+ return false;
+ }
+
+ // Check deletion window
+ $tickTime = new DateTimeImmutable($row['timestamp'], new DateTimeZone('UTC'));
+ $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
+ $hoursSinceCreation = ($now->getTimestamp() - $tickTime->getTimestamp()) / 3600;
+
+ if ($hoursSinceCreation > $this->settings->tickDeleteHours) {
+ Session::setFlashMessage('error', 'Tick is too old to delete');
+ return false;
+ }
+
+ // Delete and set success message
+ $stmt = $this->db->prepare("DELETE FROM tick WHERE id=?");
+ $stmt->execute([$id]);
+
+ Session::setFlashMessage('success', "Deleted: '{$row['tick']}'");
+ return true;
+ }
}
diff --git a/src/View/TicksView/TicksView.php b/src/View/TicksView/TicksView.php
index 1e434a6..d9e1646 100644
--- a/src/View/TicksView/TicksView.php
+++ b/src/View/TicksView/TicksView.php
@@ -23,7 +23,15 @@ class TicksView {
$relativeTime = Util::relative_time($tick['timestamp']);
?>
- 🗑️
+
+
+
+
diff --git a/tests/Controller/TickController/TickControllerTest.php b/tests/Controller/TickController/TickControllerTest.php
index 0f90d8f..0401d93 100644
--- a/tests/Controller/TickController/TickControllerTest.php
+++ b/tests/Controller/TickController/TickControllerTest.php
@@ -152,5 +152,4 @@ class TickControllerTest extends TestCase
// Should return 500 error
$this->assertStringContainsString('500 - Internal Server Error', $output);
}
-
}
\ No newline at end of file
diff --git a/tests/Model/TickModel/TickModelTest.php b/tests/Model/TickModel/TickModelTest.php
new file mode 100644
index 0000000..e0ddfb3
--- /dev/null
+++ b/tests/Model/TickModel/TickModelTest.php
@@ -0,0 +1,75 @@
+mockPdo = $this->createMock(PDO::class);
+ $this->settings = new SettingsModel($this->mockPdo);
+ $this->settings->tickDeleteHours = 1; // 1 hour deletion window
+ }
+
+ public function testDeleteWithRecentTick(): void
+ {
+ // Mock successful deletion of recent tick
+ $recentTimestamp = (new DateTimeImmutable())->format('Y-m-d H:i:s');
+
+ $mockStatement1 = $this->createMock(PDOStatement::class);
+ $mockStatement1->method('execute')->with([123]);
+ $mockStatement1->method('fetch')->willReturn([
+ 'tick' => 'Test content',
+ 'timestamp' => $recentTimestamp
+ ]);
+
+ $mockStatement2 = $this->createMock(PDOStatement::class);
+ $mockStatement2->method('execute')->with([123]);
+
+ $this->mockPdo->method('prepare')
+ ->willReturnOnConsecutiveCalls($mockStatement1, $mockStatement2);
+
+ $tickModel = new TickModel($this->mockPdo, $this->settings);
+ $result = $tickModel->delete(123);
+
+ $this->assertTrue($result);
+ }
+
+ public function testDeleteWithNonexistentTick(): void
+ {
+ $mockStatement = $this->createMock(PDOStatement::class);
+ $mockStatement->method('execute')->with([999]);
+ $mockStatement->method('fetch')->willReturn(false);
+
+ $this->mockPdo->method('prepare')->willReturn($mockStatement);
+
+ $tickModel = new TickModel($this->mockPdo, $this->settings);
+ $result = $tickModel->delete(999);
+
+ $this->assertFalse($result);
+ }
+
+ public function testDeleteWithOldTick(): void
+ {
+ // Tick from 3 hours ago (outside 1-hour window)
+ $oldTimestamp = (new DateTimeImmutable('-3 hours'))->format('Y-m-d H:i:s');
+
+ $mockStatement = $this->createMock(PDOStatement::class);
+ $mockStatement->method('execute')->with([456]);
+ $mockStatement->method('fetch')->willReturn([
+ 'tick' => 'Old content',
+ 'timestamp' => $oldTimestamp
+ ]);
+
+ $this->mockPdo->method('prepare')->willReturn($mockStatement);
+
+ $tickModel = new TickModel($this->mockPdo, $this->settings);
+ $result = $tickModel->delete(456);
+
+ $this->assertFalse($result);
+ }
+}
\ No newline at end of file