diff --git a/components/core/foot.php b/components/core/foot.php index e16c808..704fa04 100644 --- a/components/core/foot.php +++ b/components/core/foot.php @@ -18,52 +18,58 @@ return $args; } - if ($_SERVER['SCRIPT_NAME'] == '/user.php') { - $args = get_args_from_url(); + if ($_SERVER['SCRIPT_NAME'] == '/spaces.php') + echo ''; - if (isset($args['view'])) { - if ($args['view'] == 'signup') { - echo ' + function extra() + { + if ($_SERVER['SCRIPT_NAME'] == '/user.php') { + $args = get_args_from_url(); + + if (isset($args['view'])) { + if ($args['view'] == 'signup') { + echo ' '; - } else if ($args['view'] == 'display') { - echo ' + } else if ($args['view'] == 'display') { + echo ' '; + } } - } - } else if ($_SERVER['SCRIPT_NAME'] == '/admin.php') { - echo ' + } else if ($_SERVER['SCRIPT_NAME'] == '/admin.php') { + echo ' '; - $args = get_args_from_url(); + $args = get_args_from_url(); - if (isset($args['view'])) { - if ($args['view'] == 'users') { - echo ' + if (isset($args['view'])) { + if ($args['view'] == 'users') { + echo ' '; + } } - } - } else if ($_SERVER['SCRIPT_NAME'] == '/articles.php') { - $args = get_args_from_url(); + } else if ($_SERVER['SCRIPT_NAME'] == '/articles.php') { + $args = get_args_from_url(); - if (isset($args['view'])) { - if ($args['view'] == 'compose') { - echo ' + if (isset($args['view'])) { + if ($args['view'] == 'compose') { + echo ' '; + } } + } else if ($_SERVER['SCRIPT_NAME'] == '/todo.php') { + echo ''; } - } else if ($_SERVER['SCRIPT_NAME'] == '/todo.php') { - echo ''; - } - echo ' + echo ' '; + } ?> diff --git a/components/core/head.php b/components/core/head.php index 575cd78..73ac6b6 100644 --- a/components/core/head.php +++ b/components/core/head.php @@ -7,11 +7,11 @@ '; -else + else echo ''; -?> + ?> diff --git a/components/core/nav.php b/components/core/nav.php index feb2b89..94af34e 100644 --- a/components/core/nav.php +++ b/components/core/nav.php @@ -2,24 +2,24 @@ $current_page = $_SERVER['SCRIPT_NAME']; $nav = ' +
+
'; echo $nav; diff --git a/css/base.css b/css/base.css index c019022..dcc366a 100644 --- a/css/base.css +++ b/css/base.css @@ -1,6 +1,6 @@ @font-face { - font-family: 'Hack Nerd Font Mono'; - src: url('/data/resources/HackNerdFontMono-Regular.woff'); + font-family: "Hack Nerd Font Mono"; + src: url("/data/resources/HackNerdFontMono-Regular.woff"); font-weight: normal; font-style: normal; } @@ -36,7 +36,7 @@ li, select, option, span { - font-family: 'Hack Nerd Font Mono', Hack, sans-serif; + font-family: "Hack Nerd Font Mono", Hack, sans-serif; padding: 0; margin: 0; } @@ -84,7 +84,7 @@ p { } small { - font-size: .75vw; + font-size: 0.75vw; } a, @@ -307,6 +307,18 @@ table tr td label { border-radius: 50%; } +.link { + color: orange; + cursor: pointer; +} + +.divLink { + text-decoration: none; + display: block; + height: 100%; + width: 100%; +} + .underline { text-decoration: underline; text-decoration-thickness: 3px; @@ -408,7 +420,6 @@ input[type="radio"] { height: 29px; width: max-content; - } .checkmark { @@ -417,7 +428,6 @@ input[type="radio"] { left: 0; height: 25px; width: 25px; - } .form_checkbox_container input { @@ -681,7 +691,6 @@ input[type="file"] { width: 90%; display: grid; grid-template-columns: 50% 50%; - } .grid-2x1 .form_card, @@ -755,9 +764,6 @@ hr { margin-bottom: 5px; } - - - @media screen and (max-width: 992px) { body { padding-bottom: 160px; diff --git a/css/dark.css b/css/dark.css index 8f49b84..339e7aa 100644 --- a/css/dark.css +++ b/css/dark.css @@ -32,7 +32,7 @@ nav ul li a { } code { - background-color: #2F2F2F; + background-color: #2f2f2f; } .line { @@ -149,6 +149,12 @@ select:-webkit-autofill { border: 2px solid white; } +.std_border, +.med_border, +.sm_border { + border-color: white; +} + @media screen and (max-width: 992px) { nav { border-bottom: none; diff --git a/css/light.css b/css/light.css index af7a5eb..2c2ffd9 100644 --- a/css/light.css +++ b/css/light.css @@ -32,7 +32,7 @@ nav ul li a { } code { - background-color: #C6C6C6; + background-color: #c6c6c6; } .line { @@ -149,6 +149,12 @@ select:-webkit-autofill { border: 2px solid black; } +.std_border, +.med_border, +.sm_border { + border-color: black; +} + @media screen and (max-width: 992px) { nav { border-bottom: none; diff --git a/css/rewrite.css b/css/rewrite.css index 2d19761..9bce7cc 100644 --- a/css/rewrite.css +++ b/css/rewrite.css @@ -1,8 +1,32 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + box-sizing: border-box; +} + /********** Width & Sizing **********/ .full_width { width: 100%; } +.std_width { + width: 70%; +} + +.med_width { + width: 40%; +} + +.sm_width { + width: 25%; +} + /********** Borders & Outlines **********/ .std_border, .med_border, @@ -11,15 +35,15 @@ } .std_border { - border: 3px solid white; + border: 3px solid black; } .med_border { - border: 2px solid white; + border: 2px solid black; } .sm_border { - border: 1px solid white; + border: 1px solid black; } .debug { @@ -32,12 +56,8 @@ flex-direction: row; } -.row_space_between { - display: flex; +.space_between { justify-content: space-between; - align-items: center; - width: 100%; - margin: 15px 0px 15px 0px; } .column { @@ -46,6 +66,7 @@ } .center { + margin: 0 auto; align-items: center; justify-content: center; } @@ -103,74 +124,67 @@ padding-left: 15px; } -/********** FORMS **********/ - - -/********** TODO ITEMS **********/ -.todo_display { - margin: 0 auto; - margin-top: 25px; - width: 80%; - height: 80vh; - display: flex; - flex-direction: column; - align-items: center; -} - -#todo_actions { - padding: 15px; - border: 2px solid white; - border-radius: 8px; - width: 100%; - box-sizing: border-box; - margin-bottom: 25px; - - width: 100%; - display: flex; - flex-direction: row; - justify-content: space-evenly; -} - -.todo_category_window { - display: flex; - flex-direction: row; +/************ Modals ************/ +#stdModal { + position: absolute; + top: 0; + left: 0; width: 100%; height: 100%; - overflow-x: scroll; - overflow-y: none; -} + display: none; + backdrop-filter: blur(20px); -.todo_category { - display: block; - min-width: 400px; - margin-right: 50px; - overflow-y: scroll; - overflow-x: none; -} - -.todo_category h4 { - text-align: center; - position: relative; -} - -.todo_list { - display: flex; - flex-direction: column; + justify-content: center; align-items: center; } -.todo_item { - width: 100%; +#stdModal>* {} + +/******** Action Buttons ********/ +.circleLayout { + width: 350px; + /* Adjust as needed */ + height: 350px; + border-radius: 50%; + /* Important for positioning children */ + position: fixed; + bottom: 50px; + right: 50px; + overflow: hidden; } -.todo_item_details { - display: none; +.actionButton { + position: absolute; + + border-radius: 50%; + width: 75px; + height: 75px; + background-color: orange; + color: black; } -.modal_activation_area {} +#actionButton { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} -.modal_activation_area:hover { - cursor: pointer; +#newPost { + top: 50%; + left: 0%; + transform: translate(0%, -50%); +} + +#newSpace { + top: 15%; + left: 15%; + transform: translate(-15%, -15%); +} + +#filterSort { + top: 0%; + left: 50%; + transform: translate(-50%, 0%); } /********** Animations **********/ diff --git a/director.php b/director.php index 7dfb535..fb2d517 100644 --- a/director.php +++ b/director.php @@ -1,61 +1,118 @@ 'Malformed URI request.', + 'error' => pretty_dump($_POST), ]; echo json_encode($response); } +function handleNewThought() +{ + $thought = new Thought(Space::retrieveFromDB($_POST['space']), null, unserialize($_SESSION['user']), 3, 2, urldecode($_POST['thought'])); + echo json_encode(['log' => 'success']); +} + +function handleNewPostHTML() +{ + // TODO: Need to plumb this into the DB via AJAX. + // TODO: Need to store the current space in SESSION and use that to auto-select a Space + // TODO: List the user's spaces or let them search for a space or quick select a favorited space + $html = ' +
+

New Thought

+
+ + + +
+ +
+ + + + + + +
+
+
+ Thought + Moment + Essay +
+
+ '; + + echo json_encode(['html' => $html]); +} + function handle_login() { - $response = ['log' => []]; + header('Content-Type: application/json'); - if (isset($_POST['user_id']) && isset($_POST['username']) && isset($_POST['role'])) { - $response['log'][] = 'Required parameters met. Logging user in.'; - $_SESSION['user_id'] = $_POST['user_id']; - $_SESSION['username'] = $_POST['username']; - $_SESSION['role_id'] = $_POST['role']; + $response = []; - $response['redirect'] = 'articles.php?view=list'; - } + if (isset($_POST['username']) && isset($_POST['password'])) { // Corrected check + // Ensure LoginCredentials and User classes are loaded + // e.g., require_once 'path/to/LoginCredentials.php'; + // require_once 'path/to/User.php'; - echo json_encode($response); -} + try { + $creds = new LoginCredentials($_POST['username'], $_POST['password']); + $user = User::login($creds); // Assuming User::login() can return null or throw an exception -function handle_todo() -{ - include 'functions/todo_functions.php'; - $response = ['log' => []]; - if (isset($_POST['todo_id']) && isset($_POST['action_id'])) { - $response['log'][] = 'Required parameters met.'; - switch ($_POST['action_id']) { - case 'status': - $response['log'][] = 'Changing status on ' . $_POST['todo_id'] . ' to ' . $_POST['status'] . '.'; - change_status($_POST['todo_id'], $_POST['status']); - break; - case 'create_update_todo': - if ($_POST['todo_id'] != -1) { - $response['log'][] = 'Updating todo information on ' . $_POST['todo_id'] . '.'; - update_todo($_POST['todo_id'], urldecode($_POST['todo'])); - } else { - $response['log'][] = 'Creating todo information on ' . $_POST['todo_id'] . '.'; - create_todo(urldecode($_POST['todo'])); - } - break; + if ($user != null) { + $response['status'] = 'success'; // Use a clear status field + $response['message'] = 'Login Successful.'; // Renamed 'log' to 'message' for clarity + $response['redirect'] = 'spaces.php?view=list'; + + // Serialize the user object and store it in the session + $_SESSION['user'] = serialize($user); + // You might want to store specific user data instead of the whole serialized object, + // e.g., $_SESSION['user_id'] = $user->getId(); $_SESSION['username'] = $user->getUsername(); + } else { + $response['status'] = 'error'; + $response['message'] = 'Incorrect username and/or password.'; + } + } catch (Exception $e) { + // Catch potential exceptions from LoginCredentials or User::login() + // Log the detailed error for server-side debugging + error_log('Login Error: ' . $e->getMessage() . ' in ' . $e->getFile() . ' on line ' . $e->getLine()); + + $response['status'] = 'error'; + $response['message'] = 'An internal error occurred. Please try again later.'; + // Optionally, for development, you could include $e->getMessage() but not in production. } + } else { + $response['status'] = 'error'; + $response['message'] = 'Invalid Request: Username and/or password not provided.'; } echo json_encode($response); + exit; } diff --git a/functions/core.php b/functions/core.php index 21041fd..0c89a4c 100644 --- a/functions/core.php +++ b/functions/core.php @@ -11,7 +11,7 @@ if (!empty($_FILES['upload'])) */ function randomId(int $mode) { - $id = rand(1000, 999999999999); + $id = rand(10000000, 999999999); switch ($mode) { case 0: if (User::exists($id)) @@ -19,6 +19,10 @@ function randomId(int $mode) else return $id; case 1: + if (Post::exists($id)) + return randomId($mode); + else + return $id; break; case 2: break; diff --git a/functions/db.php b/functions/db.php index 0c0b64b..5529adb 100644 --- a/functions/db.php +++ b/functions/db.php @@ -29,10 +29,15 @@ function exec_stmt($stmt, $param_types, ...$args) $stmt->bind_param($param_types, ...$args); $stmt->execute(); $result = $stmt->get_result(); - if ($result) + + if ($result) { + $conn->close(); return $result; - else - return mysqli_insert_id($conn); + } else { + $id = mysqli_insert_id($conn); + $conn->close(); + return $id; + } } function get_connection() diff --git a/functions/init.php b/functions/init.php index 381cd26..67ee350 100644 --- a/functions/init.php +++ b/functions/init.php @@ -1,18 +1,18 @@ safeLoad(); +require_once 'functions/db.php'; +require_once 'functions/core.php'; +require_once 'functions/user.php'; +require_once 'functions/post.php'; +require_once 'functions/space.php'; + +/**/ // Set SESSION variable. if (!isset($_SESSION['initialized'])) { $_SESSION['initialized'] = true; @@ -25,43 +25,3 @@ if (!isset($_SESSION['initialized'])) { define('owner', 1); define('developer', 2); define('user', 3); - -// Update the database and cookies to keep the user logged in. -if (isset($_SESSION['user_id'])) { - $conn = get_connection(); - $stmt = $conn->prepare('UPDATE user SET is_active = true WHERE user_id = ?'); - $stmt->bind_param('i', $_SESSION['user_id']); - $stmt->execute(); - - $stmt = $conn->prepare('UPDATE user SET last_active = CURRENT_TIMESTAMP WHERE user_id = ?'); - $stmt->bind_param('i', $_SESSION['user_id']); - $stmt->execute(); -} else if (isset($_COOKIE['remember_user'])) { - $conn = get_connection(); - $stmt = $conn->prepare('SELECT * FROM remember_user WHERE token = ?'); - $stmt->bind_param('s', $_COOKIE['remember_user']); - $stmt->execute(); - $remember = $stmt->get_result()->fetch_assoc(); - - if (hash('sha256', $_SERVER['REMOTE_ADDR']) != $remember['remote_addr'] || (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && hash('sha256', $_SERVER['HTTP_X_FORWARDED_FOR']) != $remember['forwarded_for'])) { - // TODO: Log the attempt to use a cookie from a different browser/device than the cookie was created - echo "Naughty, you are trying to use someone else's cookie..."; - } else { - $stmt = $conn->prepare('SELECT user_id, username, role_id FROM user WHERE user_id = ?'); - $stmt->bind_param('s', $remember['user_id']); - $stmt->execute(); - $user = $stmt->get_result()->fetch_assoc(); - - $_SESSION['user_id'] = $user['user_id']; - $_SESSION['username'] = $user['username']; - $_SESSION['role_id'] = $user['role_id']; - - $stmt = $conn->prepare('UPDATE user SET is_active = true WHERE user_id = ?'); - $stmt->bind_param('i', $_SESSION['user_id']); - $stmt->execute(); - - $stmt = $conn->prepare('UPDATE user SET last_active = CURRENT_TIMESTAMP WHERE user_id = ?'); - $stmt->bind_param('i', $_SESSION['user_id']); - $stmt->execute(); - } -} diff --git a/functions/post.php b/functions/post.php index a983650..229157b 100644 --- a/functions/post.php +++ b/functions/post.php @@ -4,9 +4,9 @@ // namespace VintageCoding\Models; // namespace VintageCoding\Repositories; -require_once 'db.php'; // For exec_stmt() and get_connection() (though get_connection is used by exec_stmt) -require_once 'user.php'; // For User and Author classes -require_once 'space.php'; +require_once 'functions/db.php'; // For exec_stmt() and get_connection() (though get_connection is used by exec_stmt) +require_once 'functions/user.php'; // For User and Author classes +require_once 'functions/space.php'; // -- Data Transfer Object for Topics -- class Topic @@ -34,56 +34,87 @@ class Topic // -- Post Data Models (as previously defined, with setReadCount in Post) -- class Post { - private Space $space; - private Author $author; - private int $postTypeId; - private ?string $title; - private ?string $slug; - private int $readCount; + private ?Space $space; + private ?int $id; + private User $creator; + private int $postType; + private ?int $status; + private int $seenCount; private ?DateTime $createdAt; private ?DateTime $updatedAt; - private ?DateTime $status; - private bool $published; - public function __construct($postArray) + public function __construct(?Space $space = null, ?int $id = null, User $creator, int $postType, int $status) { - $this->postId = $postArray['postId']; - $this->author = $postArray['authorId']; - $this->postTypeId = $postArray['postType']; - $this->title = $postArray['title']; - $this->slug = $postArray['slug']; - $this->readCount = 0; - $this->status = $postArray['status']; + $this->space = $space; + $this->id = $id; + $this->creator = $creator; + $this->postType = $postType; + $this->status = $status; + + $this->seenCount = 0; } - public function getPostId(): int + public static function exists(int $id): bool { - return $this->postId; + $stmt = 'SELECT count(id) FROM post WHERE id = ?'; + $result = exec_stmt($stmt, 'i', $id)->fetch_assoc(); + if ($result['count(id)'] == 1) + return true; + else if ($result['count(id)'] > 1) { + // TODO: Add additional logging here, since that means multiple posts have the same ID. + return true; + } + + return false; } - public function getAuthor(): Author + public static function retrieveFromDB(int $id): ?Post { - return $this->author; + $stmt = 'SELECT * FROM post WHERE id = ?'; + $post = exec_stmt($stmt, 'i', $id)->fetch_assoc(); + if ($post) { + $creator = User::retrieveFromDB($post['creator']); + + switch ($post['postType']) { + case 1: + return new Essay(null, $id, $creator, 1, $post['status'], $post['title'], $post['markdown'], $post['excerpt']); + case 3: + return new Thought(null, $post['id'], $creator, 3, $post['status'], $post['thought']); + default: + return null; + } + } else + return null; } - public function getPostTypeId(): int + public function setId(int $id): void { - return $this->postTypeId; + $this->id = $id; } - public function getTitle(): ?string + public function authUser(User $user): bool { - return $this->title; + return true; } - public function getSlug(): ?string + public function getId(): int { - return $this->slug; + return $this->id; } - public function getReadCount(): int + public function getCreator(): User { - return $this->readCount; + return $this->creator; + } + + public function getPostType(): int + { + return $this->postType; + } + + public function getSeenCount(): int + { + return $this->seenCount; } public function getCreatedAt(): ?DateTime @@ -101,54 +132,80 @@ class Post return $this->status; } - public function isPublished(): bool + public function incrementSeenCount(): void { - return $this->published; + $this->seenCount++; } - public function incrementReadCount(): void - { - $this->readCount++; - } - - public function setSlug(?string $slug): void - { - $this->slug = $slug; - } - - public function setStatus(?string $status): void + public function setStatus(?int $status): void { $this->status = $status; } - public function setPublished(bool $published): void + public function setSpace(Space $space): void { - $this->published = $published; + $this->space = $space; } - public function setReadCount(int $count): void + public function getSpace(): ?Space { - $this->readCount = $count; + return $this->space; } } class Essay extends Post { - private ?string $markdown; - private ?string $excerpt; - private ?array $files; + private string $title; + private string $html; + private string $excerpt; + /* private array $files; */ - public function __construct(int $postId, Author $author, ?string $title = null, ?string $slug = null, ?string $markdown = null, ?string $excerpt = null, ?array $files = null, ?DateTime $createdAt = null, ?DateTime $updatedAt = null, ?DateTime $publishedAt = null, bool $published = true) + public function __construct(?Space $space = null, ?int $id = null, User $creator, int $postType, int $status, string $title, string $markdown, string $excerpt) { - parent::__construct($postId, $author, 1, $title, $slug, $createdAt, $updatedAt, $publishedAt, $published); - $this->markdown = $markdown; + parent::__construct($space, $id, $creator, 1, $postType, $status); + require_once 'vendor/autoload.php'; + + $parsedown = new Parsedown(); + $parsedown->setSafeMode(true); + + $this->title = $title; $this->excerpt = $excerpt; - $this->files = $files; + $this->html = $parsedown->text($markdown); } - public function getMarkdown(): ?string + public function getCardHTML(): ?string { - return $this->markdown; + /* @' . parent::getCreator()->getUsername() . ' */ + $html = ' +
+ +
+
+

' . $this->title . '

+
+ +
+ Something +
+
+
+

' . $this->excerpt . '

+
+
+
+ '; + + return $html; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function getHTML(): ?string + { + return $this->html; } public function getExcerpt(): ?string @@ -156,25 +213,18 @@ class Essay extends Post return $this->excerpt; } - public function getFiles(): ?array - { - return $this->files; - } - public function setMarkdown(?string $markdown): void { - $this->markdown = $markdown; + require_once 'vendor/autoload.php'; + $parsedown = new Parsedown(); + $parsedown->setSafeMode(true); + $this->html = $parsedown->text($markdown); } public function setExcerpt(?string $excerpt): void { $this->excerpt = $excerpt; } - - public function setFiles(?array $files): void - { - $this->files = $files; - } } class Moment extends Post @@ -182,9 +232,9 @@ class Moment extends Post private ?string $caption; private ?array $images; - public function __construct(int $postId, Author $author, ?string $caption = null, ?array $images = null, ?DateTime $createdAt = null, ?DateTime $updatedAt = null, ?DateTime $publishedAt = null, bool $published = true) + public function __construct(?Space $space = null, ?int $id = null, User $creator, int $postType, int $status, ?array $images = null, ?string $caption = null) { - parent::__construct($postId, $author, 2, null, null, $createdAt, $updatedAt, $publishedAt, $published); + parent::__construct($space, $id, $creator, $postType, $status); $this->caption = $caption; $this->images = $images; } @@ -214,10 +264,41 @@ class Thought extends Post { private ?string $thought; - public function __construct(int $postId, Author $author, ?string $thought = null, ?DateTime $createdAt = null, ?DateTime $updatedAt = null, ?DateTime $publishedAt = null, bool $published = true) + public function __construct(?Space $space = null, ?int $id = null, User $creator, int $postType, int $status, string $thought) { - parent::__construct($postId, $author, 3, null, null, $createdAt, $updatedAt, $publishedAt, $published); + parent::__construct($space, $id, $creator, $postType, $status); $this->thought = $thought; + + if ($id == null) { + $stmt = 'INSERT INTO post (id, parentSpace, creator, postType, status, thought) VALUES (?, ?, ?, ?, ?, ?)'; + $newID = randomId(1); + parent::setId($newID); + $space = isset($space) ? $space->getId() : $creator->getHomeSpace()->getId(); + exec_stmt($stmt, 'iiiiis', $newID, $space, $creator->getId(), 3, 2, $this->thought); + $stmt = 'INSERT INTO spacePosts (post, space) VALUES (?, ?)'; + exec_stmt($stmt, 'ii', $newID, $space); + } + } + + public function getCardHTML(): ?string + { + $html = ' +
+
+ @' . parent::getCreator()->getUsername() . ' + +
+ Something +
+
+
+
+

' . $this->thought . '

+
+
+ '; + + return $html; } public function getThought(): ?string @@ -230,576 +311,3 @@ class Thought extends Post $this->thought = $thought; } } - -// -- Repository for Topics -- -class TopicRepository -{ - // No $conn property needed as exec_stmt handles connection - - public function __construct() - { - // Constructor is now empty - } - - public function getAllTopics(): array - { - $result = exec_stmt('SELECT topic_id, topic FROM topics ORDER BY topic ASC', ''); // No params - $topics = []; - if ($result instanceof mysqli_result) { - while ($row = $result->fetch_assoc()) { - $topics[] = new Topic((int) $row['topic_id'], $row['topic']); - } - $result->free(); - } else { - // exec_stmt for SELECT should return mysqli_result. If not, it's an issue with exec_stmt or query. - error_log('TopicRepository::getAllTopics expected mysqli_result, got something else.'); - } - return $topics; - } - - public function findById(int $topicId): ?Topic - { - $result = exec_stmt('SELECT topic_id, topic FROM topics WHERE topic_id = ?', 'i', $topicId); - if ($result instanceof mysqli_result) { - if ($row = $result->fetch_assoc()) { - $result->free(); - return new Topic((int) $row['topic_id'], $row['topic']); - } - $result->free(); - } - return null; - } - - public function findByName(string $name): ?Topic - { - $result = exec_stmt('SELECT topic_id, topic FROM topics WHERE topic = ?', 's', $name); - if ($result instanceof mysqli_result) { - if ($row = $result->fetch_assoc()) { - $result->free(); - return new Topic((int) $row['topic_id'], $row['topic']); - } - $result->free(); - } - return null; - } - - public function findOrCreate(string $name): Topic - { - $topic = $this->findByName($name); - if ($topic) { - return $topic; - } - - // topics.topic_id is AUTO_INCREMENT, so exec_stmt should return the new ID. - $newTopicId = exec_stmt('INSERT INTO topics (topic) VALUES (?)', 's', $name); - - if (is_numeric($newTopicId) && $newTopicId > 0) { - return new Topic((int) $newTopicId, $name); - } - // If $newTopicId is 0 or not numeric, insert failed or exec_stmt behavior is unexpected. - throw new \RuntimeException('Failed to create topic or retrieve its ID: ' . $name); - } -} - -// -- Repository for Posts -- -class PostRepository -{ - private TopicRepository $topicRepository; - private string $essayContentPath; - - public function __construct(TopicRepository $topicRepository, string $essayContentPath) - { - $this->topicRepository = $topicRepository; - $this->essayContentPath = rtrim($essayContentPath, '/') . '/'; - } - - private function getAuthorFromDb(int $authorId): ?Author - { - $result = exec_stmt('SELECT user_id, username, profile_picture, bio, website FROM user WHERE user_id = ?', 'i', $authorId); - if ($result instanceof mysqli_result) { - $authorData = $result->fetch_assoc(); - $result->free(); - if ($authorData) { - return new Author( - (int) $authorData['user_id'], - $authorData['username'], - $authorData['profile_picture'], - $authorData['bio'], - $authorData['website'] - ); - } - } else { - error_log('PostRepository::getAuthorFromDb expected mysqli_result for author ID: ' . $authorId); - } - error_log('Author not found with ID: ' . $authorId); - return null; - } - - private function hydratePost(array $row): ?Post - { - $author = $this->getAuthorFromDb((int) $row['author_id']); - if (!$author) - return null; - - $postId = (int) $row['post_id']; - $postTypeId = (int) $row['post_type']; - $title = $row['title']; - $slug = $row['slug']; - $readCount = (int) $row['read_count']; - $createdAt = $row['created_at'] ? new DateTime($row['created_at']) : null; - $updatedAt = $row['updated_at'] ? new DateTime($row['updated_at']) : null; - $publishedAt = $row['published_at'] ? new DateTime($row['published_at']) : null; - $published = (bool) $row['published']; - $post = null; - - switch ($postTypeId) { - case 1: // Essay - $markdown = $this->getEssayMarkdown($postId); - $post = new Essay($postId, $author, $title, $slug, $markdown, $row['excerpt'], null, $createdAt, $updatedAt, $publishedAt, $published); - break; - case 2: // Moment - $post = new Moment($postId, $author, $row['excerpt'], null, $createdAt, $updatedAt, $publishedAt, $published); - break; - case 3: // Thought - $post = new Thought($postId, $author, $row['excerpt'], $createdAt, $updatedAt, $publishedAt, $published); - break; - default: - error_log('Unknown post type ID: ' . $postTypeId); - return null; - } - if ($post) - $post->setReadCount($readCount); - return $post; - } - - // File operations remain unchanged as they don't use the DB connection directly - private function getEssayMarkdown(int $postId): ?string - { /* ... same as before ... */ - $filePath = $this->essayContentPath . $postId . '/article.md'; - if (file_exists($filePath) && is_readable($filePath)) { - return file_get_contents($filePath); - } - return null; - } - - private function saveEssayMarkdown(int $postId, string $markdownContent): bool - { /* ... same as before ... */ - $dirPath = $this->essayContentPath . $postId . '/'; - if (!is_dir($dirPath)) { - if (!mkdir($dirPath, 0755, true)) { - error_log('Failed to create directory: ' . $dirPath); - return false; - } - } - $filePath = $dirPath . 'article.md'; - if (file_put_contents($filePath, $markdownContent) === false) { - error_log('Failed to write markdown file: ' . $filePath); - return false; - } - return true; - } - - private function deleteEssayContentDirectory(int $postId): bool - { /* ... same as before ... */ - $dirPath = $this->essayContentPath . $postId . '/'; - if (!is_dir($dirPath)) - return true; - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dirPath, RecursiveDirectoryIterator::SKIP_DOTS), - RecursiveIteratorIterator::CHILD_FIRST - ); - foreach ($files as $fileinfo) { - $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink'); - if (!@$todo($fileinfo->getRealPath())) { - error_log("Failed to {$todo} {$fileinfo->getRealPath()}"); - return false; - } - } - if (!@rmdir($dirPath)) { - error_log("Failed to remove main directory {$dirPath}"); - return false; - } - return true; - } - - public function findById(int $postId): ?Post - { - $result = exec_stmt('SELECT * FROM posts WHERE post_id = ?', 'i', $postId); - if ($result instanceof mysqli_result) { - $row = $result->fetch_assoc(); - $result->free(); - return $row ? $this->hydratePost($row) : null; - } - error_log('PostRepository::findById expected mysqli_result for post ID: ' . $postId); - return null; - } - - public function getPostIds(string $orderBy = 'published_at', string $direction = 'DESC'): array - { - // Basic validation for orderBy and direction - $orderBy = preg_replace('/[^a-zA-Z0-9_]/', '', $orderBy); // Basic sanitization - $direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC'; - - $sql = "SELECT post_id FROM posts ORDER BY $orderBy $direction"; - $result = exec_stmt($sql, ''); - $ids = []; - if ($result instanceof mysqli_result) { - while ($row = $result->fetch_assoc()) - $ids[] = (int) $row['post_id']; - $result->free(); - } else { - error_log('Error in getPostIds: Expected mysqli_result.'); - } - return $ids; - } - - public function getAllPosts(string $orderBy = 'published_at', string $direction = 'DESC'): array - { - $orderBy = preg_replace('/[^a-zA-Z0-9_]/', '', $orderBy); - $direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC'; - - $sql = "SELECT * FROM posts ORDER BY $orderBy $direction"; - $result = exec_stmt($sql, ''); - $posts = []; - if ($result instanceof mysqli_result) { - while ($row = $result->fetch_assoc()) { - if ($postObject = $this->hydratePost($row)) - $posts[] = $postObject; - } - $result->free(); - } else { - error_log('Error in getAllPosts: Expected mysqli_result.'); - } - return $posts; - } - - public function getPostIdsByAuthor(int $authorId, string $orderBy = 'published_at', string $direction = 'DESC'): array - { - $orderBy = preg_replace('/[^a-zA-Z0-9_]/', '', $orderBy); - $direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC'; - - $sql = "SELECT post_id FROM posts WHERE author_id = ? ORDER BY $orderBy $direction"; - $result = exec_stmt($sql, 'i', $authorId); - $ids = []; - if ($result instanceof mysqli_result) { - while ($row = $result->fetch_assoc()) - $ids[] = (int) $row['post_id']; - $result->free(); - } else { - error_log('Error in getPostIdsByAuthor: Expected mysqli_result.'); - } - return $ids; - } - - public function getPostsByAuthor(int $authorId, string $orderBy = 'published_at', string $direction = 'DESC'): array - { - $orderBy = preg_replace('/[^a-zA-Z0-9_.]/', '', $orderBy); // Allow dot for aliased columns - $direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC'; - - $sql = "SELECT * FROM posts WHERE author_id = ? ORDER BY $orderBy $direction"; - $result = exec_stmt($sql, 'i', $authorId); - $posts = []; - if ($result instanceof mysqli_result) { - while ($row = $result->fetch_assoc()) { - if ($postObject = $this->hydratePost($row)) - $posts[] = $postObject; - } - $result->free(); - } else { - error_log('Error in getPostsByAuthor: Expected mysqli_result.'); - } - return $posts; - } - - public function getPostIdsByTopicName(string $topicName, string $orderBy = 'p.published_at', string $direction = 'DESC'): array - { - $orderBy = preg_replace('/[^a-zA-Z0-9_.]/', '', $orderBy); - $direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC'; - - $sql = "SELECT p.post_id FROM posts p - INNER JOIN post_topics pt ON p.post_id = pt.post_id - INNER JOIN topics t ON pt.topic_id = t.topic_id - WHERE t.topic = ? - ORDER BY $orderBy $direction"; - $result = exec_stmt($sql, 's', $topicName); - $ids = []; - if ($result instanceof mysqli_result) { - while ($row = $result->fetch_assoc()) - $ids[] = (int) $row['post_id']; - $result->free(); - } else { - error_log('Error in getPostIdsByTopicName: Expected mysqli_result.'); - } - return $ids; - } - - public function getPostsByTopicName(string $topicName, string $orderBy = 'p.published_at', string $direction = 'DESC'): array - { - $orderBy = preg_replace('/[^a-zA-Z0-9_.]/', '', $orderBy); - $direction = strtoupper($direction) === 'ASC' ? 'ASC' : 'DESC'; - - $sql = "SELECT p.* FROM posts p - INNER JOIN post_topics pt ON p.post_id = pt.post_id - INNER JOIN topics t ON pt.topic_id = t.topic_id - WHERE t.topic = ? - ORDER BY $orderBy $direction"; - $result = exec_stmt($sql, 's', $topicName); - $posts = []; - if ($result instanceof mysqli_result) { - while ($row = $result->fetch_assoc()) { - if ($postObject = $this->hydratePost($row)) - $posts[] = $postObject; - } - $result->free(); - } else { - error_log('Error in getPostsByTopicName: Expected mysqli_result.'); - } - return $posts; - } - - private function updatePostTopics(int $postId, array $topicNames): void - { - // Delete existing topics for the post - // exec_stmt returns 0 for successful DELETE. No direct success check here. - exec_stmt('DELETE FROM post_topics WHERE post_id = ?', 'i', $postId); - - if (empty($topicNames)) - return; - - // Batch insert is harder with this exec_stmt. Doing one by one. - // This is inefficient but simpler with the current exec_stmt. - foreach ($topicNames as $name) { - $topicObj = $this->topicRepository->findOrCreate(trim($name)); - // post_topics has no auto-increment, exec_stmt will return 0 for success. - exec_stmt('INSERT INTO post_topics (post_id, topic_id) VALUES (?, ?)', 'ii', $postId, $topicObj->getId()); - } - } - - public function createPost(Post $post, array $topicNames = []): ?Post - { - // WARNING: No transaction possible with the provided exec_stmt for multi-step operations. - // Each exec_stmt is its own transaction. - try { - $sql = 'INSERT INTO posts (post_id, author_id, post_type, title, slug, read_count, created_at, updated_at, published_at, published, excerpt) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - - $postIdVal = $post->getPostId(); - $authorId = $post->getAuthor()->getUserId(); - $postTypeId = $post->getPostTypeId(); - $title = $post->getTitle(); - $slug = $post->getSlug(); - $readCount = $post->getReadCount(); - $createdAtDb = $post->getCreatedAt() ? $post->getCreatedAt()->format('Y-m-d H:i:s') : (new DateTime())->format('Y-m-d H:i:s'); - $updatedAtDb = $post->getUpdatedAt() ? $post->getUpdatedAt()->format('Y-m-d H:i:s') : (new DateTime())->format('Y-m-d H:i:s'); - $publishedAtDb = $post->getPublishedAt() ? $post->getPublishedAt()->format('Y-m-d H:i:s') : null; - $published = (int) $post->isPublished(); - $excerpt = null; - if ($post instanceof Essay) - $excerpt = $post->getExcerpt(); - elseif ($post instanceof Moment) - $excerpt = $post->getCaption(); - elseif ($post instanceof Thought) - $excerpt = $post->getThought(); - - // For INSERT on `posts` (no auto-increment PK), exec_stmt returns 0 on success. - $insertResult = exec_stmt( - $sql, - 'iiississsis', - $postIdVal, - $authorId, - $postTypeId, - $title, - $slug, - $readCount, - $createdAtDb, - $updatedAtDb, - $publishedAtDb, - $published, - $excerpt - ); - - // We can't reliably check $insertResult for success of this specific insert. - // Assume success if no PHP error/exception was thrown by exec_stmt (though it doesn't throw). - - if ($post instanceof Essay && $post->getMarkdown() !== null) { - if (!$this->saveEssayMarkdown($postIdVal, $post->getMarkdown())) { - // Rollback is not possible. Log error. - error_log('Failed to save essay markdown for post ID: ' . $postIdVal . '. DB insert was separate.'); - // Potentially delete the created post record if consistency is critical, but that's complex. - } - } - $this->updatePostTopics($postIdVal, $topicNames); // Also not part of a transaction - return $this->findById($postIdVal); - } catch (\Exception $e) { // Catch any exceptions from our code, not exec_stmt - error_log("Error creating post (ID: {$post->getPostId()}): " . $e->getMessage()); - return null; - } - } - - public function updatePost(Post $post, array $topicNames = []): ?Post - { - // WARNING: No transaction possible. - try { - $sql = 'UPDATE posts SET author_id = ?, post_type = ?, title = ?, slug = ?, - updated_at = CURRENT_TIMESTAMP, published_at = ?, published = ?, excerpt = ? - WHERE post_id = ?'; - - $authorId = $post->getAuthor()->getUserId(); - $postTypeId = $post->getPostTypeId(); - $title = $post->getTitle(); - $slug = $post->getSlug(); - $publishedAtDb = $post->getPublishedAt() ? $post->getPublishedAt()->format('Y-m-d H:i:s') : null; - $published = (int) $post->isPublished(); - $postIdVal = $post->getPostId(); - $excerpt = null; - if ($post instanceof Essay) - $excerpt = $post->getExcerpt(); - elseif ($post instanceof Moment) - $excerpt = $post->getCaption(); - elseif ($post instanceof Thought) - $excerpt = $post->getThought(); - - // exec_stmt returns 0 for successful UPDATE. - exec_stmt( - $sql, - 'iisssisi', - $authorId, - $postTypeId, - $title, - $slug, - $publishedAtDb, - $published, - $excerpt, - $postIdVal - ); - - if ($post instanceof Essay && $post->getMarkdown() !== null) { - if (!$this->saveEssayMarkdown($postIdVal, $post->getMarkdown())) { - error_log('Failed to save essay markdown for post ID: ' . $postIdVal . ' during update.'); - } - } - $this->updatePostTopics($postIdVal, $topicNames); - return $this->findById($postIdVal); - } catch (\Exception $e) { - error_log("Error updating post (ID: {$post->getPostId()}): " . $e->getMessage()); - return null; - } - } - - /** - * Deletes a post. - * WARNING: Due to exec_stmt, this is not a transactional operation. - * Success of individual DB deletions is not reliably checkable. - * This method attempts the operations and logs errors. - * Consider changing return type to void as boolean success is ambiguous. - */ - public function deletePost(int $postId): void - { - try { - $postDataResult = exec_stmt('SELECT post_type FROM posts WHERE post_id = ?', 'i', $postId); - $postType = null; - if ($postDataResult instanceof mysqli_result && $row = $postDataResult->fetch_assoc()) { - $postType = (int) $row['post_type']; - $postDataResult->free(); - } - - exec_stmt('DELETE FROM post_topics WHERE post_id = ?', 'i', $postId); - exec_stmt('DELETE FROM posts WHERE post_id = ?', 'i', $postId); - // Cannot reliably check if the above deletes were successful or affected rows. - - if ($postType === 1) { // Essay - if (!$this->deleteEssayContentDirectory($postId)) { - error_log('Failed to delete essay content directory for post ID: ' . $postId); - } - } - } catch (\Exception $e) { - error_log("Error during deletePost for ID $postId: " . $e->getMessage()); - // Depending on desired behavior, re-throw or handle - } - } - - /** - * Deletes posts by author. - * WARNING: Not transactional. Returns void as count of deleted posts cannot be reliably obtained from exec_stmt. - */ - public function deletePostsByAuthor(int $authorId): void - { - try { - $result = exec_stmt('SELECT post_id, post_type FROM posts WHERE author_id = ?', 'i', $authorId); - $postsToDelete = []; - if ($result instanceof mysqli_result) { - while ($row = $result->fetch_assoc()) - $postsToDelete[] = $row; - $result->free(); - } else { - error_log("Failed to fetch posts for deletion by author ID: $authorId"); - return; // Exit if we can't get the list of posts - } - - foreach ($postsToDelete as $postInfo) { - if ((int) $postInfo['post_type'] === 1) { // Essay - if (!$this->deleteEssayContentDirectory((int) $postInfo['post_id'])) { - error_log('Failed to delete essay content for post ' . $postInfo['post_id'] . ' during mass delete by author.'); - } - } - // Handle Moment image deletion if any - } - - // Must delete from child table `post_topics` first if no ON DELETE CASCADE, or if ensuring order. - // This is more complex with joins if exec_stmt doesn't handle it well. - // Simpler: iterate and delete topics for each post_id, or a broader delete. - foreach ($postsToDelete as $postInfo) { - exec_stmt('DELETE FROM post_topics WHERE post_id = ?', 'i', (int) $postInfo['post_id']); - } - exec_stmt('DELETE FROM posts WHERE author_id = ?', 'i', $authorId); - // Cannot get actual deleted count. - } catch (\Exception $e) { - error_log("Error deleting posts by author (ID: $authorId): " . $e->getMessage()); - } - } - - /** - * Increments read count for a post. - * WARNING: Success of the UPDATE is not reliably checkable with current exec_stmt. - */ - public function incrementReadCount(int $postId): void - { - // exec_stmt returns 0 for successful UPDATE. - exec_stmt('UPDATE posts SET read_count = read_count + 1 WHERE post_id = ?', 'i', $postId); - // No easy way to confirm success with current exec_stmt. - } -} - -// Example Usage (Illustrative - this would go in your controller/logic files) - -/* - * $topicRepo = new TopicRepository(); - * // Ensure $_ENV['ESSAY_CONTENT_PATH'] is set in your environment/config - * $essayPath = $_ENV['ESSAY_CONTENT_PATH'] ?? '/path/to/your/site/public/essays'; - * $postRepo = new PostRepository($topicRepo, $essayPath); - * - * // --- Fetching Posts --- - * // $allPosts = $postRepo->getAllPosts('created_at', 'DESC'); - * // $post123 = $postRepo->findById(123); - * // if ($post123 instanceof Essay) { - * // echo "Essay Title: " . $post123->getTitle() . "\n"; - * // } - * - * // --- Creating a new Essay --- - * // $author = $userRepo->findAuthorById(1); // Assuming a UserRepo or similar for authors - * // if ($author) { - * // $newEssayId = 1001; // Must be unique, not auto-incremented in schema - * // $newEssay = new Essay( - * // $newEssayId, $author, "My New OOP Essay", "my-new-oop-essay", - * // "# Hello World\n\nThis is markdown content.", "An excerpt about the essay." - * // ); - * // $createdPost = $postRepo->createPost($newEssay, ['PHP', 'OOP', 'Refactoring']); - * // if ($createdPost) { - * // echo "Created Post ID: " . $createdPost->getPostId() . "\n"; - * // } else { - * // echo "Failed to create post.\n"; - * // } - * // } - * - * // No explicit $dbConnection->close(); needed as exec_stmt handles its own connections. - */ diff --git a/functions/space.php b/functions/space.php index 0c04474..e9731dd 100644 --- a/functions/space.php +++ b/functions/space.php @@ -6,18 +6,20 @@ require_once 'post.php'; class Space { - private int $spaceId; + private int $id; private string $name; private string $description; + private int $visibility; private array $members; private array $posts; private ?Space $parent; - public function __construct(?int $spaceId, string $name, string $description, array $members, $posts, ?Space $parent = null) + public function __construct(?int $id, string $name, string $description, int $visibility, array $members, array $posts, ?Space $parent = null) { - $this->spaceId = $spaceId ?? randomId(2); + $this->id = $id ?? randomId(2); $this->name = $name; $this->description = $description; + $this->visibility = $visibility; $this->members = $members; $this->posts[] = $posts; $this->parent = $parent; @@ -25,9 +27,33 @@ class Space /* $stmt = 'INSERT INTO spaces (spaceId, name, description, parentSpaceIdk)'; */ } - public static function retrieveFromDB(int $space, ?int $user): ?Space + public function getCardHTML(): string { - if ($user) { + $html = ' +
+ +
+
+

' . $this->name . '

+
+ +
+ Something +
+
+
+

' . $this->description . '

+
+
+
+ '; + + return $html; + } + + public static function retrieveFromDB(int $space, int $user = -1): ?Space + { + if ($user != -1) { $stmt = 'SELECT * FROM spaceMembers WHERE space = ? AND user = ?'; $isMember = exec_stmt($stmt, 'ii', $space, $user)->fetch_assoc(); if (!$isMember) @@ -40,20 +66,48 @@ class Space } $stmt = 'SELECT * FROM space WHERE id = ?'; - $space = exec_stmt($stmt, 'i', $space)->fetch_assoc(); + $rawSpace = exec_stmt($stmt, 'i', $space)->fetch_assoc(); - if (!empty($space['parentSpace'])) - $parent = Space::retrieveFromDB($space['parentSpace'], $user); + if (!empty($rawSpace['parentSpace'])) + $parent = Space::retrieveFromDB($rawSpace['parentSpace'], $user); else $parent = null; $stmt = 'SELECT user FROM spaceMembers WHERE space = ?'; - $members = exec_stmt($stmt, 'i', $space)->fetch_assoc(); + $members = []; + $rawMembers = exec_stmt($stmt, 'i', $rawSpace['id']); + foreach ($rawMembers as $m) { + $members[] = User::retrieveFromDB($m['user']); + } $stmt = 'SELECT post FROM spacePosts WHERE space = ?'; - $posts = exec_stmt($stmt, 'i', $space)->fetch_assoc(); + $posts = []; + $rawPosts = exec_stmt($stmt, 'i', $rawSpace['id']); + while ($row = $rawPosts->fetch_assoc()) { + $posts[] = Post::retrieveFromDB($row['post']); + } - return new Space($space['id'], $space['name'], $space['description'], $members, $posts, $parent); + $space = new Space($rawSpace['id'], $rawSpace['name'], $rawSpace['description'], $rawSpace['visibility'], $members, $posts, $parent); + + foreach ($posts as $p) + $p->setSpace($space); + + return $space; + } + + public function allowUser(User $user) + { + $stmt = 'SELECT space FROM spaceMembers WHERE user = ?'; + $result = exec_stmt($stmt, 'i', $user->getId())->fetch_assoc(); + if ($result['space'] == $this->id) + return true; + else + return false; + } + + public function getPosts() + { + return $this->posts; } public function __toString() @@ -63,7 +117,7 @@ class Space public function getId(): int { - return $this->spaceId; + return $this->id; } public function getName(): string @@ -75,4 +129,9 @@ class Space { return $this->description; } + + public function getVisibility(): int + { + return $this->visibility; + } } diff --git a/functions/user.php b/functions/user.php index ff7fa6f..b0ed817 100644 --- a/functions/user.php +++ b/functions/user.php @@ -30,7 +30,8 @@ class LoginCredentials class User { - private int $userId; + private int $id; + private ?array $spaces; private string $username; private ?DateTime $createdAt; private ?DateTime $lastActive; @@ -38,8 +39,9 @@ class User private ?string $profilePicture; private ?string $bio; private ?string $website; + private int $role; - public function __construct(int $userId, string $username, ?string $profilePicture = null, ?string $bio = null, ?string $website = null, ?mysqli $db = null) + public function __construct(int $id = -1, ?array $spaces, string $username, ?string $profilePicture = null, ?string $bio = null, ?string $website = null, int $role = 3) { // Basic XSS prevention on construction (can be enhanced) $username = htmlspecialchars($username, ENT_QUOTES, 'UTF-8'); @@ -52,7 +54,6 @@ class User throw new InvalidArgumentException('Invalid characters in user data.'); } - $this->userId = $userId; $this->username = $username; $this->profilePicture = $profilePicture; $this->bio = $bio; @@ -60,58 +61,130 @@ class User $this->createdAt = new DateTime(); $this->lastActive = null; $this->isActive = false; + $this->role = $role; + $this->spaces = null; - if ($db) { - $stmt = $db->prepare('INSERT INTO user (user_id, username, profile_picture, bio, website) VALUES (?, ?, ?, ?, ?)'); - if ($stmt) { - $stmt->bind_param('issss', $this->userId, $this->username, $this->profilePicture, $this->bio, $this->website); - $stmt->execute(); - $stmt->close(); - } else { - error_log('Error preparing statement: ' . $db->error); - } + if ($id == -1) { + $this->id = randomId(0); + $stmt = 'INSERT INTO user (userId, username, profilePicture, bio, website) VALUES (?, ?, ?, ?, ?)'; + exec_stmt($stmt, 'issss', $this->id, $this->username, $this->profilePicture, $this->bio, $this->website); + } else + $this->id = $id; + } + + public static function exists(int $id): bool + { + $stmt = 'SELECT count(id) FROM user WHERE id = ?'; + $result = exec_stmt($stmt, 'i', $id)->fetch_assoc(); + + if ($result['count(id)'] == 1) + return true; + else if ($result['count(id)'] > 1) + return true; + else + return false; + } + + public static function retrieveFromDB(int $userId): ?User + { + $stmt = 'SELECT id, username, profilePicture, bio, website FROM user WHERE id = ?'; + $user = exec_stmt($stmt, 'i', $userId)->fetch_assoc(); + + if ($user) { + return new User($user['id'], null, $user['username'], $user['profilePicture'], $user['bio'], $user['website']); } else { - // Consider logging or throwing an exception if no database connection is provided - error_log('Database connection not provided during User creation.'); - } - } - - public static function exists(int $id) - { - $stmt = 'SELECT count(username) FROM user WHERE id = ?'; - echo pretty_dump(exec_stmt($stmt, 'i', $id)->fetch_assoc()); - } - - public static function login(LoginCredentials $credentials, ?mysqli $db): ?User - { - if (!$db) { - error_log('Database connection not provided for login.'); return null; } - $username = $credentials->getUsername(); - $password = $credentials->getPassword(); + } - $stmt = $db->prepare('SELECT user_id, username, password_hash, profile_picture, bio, website FROM user WHERE username = ?'); - if ($stmt) { - $stmt->bind_param('s', $username); - $stmt->execute(); - $result = $stmt->get_result(); - $user = $result->fetch_assoc(); - $stmt->close(); + public static function getSignupHTML() + { + $html = ' +
+

Sign Up

+
+ + + + + - if ($user && hash('sha256', $password) === $user['password_hash']) { - return new User( - (int) $user['user_id'], - $user['username'], - $user['profile_picture'], - $user['bio'], - $user['website'] - ); - } - } else { - error_log('Error preparing statement: ' . $db->error); - } - return null; + + + + + + + + + + + + + + + No file selected. + + + + +
+ + + +
+ +
+
+ '; + + return $html; + } + + public static function getLoginHTML() + { + $html = ' +
+

