Преглед изворни кода

- Post editing

- Post deleting
- Simple search functionality
- CSS now uses vars
- Fixing text box font size on iOS
master
Rocketsoup пре 3 година
родитељ
комит
522c793825
4 измењених фајлова са 324 додато и 104 уклоњено
  1. 169
    26
      htdocs/index.php
  2. 150
    74
      htdocs/journal.css
  3. BIN
      journal.db
  4. 5
    4
      source/create-tables.sql

+ 169
- 26
htdocs/index.php Прегледај датотеку

97
 
97
 
98
 /// Represents a journal post.
98
 /// Represents a journal post.
99
 class Post {
99
 class Post {
100
-	public int $post_id;
100
+	public ?int $post_id;
101
 	public string $body;
101
 	public string $body;
102
 	public int $author_id;
102
 	public int $author_id;
103
 	public int $created;
103
 	public int $created;
104
+	public ?int $updated;
104
 
105
 
105
 	function __construct(array $row) {
106
 	function __construct(array $row) {
106
 		$this->post_id = $row['rowid'];
107
 		$this->post_id = $row['rowid'];
107
 		$this->body = $row['body'];
108
 		$this->body = $row['body'];
108
 		$this->author_id = $row['author_id'];
109
 		$this->author_id = $row['author_id'];
109
 		$this->created = $row['created'];
110
 		$this->created = $row['created'];
111
+		$this->updated = $row['updated'];
110
 	}
112
 	}
111
 
113
 
112
 	/// Normalizes the body of a post.
114
 	/// Normalizes the body of a post.
167
 	public static function get_posts(
169
 	public static function get_posts(
168
 			int $user_id,
170
 			int $user_id,
169
 			int $count = RECENT_POSTS_PER_PAGE,
171
 			int $count = RECENT_POSTS_PER_PAGE,
172
+			?string $query = null,
170
 			?int $before_time = null): array {
173
 			?int $before_time = null): array {
171
 		$sql = 'SELECT rowid, * FROM posts WHERE author_id=:author_id';
174
 		$sql = 'SELECT rowid, * FROM posts WHERE author_id=:author_id';
172
 		$args = array(
175
 		$args = array(
177
 			$sql .= ' AND created < :before_time';
180
 			$sql .= ' AND created < :before_time';
178
 			$args[':before_time'] = $before_time;
181
 			$args[':before_time'] = $before_time;
179
 		}
182
 		}
183
+		$search_where = '';
184
+		if ($query) {
185
+			foreach (explode(' ', $query) as $i => $term) {
186
+				if (strlen($term) == 0) continue;
187
+				$symbol = ":wordpattern{$i}";
188
+				$search_where .= " AND body LIKE $symbol ESCAPE '!'";
189
+				$args[$symbol] = '%' . Database::escape_like($term, '!') . '%';
190
+			}
191
+			$sql .= $search_where;
192
+		}
180
 		$sql .= ' ORDER BY created DESC LIMIT :count;';
193
 		$sql .= ' ORDER BY created DESC LIMIT :count;';
181
 		$posts = Database::query_objects('Post', $sql, $args);
194
 		$posts = Database::query_objects('Post', $sql, $args);
182
 
195
 
189
 		if ($before_time) {
202
 		if ($before_time) {
190
 			// We're paged forward. Check if there are newer posts.
203
 			// We're paged forward. Check if there are newer posts.
191
 			$sql = 'SELECT rowid, * FROM posts WHERE author_id=:author_id AND ' .
204
 			$sql = 'SELECT rowid, * FROM posts WHERE author_id=:author_id AND ' .
192
-				'created >= :before_time ORDER BY created ASC LIMIT :count;';
193
-			$args = array(
194
-				':author_id' => $user_id,
195
-				':count' => $count + 1, // to see if it's the newest page
196
-				':before_time' => $before_time,
197
-			);
205
+				'created >= :before_time ' . $search_where . ' ORDER BY created ASC LIMIT :count;';
206
+			// Reusing same $args
198
 			$newer_posts = Database::query_objects('Post', $sql, $args);
207
 			$newer_posts = Database::query_objects('Post', $sql, $args);
199
 			if (sizeof($newer_posts) > $count) {
208
 			if (sizeof($newer_posts) > $count) {
200
 				$prev_date = $newer_posts[array_key_last($newer_posts)]->created;
209
 				$prev_date = $newer_posts[array_key_last($newer_posts)]->created;
203
 				$prev_page = BASE_URL;
212
 				$prev_page = BASE_URL;
204
 			}
213
 			}
205
 		}
214
 		}
215
+		if ($query) {
216
+			if ($prev_page) {
217
+				$prev_page .= (str_contains($prev_page, '?') ? '&' : '?') .
218
+					'search=' . urlencode($query);
219
+			}
220
+			if ($next_page) {
221
+				$next_page .= (str_contains($next_page, '?') ? '&' : '?') .
222
+					'search=' . urlencode($query);
223
+			}
224
+		}
206
 
225
 
207
 		return array($posts, $prev_page, $next_page);
226
 		return array($posts, $prev_page, $next_page);
208
 	}
227
 	}
228
+
229
+	/// Fetches a post by its post ID.
230
+	/// @param int $post_id  ID of the post.
231
+	/// @return ?Post  The Post, or null if not found.
232
+	public static function get_by_id(int $post_id): ?Post {
233
+		$sql = 'SELECT rowid, * FROM posts WHERE rowid=:post_id;';
234
+		$args = array(':post_id' => $post_id);
235
+		return Database::query_object('Post', $sql, $args);
236
+	}
237
+
238
+	/// Deletes this post.
239
+	public function delete(): void {
240
+		$sql = 'DELETE FROM posts WHERE rowid=:post_id;';
241
+		$args = array(':post_id' => $this->post_id);
242
+		Database::query($sql, $args);
243
+		$this->post_id = null;
244
+	}
245
+
246
+	/// Update text of post.
247
+	public function update(string $new_body): void {
248
+		$new_body = self::normalize_body($new_body);
249
+		$sql = 'UPDATE posts SET body=:body, updated=:updated WHERE rowid=:rowid;';
250
+		$args = array(
251
+			':body' => $new_body,
252
+			':updated' => time(),
253
+			':rowid' => $this->post_id,
254
+		);
255
+		Database::query($sql, $args);
256
+	}
209
 }
257
 }
