Compare commits

..

2 Commits

Author SHA1 Message Date
f54ed2f8fb added frontend search functionality with a small menubar
All checks were successful
🚀 Deploy website on push / 🎉 Deploy (push) Successful in 23s
Signed-off-by: rodude123 <rodude123@gmail.com>
2023-11-05 17:47:44 +00:00
b4ab7900db added backend search functionality
Signed-off-by: rodude123 <rodude123@gmail.com>
2023-10-31 19:36:51 +00:00
23 changed files with 1488 additions and 104 deletions

View File

@ -23,7 +23,7 @@ class blogData
public function getBlogPosts(): array public function getBlogPosts(): array
{ {
$conn = dbConn(); $conn = dbConn();
$stmt = $conn->prepare("SELECT * FROM blog ORDER BY dateCreated DESC;"); $stmt = $conn->prepare("SELECT * FROM blog ORDER BY featured DESC, dateCreated DESC;");
$stmt->execute(); $stmt->execute();
// set the resulting array to associative // set the resulting array to associative
@ -102,28 +102,9 @@ class blogData
} }
/** /**
* Get the blog posts with the given category * Get all unique categories
* @param string $category - Category of the blog post * @return string[] - Array of all categories or error message
* @return array - Array of the blog posts with the given category or error message
*/ */
public function getBlogPostsWithCategory(string $category): array
{
$conn = dbConn();
$stmt = $conn->prepare("SELECT * FROM blog WHERE categories LIKE :category;");
$stmt->bindParam(":category", $category);
$stmt->execute();
// set the resulting array to associative
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($result)
{
return $result;
}
return array("errorMessage" => "Error, blog post could not found");
}
public function getCategories(): array public function getCategories(): array
{ {
$conn = dbConn(); $conn = dbConn();
@ -185,11 +166,12 @@ class blogData
* @param bool $featured - Whether the blog post is featured or not * @param bool $featured - Whether the blog post is featured or not
* @param string $abstract - Abstract of the blog post i.e. a short description * @param string $abstract - Abstract of the blog post i.e. a short description
* @param string $body - Body of the blog post * @param string $body - Body of the blog post
* @param string $bodyText - Body of the blog post as plain text
* @param string $dateModified - Date the blog post was modified * @param string $dateModified - Date the blog post was modified
* @param string $categories - Categories of the blog post * @param string $categories - Categories of the blog post
* @return bool|string - Success or error message * @return bool|string - Success or error message
*/ */
public function updatePost(int $ID, string $title, bool $featured, string $abstract, string $body, string $dateModified, string $categories): bool|string public function updatePost(int $ID, string $title, bool $featured, string $abstract, string $body, string $bodyText, string $dateModified, string $categories): bool|string
{ {
$conn = dbConn(); $conn = dbConn();
@ -227,12 +209,13 @@ class blogData
$from = "../blog/imgs/tmp/"; $from = "../blog/imgs/tmp/";
$newBody = $this->changeHTMLSrc($body, $to, $from); $newBody = $this->changeHTMLSrc($body, $to, $from);
$stmt = $conn->prepare("UPDATE blog SET title = :title, featured = :featured, abstract = :abstract, body = :body, dateModified = :dateModified, categories = :categories WHERE ID = :ID;"); $stmt = $conn->prepare("UPDATE blog SET title = :title, featured = :featured, abstract = :abstract, body = :body, bodyText = :bodyText, dateModified = :dateModified, categories = :categories WHERE ID = :ID;");
$stmt->bindParam(":ID", $ID); $stmt->bindParam(":ID", $ID);
$stmt->bindParam(":title", $title); $stmt->bindParam(":title", $title);
$stmt->bindParam(":featured", $featured); $stmt->bindParam(":featured", $featured);
$stmt->bindParam(":abstract", $abstract); $stmt->bindParam(":abstract", $abstract);
$stmt->bindParam(":body", $newBody); $stmt->bindParam(":body", $newBody);
$stmt->bindParam(":bodyText", $bodyText);
$stmt->bindParam(":dateModified", $dateModified); $stmt->bindParam(":dateModified", $dateModified);
$stmt->bindParam(":categories", $categories); $stmt->bindParam(":categories", $categories);
@ -246,13 +229,14 @@ class blogData
* @param string $title - Title of the blog post * @param string $title - Title of the blog post
* @param string $abstract - Abstract of the blog post i.e. a short description * @param string $abstract - Abstract of the blog post i.e. a short description
* @param string $body - Body of the blog post * @param string $body - Body of the blog post
* @param string $bodyText - Body of the blog post as plain text
* @param string $dateCreated - Date the blog post was created * @param string $dateCreated - Date the blog post was created
* @param bool $featured - Whether the blog post is featured or not * @param bool $featured - Whether the blog post is featured or not
* @param string $categories - Categories of the blog post * @param string $categories - Categories of the blog post
* @param UploadedFileInterface $headerImg - Header image of the blog post * @param UploadedFileInterface $headerImg - Header image of the blog post
* @return int|string - ID of the blog post or error message * @return int|string - ID of the blog post or error message
*/ */
public function createPost(string $title, string $abstract, string $body, string $dateCreated, bool $featured, string $categories, UploadedFileInterface $headerImg): int|string public function createPost(string $title, string $abstract, string $body, string $bodyText, string $dateCreated, bool $featured, string $categories, UploadedFileInterface $headerImg): int|string
{ {
$conn = dbConn(); $conn = dbConn();
$folderID = uniqid(); $folderID = uniqid();
@ -282,8 +266,8 @@ class blogData
$stmtMainProject->execute(); $stmtMainProject->execute();
} }
$stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, abstract, body, categories, folderID) $stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, abstract, body, bodyText, categories, folderID)
VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :abstract, :body, :categories, :folderID);"); VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :abstract, :body, :bodyText, :categories, :folderID);");
$stmt->bindParam(":title", $title); $stmt->bindParam(":title", $title);
$stmt->bindParam(":dateCreated", $dateCreated); $stmt->bindParam(":dateCreated", $dateCreated);
$stmt->bindParam(":dateModified", $dateCreated); $stmt->bindParam(":dateModified", $dateCreated);
@ -292,6 +276,7 @@ class blogData
$stmt->bindParam(":headerImg", $targetFile["imgLocation"]); $stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
$stmt->bindParam(":abstract", $abstract); $stmt->bindParam(":abstract", $abstract);
$stmt->bindParam(":body", $newBody); $stmt->bindParam(":body", $newBody);
$stmt->bindParam(":bodyText", $bodyText);
$stmt->bindParam(":categories", $categories); $stmt->bindParam(":categories", $categories);
$stmt->bindParam(":folderID", $folderID); $stmt->bindParam(":folderID", $folderID);
@ -443,4 +428,103 @@ class blogData
$stmt->execute(); $stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(PDO::FETCH_ASSOC);
} }
/**
* Search for a blog post with the given search term
* @param string $searchTerm - Search term
* @return array - Array of all posts with the given search term or error message
*/
public function searchBlog(string $searchTerm): array
{
$conn = dbConn();
$stmt = $conn->prepare("SELECT * FROM blog WHERE MATCH(title, bodyText) AGAINST(:searchTerm IN NATURAL LANGUAGE MODE);");
$stmt->bindParam(":searchTerm", $searchTerm);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($result)
{
for ($i = 0; $i < count($result); $i++)
{
$result[$i]["abstract"] = $this->getShortPost($searchTerm, stripcslashes($result[$i]["bodyText"]));
}
return $result;
}
return array("errorMessage" => "Error, could not find posts");
}
/**
* Get the short post with the search term
* @param string $searchTerm - Search term
* @param $text - Body of the post as plain text
* @return string - Short post with the search term
*/
private function getShortPost(string $searchTerm, $text): string
{
$pattern = '/([,:;!?.-]+)/u';
$parts = preg_split($pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$cleanedParts = [];
foreach ($parts as $part)
{
$part = trim($part); // Remove leading/trailing spaces and newline characters
if (!empty($part))
{
$cleanedParts[] = $part;
}
}
$combinedParts = [];
$currentPart = '';
foreach ($cleanedParts as $part)
{
if (preg_match('/[,:;!?.-]/u', $part))
{
$currentPart .= $part;
}
else
{
if (!empty($currentPart))
{
$combinedParts[] = trim($currentPart);
}
$currentPart = rtrim($part);
}
}
if (!empty($currentPart))
{
$combinedParts[] = trim($currentPart);
}
$result = "";
for ($i = 0; $i < count($combinedParts); $i++)
{
$part = $combinedParts[$i];
if (stripos($part, $searchTerm) !== false)
{
$before = ($i > 0) ? $combinedParts[$i - 1] : "";
$after = ($i < count($combinedParts) - 1) ? $combinedParts[$i + 1] : "";
if ($before === "" && $i > 0)
{
$before = $combinedParts[$i - 1];
}
$result = $before . " " . $part . " " . $after;
// If the search term is found, we don't need to continue checking subsequent parts
break;
}
}
return $result;
}
} }