Log In

+
+ + + + + + + + + + + + +
+ + +
+ '; + + return $html; + } + + public static function login(LoginCredentials $credentials): ?User + { + $stmt = 'SELECT id, username, email, passwordHash, role, profilePicture, bio, website FROM user WHERE username = ?'; + $user = exec_stmt($stmt, 's', $credentials->getUsername())->fetch_assoc(); + + if (!password_verify($credentials->getPassword(), $user['passwordHash'])) + return null; + else + return new User($user['id'], null, $user['username'], $user['profilePicture'], $user['bio'], $user['website']); } public static function logout(): void @@ -124,9 +197,33 @@ class User exit(); } - public function getUserId(): int + public function getHomeSpace() { - return $this->userId; + // TODO: Need to implement. Requires fleshing out additional space types. + return Space::retrieveFromDB(1, 1); + } + + public function getSpaces() + { + $stmt = 'SELECT * FROM spaceMembers WHERE user = ?'; + $spaces = []; + $rawSpaces = exec_stmt($stmt, 'i', $this->id); + while ($row = $rawSpaces->fetch_assoc()) { + $spaces[] = Space::retrieveFromDB($row['space'], $row['user']); + } + + $this->spaces = $spaces; + return $spaces; + } + + public function getRole(): int + { + return $this->role; + } + + public function getId(): int + { + return $this->id; } public function getUsername(): string @@ -183,7 +280,7 @@ class User $this->isActive = !$this->isActive; $stmt = $db->prepare('UPDATE user SET is_active = ? WHERE user_id = ?'); if ($stmt) { - $stmt->bind_param('ii', (int) $this->isActive, $this->userId); + $stmt->bind_param('ii', (int) $this->isActive, $this->id); $result = $stmt->execute(); $stmt->close(); return $result; @@ -205,7 +302,7 @@ class User } $stmt = $db->prepare('UPDATE user SET username = ? WHERE user_id = ?'); if ($stmt) { - $stmt->bind_param('si', $newUsername, $this->userId); + $stmt->bind_param('si', $newUsername, $this->id); $result = $stmt->execute(); $stmt->close(); if ($result) { @@ -227,7 +324,7 @@ class User } $stmt = $db->prepare('UPDATE user SET password_hash = ? WHERE user_id = ?'); if ($stmt) { - $stmt->bind_param('si', $newPasswordHash, $this->userId); + $stmt->bind_param('si', $newPasswordHash, $this->id); $result = $stmt->execute(); $stmt->close(); return $result; @@ -249,7 +346,7 @@ class User } $stmt = $db->prepare('UPDATE user SET profile_picture = ? WHERE user_id = ?'); if ($stmt) { - $stmt->bind_param('si', $newProfilePicture, $this->userId); + $stmt->bind_param('si', $newProfilePicture, $this->id); $result = $stmt->execute(); $stmt->close(); if ($result) { @@ -270,7 +367,7 @@ class User } $stmt = $db->prepare('INSERT INTO follows (follower_user_id, following_user_id, notify_user) VALUES (?, ?, ?)'); if ($stmt) { - $stmt->bind_param('iii', $this->userId, $followingUserId, (int) $notify); + $stmt->bind_param('iii', $this->id, $followingUserId, (int) $notify); $result = $stmt->execute(); $stmt->close(); return $result; @@ -288,7 +385,7 @@ class User } $stmt = $db->prepare('SELECT u.user_id, u.username, u.profile_picture, u.bio, u.website FROM follows f JOIN user u ON f.follower_user_id = u.user_id WHERE f.following_user_id = ?'); if ($stmt) { - $stmt->bind_param('i', $this->userId); + $stmt->bind_param('i', $this->id); $stmt->execute(); $result = $stmt->get_result(); $followers = []; @@ -317,7 +414,7 @@ class User } $stmt = $db->prepare('SELECT u.user_id, u.username, u.profile_picture, u.bio, u.website FROM follows f JOIN user u ON f.following_user_id = u.user_id WHERE f.follower_user_id = ?'); if ($stmt) { - $stmt->bind_param('i', $this->userId); + $stmt->bind_param('i', $this->id); $stmt->execute(); $result = $stmt->get_result(); $following = []; @@ -337,35 +434,6 @@ class User return []; } } - - // Static method to fetch a User by ID (example using mysqli) - public static function findById(int $userId, ?mysqli $db): ?User - { - if (!$db) { - error_log('Database connection not provided for findById.'); - return null; - } - $stmt = $db->prepare('SELECT user_id, username, profile_picture, bio, website FROM user WHERE user_id = ?'); - if ($stmt) { - $stmt->bind_param('i', $userId); - $stmt->execute(); - $result = $stmt->get_result(); - $user = $result->fetch_assoc(); - $stmt->close(); - if ($user) { - return new User( - (int) $user['user_id'], - $user['username'], - $user['profile_picture'], - $user['bio'], - $user['website'] - ); - } - } else { - error_log('Error preparing statement: ' . $db->error); - } - return null; - } } -class Author extends User {} +class Creator extends User {} diff --git a/index.old.php b/index.old.php new file mode 100644 index 0000000..51da717 --- /dev/null +++ b/index.old.php @@ -0,0 +1,34 @@ + +