210
 
258
 
211
 /// Represents a user.
259
 /// Represents a user.
477
 	///                 and named column values.
525
 	///                 and named column values.
478
 	public static function query(string $sql, array $params = array()): bool|array {
526
 	public static function query(string $sql, array $params = array()): bool|array {
479
 		$db = new SQLite3(DB_PATH);
527
 		$db = new SQLite3(DB_PATH);
528
+		trace('SQL: ' . $sql);
480
 		$stmt = $db->prepare($sql);
529
 		$stmt = $db->prepare($sql);
481
 		foreach ($params as $name => $value) {
530
 		foreach ($params as $name => $value) {
482
-			$stmt->bindValue($name, $value, self::sqlite_type($value));
531
+			$type = self::sqlite_type($value);
532
+			$stmt->bindValue($name, $value, $type);
533
+			trace("\tbind {$name} => {$value} ({$type})");
483
 		}
534
 		}
484
 		$result = $stmt->execute();
535
 		$result = $stmt->execute();
485
 		if (gettype($result) == 'bool') {
536
 		if (gettype($result) == 'bool') {
514
 				fatal_error("Bad datatype in sqlite statement");
565
 				fatal_error("Bad datatype in sqlite statement");
515
 		}
566
 		}
516
 	}
567
 	}
568
+
569
+	/// Escapes a string for use in a LIKE clause.
570
+	/// @param string $value   String to escape.
571
+	/// @param string $escape  Character to use for escaping. Default is !
572
+	/// @return string  Escaped string
573
+	public static function escape_like(string $value, string $escape='!'): string {
574
+		$s = $value;
575
+		$s = str_replace($escape, $escape . $escape, $s); // escape char
576
+		$s = str_replace('%', $escape . '%', $s); // any chars
577
+		$s = str_replace('_', $escape . '_', $s); // one char
578
+		return $s;
579
+	}
517
 }
580
 }
518
 
581
 
519
 /// App configuration management. All config is stored in the 'config' table in
582
 /// App configuration management. All config is stored in the 'config' table in