View File

@ -123,12 +123,34 @@ class blogRoutes implements routesInterface
return $response->withStatus(400); return $response->withStatus(400);
}); });
$app->get("/blog/search/{searchTerm}", function (Request $request, $response, $args)
{
if ($args["searchTerm"] != null)
{
$posts = $this->blogData->searchBlog($args["searchTerm"]);
$json = json_encode($posts);
$response->getBody()->write($json);
if (array_key_exists("errorMessage", $posts))
{
$response->withStatus(404);
}
return $response;
}
$response->getBody()->write(json_encode(array("error" => "Please provide a search term")));
return $response->withStatus(400);
});
$app->patch("/blog/post/{id}", function (Request $request, Response $response, $args) $app->patch("/blog/post/{id}", function (Request $request, Response $response, $args)
{ {
$data = $request->getParsedBody(); $data = $request->getParsedBody();
if ($args["id"] != null) if ($args["id"] != null)
{ {
if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["dateModified"]) || empty($data["categories"])) if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["bodyText"]) || empty($data["dateModified"]) || empty($data["categories"]))
{ {
// uh oh sent some empty data // uh oh sent some empty data
$response->getBody()->write(json_encode(array("error" => "Only some of the data was sent"))); $response->getBody()->write(json_encode(array("error" => "Only some of the data was sent")));
@ -142,7 +164,7 @@ class blogRoutes implements routesInterface
return $response->withStatus(400); return $response->withStatus(400);
} }
$message = $this->blogData->updatePost($args["id"], $data["title"], intval($data["featured"]), $data["abstract"], $data["body"], $data["dateModified"], $data["categories"]); $message = $this->blogData->updatePost($args["id"], $data["title"], intval($data["featured"]), $data["abstract"], $data["body"], $data["bodyText"], $data["dateModified"], $data["categories"]);
if ($message === "post not found") if ($message === "post not found")
{ {
@ -232,7 +254,7 @@ class blogRoutes implements routesInterface
} }
$featured = $data["featured"] === "true"; $featured = $data["featured"] === "true";
$insertedID = $this->blogData->createPost($data["title"], $data["abstract"], $data["body"], $data["dateCreated"], $featured, $data["categories"], $headerImg); $insertedID = $this->blogData->createPost($data["title"], $data["abstract"], $data["body"], $data["bodyText"], $data["dateCreated"], $featured, $data["categories"], $headerImg);
if (!is_int($insertedID)) if (!is_int($insertedID))
{ {
// uh oh something went wrong // uh oh something went wrong

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Rohit Pai - Blog</title><meta name="title" content="Rohit Pai - Blog"><meta name="description" content="This is all the blog posts that Rohit Pai has posted. You'll find posts on various topics, mostly on tech but some on various other random topics."><meta name="keywords" content="Blog, all posts, rohit, pai, rohit pai, tech, web development, self-hosting, hosting"><meta name="robots" content="index, follow"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta name="language" content="English"><meta name="author" content="Rohit Pai"><link rel="stylesheet" href="/blog/css/main.css"><script src="https://kit.fontawesome.com/ed3c25598e.js" crossorigin="anonymous"></script></head><body><nav><input type="checkbox" id="nav-check"><h1><a href="/" class="link">rohit pai</a></h1><div class="nav-btn"><label for="nav-check"><span></span> <span></span> <span></span></label></div><ul><li><a href="/#about" class="textShadow link">about</a></li><li><a href="/#curriculumVitae" class="textShadow link">cv</a></li><li><a href="/#projects" class="textShadow link">projects</a></li><li><a href="/#contact" class="textShadow link">contact</a></li><li><a href="/blog" class="textShadow link active">blog</a></li></ul></nav><header><div><h1>full stack developer</h1><a href="/#sayHello" class="btn btnPrimary boxShadowIn boxShadowOut">Contact Me</a> <a href="" id="arrow"><i class="fa-solid fa-chevron-down"></i></a></div></header><main id="main"></main><footer class="flexRow"><div class="spacer"></div><p>&copy; <span id="year"></span> Rohit Pai all rights reserved</p><div class="button"><button id="goBackToTop"><i class="fa-solid fa-chevron-up"></i></button></div></footer><script src="/js/typewriter.js"></script><script src="/blog/js/index.js"></script><script id="dsq-count-scr" src="https://rohitpaiportfolio.disqus.com/count.js" async></script></body></html> <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Rohit Pai - Blog</title><meta name="title" content="Rohit Pai - Blog"><meta name="description" content="This is all the blog posts that Rohit Pai has posted. You'll find posts on various topics, mostly on tech but some on various other random topics."><meta name="keywords" content="Blog, all posts, rohit, pai, rohit pai, tech, web development, self-hosting, hosting"><meta name="robots" content="index, follow"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta name="language" content="English"><meta name="author" content="Rohit Pai"><link rel="stylesheet" href="/blog/css/main.css"><script src="https://kit.fontawesome.com/ed3c25598e.js" crossorigin="anonymous"></script></head><body><nav><input type="checkbox" id="nav-check"><h1><a href="/" class="link">rohit pai</a></h1><div class="nav-btn"><label for="nav-check"><span></span> <span></span> <span></span></label></div><ul><li><a href="/#about" class="textShadow link">about</a></li><li><a href="/#curriculumVitae" class="textShadow link">cv</a></li><li><a href="/#projects" class="textShadow link">projects</a></li><li><a href="/#contact" class="textShadow link">contact</a></li><li><a href="/blog" class="textShadow link active">blog</a></li></ul></nav><header><div><h1>full stack developer</h1><a href="/#sayHello" class="btn btnPrimary boxShadowIn boxShadowOut">Contact Me</a> <a href="" id="arrow"><i class="fa-solid fa-chevron-down"></i></a></div></header><div class="menuBar"><div class="menu"><ul><li><a href="/blog" class="link active">All posts</a></li><li><a href="/blog/category" class="link">categories</a></li><li><label for="searchField" aria-hidden="true" hidden>search</label> <input type="search" name="search" id="searchField" placeholder="Search..."> <button type="submit" id="searchBtn" class="btn btnPrimary"><i class="fa fa-search"></i></button></li></ul></div></div><main id="main"></main><footer class="flexRow"><div class="spacer"></div><p>&copy; <span id="year"></span> Rohit Pai all rights reserved</p><div class="button"><button id="goBackToTop"><i class="fa-solid fa-chevron-up"></i></button></div></footer><script src="/js/typewriter.js"></script><script src="/blog/js/index.js"></script><script id="dsq-count-scr" src="https://rohitpaiportfolio.disqus.com/count.js" async></script></body></html>

File diff suppressed because one or more lines are too long

2
dist/css/main.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/index.html vendored

File diff suppressed because one or more lines are too long

1006
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
"author": "Rohit Pai", "author": "Rohit Pai",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@ckeditor/ckeditor5-clipboard": "^40.0.0",
"browser-sync": "^2.27.5", "browser-sync": "^2.27.5",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-clean-css": "^4.3.0", "gulp-clean-css": "^4.3.0",
@ -25,5 +26,11 @@
"require": "^0.4.4", "require": "^0.4.4",
"source-map-generator": "^0.8.0", "source-map-generator": "^0.8.0",
"vinyl-ftp": "^0.6.1" "vinyl-ftp": "^0.6.1"
},
"devDependencies": {
"terser-webpack-plugin": "^5.3.9",
"vinyl-named-with-path": "^1.0.0",
"webpack-cli": "^5.1.4",
"webpack-stream": "^7.0.0"
} }
} }

