From 420022dbf89a0d6a56d0cd8630ee12f27bc688be Mon Sep 17 00:00:00 2001 From: Josh Ashton Date: Mon, 14 Apr 2025 22:32:32 -0600 Subject: [PATCH] implemented following users to receive (opt-in) email notifications when they publish an article. --- articles.php | 83 ++++++++++++++++++--------------- components/article/article.php | 40 ++++++++++++++-- components/article/card.php | 2 +- components/user/user_view.php | 40 +++++++++++++++- css/base.css | 49 ++++++++++++++----- css/dark.css | 18 +++++-- css/light.css | 18 +++++-- functions/account_functions.php | 10 ++++ functions/functions.php | 54 ++++++++++++++------- scripts/init.sql | 10 ++++ user.php | 6 +++ 11 files changed, 248 insertions(+), 82 deletions(-) diff --git a/articles.php b/articles.php index 8c02db3..e0b50b5 100644 --- a/articles.php +++ b/articles.php @@ -5,6 +5,7 @@ require_once 'functions/functions.php'; include_once 'components/head.php'; include_once 'functions/article_functions.php'; +include_once 'functions/account_functions.php'; // UI Components foreach (glob('components/article/*.php') as $file) @@ -22,49 +23,57 @@ if (isset($_POST['form_id'])) { header('Location: articles.php?article_id=' . $_POST['article_id']); } else { $id = create_article($_SESSION['user_id'], $_POST['title'], $_POST['excerpt'], (isset($_POST['tags']) ? $_POST['tags'] : []), $_POST['markdown']); + $emails = exec_stmt('SELECT email FROM user INNER JOIN follows ON user.user_id = follows.follower_user_id WHERE follows.following_user_id = ? AND follows.notify_user = 1', 'i', $_SESSION['user_id'])->fetch_assoc(); + notify_users($emails, $_SESSION['username'], $id, $_POST['title'], $_POST['excerpt']); header('Location: articles.php?article_id=' . $id); } + break; + case 'follow': + follow_user($_SESSION['user_id'], $_POST['author_id'], ($_POST['follow'] == 'email_notify' ? 1 : 0)); + break; + case 'unfollow': + unfollow_user($_SESSION['user_id'], $_POST['author_id']); break; } -} else { - if (isset($_GET['view'])) - $view = $_GET['view']; - else - $view = 'list'; +} - switch ($view) { - case 'list': - echo article_list(); - break; - case 'filter': - if (isset($_GET['tag'])) - echo article_list(article_ids_by_tag($_GET['tag'])); - else if (isset($_GET['author_id'])) - echo article_list(articles_ids_by_author($_GET['author_id'])); - break; - case 'sort': - if (isset($_GET['sort']) && $_GET['sort'] == 'oldest') - echo article_list(article_ids_by_oldest()); - else if (isset($_GET['sort']) && $_GET['sort'] == 'newest') - echo article_list(article_ids_by_newest()); - break; - case 'read': - if (isset($_SESSION['user_id']) && isset($_GET['article_id'])) { - increment_read_counter($_GET['article_id']); - echo article_page_from_markdown($_GET['article_id']); - } else { - $redirect = (isset($_GET['article_id']) ? 'articles.php?view=read&article_id=' . $_GET['article_id'] : ''); - if ($redirect) - header('Location: user.php?view=login&redirect=' . urlencode($redirect)); - else - header('Location: articles.php'); - } - break; - case 'compose': - echo compose_view((isset($_GET['article_id']) ? $_GET['article_id'] : '')); - break; - } +if (isset($_GET['view'])) + $view = $_GET['view']; +else + $view = 'list'; + +switch ($view) { + case 'list': + echo article_list(); + break; + case 'filter': + if (isset($_GET['tag'])) + echo article_list(article_ids_by_tag($_GET['tag'])); + else if (isset($_GET['author_id'])) + echo article_list(articles_ids_by_author($_GET['author_id'])); + break; + case 'sort': + if (isset($_GET['sort']) && $_GET['sort'] == 'oldest') + echo article_list(article_ids_by_oldest()); + else if (isset($_GET['sort']) && $_GET['sort'] == 'newest') + echo article_list(article_ids_by_newest()); + break; + case 'read': + if (isset($_SESSION['user_id']) && isset($_GET['article_id'])) { + increment_read_counter($_GET['article_id']); + echo article_page_from_markdown($_GET['article_id']); + } else { + $redirect = (isset($_GET['article_id']) ? 'articles.php?view=read&article_id=' . $_GET['article_id'] : ''); + if ($redirect) + header('Location: user.php?view=login&redirect=' . urlencode($redirect)); + else + header('Location: articles.php'); + } + break; + case 'compose': + echo compose_view((isset($_GET['article_id']) ? $_GET['article_id'] : '')); + break; } if (($view == 'list' || $view == 'sort') && (isset($_SESSION['role_id']) && $_SESSION['role_id'] <= 3)) { diff --git a/components/article/article.php b/components/article/article.php index 8c3e66a..c1880df 100644 --- a/components/article/article.php +++ b/components/article/article.php @@ -8,6 +8,7 @@ function article_page_from_markdown($article_id) $article = exec_stmt('SELECT * FROM article WHERE article_id = ?', 'i', $article_id)->fetch_assoc(); $author = exec_stmt('SELECT user_id, username, profile_picture FROM user WHERE user_id = ?', 'i', $article['author_id'])->fetch_assoc(); $markdown = read_file_one_string($_ENV['ARTICLES_FQ_PATH'] . $article_id . '/article.md'); + $following = exec_stmt('SELECT follower_user_id, following_user_id FROM follows WHERE follower_user_id = ? AND following_user_id = ?', 'ii', $_SESSION['user_id'], $author['user_id'])->fetch_assoc(); $parsedown = new Parsedown(); $parsedown->setSafeMode(true); @@ -21,10 +22,41 @@ function article_page_from_markdown($article_id) ' . (isset($_SESSION['user_id']) && $_SESSION['user_id'] == $article['author_id'] ? '' : '') . '
-
-
Written by ' . $author['username'] . '
-