717
 		if (User::$current) {
780
 		if (User::$current) {
718
 			print(<<<HTML
781
 			print(<<<HTML
719
 							<div class="menu-container">
782
 							<div class="menu-container">
720
-								<details>
721
-									<summary><span>☰</span></summary>
783
+								<details class="menu">
784
+									<summary class="no-indicator menu-button"><span>☰</span></summary>
722
 
785
 
723
 									<ul>
786
 									<ul>
724
-										<a href="?logout"><li>Log out</li></a>
787
+										<li><a href="?search">Search</a></li>
788
+										<li class="menu-divider"></li>
789
+										<li class="logout-item destructive"><a href="?logout">Log out</a></li>
725
 									</ul>
790
 									</ul>
726
 								</details>
791
 								</details>
727
 							</div>
792
 							</div>
758
 	}
823
 	}
759
 
824
 
760
 	public static function render_post_form(): void {
825
 	public static function render_post_form(): void {
761
-		$body = array_key_exists(SESSION_KEY_POST_BODY, $_SESSION) ? $_SESSION[SESSION_KEY_POST_BODY] : '';
762
-		unset($_SESSION[SESSION_KEY_POST_BODY]);
826
+		$action = 'post';
827
+		$verb = 'Post';
828
+		$body = '';
829
+		if ($edit_id = validate($_GET, 'edit', INPUT_TYPE_INT, required: false)) {
830
+			if ($post = Post::get_by_id($edit_id)) {
831
+				$body = $post->body;
832
+				$action = 'edit';
833
+				$verb = 'Update';
834
+			} else {
835
+				unset($edit_id);
836
+			}
837
+		} elseif (array_key_exists(SESSION_KEY_POST_BODY, $_SESSION)) {
838
+			$body = $_SESSION[SESSION_KEY_POST_BODY];
839
+			unset($_SESSION[SESSION_KEY_POST_BODY]);
840
+		}
841
+		$body_html = htmlentities($body);
763
 		print(<<<HTML
842
 		print(<<<HTML
764
 			<form id="post-form" method="POST">
843
 			<form id="post-form" method="POST">
765
-				<div class="text-container"><textarea name="body" placeholder="Your journal post here…">$body</textarea></div>
766
-				<input type="hidden" name="action" value="post" />
767
-				<div class="submit-post"><input type="submit" value="Post" /></div>
844
+				<div class="text-container"><textarea name="body" placeholder="Your journal post here…">$body_html</textarea></div>
845
+				<input type="hidden" name="action" value="{$action}" />
846
+			HTML
847
+		);
848
+		if ($edit_id) {
849
+			print("<input type=\"hidden\" name=\"edit_id\" value=\"{$edit_id}\" />");
850
+		}
851
+		print(<<<HTML
852
+				<div class="submit-post"><input type="submit" value="{$verb}" /></div>
768
 			</form>
853
 			</form>
769
 			HTML
854
 			HTML
770
 		);
855
 		);
771
 	}
856
 	}
772
 
857
 
773
 	public static function render_recent_posts(): void {
858
 	public static function render_recent_posts(): void {
859
+		$query = validate($_GET, 'search', INPUT_TYPE_STRING | INPUT_TYPE_TRIMMED, required: false);
774
 		$before = validate($_GET, 'before', INPUT_TYPE_INT, required: false);
860
 		$before = validate($_GET, 'before', INPUT_TYPE_INT, required: false);
775
-		[ $posts, $prev_url, $next_url ] = Post::get_posts(User::$current->user_id, before_time: $before);
861
+		[ $posts, $prev_url, $next_url ] = Post::get_posts(User::$current->user_id, query: $query, before_time: $before);
776
 		print("<div class=\"post-container\">\n");
862
 		print("<div class=\"post-container\">\n");
777
 		if ($prev_url !== null) {
863
 		if ($prev_url !== null) {
778
 			print("<div class=\"previous\"><a href=\"{$prev_url}\">Previous</a></div>\n");
864
 			print("<div class=\"previous\"><a href=\"{$prev_url}\">Previous</a></div>\n");
788
 
874
 
789
 	/// Encodes a post body as HTML. Inserts linebreaks and paragraph tags.
875
 	/// Encodes a post body as HTML. Inserts linebreaks and paragraph tags.
790
 	private static function post_body_html(string $body): string {
876
 	private static function post_body_html(string $body): string {
877
+		$html_p_start = "<p>";
878
+		$html_p_end = "</p>\n";
879
+		$html_newline = "<br/>\n";
791
 		$body_html = htmlentities($body);
880
 		$body_html = htmlentities($body);
792
 		// Single newlines are turned into linebreaks.
881
 		// Single newlines are turned into linebreaks.
793
-		$body_html = str_replace("\n", "<br/>\n", $body_html);
882
+		$body_html = str_replace("\n", $html_newline, $body_html);
794
 		// Pairs of newlines are turned into paragraph separators.
883
 		// Pairs of newlines are turned into paragraph separators.
795
-		$paragraphs = explode("<br/>\n<br/>\n", $body_html);
796
-		$body_html = "<p>" . implode("</p>\n\n<p>", $paragraphs) . "</p>\n";
884
+		$body_html = $html_p_start .
885
+			str_replace($html_newline . $html_newline,
886
+				$html_p_end . $html_p_start,
887
+				$body_html) .
888
+			$html_p_end;
797
 		return $body_html;
889
 		return $body_html;
798
 	}
890
 	}
799
 
891
 
804
 			<div class="post">
896
 			<div class="post">
805
 				<article>
897
 				<article>
806
 					<div class="post-body">{$body_html}</div>
898
 					<div class="post-body">{$body_html}</div>
807
-					<footer>
808
-						<div class="post-date">
809
-							Posted {$date}
810
-						</div>
811
-					</footer>
899
+					<div class="post-footer">
900
+						<footer class="secondary-text">
901
+							<div class="post-date">
902
+								Posted {$date}
903
+			HTML
904
+		);
905
+		if ($post->updated && $post->updated != $post->created) {
906
+			print('<br/>(updated ' . localized_date_string($post->updated) . ')');
907
+		}
908
+		print(<<<HTML
909
+							</div>
910
+							<details class="post-actions menu">
911
+								<summary class="no-indicator menu-button"><span>actions</span></summary>
912
+
913
+								<ul>
914
+									<li><a href="?edit={$post->post_id}">Edit</a></li>
915
+									<li class="menu-divider"></li>
916
+									<li class="post-action-delete destructive"><a href="?delete={$post->post_id}">Delete</a></li>
917
+								</ul>
918
+							</details>
919
+						</footer>
920
+					</div>
812
 				</article>
921
 				</article>
813
 			</div>
922
 			</div>
814
 			HTML
923
 			HTML
815
 		);
924
 		);
816
 	}
925
 	}
817
 
926
 
927
+	public static function render_search_form(): void {
928
+		$q = validate($_GET, 'search', INPUT_TYPE_STRING | INPUT_TYPE_TRIMMED, required: false);
929
+		$q_html = htmlentities($q);
930
+		$cancel = BASE_URL;
931
+		print(<<<HTML
932
+			<form id="search-form" method="GET">
933
+				<div>
934
+					<label for="search">Search:</label>
935
+					<input type="text" name="search" id="search" value="{$q_html}" autocapitalize="off" />
936
+					<input type="submit" value="Search" />
937
+					<a href="{$cancel}">Cancel</a>
938
+				</div>
939
+			</form>
940
+			HTML
941
+		);
942
+	}
943
+
818
 	public static function render_sign_in_form(): void {
944
 	public static function render_sign_in_form(): void {
819
 		print(<<<HTML
945
 		print(<<<HTML
820
 			<form id="signin-form" method="POST">
946
 			<form id="signin-form" method="POST">
888
 		HTMLPage::render_page_start();
1014
 		HTMLPage::render_page_start();
889
 		HTMLPage::render_error_if_needed();
1015
 		HTMLPage::render_error_if_needed();
890
 		if (User::$current) {
1016
 		if (User::$current) {
891
-			HTMLPage::render_post_form();
1017
+			if ($delete_id = validate($_GET, 'delete', INPUT_TYPE_INT, required: false)) {
1018
+				Post::get_by_id($delete_id)?->delete();
1019
+				HTMLPage::redirect_home();
1020
+			}
1021
+			if (array_key_exists('search', $_GET)) {
1022
+				HTMLPage::render_search_form();
1023
+			} else {
1024
+				HTMLPage::render_post_form();
1025
+			}
892
 			HTMLPage::render_recent_posts();
1026
 			HTMLPage::render_recent_posts();
893
 		} elseif (User::any_exist()) {
1027
 		} elseif (User::any_exist()) {
894
 			HTMLPage::render_sign_in_form();
1028
 			HTMLPage::render_sign_in_form();
912
 				}
1046
 				}
913
 				Post::create($body, $author, $created);
1047
 				Post::create($body, $author, $created);
914
 				break;
1048
 				break;
1049
+			case 'edit':
1050
+				$body = validate($_POST, 'body', $nonempty_str_type);
1051
+				$edit_id = validate($_POST, 'edit_id', INPUT_TYPE_INT);
1052
+				if (!User::$current) {
1053
+					// Not logged in. Save body for populating once they sign in.
1054
+					fatal_error('Please sign in to edit.');
1055
+				}
1056
+				Post::get_by_id($edit_id)?->update($body);
1057
+				break;
915
 			case 'createaccount':
1058
 			case 'createaccount':
916
 				$username = validate($_POST, 'username', INPUT_TYPE_USERNAME);
1059
 				$username = validate($_POST, 'username', INPUT_TYPE_USERNAME);
917
 				$password = validate($_POST, 'password', $password_type);
1060
 				$password = validate($_POST, 'password', $password_type);

+ 150
- 74
htdocs/journal.css Прегледај датотеку

1
+/* Colors */
2
+
1
 :root {
3
 :root {
2
-	background-color: white;
3
-	color: black;
4
+	--page-background: #fff;
5
+	--text-color: #000;
6
+	--secondary-text-color: #888;
7
+	--nav-background: #fff;
8
+	--menu-background: #eee;
9
+	--menu-border: #ccc;
10
+
11
+	--highlight: #d83;
12
+	--error-text: #d00;
13
+	--error-background: #fcc;
14
+	--error-border: #400;
15
+	--callout-text: #000;
16
+	--callout-background: #ffc;
17
+	--callout-border: #886;
18
+	--destructive: #e00;
19
+	--destructive-contrast: #fff;
20
+}
21
+
22
+@media(prefers-color-scheme: dark) {
23
+	:root {
24
+		--page-background: #222;
25
+		--text-color: #fff;
26
+		--secondary-text-color: #888;
27
+		--nav-background: #444;
28
+		--menu-background: #444;
29
+		--menu-border: #000;
30
+	}
4
 }
31
 }
32
+
33
+/* Base */
34
+
5
 :root, input, textarea {
35
 :root, input, textarea {
6
 	font-family: Garamond, Times New Roman, serif;
36
 	font-family: Garamond, Times New Roman, serif;
7
 	line-height: 1.25;
37
 	line-height: 1.25;
8
 	font-size: 12pt;
38
 	font-size: 12pt;
9
 }
39
 }
40
+body, textarea, input, select {
41
+	background-color: var(--page-background);
42
+	color: var(--text-color);
43
+}
10
 body {
44
 body {
11
 	margin: 0;
45
 	margin: 0;
12
 	padding: 0;
46
 	padding: 0;
13
 }
47
 }
14
 a {
48
 a {
15
-	color: #d83;
49
+	color: var(--highlight);
16
 	text-decoration: none;
50
 	text-decoration: none;
17
 }
51
 }
52
+summary {
53
+	-webkit-user-select: none;
54
+	user-select: none;
55
+}
56
+summary.no-indicator {
57
+	list-style-type: none;
58
+}
59
+summary.no-indicator::-webkit-details-marker {
60
+	display: none;
61
+}
62
+.secondary-text {
63
+	color: var(--secondary-text-color);
64
+	font-size: 0.8rem;
65
+}
66
+details.menu ul {
67
+	position: relative;
68
+	z-index: 1;
69
+	margin: 0;
70
+	padding: 0;
71
+	list-style-type: none;
72
+	border: 1px solid var(--menu-border);
73
+	background-color: var(--menu-background);
74
+	box-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 0.25);
75
+	text-align: start;
76
+}
77
+details.menu li a {
78
+	color: var(--highlight);
79
+}
80
+details.menu li a:hover {
81
+	background-color: var(--highlight);
82
+	color: black;
83
+}
84
+details.menu li {
85
+}
86
+details.menu li a {
87
+	padding: 0.4em 1em;
88
+	display: inline-block;
89
+	position: relative;
90
+	width: 10ch;
91
+}
92
+details.menu li + li {
93
+	border-top: 1px solid var(--menu-border);
94
+}
95
+details.menu li.menu-divider {
96
+	padding-top: 0.75em;
97
+}
98
+details.menu li.destructive a {
99
+	color: var(--destructive);
100
+}
101
+details.menu li.destructive a:hover {
102
+	background-color: var(--destructive);
103
+	color: var(--destructive-contrast);
104
+}
105
+summary.menu-button:hover {
106
+	color: var(--highlight);
107
+}
108
+summary.menu-button > span:first-child {
109
+	display: inline-block;
110
+	line-height: 1em;
111
+	padding: 0.5rem 0.7em;
112
+	margin: 0;
113
+	border-radius: 0.5em;
114
+}
115
+details[open] summary.menu-button > span:first-child {
116
+	background-color: var(--highlight);
117
+	color: white;
118
+}
119
+
120
+
121
+/* Navigation */
18
 
122
 
19
 .top-nav {
123
 .top-nav {
20
 	position: fixed;
124
 	position: fixed;
21
 	top: 0;
125
 	top: 0;
22
-	left: 0;
23
-	right: 0;
126
+	inset-inline-start: 0;
127
+	inset-inline-end: 0;
24
 	height: 2em;
128
 	height: 2em;
25
 	text-align: center;
129
 	text-align: center;
26
-	background-color: white;
130
+	background-color: var(--nav-background);
27
 	box-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 0.25);
131
 	box-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 0.25);
28
 }
132
 }
29
 .title {
133
 .title {
39
 	right: 0;
143
 	right: 0;
40
 }
144
 }
41
 .menu-container summary {
145
 .menu-container summary {
42
-	list-style-type: none;
43
 	text-align: right;
146
 	text-align: right;
44
-	-webkit-user-select: none;
45
-	user-select: none;
46
-}
47
-.menu-container summary:hover {
48
-	color: #d83;
49
-}
50
-.menu-container summary span {
51
-	display: inline-block;
52
-	line-height: 1rem;
53
-	padding: 0.5rem 0.7rem;
54
-	margin: 0;
55
-	border-radius: 0.5rem;
56
-}
57
-.menu-container details {
58
-	text-align: left;
59
-}
60
-.menu-container details[open] summary span {
61
-	background-color: #d83;
62
-	color: white;
63
-}
64
-.menu-container ul {
65
-	margin: 0;
66
-	padding: 0;
67
-	list-style-type: none;
68
-	border: 1px solid #ccc;
69
-	background-color: #eee;
70
-	box-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 0.25);
71
-}
72
-.menu-container a li {
73
-	color: #d83;
74
-}
75
-.menu-container a:hover li {
76
-	background-color: #d83;
77
-	color: black;
78
-}
79
-.menu-container li {
80
-	padding: 0.4em 1em;
81
-}
82
-.menu-container a + a li {
83
-	border-top: 1px solid #ccc;
84
-}
85
-summary::-webkit-details-marker {
86
-	display: none;
87
 }
147
 }
88
 
148
 
149
+/* Content container */
150
+
89
 .content {
151
 .content {
90
 	max-width: 66ch;
152
 	max-width: 66ch;
91
 	margin: 2.5em auto 2em auto;
153
 	margin: 2.5em auto 2em auto;
92
 	padding: 0.5em;
154
 	padding: 0.5em;
93
 }
155
 }
94
 
156
 
157
+/* Errors and alerts */
158
+
95
 .error {
159
 .error {
96
 	padding: 1em;
160
 	padding: 1em;
97
-	border: 1px solid #400;
98
-	background-color: #fcc;
99
-	color: #d00;
161
+	border: 1px solid var(--error-border);
162
+	background-color: var(--error-background);
163
+	color: var(--error-text);
100
 	font-weight: bold;
164
 	font-weight: bold;
101
 	margin-bottom: 1em;
165
 	margin-bottom: 1em;
102
 }
166
 }
103
 .important {
167
 .important {
104
 	padding: 1em;
168
 	padding: 1em;
105
-	border: 1px solid #886;
106
-	background-color: #ffc;
107
-	color: black;
169
+	border: 1px solid var(--callout-border);
170
+	background-color: var(--callout-background);
171
+	color: var(--callout-text);
108
 	margin-bottom: 1em;
172
 	margin-bottom: 1em;
109
 }
173
 }
110
 
174
 
175
+/* Post form */
176
+
111
 textarea {
177
 textarea {
112
 	width: 100%;
178
 	width: 100%;
113
 	height: 8em;
179
 	height: 8em;
114
 	font-family: inherit;
180
 	font-family: inherit;
181
+	font-size: 1rem;
115
 	margin: 0;
182
 	margin: 0;
116
 	box-sizing: border-box;
183
 	box-sizing: border-box;
117
 }
184
 }
126
 	min-width: 15ch;
193
 	min-width: 15ch;
127
 }
194
 }