View File

@ -23,7 +23,7 @@ class blogData
public function getBlogPosts(): array public function getBlogPosts(): array
{ {
$conn = dbConn(); $conn = dbConn();
$stmt = $conn->prepare("SELECT * FROM blog ORDER BY dateCreated DESC;"); $stmt = $conn->prepare("SELECT * FROM blog ORDER BY featured DESC, dateCreated DESC;");
$stmt->execute(); $stmt->execute();
// set the resulting array to associative // set the resulting array to associative
@ -102,28 +102,9 @@ class blogData
} }
/** /**
* Get the blog posts with the given category * Get all unique categories
* @param string $category - Category of the blog post * @return string[] - Array of all categories or error message
* @return array - Array of the blog posts with the given category or error message
*/ */
public function getBlogPostsWithCategory(string $category): array
{
$conn = dbConn();
$stmt = $conn->prepare("SELECT * FROM blog WHERE categories LIKE :category;");
$stmt->bindParam(":category", $category);
$stmt->execute();
// set the resulting array to associative
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($result)
{
return $result;
}
return array("errorMessage" => "Error, blog post could not found");
}
public function getCategories(): array public function getCategories(): array
{ {
$conn = dbConn(); $conn = dbConn();
@ -185,11 +166,12 @@ class blogData
* @param bool $featured - Whether the blog post is featured or not * @param bool $featured - Whether the blog post is featured or not
* @param string $abstract - Abstract of the blog post i.e. a short description * @param string $abstract - Abstract of the blog post i.e. a short description
* @param string $body - Body of the blog post * @param string $body - Body of the blog post
* @param string $bodyText - Body of the blog post as plain text
* @param string $dateModified - Date the blog post was modified * @param string $dateModified - Date the blog post was modified
* @param string $categories - Categories of the blog post * @param string $categories - Categories of the blog post
* @return bool|string - Success or error message * @return bool|string - Success or error message
*/ */
public function updatePost(int $ID, string $title, bool $featured, string $abstract, string $body, string $dateModified, string $categories): bool|string public function updatePost(int $ID, string $title, bool $featured, string $abstract, string $body, string $bodyText, string $dateModified, string $categories): bool|string
{ {
$conn = dbConn(); $conn = dbConn();
@ -227,12 +209,13 @@ class blogData
$from = "../blog/imgs/tmp/"; $from = "../blog/imgs/tmp/";
$newBody = $this->changeHTMLSrc($body, $to, $from); $newBody = $this->changeHTMLSrc($body, $to, $from);
$stmt = $conn->prepare("UPDATE blog SET title = :title, featured = :featured, abstract = :abstract, body = :body, dateModified = :dateModified, categories = :categories WHERE ID = :ID;"); $stmt = $conn->prepare("UPDATE blog SET title = :title, featured = :featured, abstract = :abstract, body = :body, bodyText = :bodyText, dateModified = :dateModified, categories = :categories WHERE ID = :ID;");
$stmt->bindParam(":ID", $ID); $stmt->bindParam(":ID", $ID);
$stmt->bindParam(":title", $title); $stmt->bindParam(":title", $title);
$stmt->bindParam(":featured", $featured); $stmt->bindParam(":featured", $featured);
$stmt->bindParam(":abstract", $abstract); $stmt->bindParam(":abstract", $abstract);
$stmt->bindParam(":body", $newBody); $stmt->bindParam(":body", $newBody);
$stmt->bindParam(":bodyText", $bodyText);
$stmt->bindParam(":dateModified", $dateModified); $stmt->bindParam(":dateModified", $dateModified);
$stmt->bindParam(":categories", $categories); $stmt->bindParam(":categories", $categories);
@ -246,13 +229,14 @@ class blogData
* @param string $title - Title of the blog post * @param string $title - Title of the blog post
* @param string $abstract - Abstract of the blog post i.e. a short description * @param string $abstract - Abstract of the blog post i.e. a short description
* @param string $body - Body of the blog post * @param string $body - Body of the blog post
* @param string $bodyText - Body of the blog post as plain text
* @param string $dateCreated - Date the blog post was created * @param string $dateCreated - Date the blog post was created
* @param bool $featured - Whether the blog post is featured or not * @param bool $featured - Whether the blog post is featured or not
* @param string $categories - Categories of the blog post * @param string $categories - Categories of the blog post
* @param UploadedFileInterface $headerImg - Header image of the blog post * @param UploadedFileInterface $headerImg - Header image of the blog post
* @return int|string - ID of the blog post or error message * @return int|string - ID of the blog post or error message
*/ */
public function createPost(string $title, string $abstract, string $body, string $dateCreated, bool $featured, string $categories, UploadedFileInterface $headerImg): int|string public function createPost(string $title, string $abstract, string $body, string $bodyText, string $dateCreated, bool $featured, string $categories, UploadedFileInterface $headerImg): int|string
{ {
$conn = dbConn(); $conn = dbConn();
$folderID = uniqid(); $folderID = uniqid();
@ -282,8 +266,8 @@ class blogData
$stmtMainProject->execute(); $stmtMainProject->execute();
} }
$stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, abstract, body, categories, folderID) $stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, abstract, body, bodyText, categories, folderID)
VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :abstract, :body, :categories, :folderID);"); VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :abstract, :body, :bodyText, :categories, :folderID);");
$stmt->bindParam(":title", $title); $stmt->bindParam(":title", $title);
$stmt->bindParam(":dateCreated", $dateCreated); $stmt->bindParam(":dateCreated", $dateCreated);
$stmt->bindParam(":dateModified", $dateCreated); $stmt->bindParam(":dateModified", $dateCreated);
@ -292,6 +276,7 @@ class blogData
$stmt->bindParam(":headerImg", $targetFile["imgLocation"]); $stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
$stmt->bindParam(":abstract", $abstract); $stmt->bindParam(":abstract", $abstract);
$stmt->bindParam(":body", $newBody); $stmt->bindParam(":body", $newBody);
$stmt->bindParam(":bodyText", $bodyText);
$stmt->bindParam(":categories", $categories); $stmt->bindParam(":categories", $categories);
$stmt->bindParam(":folderID", $folderID); $stmt->bindParam(":folderID", $folderID);
@ -443,4 +428,103 @@ class blogData
$stmt->execute(); $stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC); return $stmt->fetchAll(PDO::FETCH_ASSOC);
} }
/**
* Search for a blog post with the given search term
* @param string $searchTerm - Search term
* @return array - Array of all posts with the given search term or error message
*/
public function searchBlog(string $searchTerm): array
{
$conn = dbConn();
$stmt = $conn->prepare("SELECT * FROM blog WHERE MATCH(title, bodyText) AGAINST(:searchTerm IN NATURAL LANGUAGE MODE);");
$stmt->bindParam(":searchTerm", $searchTerm);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($result)
{
for ($i = 0; $i < count($result); $i++)
{
$result[$i]["abstract"] = $this->getShortPost($searchTerm, stripcslashes($result[$i]["bodyText"]));
}
return $result;
}
return array("errorMessage" => "Error, could not find posts");
}
/**
* Get the short post with the search term
* @param string $searchTerm - Search term
* @param $text - Body of the post as plain text
* @return string - Short post with the search term
*/
private function getShortPost(string $searchTerm, $text): string
{
$pattern = '/([,:;!?.-]+)/u';
$parts = preg_split($pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
$cleanedParts = [];
foreach ($parts as $part)
{
$part = trim($part); // Remove leading/trailing spaces and newline characters
if (!empty($part))
{
$cleanedParts[] = $part;
}
}
$combinedParts = [];
$currentPart = '';
foreach ($cleanedParts as $part)
{
if (preg_match('/[,:;!?.-]/u', $part))
{
$currentPart .= $part;
}
else
{
if (!empty($currentPart))
{
$combinedParts[] = trim($currentPart);
}
$currentPart = rtrim($part);
}
}
if (!empty($currentPart))
{
$combinedParts[] = trim($currentPart);
}
$result = "";
for ($i = 0; $i < count($combinedParts); $i++)
{
$part = $combinedParts[$i];
if (stripos($part, $searchTerm) !== false)
{
$before = ($i > 0) ? $combinedParts[$i - 1] : "";
$after = ($i < count($combinedParts) - 1) ? $combinedParts[$i + 1] : "";
if ($before === "" && $i > 0)
{
$before = $combinedParts[$i - 1];
}
$result = $before . " " . $part . " " . $after;
// If the search term is found, we don't need to continue checking subsequent parts
break;
}
}
return $result;
}
} }