Vintage Coding

+

Intentional Craftsmanship Over Ephemeral Trends

+
+

Vintage Coding, much like appreciating a well-crafted antique or a timeless piece of art, prioritizes quality, longevity, and deep understanding. It\'s about building software with a focus on robust architecture, maintainable code, and a thorough comprehension of the underlying principles. The analogy to vintage clothing is apt – these garments were often made with superior materials and construction, designed to last. Similarly, Vintage Coding aims to create software that withstands the test of time, is easier to understand and modify in the future, and is less prone to the rapid obsolescence that can plague hastily assembled codebases.

+

This site is built in collaboration with Google Gemini, and this highlights a key aspect of Vintage Coding: it\'s not about rejecting modern tools, but rather about employing them thoughtfully and strategically. Gemini, in this context, acts as a knowledgeable partner, offering insights and examples, but the ultimate direction and architectural decisions remain with the human developer. This partnership emphasizes augmented intelligence rather than complete reliance.

+

The following are the cornerstones of Vintage Coding:

+ +
    +
  1. AI is a partner for exploration and insight, with human oversight and critical evaluation remaining central.
  2. +
  3. Focus on core infrastructure built by humans, with AI used to identify omissions, ensuring a solid foundation.
  4. +
  5. AI tackles specific, well-defined issues, allowing for careful assessment of its output.
  6. +
  7. Emphasis on clean, concise, and readable code for long-term sustainability.
  8. +

