still some bugs and auth issues to iron out. need to implement a new action button/bar, the card for moments in a space, creating new moments and essays, dynamically refreshing the spaces view when a new post is created.

This commit is contained in:
2025-05-20 19:46:09 -06:00
parent a93c30c40f
commit 54a01f0f1f
20 changed files with 1018 additions and 1191 deletions
+30 -24
View File
@@ -18,52 +18,58 @@
return $args;
}
if ($_SERVER['SCRIPT_NAME'] == '/user.php') {
$args = get_args_from_url();
if ($_SERVER['SCRIPT_NAME'] == '/spaces.php')
echo '<script src="js/actionButton.js" defer></script>';
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 '
<script src="js/profile_picture_cropper_modal.js"></script>
<script src="js/upload.js"></script>
';
} else if ($args['view'] == 'display') {
echo '
} else if ($args['view'] == 'display') {
echo '
<script src="js/user_actions_modal.js"></script>
<script src="js/upload.js"></script>
';
}
}
}
} else if ($_SERVER['SCRIPT_NAME'] == '/admin.php') {
echo '
} else if ($_SERVER['SCRIPT_NAME'] == '/admin.php') {
echo '
<script src="js/clickable_rows.js"></script>
';
$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 '
<script src="js/upload.js"></script>
';
}
}
}
} 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 '
<script src="js/md_preview.js"></script>
';
}
}
} else if ($_SERVER['SCRIPT_NAME'] == '/todo.php') {
echo '<script src="js/todo.js"></script>';
}
} else if ($_SERVER['SCRIPT_NAME'] == '/todo.php') {
echo '<script src="js/todo.js"></script>';
}
echo '
echo '
<script src="js/override_browser_styles.js"></script>
';
}
?>
</footer>
+3 -3
View File
@@ -7,11 +7,11 @@
<link rel="stylesheet" href="https://nerdfonts.com/assets/css/webfont.css">
<link rel="icon" type="image/x-icon" href="data/vintagecoding-favicon.png">
<?php
if (!isset($_SESSION['theme']) || $_SESSION['theme'] == 'dark')
if (!isset($_SESSION['theme']) || $_SESSION['theme'] == 'dark')
echo '<link href="css/dark.css" rel="stylesheet" type="text/css">';
else
else
echo '<link href="css/light.css" rel="stylesheet" type="text/css">';
?>
?>
</head>
+21 -18
View File
@@ -2,24 +2,24 @@
$current_page = $_SERVER['SCRIPT_NAME'];
$nav = '
<nav>
<div class="row_space_between no_margin">
<div class="row space_between no_margin full_width">
<div class="site_navigation">
<ul>
<li><a href="/index.php"' . ($current_page == '/index.php' ? ' class="underline"' : '') . '>Home</a></li>
<li><a href="/spaces.php?view=displaySpaces"' . ($current_page == '/spaces.php' ? ' class="underline"' : '') . '>Spaces</a></li>
<li><a href="/spaces.php?view=list"' . ($current_page == '/spaces.php' ? ' class="underline"' : '') . '>Spaces</a></li>
';
/* <li><a href="/index.php"' . ($current_page == '/index.php' ? ' class="underline"' : '') . '>Home</a></li> */
if (isset($_SESSION['user'])) {
$nav .= '
<li><a href="/user.php?view=display&user_id=' . $_SESSION['user_id'] . '"' . ($current_page == '/user.php' ? ' class="underline"' : '') . '>Account</a></li>
<li><a href="/todo.php?view=display"' . ($current_page == '/todo.php' ? ' class="underline"' : '') . '>Todos</a></li>
';
/* $nav .= ' */
/* <li><a href="/user.php?view=display&user_id=' . unserialize($_SESSION['user'])->getId() . '"' . ($current_page == '/user.php' ? ' class="underline"' : '') . '>Account</a></li> */
/* <li><a href="/todo.php?view=display"' . ($current_page == '/todo.php' ? ' class="underline"' : '') . '>Todos</a></li> */
/* '; */
if ($_SESSION['user'] <= developer) {
$nav .= '
if (unserialize($_SESSION['user'])->getRole() <= developer) {
$nav .= '
<li><a href="/admin.php?view=users"' . ($current_page == '/admin.php' ? ' class="underline"' : '') . '>Admin</a></li>
';
}
}
}
$nav .= '
@@ -31,23 +31,23 @@ $nav .= '
$theme_toggle = '';
if ($_SESSION['theme'] == 'dark')
$theme_toggle = 'light';
$theme_toggle = 'light';
else
$theme_toggle = 'dark';
$theme_toggle = 'dark';
$nav .= '
<button id="theme_toggle" value="' . $theme_toggle . '" name="theme"><i class="nf ' . ($theme_toggle == 'dark' ? 'nf-oct-moon' : 'nf-oct-sun') . '"></i></button>
</form>
';
if (isset($_SESSION['username'])) {
$nav .= '
<a href="user.php?view=logout">Log Out</a>
if (isset($_SESSION['user'])) {
$nav .= '
<p class="link underline" id="logout">Log Out</p>
';
} else {
$nav .= '
<a href="user.php?view=login">Login</a>
<a href="user.php?view=signup">Sign Up</a>
$nav .= '
<p class="link underline" id="login">Login</p>
<p class="link underline" id="signup">Sign Up</p>
';
}
@@ -56,7 +56,10 @@ $nav .= '
</div>
<script src="js/nav.js" defer></script>
<script src="js/login.js" defer></script>
</nav>
<div id="notificationModal"></div>
<div id="stdModal"></div>
';
echo $nav;
+16 -10
View File
@@ -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;
+7 -1
View File
@@ -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;
+7 -1
View File
@@ -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;
+79 -65
View File
@@ -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 **********/
+94 -37
View File
@@ -1,61 +1,118 @@
<?php
include 'functions/functions.php';
include 'functions/init.php';
if (isset($_POST['ajax_id'])) {
switch ($_POST['ajax_id']) {
case 'todo':
handle_todo();
break;
error_log('RECEIVED: ' . pretty_dump($_POST));
if (isset($_POST['ajaxId'])) {
switch ($_POST['ajaxId']) {
case 'login':
handle_login();
break;
case 'logout':
User::logout();
break;
case 'newPostHTML':
handleNewPostHTML();
break;
case 'newThought':
handleNewThought();
break;
}
} else {
$response = [
'error' => '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 = '
<div class="med_width std_border center padding">
<h3 id="newPostLabel">New Thought</h3>
<form method="post" class="center">
<input type="hidden" name="userID" value="' . unserialize($_SESSION['user'])->getId() . '">
<input type="hidden" name="postType" value="Thought" id="postType">
<div id="newThought" class="column">
<input type="text" name="thought" placeholder="Jot a Thought." autofocus id="thought">
</div>
<div id="newMoment" class="column" style="display: none;">
<input type="text" name="caption" placeholder="Caption goes here." autofocus id="caption">
</div>
<div id="newEssay" class="column" style="display: none;">
<input type="text" name="title" placeholder="Title" autofocus id="title">
<textarea name="markdown" placeholder="#Heading" id="markdown"></textarea>
<input type="text" name="excerpt" placeholder="A 2-3 sentence summary." id="excerpt">
</div>
<input class="button" type="submit" value="Share" id="share">
</form>
<div class="line std_border"></div>
<div id="newModalNav" class="row space_between">
<span id="newNavThought" class="link">Thought</span>
<span id="newNavMoment" class="link">Moment</span>
<span id="newNavEssay" class="link">Essay</span>
</div>
</div>
';
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;
}
+5 -1
View File
@@ -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;
+8 -3
View File
@@ -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()
+9 -49
View File
@@ -1,18 +1,18 @@
<?php
session_start();
header("Content-Security-Policy: default-src 'self' localhost:7477; font-src 'self' https://www.nerdfonts.com/assets/fonts/Symbols-2048-em%20Nerd%20Font%20Complete.woff2; style-src 'self' 'unsafe-inline' https://nerdfonts.com/assets/css/webfont.css https://www.nerdfonts.com/assets/css/webfont.css; script-src 'self' 'unsafe-inline'; img-src 'self';");
header("Content-Security-Policy: default-src 'self' localhost:*; font-src 'self' https://www.nerdfonts.com/assets/fonts/Symbols-2048-em%20Nerd%20Font%20Complete.woff2; style-src 'self' 'unsafe-inline' https://nerdfonts.com/assets/css/webfont.css https://www.nerdfonts.com/assets/css/webfont.css; script-src 'self' 'unsafe-inline'; img-src 'self';");
// Initialize Composer.
require_once 'vendor/autoload.php';
require_once 'db.php';
require_once 'user.php';
require_once 'post.php';
require_once 'spaces.php';
// Load environment variables.
require_once 'functions/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable('/home/jashton/dev/me/weave.space/.env');
$dotenv->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();
}
}
+153 -645
View File
@@ -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;
/* <a href="user.php?view=display&id=' . parent::getCreator()->getId() . '">@' . parent::getCreator()->getUsername() . '</a> */
$html = '
<div class="full_width std_border padding center top_margin bottom_margin">
<a href="spaces.php?view=essay&id=' . parent::getId() . '" class="divLink">
<div class="row space_between full_width">
<div class="column std_width">
<h4>' . $this->title . '</h4>
</div>
<div class="">
<span>Something</span>
</div>
</div>
<div class="row full_width">
<p>' . $this->excerpt . '</p>
</div>
</a>
</div>
';
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 = '
<div class="std_border padding center full_width top_margin bottom_margin">
<div class="row space_between full_width">
<a href="user.php?view=display&id=' . parent::getCreator()->getId() . '">@' . parent::getCreator()->getUsername() . '</a>
<div class="">
<span>Something</span>
</div>
</div>
<div class="line"></div>
<div class="row full_width">
<p>' . $this->thought . '</p>
</div>
</div>
';
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.
*/
+71 -12
View File
@@ -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 = '
<div class="full_width std_border padding center top_margin bottom_margin">
<a href="spaces.php?view=show&id=' . $this->id . '" class="divLink">
<div class="row space_between full_width">
<div class="column std_width">
<h4>' . $this->name . '</h4>
</div>
<div class="">
<span>Something</span>
</div>
</div>
<div class="row full_width">
<p>' . $this->description . '</p>
</div>
</a>
</div>
';
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;
}
}
+156 -88
View File
@@ -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 = '
<div class="form_card">
<h3 class="underline">Sign Up</h3>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="form_id" value="signup_form">
<input type="hidden" name="crop_x" value="" id="crop_x">
<input type="hidden" name="crop_y" value="" id="crop_y">
<input type="hidden" name="crop_width" value="" id="crop_width">
<input name="required_field" id="required_field" value="">
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;
<label for="username">Username</label>
<input id="username" name="username" required>
<label for="email">Email Address</label>
<input id="email" name="email" required>
<label for="password">Password</label>
<input id="password" name="password" type="password" required>
<label for="verify_password">Verify Password</label>
<input id="verify_password" name="verify_password" type="password" required>
<label for="profile_picture_input" class="upload_button modal_button" id="profile_cropper_button">Upload a Profile Picture</label>
<input id="profile_picture_input" class="file_input" name="profile_picture" type="file" accept="image/png, image/jpeg, image/jpg">
<small class="file_input_feedback">No file selected.</small>
<label class="form_checkbox_container">Stay Logged In?
<input name="stay_logged_in" type="checkbox" value="true">
<span class="checkmark"></span>
</label>
<input class="button" type="submit" value="Sign Up">
</form>
<div id="modal" class="modal">
<div id="display_modal"></div>
</div>
<div style="display: none;">
<div id="profile_cropper" class="modal_form">' . profile_cropper() . '</div>
</div>
</div>
';
return $html;
}
public static function getLoginHTML()
{
$html = '
<div class="form_card">
<h3 class="underline">Log In</h3>
<form method="post" id="login_form" action="director.php">
<input type="hidden" name="form_id" value="login_form">
<input type="hidden" name="password_hash" value="" id="password_hash">
<label for="username">Username / Email</label>
<input id="username" name="username" spellcheck="false" required autofocus>
<label for="password">Password</label>
<input id="password" name="password" type="password" spellcheck="false" required>
<label class="form_checkbox_container">Stay Logged In?
<input name="stay_logged_in" type="checkbox" value="true">
<span class="checkmark"></span>
</label>
<input class="button" type="submit" value="Log In">
</form>
<script src="js/login.js" defer></script>
</div>
';
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 {}
+34
View File
@@ -0,0 +1,34 @@
<?php
require_once 'functions/init.php';
require_once 'functions/core.php';
include_once 'components/core/head.php';
echo home_content();
include_once 'components/core/foot.php';
function home_content()
{
$content = '
<div class="article">
<h1>Vintage Coding</h1>
<h3>Intentional Craftsmanship Over Ephemeral Trends</h3>
<div class="line"></div>
<p>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.</p>
<p>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.</p>
<p>The following are the cornerstones of Vintage Coding:</p>
<ol>
<li>AI is a partner for exploration and insight, with human oversight and critical evaluation remaining central.</li>
<li>Focus on core infrastructure built by humans, with AI used to identify omissions, ensuring a solid foundation.</li>
<li>AI tackles specific, well-defined issues, allowing for careful assessment of its output.</li>
<li>Emphasis on clean, concise, and readable code for long-term sustainability.</li>
</ol><br>
<p>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.</p>
<p>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.</p>
</div>
';
return $content;
}
+1 -33
View File
@@ -1,34 +1,2 @@
<?php
require_once 'functions/init.php';
require_once 'functions/core.php';
include_once 'components/core/head.php';
echo home_content();
include_once 'components/core/foot.php';
function home_content()
{
$content = '
<div class="article">
<h1>Vintage Coding</h1>
<h3>Intentional Craftsmanship Over Ephemeral Trends</h3>
<div class="line"></div>
<p>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.</p>
<p>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.</p>
<p>The following are the cornerstones of Vintage Coding:</p>
<ol>
<li>AI is a partner for exploration and insight, with human oversight and critical evaluation remaining central.</li>
<li>Focus on core infrastructure built by humans, with AI used to identify omissions, ensuring a solid foundation.</li>
<li>AI tackles specific, well-defined issues, allowing for careful assessment of its output.</li>
<li>Emphasis on clean, concise, and readable code for long-term sustainability.</li>
</ol><br>
<p>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.</p>
<p>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.</p>
</div>
';
return $content;
}
header('Location: spaces.php?view=list');
+118
View File
@@ -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";
}
});
+84 -63
View File
@@ -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 = `
<div class="form_card">
<h3 class="underline">Log In</h3>
<form method="post" id="login_form" action="director.php">
<input type="hidden" name="form_id" value="login_form">
<input type="hidden" name="password_hash" value="" id="password_hash">
<label for="username">Username / Email</label>
<input id="username" name="username" spellcheck="false" required autofocus>
<label for="password">Password</label>
<input id="password" name="password" type="password" spellcheck="false" required>
<label class="form_checkbox_container">Stay Logged In?
<input name="stay_logged_in" type="checkbox" value="true">
<span class="checkmark"></span>
</label>
<input class="button" type="submit" value="Log In">
</form>
<script src="js/login.js" defer></script>
</div>
`;
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"];
}
});
}
});
}
+122 -25
View File
@@ -1,47 +1,144 @@
<?php
require_once 'functions/init.php';
require_once 'functions/core.php';
require_once 'functions/space.php';
// UI Components
foreach (glob('components/core/*.php') as $file)
require_once $file;
foreach (glob('components/spaces/*.php') as $file)
require_once $file;
include_once 'components/core/head.php';
/* foreach (glob('components/spaces/*.php') as $file) */
/* require_once $file; */
/* if (!isset($_SESSION['role_id'])) { */
/* $redirect = 'todo.php?view=display'; */
/* header('Location: user.php?view=login&redirect=' . urlencode($redirect)); */
/* } */
require_once 'components/core/head.php';
// Display user's spaces
if (isset($_SESSION['user'])) {
if (isset($_GET['view'])) {
switch ($_GET['view']) {
case 'displaySpaces':
echo displaySpace();
case 'essay':
if (isset($_GET['id'])) {
// TODO: Need another check here to verify the user is in the Essay's space.
$essay = Essay::retrieveFromDB($_GET['id']);
$space = $essay->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 = '
<p>' . $space->getId() . '</p>
<div class="circleLayout">
<div id="newPost" class="actionButton" style="display: none;"><i class="nf nf-cod-new_file"></i></div>
<div id="newSpace" class="actionButton" style="display: none;"><i class="nf nf-cod-new_folder"></i></div>
<div id="filterSort" class="actionButton" style="display: none;"><i class="nf nf-oct-filter"></i></div>
<div id="actionButton" class="actionButton"><i class="nf nf-cod-symbol_property"></i></div>
</div>
';
return $html;
}
function displayEssay(Essay $e)
{
$html = '
<div class="std_width std_border padding center top_margin" style="height: 87%; overflow-y: auto;">
' . $e->getHTML() . '
</div>
';
return $html;
}
function displaySpaceList()
{
$user = unserialize($_SESSION['user']);
$html = '
<div class="std_width std_border padding center top_margin" style="height: 87%; overflow-y: auto;">
<div class="column space_between full_width">
<h1>Spaces</h4>
</div>
<div class="line"></div>
';
$spaces = $user->getSpaces();
foreach ($spaces as $s) {
$html .= $s->getCardHTML();
}
$html .= '
</div>
';
return $html;
}
function displaySpace($id = 2025)
{
$space = Space::retrieveFromDB($id);
if ($space) {
$html = '
<div class="std_width std_border padding center top_margin" style="max-height: 89vh; overflow-y: auto;">
<div class="column space_between full_width">
<h1>' . $space->getName() . '</h4>
<p>' . $space->getDescription() . '</p>
</div>
<div class="line"></div>
';
// TODO: Need to investigate this bug in Posts
$posts = $space->getPosts();
foreach ($posts as $post) {
foreach ($post as $p) {
$html .= $p->getCardHTML();
}
}
$html .= '
</div>
';
return $html;
}
}
-113
View File
@@ -1,113 +0,0 @@
<?php
require_once 'functions/init.php';
require_once 'functions/core.php';
include_once 'functions/user.php';
// UI Components
foreach (glob('components/user/*.php') as $file)
require_once $file;
$msg = '';
if (isset($_POST['form_id'])) {
switch ($_POST['form_id']) {
case 'signup_form':
$msg = create_user(
$_POST['username'],
$_POST['email'],
$_POST['password'],
$_POST['verify_password'],
4, // TODO: Need to allow for other roles to be created, if the submission is from an administrator or owner.
(!empty($_FILES) ? $_FILES : ''),
(!empty($_POST['crop_x']) ? $_POST['crop_x'] : ''),
(!empty($_POST['crop_y']) ? $_POST['crop_y'] : ''),
(!empty($_POST['crop_width']) ? $_POST['crop_width'] : ''),
$_POST['required_field']
);
if ($msg === true) {
login($_POST['username'], $_POST['password'], false);
header('Location: user.php?view=display&user_id=' . $_SESSION['user_id']);
}
break;
case 'login_form':
$msg = login(
$_POST['username'],
$_POST['password'],
(isset($_POST['stay_logged_in']) ? true : false)
);
if ($msg === true)
if (isset($_GET['redirect']))
header('Location: ' . urldecode($_GET['redirect']));
else
header('Location: .');
break;
case 'reset_pw_form':
$msg = reset_password(
($_SESSION['role_id'] <= developer && $_SESSION['user_id'] != $_POST['user_id'] ? $_POST['user_id'] : $_SESSION['user_id']),
$_POST['new_pw'],
$_POST['verify_new_pw']
);
break;
case 'update_email_form':
$msg = update_email(
($_SESSION['role_id'] <= developer && $_SESSION['user_id'] != $_POST['user_id'] ? $_POST['user_id'] : $_SESSION['user_id']),
$_POST['new_email'],
$_POST['verify_new_email']
);
break;
case 'update_username_form':
$msg = update_username(
($_SESSION['role_id'] <= developer && $_SESSION['user_id'] != $_POST['user_id'] ? $_POST['user_id'] : $_SESSION['user_id']),
$_POST['new_username'],
$_POST['verify_new_username']
);
break;
case 'update_profile_picture_form':
update_profile_picture($_SESSION['user_id']);
break;
case 'request_role_change_form':
request_role_change(
$_SESSION['user_id'],
$_POST['requested_role']
);
break;
case 'delete_account_form':
$msg = delete_account(
($_SESSION['role_id'] <= developer && $_SESSION['user_id'] != $_POST['user_id'] ? $_POST['user_id'] : $_SESSION['user_id']),
$_POST['delete_password'],
$_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;
}
include_once 'components/core/head.php';
switch ($_GET['view']) {
case 'login':
echo login_form(($msg ? $msg : ''));
break;
case 'signup':
echo signup_form(($msg ? $msg : ''));
break;
case 'logout':
logout();
break;
case 'display':
echo (isset($_GET['user_id']) ? user_view($_GET['user_id']) : header('Location: .'));
break;
}
include_once 'components/core/foot.php';