added backend search functionality

Signed-off-by: rodude123 <rodude123@gmail.com>
This commit is contained in:
2023-10-31 19:36:51 +00:00
parent 929060ce70
commit b4ab7900db
18 changed files with 1299 additions and 88 deletions
+46 -27
View File
@@ -23,7 +23,7 @@ class blogData
public function getBlogPosts(): array
{
$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();
// set the resulting array to associative
@@ -102,28 +102,9 @@ class blogData
}
/**
* Get the blog posts with the given category
* @param string $category - Category of the blog post
* @return array - Array of the blog posts with the given category or error message
* Get all unique categories
* @return string[] - Array of all categories 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
{
$conn = dbConn();
@@ -185,11 +166,12 @@ class blogData
* @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 $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 $categories - Categories of the blog post
* @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();
@@ -227,12 +209,13 @@ class blogData
$from = "../blog/imgs/tmp/";
$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(":title", $title);
$stmt->bindParam(":featured", $featured);
$stmt->bindParam(":abstract", $abstract);
$stmt->bindParam(":body", $newBody);
$stmt->bindParam(":bodyText", $bodyText);
$stmt->bindParam(":dateModified", $dateModified);
$stmt->bindParam(":categories", $categories);
@@ -246,13 +229,14 @@ class blogData
* @param string $title - Title of the blog post
* @param string $abstract - Abstract of the blog post i.e. a short description
* @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 bool $featured - Whether the blog post is featured or not
* @param string $categories - Categories of the blog post
* @param UploadedFileInterface $headerImg - Header image of the blog post
* @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();
$folderID = uniqid();
@@ -282,8 +266,8 @@ class blogData
$stmtMainProject->execute();
}
$stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, abstract, body, categories, folderID)
VALUES (: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, :bodyText, :categories, :folderID);");
$stmt->bindParam(":title", $title);
$stmt->bindParam(":dateCreated", $dateCreated);
$stmt->bindParam(":dateModified", $dateCreated);
@@ -292,6 +276,7 @@ class blogData
$stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
$stmt->bindParam(":abstract", $abstract);
$stmt->bindParam(":body", $newBody);
$stmt->bindParam(":bodyText", $bodyText);
$stmt->bindParam(":categories", $categories);
$stmt->bindParam(":folderID", $folderID);
@@ -443,4 +428,38 @@ class blogData
$stmt->execute();
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)
{
$bodyText = array_column($result, "bodyText");
// go through each body plain text and get the sentence before, the sentence with the search term and the sentence after and append it to a new array
foreach ($bodyText as $key => $text)
{
$text = strtolower($text);
$searchTerm = strtolower($searchTerm);
$pos = strpos($text, $searchTerm);
$start = strrpos(substr($text, 0, $pos), ".") + 1;
$end = strpos($text, ".", $pos);
$result[$key]["bodyText"] = substr($text, $start, $end - $start);
}
return $result;
}
return array("errorMessage" => "Error, could not find posts");
}
}
+24 -2
View File
@@ -123,12 +123,34 @@ class blogRoutes implements routesInterface
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)
{
$data = $request->getParsedBody();
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
$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);
}
$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")
{
+40 -1
View File
@@ -26,8 +26,47 @@ h3 {
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 {
/*margin: 0 5em;*/
display: flex;
flex-direction: column;
justify-content: space-evenly;
+23 -3
View File
@@ -116,6 +116,25 @@ function createLargePost(post)
*/
function loadHomeContent()
{
let menuBar = document.createElement('div');
menuBar.classList.add('menuBar');
// language=HTML
menuBar.innerHTML = `
<div class="menu">
<ul>
<li><a href="/blog/category" class="link">categories</a></li>
<li>
<label for="search" aria-hidden="true" hidden>search</label>
<input type="search" name="search" id="search"
placeholder="Search...">
<button type="submit" class="btn btnPrimary"><i
class="fa fa-search"></i></button>
</li>
</ul>
</div>
`;
document.querySelector('#main').appendChild(menuBar);
fetch('/api/blog/post').then(res => res.json().then(json =>
{
for (let i = 0; i < json.length; i++)
@@ -130,10 +149,10 @@ function loadHomeContent()
featuredPost.appendChild(h1);
let outerContent = createLargePost(json[i]);
featuredPost.appendChild(outerContent);
document.querySelector('#main').prepend(featuredPost);
document.querySelector('#main').appendChild(featuredPost);
}
if (i === 0)
if (i === 1)
{
let latestPost = document.createElement('section');
latestPost.classList.add('largePost');
@@ -143,8 +162,9 @@ function loadHomeContent()
latestPost.appendChild(h1);
let outerContent = createLargePost(json[i]);
latestPost.appendChild(outerContent);
document.querySelector('#main').prepend(latestPost);
document.querySelector('#main').appendChild(latestPost);
}
}
}));
}
+13 -16
View File
@@ -108,8 +108,9 @@ a.btnPrimary[disabled]:hover, button.btnPrimary[disabled]:hover {
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);
border: 0.3215em solid var(--primaryHover);
}
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 {
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 {
border: 4px solid var(--errorHover);
border: 0.3125em solid var(--errorHover);
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"]) {
height: 3em;
}
@@ -150,7 +147,6 @@ form .formControl {
display: flex;
flex-direction: column;
justify-content: flex-start;
/*align-items: flex-start;*/
}
form .formControl.passwordControl {
@@ -163,13 +159,13 @@ form input[type="submit"] {
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__main .ck-content {
form .formControl .ck.ck-editor__main .ck-content, div.menu input:not([type="submit"]) {
width: 100%;
border: 4px solid var(--primaryDefault);
border: 0.3125em solid var(--primaryDefault);
background: none;
outline: none;
-webkit-border-radius: 1em;
-moz-border-radius: 1em;
-webkit-border-radius: 0.5em;
-moz-border-radius: 0.5em;
border-radius: 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 {
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 {
border: 4px solid var(--errorHover);
border: 0.3125em solid var(--errorHover);
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"]):hover, form .formControl textarea:hover {
border: 4px solid var(--primaryHover);
form .formControl input:not([type="submit"]):hover, form .formControl textarea:hover,
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"]) {
+1 -1
View File
@@ -45,7 +45,7 @@ main.editor section {
flex-direction: column;
}
section#editPost {
section#curriculumVitae {
display: flex;
}
+62 -2
View File
@@ -2,6 +2,7 @@ let dateOptions = {month: 'short', year: 'numeric'};
let textareaLoaded = false;
let editors = {};
let posts = null;
const smallPaddingElements = ['figcaption', 'li'];
document.addEventListener('DOMContentLoaded', () =>
{
// 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("abstract", document.querySelector("#postAbstract").value);
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('categories', document.querySelector('#postCategories').value.toLowerCase());
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["abstract"] = document.querySelector("#editPostAbstract").value;
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['categories'] = document.querySelector('#editPostCategories').value.toLowerCase();
@@ -364,6 +367,7 @@ document.querySelector("#editPostForm").addEventListener("submit", e =>
{
document.querySelector("#editPostForm").reset();
document.querySelector("#editPostForm input[type='submit']").id = "";
console.log();
editors["CKEditorEditPost"].setData("");
showSuccessMessage("Post edited successfully", "editPost");
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
* @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.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=''" : "")}>
<span class="checkmark"></span>
</label>
</div>
</div>
<div class="formControl infoContainer">
<textarea name="info${id}" id="info${id}" disabled>${information}</textarea>
</div>
+1 -1
View File
@@ -44,7 +44,7 @@
<section id="about">
<h1>about</h1>
<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
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>