+ +

Vibe Coding\'s potential for uncritical AI adoption and reactive development risks creating fragile, difficult-to-manage systems and hindering developer growth. However, it can serve a purpose in specific contexts where rapid prototyping, immediate experimentation, or disposable solutions are prioritized over long-term maintainability and deep understanding.

+

While seemingly opposing forces, like Yin and Yang, both Vibe Coding and Vintage Coding represent different points on the spectrum of software development. Having the wisdom and understanding to know when to prioritize rapid iteration and when to invest in deliberate craftsmanship is crucial for achieving a balanced and effective approach.

+ + '; + + return $content; +} diff --git a/index.php b/index.php index 51da717..87ae9ba 100644 --- a/index.php +++ b/index.php @@ -1,34 +1,2 @@ -

Vintage Coding

-

Intentional Craftsmanship Over Ephemeral Trends

-
-

Vintage Coding, much like appreciating a well-crafted antique or a timeless piece of art, prioritizes quality, longevity, and deep understanding. It\'s about building software with a focus on robust architecture, maintainable code, and a thorough comprehension of the underlying principles. The analogy to vintage clothing is apt – these garments were often made with superior materials and construction, designed to last. Similarly, Vintage Coding aims to create software that withstands the test of time, is easier to understand and modify in the future, and is less prone to the rapid obsolescence that can plague hastily assembled codebases.