Published on ' . $article['published_at'] . '
- Lasted edited on ' . $article['updated_at'] . '

+
+
+
Written by ' . $author['username'] . '
+

Published on ' . $article['published_at'] . '
+ Lasted edited on ' . $article['updated_at'] . '

+
+ '; + + if ($_SESSION['user_id'] != $author['user_id'] && !$following) { + $html .= ' + + '; + } else if ($following) { + $html .= ' +
+
+ + + +
+
+ '; + } + + $html .= '
diff --git a/components/article/card.php b/components/article/card.php index d58f1c5..0609998 100644 --- a/components/article/card.php +++ b/components/article/card.php @@ -27,7 +27,7 @@ function article_card($article_id)

' . $article['title'] . '

- Written by: ' . $author['username'] . ' + Written by: ' . $author['username'] . ' Published: ' . $article['published_at'] . ' Updated: ' . $article['updated_at'] . '
diff --git a/components/user/user_view.php b/components/user/user_view.php index 199ab17..f1062df 100644 --- a/components/user/user_view.php +++ b/components/user/user_view.php @@ -1,14 +1,50 @@ fetch_assoc(); $user = exec_stmt('SELECT user_id, username, email, role_id, created_at, last_active, is_active, profile_picture FROM user WHERE user_id = ?', 'i', $user_id)->fetch_assoc(); + if ($user == null) - header('Location: articles.php'); + header('Location: articles.php?view=sort&sort=newest'); $view = '
-

' . $user['username'] . '

+
+

' . $user['username'] . '