128
 
195
 
196
+/* Posts */
197
+
129
 .post-container {
198
 .post-container {
130
 	margin-top: 1.5rem;
199
 	margin-top: 1.5rem;
131
 }
200
 }
140
 	margin: 0;
209
 	margin: 0;
141
 }
210
 }
142
 .post-date {
211
 .post-date {
143
-	font-size: 80%;
144
-	color: #888;
145
 	margin-top: 0.5em;
212
 	margin-top: 0.5em;
146
 }
213
 }
147
-
148
-@media(prefers-color-scheme: dark) {
149
-	:root, textarea, input, select {
150
-		background-color: #222;
151
-		color: white;
152
-	}
153
-	.top-nav {
154
-		background-color: #444;
155
-	}
156
-	.menu-container ul {
157
-		border: 1px solid black;
158
-		background-color: #444;
159
-	}
160
-	.menu-container a + a li {
161
-		border-top: 1px solid #000;
162
-	}
214
+.post-footer {
215
+	position: relative;
216
+}
217
+.post-actions {
218
+	position: absolute;
219
+	top: 0;
220
+	inset-inline-end: 0;
221
+}
222
+.post-actions summary {
223
+	text-align: end;
224
+}
225
+.post-actions summary > span:first-child {
226
+	padding-top: 0;
227
+	padding-bottom: 0;
163
 }
228
 }