-

This site is built in collaboration with Google Gemini, and this highlights a key aspect of Vintage Coding: it\'s not about rejecting modern tools, but rather about employing them thoughtfully and strategically. Gemini, in this context, acts as a knowledgeable partner, offering insights and examples, but the ultimate direction and architectural decisions remain with the human developer. This partnership emphasizes augmented intelligence rather than complete reliance.

-

The following are the cornerstones of Vintage Coding:

- -
    -
  1. AI is a partner for exploration and insight, with human oversight and critical evaluation remaining central.
  2. -
  3. Focus on core infrastructure built by humans, with AI used to identify omissions, ensuring a solid foundation.
  4. -
  5. AI tackles specific, well-defined issues, allowing for careful assessment of its output.
  6. -
  7. Emphasis on clean, concise, and readable code for long-term sustainability.
  8. -

- -

Vibe Coding\'s potential for uncritical AI adoption and reactive development risks creating fragile, difficult-to-manage systems and hindering developer growth. However, it can serve a purpose in specific contexts where rapid prototyping, immediate experimentation, or disposable solutions are prioritized over long-term maintainability and deep understanding.

-

While seemingly opposing forces, like Yin and Yang, both Vibe Coding and Vintage Coding represent different points on the spectrum of software development. Having the wisdom and understanding to know when to prioritize rapid iteration and when to invest in deliberate craftsmanship is crucial for achieving a balanced and effective approach.

