From e5af3aca66f4579a640321c9b2a42f435b7261a5 Mon Sep 17 00:00:00 2001
From: Josh Ashton
Date: Thu, 3 Apr 2025 10:50:58 -0600
Subject: [PATCH] sync, implemented sort, viewing user pages & articles
written, updating db
---
articles.php | 42 +++++++++++++++++--
components/head.php | 9 ++--
components/nav.php | 2 +-
compose.php | 3 +-
css/base.css | 74 ++++++++++++++++++++++++++++-----
functions/account_functions.php | 56 +++++++++++++++----------
functions/article_functions.php | 52 +++++++++++++++++++++--
functions/functions.php | 11 +++++
functions/init.php | 4 +-
js/md_preview.js | 17 ++++++++
scripts/init.sql | 5 +++
vintagecoding.net.md | 10 ++---
12 files changed, 231 insertions(+), 54 deletions(-)
create mode 100644 js/md_preview.js
diff --git a/articles.php b/articles.php
index 639afbf..666d896 100644
--- a/articles.php
+++ b/articles.php
@@ -6,14 +6,50 @@ include_once 'components/head.php';
include_once 'functions/article_functions.php';
-// TODO: Move to a grid, where the left hand column is either a search and filtering system, or...Table of Contents?
-
// Default view, it'll show articles by recency
if (!isset($_GET['article_id']) && !isset($_GET['tag']) && !isset($_GET['author_id'])) {
- $ids = article_ids_by_recency();
+ $filters = '
+
+ ';
+
+
+ echo '
+
+
+ ' . $filters . '
+
+
+ ';
foreach ($ids as $id)
echo article_card($id);
+ echo '
+
+
+ ';
} else if (isset($_GET['article_id'])) {
increment_read_counter($_GET['article_id']);
echo article_page_from_markdown($_GET['article_id']);
diff --git a/components/head.php b/components/head.php
index 71595d7..11f05e4 100644
--- a/components/head.php
+++ b/components/head.php
@@ -4,17 +4,18 @@
+
';
-} elseif ($_SESSION['theme'] == 'light') {
+ } elseif ($_SESSION['theme'] == 'light') {
echo '
';
-}
-?>
+ }
+ ?>
diff --git a/components/nav.php b/components/nav.php
index bbf7315..47be020 100644
--- a/components/nav.php
+++ b/components/nav.php
@@ -6,7 +6,7 @@ $nav = '
- Home
- - Articles
+ - Articles
';
if (isset($_COOKIE['role_id']) && $_COOKIE['role_id'] <= 2) {
diff --git a/compose.php b/compose.php
index f22260d..950d78c 100644
--- a/compose.php
+++ b/compose.php
@@ -4,7 +4,7 @@ require_once 'functions/functions.php';
include_once 'functions/article_functions.php';
if (!empty($_POST)) {
- $id = create_article($_COOKIE['user_id'], $_POST['title'], $_POST['excerpt'], $_POST['tags'], $_POST['markdown']);
+ $id = create_article($_COOKIE['user_id'], $_POST['title'], $_POST['excerpt'], (isset($_POST['tags']) ? $_POST['tags'] : []), $_POST['markdown']);
header('Location: articles.php?article_id=' . $id);
}
@@ -14,6 +14,7 @@ if (!isset($_COOKIE['role_id']) || $_COOKIE['role_id'] == 4)
include_once 'components/head.php';
echo form();
+
include_once 'components/foot.php';
function form()
diff --git a/css/base.css b/css/base.css
index 213a392..e978591 100644
--- a/css/base.css
+++ b/css/base.css
@@ -60,12 +60,6 @@ label,
font-size: 1vw;
}
-p::after {
- content: "";
- display: inline-block;
- width: 100%;
-}
-
small {
font-size: .75vw;
}
@@ -171,9 +165,9 @@ code {
.view_card {
margin: 0 auto;
margin-top: 50px;
+ width: 70%;
padding: 18px;
height: max-content;
- width: 70%;
border-radius: 18px;
display: flex;
@@ -184,6 +178,11 @@ code {
transition-duration: 1s;
}
+.article_card {
+ width: 100%;
+ box-sizing: border-box;
+}
+
.form_card {
width: 50%;
}
@@ -292,7 +291,8 @@ table tr td label {
text-decoration-color: orange;
}
-#theme-toggle {
+#theme-toggle,
+label i {
width: 32px;
height: 32px;
border: none;
@@ -300,16 +300,25 @@ table tr td label {
font-size: 1.5vw;
}
-#theme-toggle * {
+#theme-toggle *,
+label i {
transition-duration: 500ms;
}
-#theme-toggle *:hover {
+#theme-toggle *:hover,
+label i:hover {
color: orange;
}
+label i {
+ width: 32px;
+ height: 32px;
+ font-size: 1.5vw;
+ transition-duration: 500ms;
+}
+
.article {
- width: 50%;
+ width: 60%;
margin: 0 auto;
}
@@ -589,3 +598,46 @@ input[type="file"] {
[data-href] {
cursor: pointer;
}
+
+.article_cards_grid {
+ width: 70%;
+ margin: 0 auto;
+ display: block;
+}
+
+.article_grid {
+ display: grid;
+ grid-template-columns: 15% 85%;
+}
+
+.article_toc {
+ margin-right: 15px;
+}
+
+/* Tooltip container */
+.tooltip {
+ position: relative;
+ display: inline-block;
+ border-bottom: 1px dotted black;
+ /* If you want dots under the hoverable text */
+}
+
+/* Tooltip text */
+.tooltip .tooltiptext {
+ visibility: hidden;
+ width: 120px;
+ background-color: black;
+ color: #fff;
+ text-align: center;
+ padding: 5px 0;
+ border-radius: 6px;
+
+ /* Position the tooltip text - see examples below! */
+ position: absolute;
+ z-index: 1;
+}
+
+/* Show the tooltip text when you mouse over the tooltip container */
+.tooltip:hover .tooltiptext {
+ visibility: visible;
+}
diff --git a/functions/account_functions.php b/functions/account_functions.php
index 6fa40b2..29400ea 100644
--- a/functions/account_functions.php
+++ b/functions/account_functions.php
@@ -1,5 +1,6 @@
prepare('INSERT INTO user (user_id, username, email, password_hash, role_id, profile_picture) VALUES (?, ?, ?, ?, ?, ?)');
- $asdf = (isset($target) ? $target : null);
- $stmt->bind_param('isssis', $id, $username, $email, $password_hash, $role_id, $asdf);
+ $stmt->bind_param('isssis', $id, $username, $email, $password_hash, $role_id, $target);
$stmt->execute();
return true;
@@ -492,9 +494,6 @@ function delete_account($user_id, $password = null, $verify_password = null)
function user_view($user_id)
{
- if (!isset($_COOKIE['role_id']) || ($_COOKIE['role_id'] > 2 && $_COOKIE['user_id'] != $user_id))
- header('Location: articles.php');
-
$conn = get_connection();
$stmt = $conn->prepare('SELECT user_id, username, email, role_id, created_at, last_active, is_active, profile_picture FROM user WHERE user_id = ?');
$stmt->bind_param('i', $user_id);
@@ -513,33 +512,44 @@ function user_view($user_id)
- User Actions
';
// TODO: Need to setup an email handler.
// Request Role
// ' . request_role_change_form() . '
- $view .= '
-
-
-
-
-
-
+ if ((isset($_COOKIE['user_id']) && $_COOKIE['user_id'] == $user_id) || (isset($_COOKIE['role_id']) && $_COOKIE['role_id'] <= 2)) {
+ $view .= '
+ User Actions
+
+
+
+
+
+
-
-
+
+
+
+
' . reset_pw_form() . '
+
' . update_email_form() . '
+
' . update_username_form() . '
+
' . delete_account_form() . '
+ ';
+ } else {
+ $ids = articles_ids_by_author($_GET['user_id']);
+ $view .= '
+
+ ';
-
-
' . reset_pw_form() . '
-
' . update_email_form() . '
-
' . update_username_form() . '
-
' . delete_account_form() . '
-
-
- ';
+ foreach ($ids as $id)
+ $view .= article_card($id);
+
+ $view .= '
';
+ }
return $view;
}
diff --git a/functions/article_functions.php b/functions/article_functions.php
index 8292638..f220027 100644
--- a/functions/article_functions.php
+++ b/functions/article_functions.php
@@ -1,7 +1,7 @@
query('SELECT article_id FROM article ORDER BY published_at DESC');
@@ -14,6 +14,19 @@ function article_ids_by_recency()
return $ids;
}
+function article_ids_by_oldest()
+{
+ $conn = get_connection();
+ $results = $conn->query('SELECT article_id FROM article ORDER BY published_at ASC');
+
+ $ids = [];
+
+ while ($row = $results->fetch_assoc())
+ $ids[] = $row['article_id'];
+
+ return $ids;
+}
+
function articles_ids_by_author($author_id)
{
$conn = get_connection();
@@ -77,6 +90,13 @@ function article_page_from_markdown($article_id)
$markdown = read_file_one_string($_ENV['ARTICLES_FQ_PATH'] . $article_id . '/article.md');
$parsedown = new Parsedown();
+ $article_content = $parsedown->text($markdown);
+ $article_toc = '';
+
+ $lines = preg_split("/\r\n|\n|\r/", $article_content);
+ foreach ($lines as $line)
+ if (str_contains($line, ''))
+ $article_toc .= '
' . extract_content_from_tag($line, '
', '
') . '
';
$html = '
@@ -91,14 +111,37 @@ function article_page_from_markdown($article_id)
+
+
+ ' . $article_toc . '
+
+
+ ' . $article_content . '
+
+
+
';
- $html .= $parsedown->text($markdown);
- $html .= '';
-
return $html;
}
+function article_filters()
+{
+ $filters = '
+
+ ';
+
+ return $filters;
+}
+
/*
* Generates cards for a given article.
*/
@@ -188,6 +231,7 @@ function create_article($author_id, $title, $excerpt, $tags, $markdown_file_cont
return $id;
}
+
function delete_article($article_id)
{
$conn = get_connection();
diff --git a/functions/functions.php b/functions/functions.php
index 51deb8f..fbcb4ab 100644
--- a/functions/functions.php
+++ b/functions/functions.php
@@ -128,6 +128,17 @@ function read_file($file_path)
return $fs;
}
+function extract_content_from_tag($string, $start, $end)
+{
+ $ini = strpos($string, $start);
+ if ($ini != 0)
+ return "nope";
+
+ $ini += strlen($start);
+ $len = strpos($string, $end, $ini) - $ini;
+ return substr($string, $ini, $len);
+}
+
function check_image_upload($target, $filetype, $image)
{
return true;
diff --git a/functions/init.php b/functions/init.php
index 0721bd0..4dbf3bc 100644
--- a/functions/init.php
+++ b/functions/init.php
@@ -37,7 +37,7 @@ if (isset($_COOKIE['user_id'])) {
$stmt->execute();
$user = $stmt->get_result()->fetch_assoc();
- $time = time() + 60 * 60 * 1;
+ $time = time() + 60 * 15;
cookies($user, $time);
} else if (isset($_COOKIE['remember_user'])) {
$conn = get_connection();
@@ -64,7 +64,7 @@ if (isset($_COOKIE['user_id'])) {
$stmt->bind_param('i', $_COOKIE['user_id']);
$stmt->execute();
- $time = time() + 60 * 60 * 1;
+ $time = time() + 60 * 15;
cookies($user, $time);
}
}
diff --git a/js/md_preview.js b/js/md_preview.js
new file mode 100644
index 0000000..233fce5
--- /dev/null
+++ b/js/md_preview.js
@@ -0,0 +1,17 @@
+// Optional: Real-time preview using JavaScript
+const textarea = document.querySelector('textarea');
+const preview = document.getElementById('preview');
+
+textarea.addEventListener('input', function() {
+ fetch('preview.php', { // Create a separate preview.php file for real-time updates
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: 'markdown=' + encodeURIComponent(this.value),
+ })
+ .then(response => response.text())
+ .then(data => {
+ preview.innerHTML = data;
+ });
+});
diff --git a/scripts/init.sql b/scripts/init.sql
index e9c84b1..23c9d4f 100644
--- a/scripts/init.sql
+++ b/scripts/init.sql
@@ -21,6 +21,11 @@ CREATE TABLE user (
last_active TIMESTAMP DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT FALSE,
profile_picture VARCHAR(256),
+ bio VARCHAR(256),
+ x_username VARCHAR(32),
+ fb_username VARCHAR(32),
+ ig_username VARCHAR(32),
+ website VARCHAR(64),
FOREIGN KEY (role_id) REFERENCES roles (role_id)
);
diff --git a/vintagecoding.net.md b/vintagecoding.net.md
index eb97b94..29cdf39 100644
--- a/vintagecoding.net.md
+++ b/vintagecoding.net.md
@@ -8,7 +8,7 @@
- [x] Ensure a consistent design & UX aesthetic --- v0.1.0
- [ ] Sensible CSS class names for a modular styling approach --- v0.2.0
-- [ ] Convert db_functions.php queries/statements into prepared SQL for protection against SQL injection --- v0.2.0
+- [x] Convert db_functions.php queries/statements into prepared SQL for protection against SQL injection --- v0.2.0
- [ ] Email Handler:
- [ ] Email users subscribed to author or tag whenever a new article is posted. --- v0.3.0
@@ -17,7 +17,7 @@
#### Systems Interfaces
- [x] Admin panel (manage articles & accounts) --- v0.2.0
-- [ ] Implement a notification modal system for handling error and success messages. --- v0.2.0
+- [ ] Implement a notification modal system for handling error and success messages. --- v0.3.0
- [ ] Accounts:
#### Account CRUD --- account_functions.php
@@ -61,11 +61,11 @@
- [x] Delete Articles by Author (for account deletion) --- v0.1.0
- [ ] Draft/unpublished status --- v0.2.0
- [ ] Update Article by ID (required to be the author updating) --- v0.2.0
- - [ ] Get Articles by Author --- v0.2.0
- - [ ] Sort / filter system --- v0.2.0
+ - [x] Get Articles by Author --- v0.2.0
+ - [x] Sort / filter system --- v0.2.0
#### Article System Interface
- - [ ] Convert articles.php layout to a grid, to display a left hand column with a search/filtering system or table of contents. --- v0.2.0
+ - [x] Convert articles.php layout to a grid, to display a left hand column with a search/filtering system or table of contents. --- v0.2.0
- [ ] Advanced markdown editor or upload markdown, with media upload support (images & code files, requires contributor or higher auth) --- v0.3.0
### Unplanned but implemented