View File

@ -123,12 +123,34 @@ class blogRoutes implements routesInterface
return $response->withStatus(400); return $response->withStatus(400);
}); });
$app->get("/blog/search/{searchTerm}", function (Request $request, $response, $args)
{
if ($args["searchTerm"] != null)
{
$posts = $this->blogData->searchBlog($args["searchTerm"]);
$json = json_encode($posts);
$response->getBody()->write($json);
if (array_key_exists("errorMessage", $posts))
{
$response->withStatus(404);
}
return $response;
}
$response->getBody()->write(json_encode(array("error" => "Please provide a search term")));
return $response->withStatus(400);
});
$app->patch("/blog/post/{id}", function (Request $request, Response $response, $args) $app->patch("/blog/post/{id}", function (Request $request, Response $response, $args)
{ {
$data = $request->getParsedBody(); $data = $request->getParsedBody();
if ($args["id"] != null) if ($args["id"] != null)
{ {
if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["dateModified"]) || empty($data["categories"])) if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["bodyText"]) || empty($data["dateModified"]) || empty($data["categories"]))
{ {
// uh oh sent some empty data // uh oh sent some empty data
$response->getBody()->write(json_encode(array("error" => "Only some of the data was sent"))); $response->getBody()->write(json_encode(array("error" => "Only some of the data was sent")));
@ -142,7 +164,7 @@ class blogRoutes implements routesInterface
return $response->withStatus(400); return $response->withStatus(400);
} }
$message = $this->blogData->updatePost($args["id"], $data["title"], intval($data["featured"]), $data["abstract"], $data["body"], $data["dateModified"], $data["categories"]); $message = $this->blogData->updatePost($args["id"], $data["title"], intval($data["featured"]), $data["abstract"], $data["body"], $data["bodyText"], $data["dateModified"], $data["categories"]);
if ($message === "post not found") if ($message === "post not found")
{ {
@ -232,7 +254,7 @@ class blogRoutes implements routesInterface
} }
$featured = $data["featured"] === "true"; $featured = $data["featured"] === "true";
$insertedID = $this->blogData->createPost($data["title"], $data["abstract"], $data["body"], $data["dateCreated"], $featured, $data["categories"], $headerImg); $insertedID = $this->blogData->createPost($data["title"], $data["abstract"], $data["body"], $data["bodyText"], $data["dateCreated"], $featured, $data["categories"], $headerImg);
if (!is_int($insertedID)) if (!is_int($insertedID))
{ {
// uh oh something went wrong // uh oh something went wrong

View File

@ -26,8 +26,47 @@ h3 {
line-height: 2.1875rem; line-height: 2.1875rem;
} }
div.menu {
width: 100%;
border-bottom: 5px solid var(--mutedGrey);
}
div.menu input:not([type="submit"]) {
width: auto;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
div.menu > ul {
list-style: none;
display: flex;
flex-direction: row;
justify-content: space-around;
}
div.menu ul li {
display: flex;
flex-direction: row;
}
div.menu ul li button.btn {
padding: initial;
border-radius: 0 0.5em 0.5em 0;
}
div.menu ul li input:not([type="submit"]):focus + button.btn,
div.menu ul li:hover button.btn,
div.menu ul li:focus button.btn {
background: var(--primaryHover);
border: 0.3215em solid var(--primaryHover);
}
div.menu ul li:hover input:not([type="submit"]),
div.menu ul li:focus input:not([type="submit"]) {
border: 0.3215em solid var(--primaryHover);
}
section.largePost { section.largePost {
/*margin: 0 5em;*/
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-evenly; justify-content: space-evenly;

View File

@ -49,6 +49,22 @@
</div> </div>
</header> </header>
<div class="menuBar">
<div class="menu">
<ul>
<li><a href="/blog" class="link active">All posts</a></li>
<li><a href="/blog/category" class="link">categories</a></li>
<li>
<label for="searchField" aria-hidden="true" hidden>search</label>
<input type="search" name="search" id="searchField"
placeholder="Search...">
<button type="submit" id="searchBtn" class="btn btnPrimary"><i
class="fa fa-search"></i></button>
</li>
</ul>
</div>
</div>
<main id="main"> <main id="main">
</main> </main>

View File

@ -44,7 +44,6 @@ function goToURL(url)
if (urlArray[2] === 'post') if (urlArray[2] === 'post')
{ {
// Create a new URL with the dynamic part // Create a new URL with the dynamic part
// window.history.pushState(null, null, url);
loadIndividualPost(urlArray[urlArray.length - 1]).catch(err => console.log(err)); loadIndividualPost(urlArray[urlArray.length - 1]).catch(err => console.log(err));
return; return;
} }
@ -52,14 +51,20 @@ function goToURL(url)
if (urlArray[2] === 'category') if (urlArray[2] === 'category')
{ {
// Create a new URL with the dynamic part // Create a new URL with the dynamic part
// window.history.pushState(null, null, url);
if (urlArray[3]) if (urlArray[3])
{ {
loadPostsByCategory(urlArray[urlArray.length - 1]); loadPostsByCategory(urlArray[urlArray.length - 1]);
return; return;
} }
loadAllCategories(); loadAllCategories().catch(err => console.log(err));
return;
}
if (urlArray[2] === 'search' && urlArray[3])
{
// Create a new URL with the dynamic part
loadSearchResults(urlArray[urlArray.length - 1]);
return; return;
} }
@ -67,6 +72,26 @@ function goToURL(url)
} }
document.querySelector('#searchBtn').addEventListener('click', _ =>
{
let searchTerm = document.querySelector('#searchField').value;
if (searchTerm.length > 0)
{
window.history.pushState(null, null, `/blog/search/${searchTerm}`);
document.querySelector('#searchField').value = '';
document.querySelector('#main').innerHTML = '';
goToURL(`/blog/search/${searchTerm}`);
}
});
document.querySelector('#searchField').addEventListener('keyup', e =>
{
if (e.key === 'Enter')
{
document.querySelector('#searchBtn').click();
}
});
/** /**
* Creates a large post element * Creates a large post element
* @param post the post object * @param post the post object
@ -130,10 +155,10 @@ function loadHomeContent()
featuredPost.appendChild(h1); featuredPost.appendChild(h1);
let outerContent = createLargePost(json[i]); let outerContent = createLargePost(json[i]);
featuredPost.appendChild(outerContent); featuredPost.appendChild(outerContent);
document.querySelector('#main').prepend(featuredPost); document.querySelector('#main').appendChild(featuredPost);
} }
if (i === 0) if (i === 1)
{ {
let latestPost = document.createElement('section'); let latestPost = document.createElement('section');
latestPost.classList.add('largePost'); latestPost.classList.add('largePost');
@ -143,8 +168,9 @@ function loadHomeContent()
latestPost.appendChild(h1); latestPost.appendChild(h1);
let outerContent = createLargePost(json[i]); let outerContent = createLargePost(json[i]);
latestPost.appendChild(outerContent); latestPost.appendChild(outerContent);
document.querySelector('#main').prepend(latestPost); document.querySelector('#main').appendChild(latestPost);
} }
} }
})); }));
} }
@ -438,6 +464,34 @@ function loadPostsByCategory(category)
})); }));
} }
function loadSearchResults(searchTerm)
{
document.title = 'Rohit Pai - Search Results for ' + decodeURI(searchTerm);
fetch(`/api/blog/search/${searchTerm}`).then(res => res.json().then(json =>
{
let main = document.querySelector('#main');
let posts = document.createElement('section');
posts.classList.add('catPosts');
posts.id = 'searchResults';
let h1 = document.createElement('h1');
h1.innerHTML = 'Search Results';
main.appendChild(h1);
for (let i = 0; i < json.length; i++)
{
let largePost = document.createElement('section');
largePost.classList.add('largePost');
if (i < json.length - 1)
{
largePost.classList.add('categoryPost');
}
let outerContent = createLargePost(json[i]);
largePost.appendChild(outerContent);
posts.appendChild(largePost);
}
main.appendChild(posts);
}));
}
/** /**
* Shows the 404 page * Shows the 404 page
*/ */

View File

@ -64,7 +64,6 @@ nav ul li span {
visibility: hidden; visibility: hidden;
} }
nav ul li .active::before, nav ul li .active::before,
nav ul li .active::after { nav ul li .active::after {
visibility: visible; visibility: visible;

View File

@ -108,8 +108,9 @@ a.btnPrimary[disabled]:hover, button.btnPrimary[disabled]:hover {
border: 0.3215em solid var(--notAvailableHover); border: 0.3215em solid var(--notAvailableHover);
} }
a.btnPrimary:hover, button.btnPrimary:hover form input[type="submit"]:hover { a.btnPrimary:hover, button.btnPrimary:hover, form input[type="submit"]:hover {
background: var(--primaryHover); background: var(--primaryHover);
border: 0.3215em solid var(--primaryHover);
} }
a.btn:active, button.btn:active, form input[type="submit"]:active { a.btn:active, button.btn:active, form input[type="submit"]:active {
@ -129,18 +130,14 @@ a.btn:active, button.btn:active, form input[type="submit"]:active {
} }
form .formControl input:not([type="submit"]).invalid:invalid, form .formControl textarea.invalid:invalid { form .formControl input:not([type="submit"]).invalid:invalid, form .formControl textarea.invalid:invalid {
border: 4px solid var(--errorDefault); border: 0.3125em solid var(--errorDefault);
} }
form .formControl input:not([type="submit"]).invalid:invalid:focus, form .formControl textarea.invalid:invalid:focus { form .formControl input:not([type="submit"]).invalid:invalid:focus, form .formControl textarea.invalid:invalid:focus {
border: 4px solid var(--errorHover); border: 0.3125em solid var(--errorHover);
box-shadow: 0 4px 2px 0 var(--mutedBlack); box-shadow: 0 4px 2px 0 var(--mutedBlack);
} }
form .formControl input:not([type="submit"]):focus, form .formControl textarea:focus {
border: 4px solid var(--primaryHover);
}
form .formControl input:not([type="submit"]) { form .formControl input:not([type="submit"]) {
height: 3em; height: 3em;
} }
@ -150,7 +147,6 @@ form .formControl {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
/*align-items: flex-start;*/
} }
form .formControl.passwordControl { form .formControl.passwordControl {
@ -163,13 +159,13 @@ form input[type="submit"] {
form .formControl input:not([type="submit"]), form .formControl textarea, form .formControl input:not([type="submit"]), form .formControl textarea,
form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar, form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar,
form .formControl .ck.ck-editor__main .ck-content { form .formControl .ck.ck-editor__main .ck-content, div.menu input:not([type="submit"]) {
width: 100%; width: 100%;
border: 4px solid var(--primaryDefault); border: 0.3125em solid var(--primaryDefault);
background: none; background: none;
outline: none; outline: none;
-webkit-border-radius: 1em; -webkit-border-radius: 0.5em;
-moz-border-radius: 1em; -moz-border-radius: 0.5em;
border-radius: 0.5em; border-radius: 0.5em;
padding: 0 0.5em; padding: 0 0.5em;
} }
@ -179,17 +175,18 @@ form .formControl textarea {
} }
form .formControl input:not([type="submit"]).invalid:invalid, form .formControl textarea.invalid:invalid { form .formControl input:not([type="submit"]).invalid:invalid, form .formControl textarea.invalid:invalid {
border: 4px solid var(--errorDefault); border: 0.3125em solid var(--errorDefault);
} }
form .formControl input:not([type="submit"]).invalid:invalid:focus, form .formControl textarea.invalid:invalid:focus { form .formControl input:not([type="submit"]).invalid:invalid:focus, form .formControl textarea.invalid:invalid:focus {
border: 4px solid var(--errorHover); border: 0.3125em solid var(--errorHover);
box-shadow: 0 4px 2px 0 var(--mutedBlack); box-shadow: 0 4px 2px 0 var(--mutedBlack);
} }
form .formControl input:not([type="submit"]):focus, form .formControl textarea:focus, form .formControl input:not([type="submit"]):focus, form .formControl textarea:focus,
form .formControl input:not([type="submit"]):hover, form .formControl textarea:hover { form .formControl input:not([type="submit"]):hover, form .formControl textarea:hover,
border: 4px solid var(--primaryHover); div.menu input:not([type="submit"]):focus, div.menu input:not([type="submit"]):hover {
border: 0.3125em solid var(--primaryHover);
} }
form .formControl input:not([type="submit"]) { form .formControl input:not([type="submit"]) {

View File

@ -45,7 +45,7 @@ main.editor section {
flex-direction: column; flex-direction: column;
} }
section#editPost { section#curriculumVitae {
display: flex; display: flex;
} }

View File

@ -200,10 +200,7 @@
</div> </div>
<div class="formControl"> <div class="formControl">
<label for="postCategories">Categories</label> <label for="postCategories">Categories</label>
<input type="text" name="postCategories" id="postCategories" <input type="text" name="postCategories" id="postCategories" title="CSV format" required>
pattern='[a-zA-Z0-9 ]+, |\w+' title="CSV format"
oninvalid="this.setCustomValidity('This field takes a CSV like format')"
oninput="this.setCustomValidity('')" required>
</div> </div>
<div class="formControl"> <div class="formControl">
@ -264,10 +261,7 @@
</div> </div>
<div class="formControl"> <div class="formControl">
<label for="editPostCategories">Categories</label> <label for="editPostCategories">Categories</label>
<input type="text" name="editPostCategories" id="editPostCategories" <input type="text" name="editPostCategories" id="editPostCategories" title="CSV format" required>
pattern='[a-zA-Z0-9 ]+, |\w+' title="CSV format"
oninvalid="this.setCustomValidity('This field takes a CSV like format')"
oninput="this.setCustomValidity('')" required>
</div> </div>
<div class="formControl"> <div class="formControl">

View File

@ -2,6 +2,7 @@ let dateOptions = {month: 'short', year: 'numeric'};
let textareaLoaded = false; let textareaLoaded = false;
let editors = {}; let editors = {};
let posts = null; let posts = null;
const smallPaddingElements = ['figcaption', 'li'];
document.addEventListener('DOMContentLoaded', () => document.addEventListener('DOMContentLoaded', () =>
{ {
// check if the userData is logged in, if not redirect to log in // check if the userData is logged in, if not redirect to log in
@ -262,6 +263,7 @@ document.querySelector("#addPostForm").addEventListener("submit", e =>
data.append("featured", document.querySelector("#isFeatured").checked ? "1" : "0"); data.append("featured", document.querySelector("#isFeatured").checked ? "1" : "0");
data.append("abstract", document.querySelector("#postAbstract").value); data.append("abstract", document.querySelector("#postAbstract").value);
data.append("body", editors["CKEditorAddPost"].getData()); data.append("body", editors["CKEditorAddPost"].getData());
data.append('bodyText', viewToPlainText(editors['CKEditorAddPost'].editing.view.document.getRoot()));
data.append("dateCreated", new Date().toISOString().slice(0, 19).replace('T', ' ')); data.append("dateCreated", new Date().toISOString().slice(0, 19).replace('T', ' '));
data.append('categories', document.querySelector('#postCategories').value.toLowerCase()); data.append('categories', document.querySelector('#postCategories').value.toLowerCase());
data.append("headerImg", document.querySelector("#headerImg").files[0]); data.append("headerImg", document.querySelector("#headerImg").files[0]);
@ -315,6 +317,7 @@ document.querySelector("#editPostForm").addEventListener("submit", e =>
data["featured"] = document.querySelector("#editIsFeatured").checked ? "1" : "0"; data["featured"] = document.querySelector("#editIsFeatured").checked ? "1" : "0";
data["abstract"] = document.querySelector("#editPostAbstract").value; data["abstract"] = document.querySelector("#editPostAbstract").value;
data["body"] = editors["CKEditorEditPost"].getData(); data["body"] = editors["CKEditorEditPost"].getData();
data['bodyText'] = viewToPlainText(editors['CKEditorEditPost'].editing.view.document.getRoot());
data["dateModified"] = new Date().toISOString().slice(0, 19).replace('T', ' '); data["dateModified"] = new Date().toISOString().slice(0, 19).replace('T', ' ');
data['categories'] = document.querySelector('#editPostCategories').value.toLowerCase(); data['categories'] = document.querySelector('#editPostCategories').value.toLowerCase();
@ -364,6 +367,7 @@ document.querySelector("#editPostForm").addEventListener("submit", e =>
{ {
document.querySelector("#editPostForm").reset(); document.querySelector("#editPostForm").reset();
document.querySelector("#editPostForm input[type='submit']").id = ""; document.querySelector("#editPostForm input[type='submit']").id = "";
console.log();
editors["CKEditorEditPost"].setData(""); editors["CKEditorEditPost"].setData("");
showSuccessMessage("Post edited successfully", "editPost"); showSuccessMessage("Post edited successfully", "editPost");
return; return;
@ -468,6 +472,62 @@ function goToPage(id)
} }
/**
* Converts th CKEditor data to plain text
* @param viewItem - The CKEditor data
* @returns {string} - The plain text
*/
function viewToPlainText(viewItem)
{
let text = '';
if (viewItem.is('$text') || viewItem.is('$textProxy'))
{
// If item is `Text` or `TextProxy` simple take its text data.
text = viewItem.data;
}
// else if (viewItem.is('element', 'img') && viewItem.hasAttribute('alt'))
// {
// // Special case for images - use alt attribute if it is provided.
// text = viewItem.getAttribute('alt');
// }
else if (viewItem.is('element', 'br'))
{
// A soft break should be converted into a single line break (#8045).
text = '\n';
}
else
{
// Other elements are document fragments, attribute elements or container elements.
// They don't have their own text value, so convert their children.
let prev = null;
for (const child of viewItem.getChildren())
{
const childText = viewToPlainText(child);
// Separate container element children with one or more new-line characters.
if (prev && (prev.is('containerElement') || child.is('containerElement')))
{
if (smallPaddingElements.includes(prev.name) || smallPaddingElements.includes(child.name))
{
text += '\n';
}
else
{
text += '\n\n';
}
}
text += childText;
prev = child;
}
}
return text;
}
/** /**
* Removes the active class from all nav items and adds it to the one with the given id * Removes the active class from all nav items and adds it to the one with the given id
* @param {string} id - The id to add the active class to * @param {string} id - The id to add the active class to
@ -768,7 +828,7 @@ function createEditors(...ids)
console.warn('Build id: 1eo8ioyje2om-vgar4aghypdm'); console.warn('Build id: 1eo8ioyje2om-vgar4aghypdm');
console.error(error); console.error(error);
}); });
}) });
} }
/** /**
@ -1250,7 +1310,7 @@ function addProject(ID, isMainProject, imgLocation, title, information, projectL
<input type="checkbox" id="isMainProject${id}" name="isMainProject${id}" ${(isMainProject === "true" ? "checked=''" : "")}> <input type="checkbox" id="isMainProject${id}" name="isMainProject${id}" ${(isMainProject === "true" ? "checked=''" : "")}>
<span class="checkmark"></span> <span class="checkmark"></span>
</label> </label>
</div> </div>
<div class="formControl infoContainer"> <div class="formControl infoContainer">
<textarea name="info${id}" id="info${id}" disabled>${information}</textarea> <textarea name="info${id}" id="info${id}" disabled>${information}</textarea>
</div> </div>

View File

@ -44,7 +44,7 @@
<section id="about"> <section id="about">
<h1>about</h1> <h1>about</h1>
<div> <div>
<p>Hi, I'm Rohit, a computer science student at The University of Nottingham with experience in multiple <p>Hi, I'm Rohit, a Full Stack Developer at Cadonix with experience in multiple
programming languages such as Java, C#, Python, HTML, CSS, JS, PHP. Bringing forth a motivated programming languages such as Java, C#, Python, HTML, CSS, JS, PHP. Bringing forth a motivated
attitude and a variety of powerful skills. Very good at bringing a team together to get a project attitude and a variety of powerful skills. Very good at bringing a team together to get a project
finished. Below are some of my projects that I have worked on. </p> finished. Below are some of my projects that I have worked on. </p>