- - '; - - return $content; -} +header('Location: spaces.php?view=list'); diff --git a/js/actionButton.js b/js/actionButton.js new file mode 100644 index 0000000..52233b1 --- /dev/null +++ b/js/actionButton.js @@ -0,0 +1,118 @@ +let actionButton = document.querySelector("#actionButton"); +let newPostButton = document.querySelector("#newPost"); +let newSpaceButton = document.querySelector("#newSpace"); +let filterSortButton = document.querySelector("#filterSort"); +let circle = document.querySelector(".circleLayout"); +let modal = document.querySelector("#stdModal"); + +let state = "hide"; + +actionButton.addEventListener("click", () => { + if (state == "hide") { + state = "show"; + newPostButton.style.display = "flex"; + newSpaceButton.style.display = "flex"; + filterSortButton.style.display = "flex"; + circle.style.backgroundColor = "#FFFFFF"; + + newPostButton.addEventListener("click", () => { + fetch("director.php", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: "ajaxId=newPostHTML", + }) + .then((response) => { + return response.json(); + }) + .then((data) => { + modal.innerHTML = data["html"]; + modal.style.display = "flex"; + + let newNavThought = document.querySelector("#newNavThought"); + let newThought = document.querySelector("#newThought"); + let newNavMoment = document.querySelector("#newNavMoment"); + let newMoment = document.querySelector("#newMoment"); + let newNavEssay = document.querySelector("#newNavEssay"); + let newEssay = document.querySelector("#newEssay"); + let newPostLabel = document.querySelector("#newPostLabel"); + let postType = document.querySelector("#postType"); + + let share = document.querySelector("#share"); + + share.addEventListener("click", (e) => { + e.preventDefault(); + switch (postType.value) { + case "Thought": + let thought = document.querySelector("#thought"); + + // TODO: Retrieve space dynamically. + + fetch("director.php", { + method: "POST", + + // TODO: Add Authorization header with JWT + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: + "ajaxId=newThought&space=1&thought=" + + encodeURIComponent(thought.value), + }).then((response) => { + console.log(response.json()); + modal.style.display = "none"; + }); + break; + case "Moment": + break; + case "Essay": + break; + } + }); + + newNavThought.addEventListener("click", () => { + newMoment.style.display = "none"; + newEssay.style.display = "none"; + + newThought.style.display = "flex"; + newPostLabel.innerText = "New Thought"; + newThought.querySelector('input[type="text"]').focus(); + postType.value = "Thought"; + }); + + newNavMoment.addEventListener("click", () => { + newThought.style.display = "none"; + newEssay.style.display = "none"; + + newMoment.style.display = "flex"; + newPostLabel.innerText = "New Moment"; + newMoment.querySelector('input[type="text"]').focus(); + postType.value = "Moment"; + }); + + newNavEssay.addEventListener("click", () => { + newThought.style.display = "none"; + newMoment.style.display = "none"; + + newEssay.style.display = "flex"; + newPostLabel.innerText = "New Essay"; + newEssay.querySelector('input[type="text"]').focus(); + postType.value = "Essay"; + }); + + return data; + }); + }); + + newSpaceButton.addEventListener("click", () => { }); + + filterSortButton.addEventListener("click", () => { }); + } else if (state == "show") { + state = "hide"; + newPostButton.style.display = "none"; + newSpaceButton.style.display = "none"; + filterSortButton.style.display = "none"; + circle.style.backgroundColor = "transparent"; + } +}); diff --git a/js/login.js b/js/login.js index 1a71481..498de18 100644 --- a/js/login.js +++ b/js/login.js @@ -1,72 +1,93 @@ -const form = document.querySelector('#login_form'); -const username_field = form.querySelector('#username'); -const password_field = form.querySelector('#password'); -const password_hash_field = form.querySelector('#password_hash'); -const submit_button = form.querySelector('input[type="submit"]'); +const login = document.querySelector("#login"); +const logout = document.querySelector("#logout"); +const stdModal = document.querySelector("#stdModal"); -submit_button.addEventListener('click', async (e) => { - e.preventDefault(); - const password_hash = await hashString(password_field.value); - const urlParams = new URLSearchParams(document.location.search); - const redirect = decodeURI(urlParams.get('redirect')); - requestAuth(username_field.value, password_hash, redirect); -}); - -function requestAuth(username, password_hash, redirect) { - fetch('http://localhost:7477/auth', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: 'username=' + username + '&password_hash=' + password_hash, - }) - .then(response => { - if (!response.ok) { - console.log('Login Failed!'); - } - - return response.json(); // Parse response body as JSON - }) - .then(data => { - notifyServer(data["id"], username, 1, redirect); - }); -} - -function notifyServer(id, username, role, redirect) { - fetch('director.php', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: 'ajax_id=login&user_id=' + id + '&username=' + username + '&role=' + role, - }) - .then(response => { - return response.json(); - }) - .then(data => { - if ('startViewTransition' in document) { +if (logout) { + logout.addEventListener("click", () => { + fetch("director.php", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: "ajaxId=logout", + }).then(() => { + if ("startViewTransition" in document) { document.startViewTransition(() => { - if (redirect == null) { - window.location.href = 'articles.php?view=list&sort=new'; - } else { - window.location.href = redirect; - } + window.location.href = "spaces.php?view=list"; }); } else { - // Fallback for browsers that don't support View Transitions - if (redirect == null) { - window.location.href = 'articles.php?view=list&sort=new'; - } else { - window.location.href = redirect; - } + window.location.href = "spaces.php?view=list"; } }); + }); } -async function hashString(str) { - const encoded = new TextEncoder().encode(str); - const digest = await crypto.subtle.digest('SHA-256', encoded); - const hashArray = Array.from(new Uint8Array(digest)); - const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); - return hashHex; +if (login) { + login.addEventListener("click", () => { + console.log("click"); + stdModal.innerHTML = ` +
+

