From 0a27cd1f5cd02735d06a64090a05937a5d6bcf3a Mon Sep 17 00:00:00 2001 From: Joshua Ashton Date: Tue, 25 Mar 2025 13:09:05 -0600 Subject: [PATCH] implemented article composer functionality + interface, login & signup functionality + interface, auth permission functionality reflected in the nav, and determined a unified accent color accessible in dark and light mode --- components/nav.php | 31 +++++- compose.php | 56 +++++++++- css/base.css | 183 ++++++++++++++++++++++++++++++-- css/dark.css | 41 ++++++- css/light.css | 66 ++++++++---- functions/account_functions.php | 170 +++++++++++++++++++++++++++++ functions/article_functions.php | 70 ++++++++++-- functions/db_functions.php | 21 +++- index.php | 5 +- scripts/init.sql | 2 +- user.php | 32 ++++++ 11 files changed, 624 insertions(+), 53 deletions(-) create mode 100644 functions/account_functions.php create mode 100644 user.php diff --git a/components/nav.php b/components/nav.php index b7c872a..2765ee9 100644 --- a/components/nav.php +++ b/components/nav.php @@ -7,9 +7,19 @@ $nav = ' -
+
'; diff --git a/compose.php b/compose.php index da33b09..cfe86b8 100644 --- a/compose.php +++ b/compose.php @@ -1,7 +1,61 @@ + + '; + + foreach ($raw_tags as $row) { + $tags .= ' + + '; + } + + $tags .= ' + + '; + + $form = ' +
+
+ + + + + + + + +' . $tags . ' + +
+
+'; + + return $form; +} diff --git a/css/base.css b/css/base.css index 21ad8bb..66d0f01 100644 --- a/css/base.css +++ b/css/base.css @@ -10,7 +10,9 @@ margin: 0; } -body {} +body { + padding-bottom: 50px; +} h1, h2, @@ -19,6 +21,9 @@ h4, h5, p, small, +label, +input, +textarea, a { font-family: 'HackNerdFontMono', Hack, sans-serif; padding: 0; @@ -42,15 +47,22 @@ h4 { } h5 { + font-size: 1.5vw; +} + +p, +a, +label { font-size: 1vw; } -p { +small { font-size: .75vw; } -small { - font-size: .5vw; +a { + color: orange; + transition-duration: 1s; } a.fill_div { @@ -84,6 +96,12 @@ nav ul li a { height: 100%; width: 100%; padding: 15px; + + transition-duration: 500ms; +} + +nav ul li a:hover { + color: orange; } nav form { @@ -95,6 +113,17 @@ nav form { justify-content: center; } +.nav_right_hand { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.nav_right_hand * { + margin: 0px 15px 0px 15px; +} + code { padding: 1px; border-radius: 4px; @@ -126,7 +155,8 @@ code { margin: 0 auto; } -.article_card { +.article_card, +.form_card { margin: 0 auto; margin-top: 50px; padding: 18px; @@ -140,6 +170,14 @@ code { justify-content: center; } +.form_card { + width: 40%; +} + +.form_card form { + width: 80%; +} + .card_info_box { display: flex; } @@ -160,17 +198,20 @@ code { } .button { + font-size: 1vw; + background-color: transparent; padding: 15px; text-decoration: none; - border: 2px solid #00FF41; - border-radius: 9px; + border-radius: 12px; + transition-duration: 1s; + border: 3px solid orange; } .tag { margin: 15px; padding: 10px; border-radius: 18px; - border: 2px solid #008F11; + border: 2px solid orange; text-decoration: none; } @@ -191,6 +232,8 @@ code { .underline { text-decoration: underline; + text-decoration-thickness: 3px; + text-decoration-color: orange; } #theme-toggle { @@ -201,6 +244,14 @@ code { font-size: 1.5vw; } +#theme-toggle * { + transition-duration: 500ms; +} + +#theme-toggle *:hover { + color: orange; +} + .article { width: 50%; margin: 0 auto; @@ -210,6 +261,122 @@ code { margin-left: 50px; } +#compose { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: left; +} + +input, +textarea { + border-radius: 12px; + width: 100%; + padding: 15px; + margin: 15px 15px 15px 0px; + box-sizing: border-box; + transition-duration: 500ms; +} + +input:focus, +textarea:focus, +input:hover, +textarea:hover { + border: 3px solid orange; + outline: none; +} + +textarea { + border-radius: 18px; + height: 500px; +} + +.form_tags {} + +.form_checkbox_container { + display: block; + position: relative; + padding-left: 35px; + margin: 15px 15px 15px 0px; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + height: 29px; + width: max-content; +} + +.checkmark { + position: absolute; + top: 0; + left: 0; + height: 25px; + width: 25px; + +} + +.form_checkbox_container input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +/* Create the checkmark/indicator (hidden when not checked) */ +.checkmark:after { + content: ""; + position: absolute; + display: none; +} + +/* Show the checkmark when checked */ +.form_checkbox_container input:checked~.checkmark:after { + display: block; +} + +/* When the checkbox is checked, add a blue background */ +.form_checkbox_container input:checked~.checkmark { + background-color: orange; +} + +/* Style the checkmark/indicator */ +.form_checkbox_container .checkmark:after { + left: 9px; + top: 5px; + width: 5px; + height: 10px; + border: solid white; + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} + +input[type="file"] { + display: none; +} + +.upload_button { + border-radius: 12px; + display: inline-block; + padding: 6px 12px; + cursor: pointer; +} + +.error { + color: red; + border: 3px solid red; + border-radius: 12px; + padding: 15px; + margin: 15px 15px 15px 0px; + width: 100%; + box-sizing: border-box; +} + @media (max-width: 980px) { h1 { font-size: 4vw; diff --git a/css/dark.css b/css/dark.css index 57ab441..df14e75 100644 --- a/css/dark.css +++ b/css/dark.css @@ -1,3 +1,7 @@ +* { + color: white; +} + body { background-color: black; } @@ -9,7 +13,11 @@ h4, h5, p, small, -a { +label { + color: white; +} + +a:hover { color: white; } @@ -17,6 +25,10 @@ nav { border-bottom: 3px solid white; } +nav ul li a { + color: white; +} + code { background-color: #2F2F2F; } @@ -26,7 +38,8 @@ code { border-top: 1px solid white; } -.article_card { +.article_card, +.form_card { border: 5px solid white; } @@ -34,6 +47,11 @@ code { color: white; } +.button:hover { + color: black; + background-color: white; +} + .tag { color: white; } @@ -45,3 +63,22 @@ code { .article ul li { color: white; } + +input, +textarea { + background-color: transparent; + color: white; + border: 3px solid white; +} + +.checkmark { + border: 2px solid white; +} + +.form_checkbox_container:hover input~.checkmark { + background-color: white; +} + +.upload_button { + border: 3px solid white; +} diff --git a/css/light.css b/css/light.css index 5db7236..e09a5d4 100644 --- a/css/light.css +++ b/css/light.css @@ -1,32 +1,23 @@ +* { + color: black; +} + body { background-color: white; } -h1 { +h1, +h2, +h3, +h4, +h5, +p, +small, +label { color: black; } -h2 { - color: black; -} - -h3 { - color: black; -} - -h4 { - color: black; -} - -h5 { - color: black; -} - -p { - color: black; -} - -small { +a:hover { color: black; } @@ -38,12 +29,17 @@ nav ul li a { color: black; } +code { + background-color: #C6C6C6; +} + .line { background-color: black; border-top: 1px solid black; } -.article_card { +.article_card, +.form_card { border: 5px solid black; } @@ -51,6 +47,11 @@ nav ul li a { color: black; } +.button:hover { + color: white; + background-color: black; +} + .tag { color: black; } @@ -62,3 +63,22 @@ nav ul li a { .article ul li { color: black; } + +input, +textarea { + background-color: transparent; + color: black; + border: 3px solid black; +} + +.checkmark { + border: 2px solid black; +} + +.form_checkbox_container:hover input~.checkmark { + background-color: black; +} + +.upload_button { + border: 3px solid black; +} diff --git a/functions/account_functions.php b/functions/account_functions.php new file mode 100644 index 0000000..9e6dd92 --- /dev/null +++ b/functions/account_functions.php @@ -0,0 +1,170 @@ + + Passwords do not match! + + '; + + return $error_msg; + } + + $password_hash = hash('sha256', $password); + + $id = rand(1000, 999999999); + while (user_id_exists($id)) + $id = rand(1000, 999999999); + + if (!empty($profile_picture)) { + $filetype = explode('.', $profile_picture); + $pic_path = $id . $filetype[count($filetype) - 1]; + } + + $stmt = 'INSERT INTO user (user_id, username, email, password_hash, role_id' . (!empty($profile_picture) ? ', profile_picture' : '') . ') VALUES (' . $id . ', "' . $username . '", "' . $email . '", "' . $password_hash . '", ' . $role_id . (!empty($profile_picture) ? '", "' . $pic_path . '"' : '') . ');'; + exec_statement($stmt, 0); + + login($username, $password, false); + + return true; +} + +function login($username, $password, $stay_logged_in) +{ + $password_hash = hash('sha256', $password); + $query = 'SELECT user_id, username, email, role_id, profile_picture FROM user WHERE username ="' . $username . '" AND password_hash = "' . $password_hash . '";'; + $user = query_one_result($query); + + if ($user == null) { + $error_msg = ' +
+ Username and/or Password are invalid! +
+ '; + + return $error_msg; + } + + if ($stay_logged_in) + $time = time() + 60 * 60 * 24 * 365; + else + $time = time() + 60 * 60 * 24 * 1; + + cookies($user, $time); + + return true; +} + +function cookies($user, $time) +{ + setcookie('user_id', $user['user_id'], $time); + setcookie('username', $user['username'], $time); + setcookie('email', $user['email'], $time); + setcookie('role_id', $user['role_id'], $time); + setcookie('profile_picture', $user['profile_picture'], $time); +} + +function logout() +{ + foreach ($_COOKIE as $key => $value) { + echo 'unsetting ' . $key . ' now.'; + setcookie($key, '', time() - 3600, '/'); + unset($_COOKIE[$key]); + } + + header('Location: articles.php'); +} + +function login_form($error_msg = '') +{ + $form = ' +
+

Log In

+
+ + + + + + + + + Sign Up + + ' . $error_msg . ' +
+
+ '; + + return $form; +} + +function signup_form($error_msg = '') +{ + $form = ' +
+

Sign Up

+
+ + + + + + + + + + + + + + + + + + Log In + + ' . $error_msg . ' +
+
+ '; + + return $form; +} diff --git a/functions/article_functions.php b/functions/article_functions.php index 2d943f0..b9693db 100644 --- a/functions/article_functions.php +++ b/functions/article_functions.php @@ -4,7 +4,7 @@ require 'db_functions.php'; function article_ids_by_recency() { $query = 'SELECT article_id FROM article ORDER BY published_at DESC;'; - $results = query_db_one_or_more_results($query); + $results = query_one_or_more_results($query); $ids = []; @@ -14,10 +14,22 @@ function article_ids_by_recency() return $ids; } +function article_tags() +{ + $query = 'SELECT * FROM tags;'; + $results = query_one_or_more_results($query); + $tags = []; + + while ($row = mysqli_fetch_array($results)) + $tags[] = $row; + + return $tags; +} + function article_ids_by_tag($tag_id) { $query = 'SELECT article_id FROM article_tags INNER JOIN tags ON article_tags.tag_id = tags.tag_id WHERE tag = "' . $tag_id . '";'; - $results = query_db_one_or_more_results($query); + $results = query_one_or_more_results($query); $ids = []; @@ -33,12 +45,13 @@ function article_ids_by_tag($tag_id) function article_page_from_markdown($article_id) { $query = 'SELECT * FROM article WHERE article_id = ' . $article_id . ';'; - $article = query_db_one_result($query); + $article = query_one_result($query); $markdown = read_file_one_string($_ENV['ARTICLES_PATH'] . $article_id . '/article.md'); $parsedown = new Parsedown(); $html = '
'; + $html .= '

' . $article['title'] . '

'; $html .= $parsedown->text($markdown); $html .= '
'; @@ -51,10 +64,10 @@ function article_page_from_markdown($article_id) function article_card($article_id) { $article_query = 'SELECT * FROM article WHERE article_id = ' . $article_id . ';'; - $article = query_db_one_result($article_query); + $article = query_one_result($article_query); $article_tags_query = 'SELECT tag FROM article_tags INNER JOIN tags ON article_tags.tag_id = tags.tag_id WHERE article_id = ' . $article_id . ';'; - $tags_results = query_db_one_or_more_results($article_tags_query); + $tags_results = query_one_or_more_results($article_tags_query); $tags_html = ''; if ($tags_results != null) { $tags_html .= '
'; @@ -66,7 +79,7 @@ function article_card($article_id) } $author_query = 'SELECT username, profile_picture FROM user WHERE user_id = ' . $article['author_id'] . ';'; - $author = query_db_one_result($author_query); + $author = query_one_result($author_query); $card = '
@@ -94,7 +107,44 @@ function article_card($article_id) return $card; } -/* - * Displays the article creation wizard and opens a new markdown file. - */ -function create_article() {} +function create_article($author_id, $title, $excerpt, $tags, $markdown_file_contents) +{ + $stmt = 'INSERT INTO article (author_id, title, excerpt, published_at) VALUES (' . $author_id . ', "' . $title . '", "' . $excerpt . '", CURRENT_TIMESTAMP);'; + $id = exec_statement($stmt, 0); + + $path = $_ENV['ARTICLES_PATH'] . $id . '/'; + mkdir($path); + $fs = fopen($path . 'article.md', 'a'); + fwrite($fs, $markdown_file_contents); + fclose($fs); + + $stmt = 'INSERT INTO article_tags (article_id, tag_id) VALUES '; + for ($i = 0; $i < count($tags); $i++) { + $stmt .= '(' . $id . ', ' . $tags[$i] . ')'; + + if ($i < count($tags) - 1) + $stmt .= ', '; + + if ($i == count($tags) - 1) + $stmt .= ';'; + } + + echo $stmt; + pretty_dump($tags); + if (count($tags) > 0) + exec_statement($stmt, 0); + + return $id; +} + +function delete_article($article_id) +{ + $stmt = 'DELETE FROM article WHERE article_id = ' . $article_id . ';'; + return exec_statement($stmt, 1); +} + +function delete_articles_by_author($author_id) +{ + $stmt = 'DELETE FROM article WHERE author_id = "' . $author_id . '";'; + return exec_statement($stmt, 1); +} diff --git a/functions/db_functions.php b/functions/db_functions.php index ae71f38..9b5d7e3 100644 --- a/functions/db_functions.php +++ b/functions/db_functions.php @@ -20,7 +20,7 @@ if ($_SERVER['HTTP_HOST'] == 'localhost') { /* * Handles any queries that should have only one row results. */ -function query_db_one_result($query_stmt) +function query_one_result($query_stmt) { $conn = get_connection(); $results = mysqli_query($conn, $query_stmt); @@ -32,7 +32,7 @@ function query_db_one_result($query_stmt) return mysqli_fetch_assoc($results); } -function query_db_one_or_more_results($query_stmt) +function query_one_or_more_results($query_stmt) { $conn = get_connection(); $results = mysqli_query($conn, $query_stmt); @@ -44,6 +44,23 @@ function query_db_one_or_more_results($query_stmt) return $results; } +/* + * Executes a statement. Type is typically either 0 or 1, where 0 is an INSERT or UPDATE, + * and 1 is DELETE. This is to determine whether a boolean value should be returned or + * the ID of the row(s). + */ +function exec_statement($stmt, $type) +{ + $conn = get_connection(); + $results = mysqli_query($conn, $stmt); + + if ($type == 0) + $results = mysqli_insert_id($conn); + + mysqli_close($conn); + return $results; +} + function get_connection() { // Connect to the DB diff --git a/index.php b/index.php index c9257f4..d214aec 100644 --- a/index.php +++ b/index.php @@ -1,11 +1,10 @@ Home'; + include_once 'components/foot.php'; diff --git a/scripts/init.sql b/scripts/init.sql index 73b1dac..1de4682 100644 --- a/scripts/init.sql +++ b/scripts/init.sql @@ -33,7 +33,7 @@ CREATE TABLE article ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, published_at TIMESTAMP DEFAULT NULL, - published BOOLEAN DEFAULT FALSE, + published BOOLEAN DEFAULT TRUE, excerpt VARCHAR(256), FOREIGN KEY (author_id) REFERENCES user(user_id) ); diff --git a/user.php b/user.php new file mode 100644 index 0000000..d1d47cc --- /dev/null +++ b/user.php @@ -0,0 +1,32 @@ +