229
+details.menu li.post-action-delete a {
230
+	color: var(--destructive);
231
+}
232
+details.menu li.post-action-delete a:hover {
233
+	background-color: var(--destructive);
234
+	color: var(--destructive-contrast);
235
+}
236
+
237
+
238
+/* Mobile */
239
+
164
 @media screen and (max-width: 450px) {
240
 @media screen and (max-width: 450px) {
165
 	:root {
241
 	:root {
166
 		font-size: 16pt;
242
 		font-size: 16pt;


+ 5
- 4
source/create-tables.sql Прегледај датотеку

23
 CREATE TABLE posts (
23
 CREATE TABLE posts (
24
 	body TEXT,                    -- main text of the post
24
 	body TEXT,                    -- main text of the post
25
 	created INTEGER,              -- timestamp when the post was created
25
 	created INTEGER,              -- timestamp when the post was created
26
+	updated INTEGER,              -- timestamp when the post was last edited
26
 	author_id INTEGER NOT NULL,   -- user_id of author
27
 	author_id INTEGER NOT NULL,   -- user_id of author
27
 	FOREIGN KEY (author_id) REFERENCES users (user_id)
28
 	FOREIGN KEY (author_id) REFERENCES users (user_id)
28
 );
29
 );
30
 
31
 
31
 DROP TABLE IF EXISTS sessions;
32
 DROP TABLE IF EXISTS sessions;
32
 CREATE TABLE sessions (
33
 CREATE TABLE sessions (
33
-	token TEXT UNIQUE NOT NULL,
34
-	user_id INTEGER NOT NULL,
35
-	created INTEGER,
36
-	updated INTEGER,
34
+	token TEXT UNIQUE NOT NULL,  -- random hex string
35
+	user_id INTEGER NOT NULL,    -- owner of the token
36
+	created INTEGER,             -- timestamp when the session was created
37
+	updated INTEGER,             -- timestamp when the session was last used
37
 	FOREIGN KEY (user_id) REFERENCES users (user_id)
38
 	FOREIGN KEY (user_id) REFERENCES users (user_id)
38
 );
39
 );
39
 CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_token ON sessions (token);
40
 CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_token ON sessions (token);

Loading…
Откажи
Сачувај