Log In

+
+ + + + + + + + + + + + +
+ + +
+ `; + + stdModal.style.display = "flex"; + + const form = document.querySelector("#login_form"); + const username_field = form.querySelector("#username"); + const passwordField = form.querySelector("#password"); + const submit_button = form.querySelector('input[type="submit"]'); + + submit_button.addEventListener("click", async (e) => { + e.preventDefault(); + requestAuth(username_field.value, passwordField.value); + }); + + function requestAuth(username, password) { + fetch("director.php", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: "ajaxId=login&username=" + username + "&password=" + password, + }) + .then((response) => { + if (!response.ok) { + console.log("Login Failed!"); + } + + return response.json(); + }) + .then((data) => { + console.log(data); + if ("startViewTransition" in document) { + document.startViewTransition(() => { + window.location.href = data["redirect"]; + }); + } else { + // Fallback for browsers that don't support View Transitions + window.location.href = data["redirect"]; + } + }); + } + }); } diff --git a/spaces.php b/spaces.php index f7b3103..3681c9b 100644 --- a/spaces.php +++ b/spaces.php @@ -1,47 +1,144 @@ getSpace(); + + if (isset($_SESSION['user']) && $space->allowUser($_SESSION['user'])) + echo displayEssay($essay); + else if ($space->getVisibility() == 1) + echo displayEssay($essay); + else + echo displaySpace(2025); + } else + echo displaySpace(2025); break; - case 'displaySpace': + case 'show': + // TODO: After implementing the notification modal, notify the user on success/fail auth events. + if (isset($_GET['id'])) { + $space = Space::retrieveFromDB($_GET['id']); + if (isset($_SESSION['user'])) + if ($space->allowUser(unserialize($_SESSION['user']))) + echo displaySpace($_GET['id']); + else if ($space->getVisibility() == 1) + echo displaySpace($_GET['id']); + else + echo 'TODO: Notification Modal -> User not a member'; + else { + if ($space->getVisibility() == 1) + echo displaySpace($_GET['id']); + else + echo 'TODO: Notification Modal -> Space is not Open'; + } + } else + echo displaySpaceList(); break; - case 'displayFeed': - /* if (isset($_GET['space_id']) || isset($_GET['user_id'])) */ + case 'list': + if (isset($_SESSION['user'])) + echo displaySpaceList(); + else + echo displaySpace(2025); break; - case 'displaySpacePosts': + + default: + echo displaySpaceList(); break; } } else { - // Treat as displaySpacePosts - echo displaySpace(); + echo displaySpace(2025); } -include_once 'components/core/foot.php'; +if (isset($_SESSION['user'])) + echo actionButton(); -function displaySpace($spaceId = 2025) +require_once 'components/core/foot.php'; + +function actionButton() { - $space = Space::retrieveFromDB($spaceId, null); + // TODO: Position these on the arc of a circle. $html = ' -

' . $space->getId() . '

+
+ + + +
+
'; return $html; } + +function displayEssay(Essay $e) +{ + $html = ' +
+ ' . $e->getHTML() . ' +
+ '; + + return $html; +} + +function displaySpaceList() +{ + $user = unserialize($_SESSION['user']); + + $html = ' +
+
+

Spaces

+
+
+ '; + + $spaces = $user->getSpaces(); + foreach ($spaces as $s) { + $html .= $s->getCardHTML(); + } + + $html .= ' +
+ '; + + return $html; +} + +function displaySpace($id = 2025) +{ + $space = Space::retrieveFromDB($id); + if ($space) { + $html = ' +
+
+

' . $space->getName() . '

+

' . $space->getDescription() . '

+
+
+ '; + + // TODO: Need to investigate this bug in Posts + $posts = $space->getPosts(); + foreach ($posts as $post) { + foreach ($post as $p) { + $html .= $p->getCardHTML(); + } + } + + $html .= ' +
+ '; + + return $html; + } +} diff --git a/user.php b/user.php deleted file mode 100644 index 551834a..0000000 --- a/user.php +++ /dev/null @@ -1,113 +0,0 @@ -