+ '; + + if ($_SESSION['user_id'] != $user_id && !$following) { + $view .= ' + + '; + } else if ($following) { + $view .= ' +
+
+ + + +
+
+ '; + } + + $view .= ' +
diff --git a/css/base.css b/css/base.css index 8e3bdfc..ccad11c 100644 --- a/css/base.css +++ b/css/base.css @@ -31,7 +31,8 @@ a, .modal_button, th, td, -li { +li, +span { font-family: 'Hack Nerd Font Mono', Hack, sans-serif; padding: 0; margin: 0; @@ -65,7 +66,8 @@ a, label, li, .modal_button, -.button { +.button, +span { font-size: 1vw; } @@ -285,10 +287,6 @@ table tr td label { border: 3px solid orange; } -.button:hover { - box-shadow: 0 2px 4px 0 rgba(255, 165, 0, 0.3), 0 3px 8px 0 rgba(255, 165, 0, 0.7); -} - .narrow { width: 20%; } @@ -301,10 +299,6 @@ table tr td label { text-decoration: none; } -.tag:hover { - box-shadow: 0 2px 4px 0 rgba(255, 165, 0, 0.3), 0 3px 8px 0 rgba(255, 165, 0, 0.7); -} - .tag_row {} .tag_box { @@ -383,7 +377,6 @@ textarea:focus, input:hover, textarea:hover { border: 3px solid orange; - box-shadow: 0 2px 4px 0 rgba(255, 165, 0, 0.3), 0 3px 8px 0 rgba(255, 165, 0, 0.7); outline: none; } @@ -734,6 +727,37 @@ hr { border-radius: 9px; } +.dropdown { + position: relative; + display: block; + margin-right: 15px; + padding: 15px; + border-radius: 9px; +} + +.dropdown_content { + display: none; + position: absolute; + min-width: 50px; + padding: 15px; + z-index: 2; +} + +.dropdown:hover .dropdown_content { + display: block; + margin-left: -15px; + margin-top: 15px; + border-radius: 12px; +} + +.dropdown_content form * { + margin-top: 5px; + margin-bottom: 5px; +} + +.margin_right { + margin-right: 15px; +} @media screen and (max-width: 992px) { body { @@ -772,7 +796,8 @@ hr { ul li a, ol li a, .modal_button, - .button { + .button, + span { font-size: 2vw; } diff --git a/css/dark.css b/css/dark.css index 47c2232..8f49b84 100644 --- a/css/dark.css +++ b/css/dark.css @@ -13,7 +13,8 @@ h4, h5, p, small, -label { +label, +span { color: white; } @@ -54,9 +55,9 @@ table tr { color: white; } -.button:hover { - color: black; - background-color: white; +.button:hover, +.button:hover i { + color: orange; } .tag { @@ -139,6 +140,15 @@ select:-webkit-autofill { border: 2px solid orange; } +.dropdown { + border: 2px solid white; +} + +.dropdown_content { + background-color: black; + border: 2px solid white; +} + @media screen and (max-width: 992px) { nav { border-bottom: none; diff --git a/css/light.css b/css/light.css index 10fcb10..af7a5eb 100644 --- a/css/light.css +++ b/css/light.css @@ -13,7 +13,8 @@ h4, h5, p, small, -label { +label, +span { color: black; } @@ -54,9 +55,9 @@ table tr { color: black; } -.button:hover { - color: white; - background-color: black; +.button:hover, +.button:hover * { + color: orange; } .tag { @@ -139,6 +140,15 @@ select:-webkit-autofill { border: 2px solid orange; } +.dropdown { + border: 2px solid black; +} + +.dropdown_content { + background-color: white; + border: 2px solid black; +} + @media screen and (max-width: 992px) { nav { border-bottom: none; diff --git a/functions/account_functions.php b/functions/account_functions.php index f69acf1..86fee69 100644 --- a/functions/account_functions.php +++ b/functions/account_functions.php @@ -4,6 +4,16 @@ require_once 'article_functions.php'; use Postmark\PostmarkClient; +function follow_user($follower_user_id, $following_user_id, $notify) +{ + exec_stmt('INSERT INTO follows (follower_user_id, following_user_id, notify_user) VALUES (?, ?, ?)', 'iii', $follower_user_id, $following_user_id, $notify); +} + +function unfollow_user($follower_user_id, $following_user_id) +{ + exec_stmt('DELETE FROM follows WHERE follower_user_id = ? AND following_user_id = ?', 'ii', $follower_user_id, $following_user_id); +} + function user_id_exists($user_id) { $result = exec_stmt('SELECT user_id FROM user WHERE user_id = ?', 'i', $user_id)->fetch_assoc(); diff --git a/functions/functions.php b/functions/functions.php index fbcb4ab..3f6ce2b 100644 --- a/functions/functions.php +++ b/functions/functions.php @@ -1,17 +1,36 @@ safeLoad(); +if (!empty($_FILES['upload'])) upload($_FILES['upload'], $_ENV['UPLOAD_TMP_FQ_PATH'], isset($_POST['filename']) ? $_POST['filename'] : null); + +function notify_users($emails, $auther_username, $article_id, $article_title, $excerpt) +{ + foreach ($emails as $email) { + $email_html = ' +

' . $article_title . '

+

An author you follow just published a new article! Read it here

+

Here is a snippet from the article!

+

' . $excerpt . '

+ '; + + $client = new PostmarkClient($_ENV['POSTMARK_API_TOKEN']); + + $client->sendEmail( + 'mailer@joshashton.dev', + $email, + $auther_username . ' just published a new article! - vintagecoding.net', + $email_html + ); + } } function delete_dir($dir) { - if (!is_dir($dir)) return false; + if (!is_dir($dir)) + return false; if (substr($dir, strlen($dir) - 1, 1) != '/') { $dir .= '/'; @@ -28,7 +47,8 @@ function delete_dir($dir) function delete_file($file) { - if (!file_exists($file)) return false; + if (!file_exists($file)) + return false; unlink($file); } @@ -54,7 +74,7 @@ function upload($input, $dest_dir, $filename = null) break; default: - # code... + // code... break; } @@ -66,7 +86,7 @@ function upload($input, $dest_dir, $filename = null) function crop_image($img_path, $x, $y, $height, $width) { - //$dest = $_ENV['UPLOAD_TMP_FQ_PATH']; + // $dest = $_ENV['UPLOAD_TMP_FQ_PATH']; $dest = '/tmp/'; // Get the image type $imageType = exif_imagetype($img_path); @@ -80,11 +100,11 @@ function crop_image($img_path, $x, $y, $height, $width) $sourceImage = imagecreatefrompng($img_path); break; default: - return false; // Unsupported image type + return false; // Unsupported image type } if (!$sourceImage) - return false; // Failed to create image resource + return false; // Failed to create image resource // Create a new image resource for the cropped image $croppedImage = imagecreatetruecolor($width, $height); @@ -109,7 +129,7 @@ function crop_image($img_path, $x, $y, $height, $width) imagedestroy($sourceImage); imagedestroy($croppedImage); - return true; // Cropping successful + return true; // Cropping successful } /* @@ -132,7 +152,7 @@ function extract_content_from_tag($string, $start, $end) { $ini = strpos($string, $start); if ($ini != 0) - return "nope"; + return 'nope'; $ini += strlen($start); $len = strpos($string, $end, $ini) - $ini; @@ -151,9 +171,7 @@ function check_csv_upload($target) function pretty_dump($arr) { - echo ' -
-        ' . print_r($arr) . '
-    
- '; + echo '
';
+    print_r($arr);
+    echo '
'; } diff --git a/scripts/init.sql b/scripts/init.sql index 23c9d4f..6a005d9 100644 --- a/scripts/init.sql +++ b/scripts/init.sql @@ -60,6 +60,16 @@ CREATE TABLE article_tags ( FOREIGN KEY (tag_id) REFERENCES tags(tag_id) ); +CREATE TABLE follows ( + follower_user_id INT NOT NULL, + following_user_id INT NOT NULL, + notify_user BOOLEAN DEFAULT FALSE, + followed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (follower_user_id, following_user_id), + FOREIGN KEY (follower_user_id) REFERENCES user(user_id) ON DELETE CASCADE, + FOREIGN KEY (following_user_id) REFERENCES user(user_id) ON DELETE CASCADE +); + INSERT INTO roles (role) VALUES ('owner'), ('admin'), ('contributor'), ('reader'); diff --git a/user.php b/user.php index edbe75e..01b112a 100644 --- a/user.php +++ b/user.php @@ -82,6 +82,12 @@ if (isset($_POST['form_id'])) { $_POST['delete_verify_password'] ); + break; + case 'follow': + follow_user($_SESSION['user_id'], $_POST['author_id'], ($_POST['follow'] == 'email_notify' ? 1 : 0)); + break; + case 'unfollow': + unfollow_user($_SESSION['user_id'], $_POST['author_id']); break; } echo $msg;