diff --git a/dist/api/blog/blogData.php b/dist/api/blog/blogData.php
index e0eb5cd..951bda2 100644
--- a/dist/api/blog/blogData.php
+++ b/dist/api/blog/blogData.php
@@ -21,7 +21,8 @@ class blogData
public function getBlogPosts(): array
{
$conn = dbConn();
- $stmt = $conn->prepare("SELECT ID, title, dateCreated, dateModified, body, categories FROM blog ORDER BY dateCreated DESC;");
+ $stmt = $conn->prepare("SELECT ID, title, dateCreated, dateModified, body, categories, featured
+ FROM blog ORDER BY dateCreated;");
$stmt->execute();
// set the resulting array to associative
@@ -99,6 +100,164 @@ class blogData
return array("errorMessage" => "Error, blog post could not found");
}
+ /**
+ * Delete a blog post with the given ID
+ * @param int $ID - ID of the blog post to delete
+ * @return string - Success or error message
+ */
+ public function deletePost(int $ID): string
+ {
+ $conn = dbConn();
+
+ $stmtCheckPost = $conn->prepare("SELECT * FROM blog WHERE ID = :ID");
+ $stmtCheckPost->bindParam(":ID", $ID);
+ $stmtCheckPost->execute();
+ $result = $stmtCheckPost->fetch(PDO::FETCH_ASSOC);
+
+ if (!$result)
+ {
+ return "post not found";
+ }
+
+ if ($result["featured"] === 1)
+ {
+ return "cannot delete";
+ }
+
+ $stmt = $conn->prepare("DELETE FROM blog WHERE ID = :ID");
+ $stmt->bindParam(":ID", $ID);
+
+ if ($stmt->execute())
+ {
+ $imagUtils = new imgUtils();
+ $imagUtils->deleteDirectory("../blog/imgs/" . $result["title"] . "_" . $result["folderID"] . "/");
+ return "success";
+ }
+
+ return "error";
+ }
+
+ /**
+ * Update the blog post with the given ID
+ * @param int $ID - ID of the blog post to update
+ * @param string $title - Title of the blog post
+ * @param bool $featured - Whether the blog post is featured or not
+ * @param string $body - Body of the blog post
+ * @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 $body, string $dateModified, string $categories): bool|string
+ {
+ $conn = dbConn();
+
+ $stmtCheckPost = $conn->prepare("SELECT * FROM blog WHERE ID = :ID");
+ $stmtCheckPost->bindParam(":ID", $ID);
+ $stmtCheckPost->execute();
+ $result = $stmtCheckPost->fetch(PDO::FETCH_ASSOC);
+
+ if (!$result)
+ {
+ return "post not found";
+ }
+
+ if (!$featured && $result["featured"] === 1)
+ {
+ return "unset feature";
+ }
+
+ if ($featured)
+ {
+ $stmtUnsetFeatured = $conn->prepare("UPDATE blog SET featured = 0 WHERE featured = 1;");
+ $stmtUnsetFeatured->execute();
+ }
+
+ $to = "../blog/imgs/" . $title . "_" . $result["folderID"] . "/";
+ if ($result["title"] !== $title)
+ {
+ $from = "../blog/imgs/" . $result["title"] . "_" . $result["folderID"] . "/";
+ mkdir($to, 0777, true);
+ rename($result["headerImg"], $to . basename($result["headerImg"]));
+ $body = $this->changeHTMLSrc($body, $to, $from);
+ rmdir($from);
+ }
+
+ $from = "../blog/imgs/tmp/";
+ $newBody = $this->changeHTMLSrc($body, $to, $from);
+
+ $stmt = $conn->prepare("UPDATE blog SET title = :title, featured = :featured, body = :body, dateModified = :dateModified, categories = :categories WHERE ID = :ID;");
+ $stmt->bindParam(":ID", $ID);
+ $stmt->bindParam(":title", $title);
+ $stmt->bindParam(":featured", $featured);
+ $stmt->bindParam(":body", $newBody);
+ $stmt->bindParam(":dateModified", $dateModified);
+ $stmt->bindParam(":categories", $categories);
+
+ return $stmt->execute();
+ }
+
+ /**
+ * Creates a new post di rectory, uploads the header image and moves the images from the
+ * temp folder to the new folder, then updates the post html to point to the new images, finally
+ * it creates the post in the database
+ * @param string $title - Title of the blog post
+ * @param string $body - Body of the blog post
+ * @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 $body, string $dateCreated, bool $featured, string $categories, UploadedFileInterface $headerImg): int|string
+ {
+ $conn = dbConn();
+ $folderID = uniqid();
+ $targetFile = array("imgLocation" => "../blog/imgs/placeholder.png");
+
+ $targetDir = "../blog/imgs/" . $title . "_" . $folderID . "/";
+ mkdir($targetDir, 0777, true);
+
+ if ($headerImg !== null)
+ {
+ $imagUtils = new imgUtils();
+ $targetFile = $imagUtils->uploadFile($targetDir, $headerImg);
+ }
+
+
+ if (!is_array($targetFile))
+ {
+ return $targetFile;
+ }
+
+ $newBody = $this->changeHTMLSrc($body, $targetDir, "../blog/imgs/tmp/");
+
+
+ if ($featured)
+ {
+ $stmtMainProject = $conn->prepare("UPDATE blog SET featured = 0 WHERE featured = 1;");
+ $stmtMainProject->execute();
+ }
+
+ $stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, body, categories, folderID)
+ VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :body, :categories, :folderID);");
+ $stmt->bindParam(":title", $title);
+ $stmt->bindParam(":dateCreated", $dateCreated);
+ $stmt->bindParam(":dateModified", $dateCreated);
+ $isFeatured = $featured ? 1 : 0;
+ $stmt->bindParam(":featured", $isFeatured);
+ $stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
+ $stmt->bindParam(":body", $newBody);
+ $stmt->bindParam(":categories", $categories);
+ $stmt->bindParam(":folderID", $folderID);
+
+ if ($stmt->execute())
+ {
+ return intval($conn->lastInsertId());
+ }
+
+ return "Error, couldn't create post";
+ }
+
/**
* Upload the images in the post to temp folder and return image location
* @param UploadedFileInterface $img - Image to upload
@@ -131,37 +290,60 @@ class blogData
}
/**
- * Creates a new post directory, uploads the header image and moves the images from the
- * temp folder to the new folder, then updates the post html to point to the new images, finally
- * it creates the post in the database
- * @param string $title - Title of the blog post
- * @param string $body - Body of the blog post
- * @param string $dateCreated - Date the blog post was created
- * @param string $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
+ * Upload the header image of the post and update the database
+ * @param int $ID - ID of the post
+ * @param UploadedFileInterface $img - Image to upload
+ * @return string|array - String with error message or array with the location of the uploaded file
*/
- public function createPost(string $title, string $body, string $dateCreated, string $featured, string $categories, UploadedFileInterface $headerImg): int|string
+ public function uploadHeaderImage(int $ID, UploadedFileInterface $img): string|array
{
$conn = dbConn();
- $targetFile = "";
- $folderID = uniqid();
- if ($headerImg !== null)
+ $stmt = $conn->prepare("SELECT * FROM blog WHERE ID = :ID;");
+ $stmt->bindParam(":ID", $ID);
+ $stmt->execute();
+ $result = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (!$result)
{
- $targetDir = "../blog/imgs/" . $title . "_" . $folderID . "/";
- mkdir($targetDir, 0777, true);
- $imagUtils = new imgUtils();
- $targetFile = $imagUtils->uploadFile($targetDir, $headerImg);
+ return "Couldn't find the post";
}
- $targetFile = array("imgLocation" => ".../blog/imgs/placeholder.png");
+ $targetDir = "../blog/imgs/" . $result["title"] . "_" . $result["folderID"] . "/";
+ $imagUtils = new imgUtils();
+ $targetFile = $imagUtils->uploadFile($targetDir, $img);
if (!is_array($targetFile))
{
return $targetFile;
}
+ if (file_exists($targetFile["imgLocation"]))
+ {
+ unlink($result["headerImg"]);
+ $stmt = $conn->prepare("UPDATE blog SET headerImg = :headerImg WHERE ID = :ID;");
+ $stmt->bindParam(":ID", $ID);
+ $stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
+ $stmt->execute();
+ if ($stmt->rowCount() > 0)
+ {
+ return $targetFile;
+ }
+
+ return "Couldn't update the post";
+ }
+
+ return "Couldn't upload the image";
+ }
+
+ /**
+ * Change the HTML src of the images in the post to point to the new location
+ * @param string $body - Body of the post
+ * @param string $to - New location of the images
+ * @param string $from - Old location of the images
+ * @return string - Body of the post with the new image locations
+ */
+ public function changeHTMLSrc(string $body, string $to, string $from): string
+ {
$htmlDoc = new DOMDocument();
$htmlDoc->loadHTML($body, LIBXML_NOERROR);
$doc = $htmlDoc->getElementsByTagName('body')->item(0);
@@ -172,24 +354,25 @@ class blogData
foreach ($imgs as $img)
{
$src = $img->getAttribute("src");
+ $src = urldecode($src);
$srcList[] = $src;
$fileName = basename($src);
- $img->setAttribute("src", $targetDir . $fileName);
+ $img->setAttribute("src", $to . $fileName);
}
- $files = scandir("../blog/imgs/tmp/");
+ $files = scandir($from);
foreach ($files as $file)
{
if ($file != "." && $file != "..")
{
- if (!in_array("../blog/imgs/tmp/" . $file, $srcList))
+ if (!in_array($from . $file, $srcList))
{
- unlink("../blog/imgs/tmp/" . $file);
+ unlink($from . $file);
}
else
{
- rename("../blog/imgs/tmp/" . $file, $targetDir . $file);
+ rename($from . $file, $to . $file);
}
}
}
@@ -199,23 +382,6 @@ class blogData
{
$newBody .= $htmlDoc->saveHTML($node);
}
-
- $stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, body, categories, folderID)
- VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :body, :categories, :folderID);");
- $stmt->bindParam(":title", $title);
- $stmt->bindParam(":dateCreated", $dateCreated);
- $stmt->bindParam(":dateModified", $dateCreated);
- $stmt->bindParam(":featured", $featured);
- $stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
- $stmt->bindParam(":body", $newBody);
- $stmt->bindParam(":categories", $categories);
- $stmt->bindParam(":folderID", $folderID);
-
- if ($stmt->execute())
- {
- return intval($conn->lastInsertId());
- }
-
- return "Error, couldn't create post";
+ return $newBody;
}
}
\ No newline at end of file
diff --git a/dist/api/blog/blogRoutes.php b/dist/api/blog/blogRoutes.php
index a12075a..a0770a9 100644
--- a/dist/api/blog/blogRoutes.php
+++ b/dist/api/blog/blogRoutes.php
@@ -29,12 +29,122 @@ class blogRoutes implements routesInterface
*/
public function createRoutes(App $app): void
{
- $app->post("/blog/post", function (Request $request, Response $response, array $args)
+ $app->get("/blog/post", function (Request $request, Response $response)
+ {
+ $posts = $this->blogData->getBlogPosts();
+
+ $json = json_encode($posts);
+
+ $response->getBody()->write($json);
+
+ if (array_key_exists("errorMessage", $posts))
+ {
+ $response->withStatus(404);
+ }
+
+ return $response;
+ });
+
+ $app->get("/blog/post/{id}", function (Request $request, Response $response, $args)
+ {
+ if ($args["id"] != null)
+ {
+ $post = $this->blogData->getBlogPost($args["id"]);
+ if (array_key_exists("errorMessage", $post))
+ {
+ $response->getBody()->write(json_encode($post));
+ return $response->withStatus(404);
+ }
+
+ $response->getBody()->write(json_encode($post));
+ return $response;
+ }
+
+ $response->getBody()->write(json_encode(array("error" => "Please provide an ID")));
+ 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"]))
+ {
+ // uh oh sent some empty data
+ $response->getBody()->write(json_encode(array("error" => "Only some of the data was sent")));
+ return $response->withStatus(400);
+ }
+
+ $message = $this->blogData->updatePost($args["id"], $data["title"], intval($data["featured"]), $data["body"], $data["dateModified"], $data["categories"]);
+
+ if ($message === "post not found")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Error, post not found")));
+ return $response->withStatus(404);
+ }
+
+ if ($message === "unset featured")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Error, cannot unset featured post, try updating another post to be featured first")));
+ return $response->withStatus(409);
+ }
+
+ if (!is_bool($message) || $message === false)
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => $message)));
+ return $response->withStatus(500);
+ }
+
+ return $response;
+ }
+
+ $response->getBody()->write(json_encode(array("error" => "Please provide an ID")));
+ return $response->withStatus(400);
+ });
+
+ $app->delete("/blog/post/{id}", function (Request $request, Response $response, $args)
+ {
+ if ($args["id"] != null)
+ {
+ $message = $this->blogData->deletePost($args["id"]);
+
+ if ($message === "post not found")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Error, post not found")));
+ return $response->withStatus(404);
+ }
+
+ if ($message === "error")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Error, something went wrong")));
+ return $response->withStatus(500);
+ }
+
+ if ($message === "cannot delete")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Error, cannot delete featured post")));
+ return $response->withStatus(409);
+ }
+ return $response;
+ }
+
+ $response->getBody()->write(json_encode(array("error" => "Please provide an ID")));
+ return $response->withStatus(400);
+ });
+
+ $app->post("/blog/post", function (Request $request, Response $response)
{
$data = $request->getParsedBody();
$files = $request->getUploadedFiles();
$headerImg = $files["headerImg"];
- if (empty($data["title"]) || empty($data["body"]) || empty($data["dateCreated"]) || empty($data["featured"]) || empty($data["categories"]))
+ if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["dateCreated"]) || empty($data["categories"]))
{
// uh oh sent some empty data
$response->getBody()->write(json_encode(array("error" => "Error, empty data sent")));
@@ -46,7 +156,8 @@ class blogRoutes implements routesInterface
$headerImg = null;
}
- $insertedID = $this->blogData->createPost($data["title"], $data["body"], $data["dateCreated"], $data["featured"], $data["categories"], $headerImg);
+ $featured = $data["featured"] === "true";
+ $insertedID = $this->blogData->createPost($data["title"], $data["body"], $data["dateCreated"], $featured, $data["categories"], $headerImg);
if (!is_int($insertedID))
{
// uh oh something went wrong
@@ -74,9 +185,36 @@ class blogRoutes implements routesInterface
return $response->withStatus(500);
}
-
$response->getBody()->write(json_encode($message));
return $response->withStatus(201);
});
+
+ $app->post("/blog/headerImage/{id}", function (Request $request, Response $response, $args)
+ {
+ $files = $request->getUploadedFiles();
+
+ if ($args["id"] != null)
+ {
+ if (empty($files))
+ {
+ // uh oh sent some empty data
+ $response->getBody()->write(json_encode(array("error" => array("message" => "Error, empty data sent"))));
+ return $response->withStatus(400);
+ }
+
+ $message = $this->blogData->uploadHeaderImage($args["id"], $files["headerImg"]);
+ if (!is_array($message))
+ {
+ $response->getBody()->write(json_encode(array("error" => array("message" => $message))));
+ return $response->withStatus(500);
+ }
+
+ $response->getBody()->write(json_encode($message));
+ return $response->withStatus(201);
+ }
+
+ $response->getBody()->write(json_encode(array("error" => "Please provide an ID")));
+ return $response->withStatus(400);
+ });
}
}
\ No newline at end of file
diff --git a/dist/api/project/projectData.php b/dist/api/project/projectData.php
index 77a65a6..a69b22f 100644
--- a/dist/api/project/projectData.php
+++ b/dist/api/project/projectData.php
@@ -39,30 +39,32 @@ class projectData
* Update project data in the database with the given ID
* @param string $ID - ID of the project in the database to update
* @param string $title - Title of the project
- * @param string $isMainProject - Is the project a main project or not
+ * @param bool $isMainProject - Is the project a main project or not
* @param string $information - Information about the project
* @param string $projectLink - Link to the project
* @param string $gitLink - Link to the git repository
* @return bool|string - True if project was updated, false if not and there was an error, or an error string
*/
- public function updateProjectData(string $ID, string $title, string $isMainProject, string $information, string $projectLink, string $gitLink): bool|string
+ public function updateProjectData(string $ID, string $title, bool $isMainProject, string $information, string $projectLink, string $gitLink): bool|string
{
$conn = dbConn();
- if ($isMainProject === "false")
- {
- $stmtMainProject = $conn->prepare("SELECT isMainProject FROM projects WHERE ID = :ID");
- $stmtMainProject->bindParam(":ID", $ID);
- $stmtMainProject->execute();
- $result = $stmtMainProject->fetch(PDO::FETCH_ASSOC);
+ $stmtMainProject = $conn->prepare("SELECT isMainProject FROM projects WHERE ID = :ID");
+ $stmtMainProject->bindParam(":ID", $ID);
+ $stmtMainProject->execute();
+ $result = $stmtMainProject->fetch(PDO::FETCH_ASSOC);
- if ($result["isMainProject"] === "1")
- {
- return "unset main project";
- }
+ if (!$result)
+ {
+ return "project not found";
}
- if ($isMainProject === "true")
+ if (!$isMainProject && $result["isMainProject"] === "1")
+ {
+ return "unset main project";
+ }
+
+ if ($isMainProject)
{
$stmtMainProject = $conn->prepare("UPDATE projects SET isMainProject = 0 WHERE isMainProject = 1;");
$stmtMainProject->execute();
@@ -70,7 +72,7 @@ class projectData
$stmt = $conn->prepare("UPDATE projects SET title = :title, isMainProject = :isMainProject, information = :information, projectLink = :projectLink, gitLink = :gitLink WHERE ID = :ID");
$stmt->bindParam(":title", $title);
- $isMainProj = ($isMainProject === "true") ? 1 : 0;
+ $isMainProj = $isMainProject ? 1 : 0;
$stmt->bindParam(":isMainProject", $isMainProj);
$stmt->bindParam(":information", $information);
$stmt->bindParam(":projectLink", $projectLink);
@@ -89,12 +91,16 @@ class projectData
$conn = dbConn();
// check if the project is a main project if it is return false
-
$stmtMainProject = $conn->prepare("SELECT isMainProject FROM projects WHERE ID = :ID");
$stmtMainProject->bindParam(":ID", $ID);
$stmtMainProject->execute();
$result = $stmtMainProject->fetch(PDO::FETCH_ASSOC);
+ if (!$result)
+ {
+ return "project not found";
+ }
+
if ($result["isMainProject"] === "1")
{
return "cannot delete";
@@ -158,6 +164,20 @@ class projectData
*/
public function uploadImage(int $ID, UploadedFileInterface $img): string | array
{
+
+ $conn = dbConn();
+
+ $stmt = $conn->prepare("SELECT ID FROM projects WHERE ID = :ID");
+ $stmt->bindParam(":ID", $ID);
+ $stmt->execute();
+ $result = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (!$result)
+ {
+ return "Project with ID $ID not found";
+ }
+
+
$targetDir = "../imgs/projects/";
$imgUtils = new imgUtils();
$targetFile = $imgUtils->uploadFile($targetDir, $img);
@@ -171,7 +191,6 @@ class projectData
{
$this->deleteImage($ID);
// update the database with the new image location
- $conn = dbConn();
$stmt = $conn->prepare("UPDATE projects SET imgLocation = :imgLocation WHERE ID = :ID");
$stmt->bindParam(":imgLocation", $targetFile["imgLocation"]);
$stmt->bindParam(":ID", $ID);
diff --git a/dist/api/project/projectRoutes.php b/dist/api/project/projectRoutes.php
index 1b471cb..a7a332e 100644
--- a/dist/api/project/projectRoutes.php
+++ b/dist/api/project/projectRoutes.php
@@ -39,7 +39,7 @@ class projectRoutes implements routesInterface
if(array_key_exists("errorMessage", $result))
{
- $response = $response->withStatus(404);
+ $response->withStatus(404);
}
//use content type json to indicate json data on frontend.
@@ -49,7 +49,7 @@ class projectRoutes implements routesInterface
$app->patch("/projectData/{id}", function (Request $request, Response $response, array $args)
{
$data = $request->getParsedBody();
- if ($args["id"] != "undefined")
+ if ($args["id"] != null)
{
if (empty($data["title"]) || empty($data["isMainProject"]) || empty($data["information"]) || empty($data["gitLink"]))
{
@@ -58,7 +58,15 @@ class projectRoutes implements routesInterface
return $response->withStatus(400);
}
- $update = $this->projectData->updateProjectData($args["id"], $data["title"], $data["isMainProject"], $data["information"], $data["projectLink"], $data["gitLink"]);
+ $isMainProject = $data["isMainProject"] === "true";
+ $update = $this->projectData->updateProjectData($args["id"], $data["title"], $isMainProject, $data["information"], $data["projectLink"], $data["gitLink"]);
+
+ if ($update === "project not found")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Project with ID " . $args["id"] . " not found")));
+ return $response->withStatus(404);
+ }
if ($update === "unset main project")
{
@@ -73,6 +81,7 @@ class projectRoutes implements routesInterface
$response->getBody()->write(json_encode(array("error" => "Something went wrong")));
return $response->withStatus(500);
}
+
return $response;
}
@@ -85,11 +94,12 @@ class projectRoutes implements routesInterface
if ($args["id"] != null)
{
$message = $this->projectData->deleteProjectData($args["id"]);
- if ($message === "error")
+
+ if ($message === "project not found")
{
// uh oh something went wrong
- $response->getBody()->write(json_encode(array("error" => "Something went wrong or the project with ID ".$args["id"]."does not exist")));
- return $response->withStatus(500);
+ $response->getBody()->write(json_encode(array("error" => "Project with ID " . $args["id"] . " not found")));
+ return $response->withStatus(404);
}
if ($message === "cannot delete")
@@ -99,6 +109,13 @@ class projectRoutes implements routesInterface
return $response->withStatus(409);
}
+ if ($message === "error")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Something went wrong")));
+ return $response->withStatus(500);
+ }
+
return $response;
}
diff --git a/dist/api/timeline/timelineData.php b/dist/api/timeline/timelineData.php
index b72dbdf..9c1df75 100644
--- a/dist/api/timeline/timelineData.php
+++ b/dist/api/timeline/timelineData.php
@@ -58,19 +58,33 @@ class timelineData
* @param string $dateTo - End date
* @param string $grade - Grade
* @param string $course - Course
- * @param string $id - ID of the education data
- * @return bool - True if successful, false if not
+ * @param string $ID - ID of the education data
+ * @return string - "not found" if the ID is not found, "ok" if successful, "error" if not
*/
- public function updateEduData(string $dateFrom, string $dateTo, string $grade, string $course, string $id): bool
+ public function updateEduData(string $dateFrom, string $dateTo, string $grade, string $course, string $ID): string
{
$conn = dbConn();
+ $chkStmt = $conn->prepare("SELECT ID FROM edu WHERE ID = :id;");
+ $chkStmt->bindParam(":id", $ID);
+ $chkStmt->execute();
+ $result = $chkStmt->fetch(PDO::FETCH_ASSOC);
+ if (!$result)
+ {
+ return "not found";
+ }
+
$stmt = $conn->prepare("UPDATE edu SET startPeriod = :dateFrom, endPeriod = :dateTo, grade = :grade, course = :course WHERE ID = :id;");
$stmt->bindParam(":dateFrom", $dateFrom);
$stmt->bindParam(":dateTo", $dateTo);
$stmt->bindParam(":grade", $grade);
$stmt->bindParam(":course", $course);
- $stmt->bindParam(":id", $id);
- return $stmt->execute();
+ $stmt->bindParam(":id", $ID);
+ if ($stmt->execute())
+ {
+ return "ok";
+ }
+
+ return "error";
}
/**
@@ -80,11 +94,21 @@ class timelineData
* @param string $companyName - Company name
* @param string $area - Area
* @param string $title - Title
- * @param string $id - ID of the work data
- * @return bool - True if successful, false if not
+ * @param string $ID - ID of the work data
+ * @return string - "not found" if the ID is not found, "ok" if successful, "error" if not
*/
- public function updateWorkData(string $dateFrom, string $dateTo, string $companyName, string $area, string $title, string $id): bool
+ public function updateWorkData(string $dateFrom, string $dateTo, string $companyName, string $area, string $title, string $ID): string
{
+ $conn = dbConn();
+ $chkStmt = $conn->prepare("SELECT ID FROM work WHERE ID = :id;");
+ $chkStmt->bindParam(":id", $ID);
+ $chkStmt->execute();
+ $result = $chkStmt->fetch(PDO::FETCH_ASSOC);
+ if (!$result)
+ {
+ return "not found";
+ }
+
$conn = dbConn();
$stmt = $conn->prepare("UPDATE work SET startPeriod = :dateFrom, endPeriod = :dateTo, companyName = :companyName, area = :area, title = :title WHERE ID = :id;");
$stmt->bindParam(":dateFrom", $dateFrom);
@@ -92,34 +116,67 @@ class timelineData
$stmt->bindParam(":companyName", $companyName);
$stmt->bindParam(":area", $area);
$stmt->bindParam(":title", $title);
- $stmt->bindParam(":id", $id);
- return $stmt->execute();
+ $stmt->bindParam(":id", $ID);
+ if ($stmt->execute())
+ {
+ return "ok";
+ }
+
+ return "error";
}
/**
* Delete education data by ID
- * @param int $id
- * @return bool - True if successful, false if not
+ * @param int $ID
+ * @return string - "not found" if the ID is not found, "ok" if successful, "error" if not
*/
- public function deleteEduData(int $id): bool
+ public function deleteEduData(int $ID): string
{
$conn = dbConn();
+ $chkStmt = $conn->prepare("SELECT ID FROM edu WHERE ID = :id;");
+ $chkStmt->bindParam(":id", $ID);
+ $chkStmt->execute();
+ $result = $chkStmt->fetch(PDO::FETCH_ASSOC);
+ if (!$result)
+ {
+ return "not found";
+ }
+
$stmt = $conn->prepare("DELETE FROM edu WHERE ID = :id;");
- $stmt->bindParam(":id", $id);
- return $stmt->execute();
+ $stmt->bindParam(":id", $ID);
+ if ($stmt->execute())
+ {
+ return "ok";
+ }
+
+ return "error";
}
/**
* Delete work data by ID
- * @param int $id
- * @return bool - True if successful, false if not
+ * @param int $ID
+ * @return string - "not found" if the ID is not found, "ok" if successful, "error" if not
*/
- function deleteWorkData(int $id): bool
+ function deleteWorkData(int $ID): string
{
$conn = dbConn();
+ $chkStmt = $conn->prepare("SELECT ID FROM work WHERE ID = :id;");
+ $chkStmt->bindParam(":id", $ID);
+ $chkStmt->execute();
+ $result = $chkStmt->fetch(PDO::FETCH_ASSOC);
+ if (!$result)
+ {
+ return "not found";
+ }
+
$stmt = $conn->prepare("DELETE FROM work WHERE ID = :id;");
- $stmt->bindParam(":id", $id);
- return $stmt->execute();
+ $stmt->bindParam(":id", $ID);
+ if ($stmt->execute())
+ {
+ return "ok";
+ }
+
+ return "error";
}
/**
diff --git a/dist/api/timeline/timelineRoutes.php b/dist/api/timeline/timelineRoutes.php
index d6ab9d6..72ea6f0 100644
--- a/dist/api/timeline/timelineRoutes.php
+++ b/dist/api/timeline/timelineRoutes.php
@@ -53,7 +53,7 @@ class timelineRoutes implements routesInterface
$app->patch("/timelineData/{timeline}/{id}", function (Request $request, Response $response, array $args)
{
$data = $request->getParsedBody();
- if ($args["timeline"] == "edu" && $args["id"] != "undefined")
+ if ($args["timeline"] == "edu" && $args["id"] != null)
{
if (empty($data["dateFrom"]) || empty($data["dateTo"]) || empty($data["grade"]) || empty($data["course"]))
{
@@ -61,8 +61,16 @@ class timelineRoutes implements routesInterface
$response->getBody()->write(json_encode(array("error" => "Only some of the data was sent")));
return $response->withStatus(400);
}
+ $message = $this->timelineData->updateEduData($data["dateFrom"], $data["dateTo"], $data["grade"], $data["course"], $args["id"]);
- if (!$this->timelineData->updateEduData($data["dateFrom"], $data["dateTo"], $data["grade"], $data["course"], $args["id"]))
+ if ($message == "not found")
+ {
+ // uh oh sent some empty data
+ $response->getBody()->write(json_encode(array("error" => "Edu data with ID " . $args["id"] . " was not found")));
+ return $response->withStatus(404);
+ }
+
+ if ($message == "error")
{
// uh oh something went wrong
$response->getBody()->write(json_encode(array("error" => "Something went wrong")));
@@ -82,7 +90,16 @@ class timelineRoutes implements routesInterface
return $response->withStatus(400);
}
- if (!$this->timelineData->updateWorkData($data["dateFrom"], $data["dateTo"], $data["companyName"], $data["area"], $data["title"], $args["id"]))
+ $message = $this->timelineData->updateWorkData($data["dateFrom"], $data["dateTo"], $data["companyName"], $data["area"], $data["title"], $args["id"]);
+
+ if ($message == "not found")
+ {
+ // uh oh sent some empty data
+ $response->getBody()->write(json_encode(array("error" => "Work data with ID " . $args["id"] . " was not found")));
+ return $response->withStatus(404);
+ }
+
+ if ($message == "error")
{
// uh oh something went wrong
$response->getBody()->write(json_encode(array("error" => "Something went wrong")));
@@ -101,7 +118,16 @@ class timelineRoutes implements routesInterface
{
if ($args["timeline"] == "edu" && $args["id"] != null)
{
- if (!$this->timelineData->deleteEduData($args["id"]))
+ $message = $this->timelineData->deleteEduData($args["id"]);
+
+ if ($message == "not found")
+ {
+ // uh oh sent some empty data
+ $response->getBody()->write(json_encode(array("error" => "Edu data with ID " . $args["id"] . " was not found")));
+ return $response->withStatus(404);
+ }
+
+ if ($message == "error")
{
// uh oh something went wrong
$response->getBody()->write(json_encode(array("error" => "Something went wrong")));
@@ -113,7 +139,16 @@ class timelineRoutes implements routesInterface
if ($args["timeline"] == "work" && $args["id"] != null)
{
- if (!$this->timelineData->deleteWorkData($args["id"]))
+ $message = $this->timelineData->deleteWorkData($args["id"]);
+
+ if ($message == "not found")
+ {
+ // uh oh sent some empty data
+ $response->getBody()->write(json_encode(array("error" => "Work data with ID " . $args["id"] . " was not found")));
+ return $response->withStatus(404);
+ }
+
+ if ($message == "error")
{
// uh oh something went wrong
$response->getBody()->write(json_encode(array("error" => "Something went wrong")));
diff --git a/dist/api/user/userData.php b/dist/api/user/userData.php
index 0aa11b2..493b6db 100644
--- a/dist/api/user/userData.php
+++ b/dist/api/user/userData.php
@@ -46,7 +46,7 @@ class userData
public function createToken(string $username): string
{
$now = time();
- $future = strtotime('+6 hour', $now);
+ $future = strtotime('+2 day', $now);
$secretKey = getSecretKey();
$payload = [
"jti" => $username,
diff --git a/dist/api/user/userRoutes.php b/dist/api/user/userRoutes.php
index 2bde66c..465ca7a 100644
--- a/dist/api/user/userRoutes.php
+++ b/dist/api/user/userRoutes.php
@@ -36,15 +36,19 @@ class userRoutes implements routesInterface
if (empty($data["username"]) || empty($data["password"]))
{
- // uh oh userData sent empty data
+ // uh oh user sent empty data
return $response->withStatus(400);
}
if ($this->user->checkUser($data["username"], $data["password"]))
{
- // yay, userData is logged in
+ // yay, user is logged in
$_SESSION["token"] = $this->user->createToken($data["username"]);
$_SESSION["username"] = $data["username"];
+
+ $inactive = 60 * 60 * 48; // 2 days
+ $_SESSION["timeout"] = time() + $inactive;
+
$response->getBody()->write(json_encode(array("token" => $_SESSION["token"])));
return $response;
}
@@ -62,15 +66,24 @@ class userRoutes implements routesInterface
{
if (empty($_SESSION["token"]) && empty($_SESSION["username"]))
{
- // uh oh userData not logged in
+ // uh oh user not logged in
+ return $response->withStatus(401);
+ }
+
+ $inactive = 60 * 60 * 48; // 2 days
+ $sessionLife = time() - $_SESSION["timeout"];
+ if ($sessionLife > $inactive)
+ {
+ // uh oh user session expired
+ session_destroy();
return $response->withStatus(401);
}
if (empty($_SESSION["token"]))
{
- // userData is logged in but no token was created
+ // user is logged in but no token was created
$_SESSION["token"] = $this->user->createToken($_SESSION["username"]);
- return $response;
+ return $response->withStatus(201);
}
$response->getBody()->write(json_encode(array("token" => $_SESSION["token"])));
diff --git a/dist/api/utils/imgUtils.php b/dist/api/utils/imgUtils.php
index 40f9487..1deab53 100644
--- a/dist/api/utils/imgUtils.php
+++ b/dist/api/utils/imgUtils.php
@@ -3,6 +3,8 @@
namespace api\utils;
use Psr\Http\Message\UploadedFileInterface;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
class imgUtils
{
@@ -40,4 +42,29 @@ class imgUtils
return array("imgLocation" => $targetFile);
}
+
+ /**
+ * Deletes a directory and all its contents
+ * @param string $path - Path to the directory to delete
+ */
+ public function deleteDirectory(string $path): void
+ {
+ $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path,
+ RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
+
+ foreach ($iterator as $file)
+ {
+ if ($file->isDir())
+ {
+ rmdir($file->getPathname());
+ }
+ else
+ {
+ unlink($file->getPathname());
+ }
+ }
+
+ rmdir($path);
+ }
+
}
\ No newline at end of file
diff --git a/dist/css/main.css b/dist/css/main.css
index c81af48..62cb9d7 100644
--- a/dist/css/main.css
+++ b/dist/css/main.css
@@ -1 +1 @@
-/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}:root{--mainHue:80;--mainSat:60%;--mainLight:50%;--primaryDefault:hsla(var(--mainHue), var(--mainSat), var(--mainLight), 1);--primaryHover:hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) - 10%), 1);--timelineItemBrdr:hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) - 20%), 1);--errorDefault:hsla(0, calc(var(--mainSat) + 10%),calc(var(--mainLight) + 10%), 1);--errorHover:hsla(0, calc(var(--mainSat) + 10%), calc(var(--mainLight) - 10%), 1);--grey:hsla(0, 0%, 39%, 1);--notAvailableDefault:hsla(0, 0%, 39%, 1);--notAvailableHover:hsla(0, 0%,32%, 1);--mutedGrey:hsla(0, 0%, 78%, 1);--mutedBlack:hsla(0, 0%, 0%, 0.25);--navBack:hsla(0, 0%, 30%, 1);--titleFS:2.25rem;--generalFS:1.125rem;--headingFS:1.5rem}*{box-sizing:border-box}html{scroll-behavior:smooth}body{font-family:Noto Sans KR,sans-serif;font-style:normal;font-weight:500;font-size:var(--generalFS);line-height:1.625rem}a:visited{color:inherit}h1,nav{font-family:Share Tech Mono,monospace;font-style:normal;font-weight:400;font-size:var(--titleFS);line-height:2.5625rem;text-transform:lowercase}h2{font-family:Noto Sans KR,sans-serif;font-style:normal;font-weight:500;font-size:var(--headingFS);line-height:2.1875rem}a.btn,form input[type=submit]{text-decoration:none;display:inline-flex;padding:1em 2em;border-radius:.625em;border:.3215em solid var(--primaryDefault);color:#fff;text-align:center;align-items:center;max-height:4em}form input[type=submit]{padding:1.1em 2em}a.btn:hover,form input[type=submit]:hover{border:.3215em solid var(--primaryHover)}a.btnPrimary,form input[type=submit]{background-color:var(--primaryDefault);cursor:pointer}a.btnOutline{background:#fff;color:var(--primaryDefault)}a.btnPrimary[disabled]{pointer-events:none;background:var(--notAvailableDefault);border:.3215em solid var(--notAvailableDefault)}a.btnPrimary[disabled]:hover{background:var(--notAvailableHover);border:.3215em solid var(--notAvailableHover)}a.btnPrimary:hover,form input[type=submit]:hover{background:var(--primaryHover)}a.btn:active,form input[type=submit]:active{padding:.8rem 1.8rem}.boxShadowOut:hover{box-shadow:0 6px 4px 0 var(--mutedBlack)}.boxShadowIn:active{box-shadow:inset 0 6px 4px 0 var(--mutedBlack)}.textShadow:hover{text-shadow:0 6px 4px var(--mutedBlack)}form .formControl input:not([type=submit]).invalid:invalid,form .formControl textarea.invalid:invalid{border:4px solid var(--errorDefault)}form .formControl input:not([type=submit]).invalid:invalid:focus,form .formControl textarea.invalid:invalid:focus{border:4px 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}form .formControl{width:100%;display:flex;flex-direction:column;justify-content:flex-start}form .formControl.passwordControl{display:block}form input[type=submit]{align-self:flex-start}form .formControl .ck.ck-editor__main .ck-content,form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar,form .formControl input:not([type=submit]),form .formControl textarea{width:100%;border:4px solid var(--primaryDefault);background:0 0;outline:0;-webkit-border-radius:1em;-moz-border-radius:1em;border-radius:.5em;padding:0 .5em}form .formControl textarea{padding:.5em}form .formControl input:not([type=submit]).invalid:invalid,form .formControl textarea.invalid:invalid{border:4px solid var(--errorDefault)}form .formControl input:not([type=submit]).invalid:invalid:focus,form .formControl textarea.invalid:invalid:focus{border:4px solid var(--errorHover);box-shadow:0 4px 2px 0 var(--mutedBlack)}form .formControl input:not([type=submit]):focus,form .formControl input:not([type=submit]):hover,form .formControl textarea:focus,form .formControl textarea:hover{border:4px solid var(--primaryHover)}form .formControl input:not([type=submit]){height:3em}form .formControl i.fa-eye,form .formControl i.fa-eye-slash{margin-left:-40px;cursor:pointer;color:var(--primaryDefault)}form .formControl input:not([type=submit]):focus+i.fa-eye,form .formControl input:not([type=submit]):focus+i.fa-eye-slash{color:var(--primaryHover)}form .formControl .checkContainer{display:block;position:relative;margin-bottom:1.25em;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}form .formControl .checkContainer input{position:absolute;opacity:0;cursor:pointer;height:0;width:0}form .formControl .checkContainer .checkmark{position:absolute;top:1.25em;left:0;height:25px;width:25px;background-color:var(--mutedGrey)}form .formControl .checkContainer:hover input~.checkmark{background-color:var(--grey)}form .formControl .checkContainer input:checked~.checkmark{background-color:var(--primaryDefault)}form .formControl .checkContainer input:checked:hover~.checkmark{background-color:var(--primaryHover)}form .formControl .checkContainer .checkmark:after{content:"";position:absolute;display:none}form .formControl .checkContainer input:checked~.checkmark:after{display:block}form .formControl .checkContainer .checkmark:after{left:9px;top:5px;width:5px;height:10px;border:solid #fff;border-width:0 3px 3px 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}form .formControl input[type=file]{padding:0;cursor:pointer}form .formControl input[type=file]::file-selector-button{background-color:var(--primaryDefault);color:#fff;border:0;border-right:5px solid var(--mutedBlack);padding:15px;margin-right:20px;-webkit-transition:all .5s;-moz-transition:all .5s;-ms-transition:all .5s;-o-transition:all .5s;transition:all .5s}form .formControl input[type=file]:hover::file-selector-button{background-color:var(--primaryHover)}section#about,section#curriculumVitae h1{padding:0 5rem}header{background:#6a6a6a url(../imgs/hero.jpg) no-repeat bottom;background-size:cover;height:40%;color:#fff;backdrop-filter:grayscale(100%);position:relative}nav{display:flex;flex-direction:row;justify-content:space-between;padding:.25em;position:fixed;top:0;width:100%;transition:background-color .4s ease-in;color:#fff;z-index:1}nav.scrolled{background-color:var(--navBack)}nav #nav-check{display:none}nav .nav-btn{display:none}nav h1{margin:0}nav a{text-decoration:none;color:#fff}nav ul{display:flex;flex-direction:row;gap:1em;margin:0;justify-content:flex-end;align-items:flex-end}nav ul li{list-style:none}nav ul li span{visibility:hidden}nav ul li .active span,nav ul li a:hover span{visibility:visible}header div{display:flex;flex-direction:column;justify-content:center;align-items:center;padding-top:10em}header div .btn{margin:2em 0}header div button{background:0 0;border:none;display:inline-block;text-align:center;text-decoration:none;font-size:2rem;cursor:pointer}i.fa-chevron-down{color:hsla(0,0%,67%,.58);font-size:3.75em;margin:1.5rem 0}div h1 span{visibility:visible;animation:caret 1s steps(1) infinite}@keyframes caret{50%{visibility:hidden}}section#about{margin-bottom:5rem}section#about div{padding:.1em 5em}section#curriculumVitae{background-color:var(--primaryDefault);color:#fff;padding:2em 0}section#curriculumVitae .cvGrid{display:flex;flex-direction:row;padding:0 1.5rem;flex-wrap:wrap}section#curriculumVitae .cvGrid>div{width:45%;display:flex;flex-direction:column;min-height:100%}section#curriculumVitae .cvGrid h2{text-align:center}section#curriculumVitae .timeline{position:relative;max-width:30em;gap:1em;display:flex;flex-direction:column;height:100%}section#curriculumVitae #work{margin:0 auto 0 8rem}section#curriculumVitae .timeline:before{content:"";position:absolute;height:100%;border:4px var(--timelineItemBrdr) solid;right:194px;top:0}section#curriculumVitae .timeline:after{content:"";display:table;clear:both}section#curriculumVitae .timelineItem{border:2px solid var(--timelineItemBrdr);-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;padding:0 1rem;width:50%;position:relative;background-color:var(--primaryHover)}.timelineItem:after,section#curriculumVitae .timelineItem:before{content:'';position:absolute}section#curriculumVitae .timelineItem:before{content:'';right:-20px;top:calc(50% - 5px);border-style:solid;border-color:var(--timelineItemBrdr) var(--timelineItemBrdr) transparent transparent;border-width:20px;transform:rotate(45deg)}section#curriculumVitae .timelineItem:nth-child(2n){margin-left:21em}section#curriculumVitae .timelineItem:nth-child(2n):before{right:auto;left:-20px;border-color:transparent transparent var(--timelineItemBrdr) var(--timelineItemBrdr)}section#curriculumVitae .timelineItem h3{font-weight:400}section#curriculumVitae .timelineItem span{color:#e5e5e5}section#projects{display:flex;flex-direction:row;padding:0 2.5rem;border-bottom:2px solid var(--mutedGrey)}section#projects .mainProj,section#projects .otherProj{width:50%;display:flex;flex-direction:column;align-items:center;gap:1em}section#projects .mainProj{border-right:2px solid var(--mutedGrey);padding:0 2.5em 5em 0}section#allProjects img,section#projects img{-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;display:block;margin:1em auto}section#projects .mainProj img{width:100%;max-width:40rem}section#projects .mainProj .flexRow{display:flex;flex-direction:row;gap:4em}section#projects .mainProj .flexCol{display:flex;flex-direction:column;gap:2.5em}section#projects .otherProj>a{margin:5rem 0}section#projects .otherProj>div{display:flex;flex-direction:column;gap:2em}section#allProjects #otherProj .oProjItem,section#projects .otherProj>div .oProjItem{display:flex;justify-content:center;align-items:center;flex-direction:row;width:90%;border:1px solid var(--grey);gap:1em;box-shadow:0 6px 4px 0 var(--mutedBlack);-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;padding:.75em 1em}section#projects .otherProj>div .oProjItem{margin:0 auto}section#projects .otherProj>div .oProjItem:nth-child(2){flex-direction:row-reverse}section#projects .otherProj .oProjItem img{max-width:15rem;width:100%;padding:0 1em}section#projects .oProjItem .flexCol div:nth-child(2){display:flex;flex-direction:row;justify-content:flex-start;gap:3em;margin-left:2em}section#projects .flexCol div:nth-child(2) .btn{padding:.25em .5em}section#allProjects{display:flex;justify-content:center;align-items:center;flex-direction:column;gap:5em}section#allProjects #mainProj{display:flex;flex-direction:column;justify-content:center;align-items:center;border:1px solid var(--grey);box-shadow:0 6px 4px 0 var(--mutedBlack);-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;padding:1.5em 2em;margin:3em 2.5rem 0;width:50%}section#allProjects #mainProj img{width:100%;max-width:30rem}section#allProjects #otherProj{display:flex;flex-direction:row;justify-content:space-between;align-items:stretch;flex-wrap:wrap;gap:2rem;border-top:2px solid var(--grey);padding:5em 2.5rem 0}section#allProjects #otherProj .oProjItem{flex-direction:column;width:30%;height:auto}section#allProjects #otherProj img{width:100%;max-width:20rem}section#contact{display:flex;flex-direction:row;padding:0 2.5em}div#findMe,div#sayHello{width:50%;display:flex;flex-direction:column;align-items:center;gap:1em}div#findMe .findMeContainer{display:flex;flex-direction:row;justify-content:space-around;align-items:center;gap:2em;width:100%;height:100%;margin:5em 0}div#findMe .findMeContainer .profile{-webkit-border-radius:50%;-moz-border-radius:50%;border-radius:50%}div#findMe .socialIcons{display:flex;flex-direction:row;justify-content:center;align-items:center;gap:2em}div#findMe .socialIcons div{display:flex;flex-direction:column;gap:1.5em}div#findMe .socialIcons div svg{width:2.5em;fill:var(--primaryDefault);font-size:2em}div#findMe .socialIcons div a:hover svg{fill:var(--primaryHover)}div#sayHello #contactForm{display:flex;flex-direction:column;justify-content:center;align-items:center}#contactForm .flName{display:flex;flex-direction:row;gap:1em;width:100%}div.message{background:var(--primaryDefault);color:#fff;padding:.5em .8em;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;display:flex;justify-content:center;align-items:center;align-self:flex-start;flex-direction:row-reverse;position:relative;height:75px;visibility:visible;overflow:hidden;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;opacity:1;margin-top:1em;margin-bottom:1em}div.message.error{background:var(--errorDefault)}div.message button{border:none;background:0 0;outline:0;cursor:pointer;color:#fff;font-size:1.25rem;margin-top:-5px;position:absolute;transform:translate(0,0);transform-origin:0 0;right:10px;top:10px}div.message.hidden{opacity:0;visibility:hidden;height:0}div.message button:hover{text-shadow:-1px 2px var(--mutedBlack)}footer{background-color:var(--primaryDefault);margin-top:5em;padding:2em;display:flex;color:#fff}footer .spacer{width:100%;margin-right:auto}footer p{margin:auto;width:100%;text-align:center}footer .button{margin-left:auto;width:100%;text-align:center}footer .button button{border:5px solid #fff;background:0 0;font-size:3em;padding:.5rem 1rem;width:2em;color:#fff;-webkit-border-radius:.25em;-moz-border-radius:.25em;border-radius:.25em;cursor:pointer}@media screen and (max-width:90em){section#curriculumVitae .cvGrid{flex-direction:column;justify-content:center;align-items:center}section#curriculumVitae .cvGrid>div{width:100%}section#curriculumVitae .cvGrid>div:first-child{padding-bottom:2.5em;margin-bottom:2.5em;border-bottom:5px #fff solid}section#curriculumVitae .cvGrid h2{margin-left:5em}section#curriculumVitae .cvGrid .timeline{margin:0 auto}}@media screen and (max-width:75em){section#about,section#curriculumVitae h1{padding:0 1em}nav{display:block;height:50px;width:100%;background-color:var(--navBack);position:fixed;top:0;padding:0}nav a h1{margin-left:1ch}nav .nav-btn{display:inline-block;position:absolute;right:75px;top:0}nav ul{position:fixed;display:block;width:100%;background-color:#333;transition:all .4s ease-in;overflow-y:hidden;padding-left:0;margin-top:7px}nav ul li a{display:block;width:100%;transform:translateX(-30px);transition:all .4s ease-in;opacity:0}.nav-btn label{display:inline-block;cursor:pointer;width:60px;height:50px;position:fixed;-webkit-transform:rotate(0);-moz-transform:rotate(0);-o-transform:rotate(0);transform:rotate(0);-webkit-transition:.5s ease-in;-moz-transition:.5s ease-in;-o-transition:.5s ease-in;transition:.5s ease-in}.nav-btn label span{display:block;position:absolute;height:5px;width:100%;background-color:#fff;opacity:1;right:0;top:20px;-webkit-transform:rotate(0);-moz-transform:rotate(0);-o-transform:rotate(0);transform:rotate(0);-webkit-transition:.25s ease-in;-moz-transition:.25s ease-in;-o-transition:.25s ease-in;transition:.25s ease-in}nav #nav-check:not(:checked)~ul{height:auto;max-height:0}nav #nav-check:not(:checked)~.nav-btn label span:nth-child(1){top:8px;-webkit-transform-origin:left center;-moz-transform-origin:left center;-o-transform-origin:left center;transform-origin:left center}nav #nav-check:not(:checked)~.nav-btn label span:nth-child(2){top:23px;-webkit-transform-origin:left center;-moz-transform-origin:left center;-o-transform-origin:left center;transform-origin:left center}nav #nav-check:not(:checked)~.nav-btn label span:nth-child(3){top:38px;-webkit-transform-origin:left center;-moz-transform-origin:left center;-o-transform-origin:left center;transform-origin:left center}nav #nav-check:checked~.nav-btn label,nav .nav-btn label:hover{background-color:rgba(-1,0,0,.3)}nav #nav-check:checked~ul{max-height:50vh;overflow-y:hidden}nav #nav-check:checked~ul li a{opacity:1;transform:translateX(0)}nav #nav-check:checked~ul li:nth-child(1) a{transition-delay:.15s}nav #nav-check:checked~ul li:nth-child(2) a{transition-delay:.25s}nav #nav-check:checked~ul li:nth-child(3) a{transition-delay:.35s}nav #nav-check:checked~ul li:nth-child(4) a{transition-delay:.45s}nav #nav-check:checked~ul li:nth-child(5) a{transition-delay:.55s}nav #nav-check:checked~.nav-btn label span:first-child{-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}nav #nav-check:checked~.nav-btn label span:nth-child(2){width:0;opacity:0}nav #nav-check:checked~.nav-btn label span:last-child{-webkit-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-o-transform:rotate(-45deg);transform:rotate(-45deg)}section#about div{padding:.1em 2.5em}section#curriculumVitae .cvGrid{padding:0}section#projects{flex-direction:column;justify-content:center;align-items:center}section#projects .mainProj{border-right:0;padding:0;width:100%;margin:0 5em}section#projects .mainProj img{padding:0 1em}section#projects .mainProj .flexRow{flex-direction:column;margin:0 2.5em}section#projects .mainProj .flexCol{flex-direction:row;justify-content:center;align-items:center}section#projects .otherProj{width:100%}section#projects .otherProj .btn{width:10em;text-align:center}section#projects .otherProj>div .oProjItem,section#projects .otherProj>div .oProjItem:nth-child(2){flex-direction:column}section#projects .oProjItem .flexCol div:nth-child(2){justify-content:center;margin-left:0;margin-bottom:1em}section#projects .otherProj>a{margin-left:3em;margin-right:3em;text-align:center}section#allProjects #otherProj .oProjItem{width:45%}section#contact{flex-direction:column;justify-content:center;align-items:center}div#findMe,div#sayHello{width:100%}div#findMe .findMeContainer{flex-direction:column;justify-content:center}div#findMe .socialIcons{flex-direction:row}div#findMe .socialIcons div{flex-direction:row}}@media screen and (max-width:55em){section#curriculumVitae .cvGrid .timeline,section#curriculumVitae .cvGrid .timeline#work{margin:0 auto;width:100%}section#curriculumVitae .timeline:before{border:none}section#curriculumVitae .timelineItem,section#curriculumVitae .timelineItem:nth-child(2n){width:95%;padding:0;margin:0 auto}section#curriculumVitae .timelineItem:before{right:unset;left:unset;border:none}section#allProjects #mainProj{width:auto}section#allProjects #otherProj{flex-direction:column;align-items:center}section#allProjects #otherProj .oProjItem{width:auto}div#findMe .socialIcons{flex-direction:column}#contactForm .flName{flex-direction:column;gap:0;width:100%}}@media screen and (max-width:31em){section#about,section#curriculumVitae h1{padding:0 1em}header div h1{text-align:center;height:5.125rem}section#about div{padding:.1em 1em}section#projects .mainProj .flexCol{flex-direction:column}section#projects .oProjItem .flexCol div:nth-child(2){flex-direction:column;justify-content:center;align-items:center}}
\ No newline at end of file
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}:root{--mainHue:80;--mainSat:60%;--mainLight:50%;--primaryDefault:hsla(var(--mainHue), var(--mainSat), var(--mainLight), 1);--primaryHover:hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) - 10%), 1);--timelineItemBrdr:hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) - 20%), 1);--errorDefault:hsla(0, calc(var(--mainSat) + 10%), calc(var(--mainLight) + 10%), 1);--errorHover:hsla(0, calc(var(--mainSat) + 10%), calc(var(--mainLight) - 10%), 1);--grey:hsla(0, 0%, 39%, 1);--notAvailableDefault:hsla(0, 0%, 39%, 1);--notAvailableHover:hsla(0, 0%, 32%, 1);--mutedGrey:hsla(0, 0%, 78%, 1);--mutedBlack:hsla(0, 0%, 0%, 0.25);--mutedGreen:hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) + 20%), 0.5);--navBack:hsla(0, 0%, 30%, 1);--titleFS:2.25rem;--generalFS:1.125rem;--headingFS:1.5rem}*{box-sizing:border-box}html{scroll-behavior:smooth}body{font-family:Noto Sans KR,sans-serif;font-style:normal;font-weight:500;font-size:var(--generalFS);line-height:1.625rem}a:visited{color:inherit}h1,nav{font-family:Share Tech Mono,monospace;font-style:normal;font-weight:400;font-size:var(--titleFS);line-height:2.5625rem;text-transform:lowercase}h2{font-family:Noto Sans KR,sans-serif;font-style:normal;font-weight:500;font-size:var(--headingFS);line-height:2.1875rem}a.btn,form input[type=submit]{text-decoration:none;display:inline-flex;padding:1em 2em;border-radius:.625em;border:.3215em solid var(--primaryDefault);color:#fff;text-align:center;align-items:center;max-height:4em}form input[type=submit]{padding:1.1em 2em}a.btn:hover,form input[type=submit]:hover{border:.3215em solid var(--primaryHover)}a.btnPrimary,form input[type=submit]{background-color:var(--primaryDefault);cursor:pointer}a.btnOutline{background:#fff;color:var(--primaryDefault)}a.btnPrimary[disabled]{pointer-events:none;background:var(--notAvailableDefault);border:.3215em solid var(--notAvailableDefault)}a.btnPrimary[disabled]:hover{background:var(--notAvailableHover);border:.3215em solid var(--notAvailableHover)}a.btnPrimary:hover,form input[type=submit]:hover{background:var(--primaryHover)}a.btn:active,form input[type=submit]:active{padding:.8rem 1.8rem}.boxShadowOut:hover{box-shadow:0 6px 4px 0 var(--mutedBlack)}.boxShadowIn:active{box-shadow:inset 0 6px 4px 0 var(--mutedBlack)}.textShadow:hover{text-shadow:0 6px 4px var(--mutedBlack)}form .formControl input:not([type=submit]).invalid:invalid,form .formControl textarea.invalid:invalid{border:4px solid var(--errorDefault)}form .formControl input:not([type=submit]).invalid:invalid:focus,form .formControl textarea.invalid:invalid:focus{border:4px 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}form .formControl{width:100%;display:flex;flex-direction:column;justify-content:flex-start}form .formControl.passwordControl{display:block}form input[type=submit]{align-self:flex-start}form .formControl .ck.ck-editor__main .ck-content,form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar,form .formControl input:not([type=submit]),form .formControl textarea{width:100%;border:4px solid var(--primaryDefault);background:0 0;outline:0;-webkit-border-radius:1em;-moz-border-radius:1em;border-radius:.5em;padding:0 .5em}form .formControl textarea{padding:.5em}form .formControl input:not([type=submit]).invalid:invalid,form .formControl textarea.invalid:invalid{border:4px solid var(--errorDefault)}form .formControl input:not([type=submit]).invalid:invalid:focus,form .formControl textarea.invalid:invalid:focus{border:4px solid var(--errorHover);box-shadow:0 4px 2px 0 var(--mutedBlack)}form .formControl input:not([type=submit]):focus,form .formControl input:not([type=submit]):hover,form .formControl textarea:focus,form .formControl textarea:hover{border:4px solid var(--primaryHover)}form .formControl input:not([type=submit]){height:3em}form .formControl i.fa-eye,form .formControl i.fa-eye-slash{margin-left:-40px;cursor:pointer;color:var(--primaryDefault)}form .formControl input:not([type=submit]):focus+i.fa-eye,form .formControl input:not([type=submit]):focus+i.fa-eye-slash{color:var(--primaryHover)}form .formControl .checkContainer{display:block;position:relative;margin-bottom:1.25em;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}form .formControl .checkContainer input{position:absolute;opacity:0;cursor:pointer;height:0;width:0}form .formControl .checkContainer .checkmark{position:absolute;top:1.25em;left:0;height:25px;width:25px;background-color:var(--mutedGrey)}form .formControl .checkContainer:hover input~.checkmark{background-color:var(--grey)}form .formControl .checkContainer input:checked~.checkmark{background-color:var(--primaryDefault)}form .formControl .checkContainer input:checked:hover~.checkmark{background-color:var(--primaryHover)}form .formControl .checkContainer .checkmark:after{content:"";position:absolute;display:none}form .formControl .checkContainer input:checked~.checkmark:after{display:block}form .formControl .checkContainer .checkmark:after{left:9px;top:5px;width:5px;height:10px;border:solid #fff;border-width:0 3px 3px 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}form .formControl input[type=file]{padding:0;cursor:pointer}form .formControl input[type=file]::file-selector-button{background-color:var(--primaryDefault);color:#fff;border:0;border-right:5px solid var(--mutedBlack);padding:15px;margin-right:20px;-webkit-transition:all .5s;-moz-transition:all .5s;-ms-transition:all .5s;-o-transition:all .5s;transition:all .5s}form .formControl input[type=file]:hover::file-selector-button{background-color:var(--primaryHover)}section#about,section#curriculumVitae h1{padding:0 5rem}header{background:#6a6a6a url(../imgs/hero.jpg) no-repeat bottom;background-size:cover;height:40%;color:#fff;backdrop-filter:grayscale(100%);position:relative}nav{display:flex;flex-direction:row;justify-content:space-between;padding:.25em;position:fixed;top:0;width:100%;transition:background-color .4s ease-in;color:#fff;z-index:1}nav.scrolled{background-color:var(--navBack)}nav #nav-check{display:none}nav .nav-btn{display:none}nav h1{margin:0}nav a{text-decoration:none;color:#fff}nav ul{display:flex;flex-direction:row;gap:1em;margin:0;justify-content:flex-end;align-items:flex-end}nav ul li{list-style:none}nav ul li span{visibility:hidden}nav ul li .active span,nav ul li a:hover span{visibility:visible}header div{display:flex;flex-direction:column;justify-content:center;align-items:center;padding-top:10em}header div .btn{margin:2em 0}header div button{background:0 0;border:none;display:inline-block;text-align:center;text-decoration:none;font-size:2rem;cursor:pointer}i.fa-chevron-down{color:hsla(0,0%,67%,.58);font-size:3.75em;margin:1.5rem 0}div h1 span{visibility:visible;animation:caret 1s steps(1) infinite}@keyframes caret{50%{visibility:hidden}}section#about{margin-bottom:5rem}section#about div{padding:.1em 5em}section#curriculumVitae{background-color:var(--primaryDefault);color:#fff;padding:2em 0}section#curriculumVitae .cvGrid{display:flex;flex-direction:row;padding:0 1.5rem;flex-wrap:wrap}section#curriculumVitae .cvGrid>div{width:45%;display:flex;flex-direction:column;min-height:100%}section#curriculumVitae .cvGrid h2{text-align:center}section#curriculumVitae .timeline{position:relative;max-width:30em;gap:1em;display:flex;flex-direction:column;height:100%}section#curriculumVitae #work{margin:0 auto 0 8rem}section#curriculumVitae .timeline:before{content:"";position:absolute;height:100%;border:4px var(--timelineItemBrdr) solid;right:194px;top:0}section#curriculumVitae .timeline:after{content:"";display:table;clear:both}section#curriculumVitae .timelineItem{border:2px solid var(--timelineItemBrdr);-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;padding:0 1rem;width:50%;position:relative;background-color:var(--primaryHover)}.timelineItem:after,section#curriculumVitae .timelineItem:before{content:'';position:absolute}section#curriculumVitae .timelineItem:before{content:'';right:-20px;top:calc(50% - 5px);border-style:solid;border-color:var(--timelineItemBrdr) var(--timelineItemBrdr) transparent transparent;border-width:20px;transform:rotate(45deg)}section#curriculumVitae .timelineItem:nth-child(2n){margin-left:21em}section#curriculumVitae .timelineItem:nth-child(2n):before{right:auto;left:-20px;border-color:transparent transparent var(--timelineItemBrdr) var(--timelineItemBrdr)}section#curriculumVitae .timelineItem h3{font-weight:400}section#curriculumVitae .timelineItem span{color:#e5e5e5}section#projects{display:flex;flex-direction:row;padding:0 2.5rem;border-bottom:2px solid var(--mutedGrey)}section#projects .mainProj,section#projects .otherProj{width:50%;display:flex;flex-direction:column;align-items:center;gap:1em}section#projects .mainProj{border-right:2px solid var(--mutedGrey);padding:0 2.5em 5em 0}section#allProjects img,section#projects img{-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;display:block;margin:1em auto}section#projects .mainProj img{width:100%;max-width:40rem}section#projects .mainProj .flexRow{display:flex;flex-direction:row;gap:4em}section#projects .mainProj .flexCol{display:flex;flex-direction:column;gap:2.5em}section#projects .otherProj>a{margin:5rem 0}section#projects .otherProj>div{display:flex;flex-direction:column;gap:2em}section#allProjects #otherProj .oProjItem,section#projects .otherProj>div .oProjItem{display:flex;justify-content:center;align-items:center;flex-direction:row;width:90%;border:1px solid var(--grey);gap:1em;box-shadow:0 6px 4px 0 var(--mutedBlack);-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;padding:.75em 1em}section#projects .otherProj>div .oProjItem{margin:0 auto}section#projects .otherProj>div .oProjItem:nth-child(2){flex-direction:row-reverse}section#projects .otherProj .oProjItem img{max-width:15rem;width:100%;padding:0 1em}section#projects .oProjItem .flexCol div:nth-child(2){display:flex;flex-direction:row;justify-content:flex-start;gap:3em;margin-left:2em}section#projects .flexCol div:nth-child(2) .btn{padding:.25em .5em}section#allProjects{display:flex;justify-content:center;align-items:center;flex-direction:column;gap:5em}section#allProjects #mainProj{display:flex;flex-direction:column;justify-content:center;align-items:center;border:1px solid var(--grey);box-shadow:0 6px 4px 0 var(--mutedBlack);-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;padding:1.5em 2em;margin:3em 2.5rem 0;width:50%}section#allProjects #mainProj img{width:100%;max-width:30rem}section#allProjects #otherProj{display:flex;flex-direction:row;justify-content:space-between;align-items:stretch;flex-wrap:wrap;gap:2rem;border-top:2px solid var(--grey);padding:5em 2.5rem 0}section#allProjects #otherProj .oProjItem{flex-direction:column;width:30%;height:auto}section#allProjects #otherProj img{width:100%;max-width:20rem}section#contact{display:flex;flex-direction:row;padding:0 2.5em}div#findMe,div#sayHello{width:50%;display:flex;flex-direction:column;align-items:center;gap:1em}div#findMe .findMeContainer{display:flex;flex-direction:row;justify-content:space-around;align-items:center;gap:2em;width:100%;height:100%;margin:5em 0}div#findMe .findMeContainer .profile{-webkit-border-radius:50%;-moz-border-radius:50%;border-radius:50%}div#findMe .socialIcons{display:flex;flex-direction:row;justify-content:center;align-items:center;gap:2em}div#findMe .socialIcons div{display:flex;flex-direction:column;gap:1.5em}div#findMe .socialIcons div svg{width:2.5em;fill:var(--primaryDefault);font-size:2em}div#findMe .socialIcons div a:hover svg{fill:var(--primaryHover)}div#sayHello #contactForm{display:flex;flex-direction:column;justify-content:center;align-items:center}#contactForm .flName{display:flex;flex-direction:row;gap:1em;width:100%}div.message{background:var(--primaryDefault);color:#fff;padding:.5em .8em;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;display:flex;justify-content:center;align-items:center;align-self:flex-start;flex-direction:row-reverse;position:relative;height:75px;visibility:visible;overflow:hidden;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;opacity:1;margin-top:1em;margin-bottom:1em}div.message.error{background:var(--errorDefault)}div.message button{border:none;background:0 0;outline:0;cursor:pointer;color:#fff;font-size:1.25rem;margin-top:-5px;position:absolute;transform:translate(0,0);transform-origin:0 0;right:10px;top:10px}div.message.hidden{opacity:0;visibility:hidden;height:0}div.message button:hover{text-shadow:-1px 2px var(--mutedBlack)}footer{background-color:var(--primaryDefault);margin-top:5em;padding:2em;display:flex;color:#fff}footer .spacer{width:100%;margin-right:auto}footer p{margin:auto;width:100%;text-align:center}footer .button{margin-left:auto;width:100%;text-align:center}footer .button button{border:5px solid #fff;background:0 0;font-size:3em;padding:.5rem 1rem;width:2em;color:#fff;-webkit-border-radius:.25em;-moz-border-radius:.25em;border-radius:.25em;cursor:pointer}@media screen and (max-width:90em){section#curriculumVitae .cvGrid{flex-direction:column;justify-content:center;align-items:center}section#curriculumVitae .cvGrid>div{width:100%}section#curriculumVitae .cvGrid>div:first-child{padding-bottom:2.5em;margin-bottom:2.5em;border-bottom:5px #fff solid}section#curriculumVitae .cvGrid h2{margin-left:5em}section#curriculumVitae .cvGrid .timeline{margin:0 auto}}@media screen and (max-width:75em){section#about,section#curriculumVitae h1{padding:0 1em}nav{display:block;height:50px;width:100%;background-color:var(--navBack);position:fixed;top:0;padding:0}nav a h1{margin-left:1ch}nav .nav-btn{display:inline-block;position:absolute;right:75px;top:0}nav ul{position:fixed;display:block;width:100%;background-color:#333;transition:all .4s ease-in;overflow-y:hidden;padding-left:0;margin-top:7px}nav ul li a{display:block;width:100%;transform:translateX(-30px);transition:all .4s ease-in;opacity:0}.nav-btn label{display:inline-block;cursor:pointer;width:60px;height:50px;position:fixed;-webkit-transform:rotate(0);-moz-transform:rotate(0);-o-transform:rotate(0);transform:rotate(0);-webkit-transition:.5s ease-in;-moz-transition:.5s ease-in;-o-transition:.5s ease-in;transition:.5s ease-in}.nav-btn label span{display:block;position:absolute;height:5px;width:100%;background-color:#fff;opacity:1;right:0;top:20px;-webkit-transform:rotate(0);-moz-transform:rotate(0);-o-transform:rotate(0);transform:rotate(0);-webkit-transition:.25s ease-in;-moz-transition:.25s ease-in;-o-transition:.25s ease-in;transition:.25s ease-in}nav #nav-check:not(:checked)~ul{height:auto;max-height:0}nav #nav-check:not(:checked)~.nav-btn label span:nth-child(1){top:8px;-webkit-transform-origin:left center;-moz-transform-origin:left center;-o-transform-origin:left center;transform-origin:left center}nav #nav-check:not(:checked)~.nav-btn label span:nth-child(2){top:23px;-webkit-transform-origin:left center;-moz-transform-origin:left center;-o-transform-origin:left center;transform-origin:left center}nav #nav-check:not(:checked)~.nav-btn label span:nth-child(3){top:38px;-webkit-transform-origin:left center;-moz-transform-origin:left center;-o-transform-origin:left center;transform-origin:left center}nav #nav-check:checked~.nav-btn label,nav .nav-btn label:hover{background-color:rgba(-1,0,0,.3)}nav #nav-check:checked~ul{max-height:50vh;overflow-y:hidden}nav #nav-check:checked~ul li a{opacity:1;transform:translateX(0)}nav #nav-check:checked~ul li:nth-child(1) a{transition-delay:.15s}nav #nav-check:checked~ul li:nth-child(2) a{transition-delay:.25s}nav #nav-check:checked~ul li:nth-child(3) a{transition-delay:.35s}nav #nav-check:checked~ul li:nth-child(4) a{transition-delay:.45s}nav #nav-check:checked~ul li:nth-child(5) a{transition-delay:.55s}nav #nav-check:checked~.nav-btn label span:first-child{-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}nav #nav-check:checked~.nav-btn label span:nth-child(2){width:0;opacity:0}nav #nav-check:checked~.nav-btn label span:last-child{-webkit-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-o-transform:rotate(-45deg);transform:rotate(-45deg)}section#about div{padding:.1em 2.5em}section#curriculumVitae .cvGrid{padding:0}section#projects{flex-direction:column;justify-content:center;align-items:center}section#projects .mainProj{border-right:0;padding:0;width:100%;margin:0 5em}section#projects .mainProj img{padding:0 1em}section#projects .mainProj .flexRow{flex-direction:column;margin:0 2.5em}section#projects .mainProj .flexCol{flex-direction:row;justify-content:center;align-items:center}section#projects .otherProj{width:100%}section#projects .otherProj .btn{width:10em;text-align:center}section#projects .otherProj>div .oProjItem,section#projects .otherProj>div .oProjItem:nth-child(2){flex-direction:column}section#projects .oProjItem .flexCol div:nth-child(2){justify-content:center;margin-left:0;margin-bottom:1em}section#projects .otherProj>a{margin-left:3em;margin-right:3em;text-align:center}section#allProjects #otherProj .oProjItem{width:45%}section#contact{flex-direction:column;justify-content:center;align-items:center}div#findMe,div#sayHello{width:100%}div#findMe .findMeContainer{flex-direction:column;justify-content:center}div#findMe .socialIcons{flex-direction:row}div#findMe .socialIcons div{flex-direction:row}}@media screen and (max-width:55em){section#curriculumVitae .cvGrid .timeline,section#curriculumVitae .cvGrid .timeline#work{margin:0 auto;width:100%}section#curriculumVitae .timeline:before{border:none}section#curriculumVitae .timelineItem,section#curriculumVitae .timelineItem:nth-child(2n){width:95%;padding:0;margin:0 auto}section#curriculumVitae .timelineItem:before{right:unset;left:unset;border:none}section#allProjects #mainProj{width:auto}section#allProjects #otherProj{flex-direction:column;align-items:center}section#allProjects #otherProj .oProjItem{width:auto}div#findMe .socialIcons{flex-direction:column}#contactForm .flName{flex-direction:column;gap:0;width:100%}}@media screen and (max-width:31em){section#about,section#curriculumVitae h1{padding:0 1em}header div h1{text-align:center;height:5.125rem}section#about div{padding:.1em 1em}section#projects .mainProj .flexCol{flex-direction:column}section#projects .oProjItem .flexCol div:nth-child(2){flex-direction:column;justify-content:center;align-items:center}}
\ No newline at end of file
diff --git a/dist/editor/css/main.css b/dist/editor/css/main.css
index bb91664..8299759 100644
--- a/dist/editor/css/main.css
+++ b/dist/editor/css/main.css
@@ -1 +1 @@
-/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}:root{--mainHue:80;--mainSat:60%;--mainLight:50%;--primaryDefault:hsla(var(--mainHue), var(--mainSat), var(--mainLight), 1);--primaryHover:hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) - 10%), 1);--timelineItemBrdr:hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) - 20%), 1);--errorDefault:hsla(0, calc(var(--mainSat) + 10%),calc(var(--mainLight) + 10%), 1);--errorHover:hsla(0, calc(var(--mainSat) + 10%), calc(var(--mainLight) - 10%), 1);--grey:hsla(0, 0%, 39%, 1);--notAvailableDefault:hsla(0, 0%, 39%, 1);--notAvailableHover:hsla(0, 0%,32%, 1);--mutedGrey:hsla(0, 0%, 78%, 1);--mutedBlack:hsla(0, 0%, 0%, 0.25);--navBack:hsla(0, 0%, 30%, 1);--titleFS:2.25rem;--generalFS:1.125rem;--headingFS:1.5rem}*{box-sizing:border-box}html{scroll-behavior:smooth}body{font-family:Noto Sans KR,sans-serif;font-style:normal;font-weight:500;font-size:var(--generalFS);line-height:1.625rem}a:visited{color:inherit}h1,nav{font-family:Share Tech Mono,monospace;font-style:normal;font-weight:400;font-size:var(--titleFS);line-height:2.5625rem;text-transform:lowercase}h2{font-family:Noto Sans KR,sans-serif;font-style:normal;font-weight:500;font-size:var(--headingFS);line-height:2.1875rem}a.btn,form input[type=submit]{text-decoration:none;display:inline-flex;padding:1em 2em;border-radius:.625em;border:.3215em solid var(--primaryDefault);color:#fff;text-align:center;align-items:center;max-height:4em}form input[type=submit]{padding:1.1em 2em}a.btn:hover,form input[type=submit]:hover{border:.3215em solid var(--primaryHover)}a.btnPrimary,form input[type=submit]{background-color:var(--primaryDefault);cursor:pointer}a.btnOutline{background:#fff;color:var(--primaryDefault)}a.btnPrimary[disabled]{pointer-events:none;background:var(--notAvailableDefault);border:.3215em solid var(--notAvailableDefault)}a.btnPrimary[disabled]:hover{background:var(--notAvailableHover);border:.3215em solid var(--notAvailableHover)}a.btnPrimary:hover,form input[type=submit]:hover{background:var(--primaryHover)}a.btn:active,form input[type=submit]:active{padding:.8rem 1.8rem}.boxShadowOut:hover{box-shadow:0 6px 4px 0 var(--mutedBlack)}.boxShadowIn:active{box-shadow:inset 0 6px 4px 0 var(--mutedBlack)}.textShadow:hover{text-shadow:0 6px 4px var(--mutedBlack)}form .formControl input:not([type=submit]).invalid:invalid,form .formControl textarea.invalid:invalid{border:4px solid var(--errorDefault)}form .formControl input:not([type=submit]).invalid:invalid:focus,form .formControl textarea.invalid:invalid:focus{border:4px 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}form .formControl{width:100%;display:flex;flex-direction:column;justify-content:flex-start}form .formControl.passwordControl{display:block}form input[type=submit]{align-self:flex-start}form .formControl .ck.ck-editor__main .ck-content,form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar,form .formControl input:not([type=submit]),form .formControl textarea{width:100%;border:4px solid var(--primaryDefault);background:0 0;outline:0;-webkit-border-radius:1em;-moz-border-radius:1em;border-radius:.5em;padding:0 .5em}form .formControl textarea{padding:.5em}form .formControl input:not([type=submit]).invalid:invalid,form .formControl textarea.invalid:invalid{border:4px solid var(--errorDefault)}form .formControl input:not([type=submit]).invalid:invalid:focus,form .formControl textarea.invalid:invalid:focus{border:4px solid var(--errorHover);box-shadow:0 4px 2px 0 var(--mutedBlack)}form .formControl input:not([type=submit]):focus,form .formControl input:not([type=submit]):hover,form .formControl textarea:focus,form .formControl textarea:hover{border:4px solid var(--primaryHover)}form .formControl input:not([type=submit]){height:3em}form .formControl i.fa-eye,form .formControl i.fa-eye-slash{margin-left:-40px;cursor:pointer;color:var(--primaryDefault)}form .formControl input:not([type=submit]):focus+i.fa-eye,form .formControl input:not([type=submit]):focus+i.fa-eye-slash{color:var(--primaryHover)}form .formControl .checkContainer{display:block;position:relative;margin-bottom:1.25em;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}form .formControl .checkContainer input{position:absolute;opacity:0;cursor:pointer;height:0;width:0}form .formControl .checkContainer .checkmark{position:absolute;top:1.25em;left:0;height:25px;width:25px;background-color:var(--mutedGrey)}form .formControl .checkContainer:hover input~.checkmark{background-color:var(--grey)}form .formControl .checkContainer input:checked~.checkmark{background-color:var(--primaryDefault)}form .formControl .checkContainer input:checked:hover~.checkmark{background-color:var(--primaryHover)}form .formControl .checkContainer .checkmark:after{content:"";position:absolute;display:none}form .formControl .checkContainer input:checked~.checkmark:after{display:block}form .formControl .checkContainer .checkmark:after{left:9px;top:5px;width:5px;height:10px;border:solid #fff;border-width:0 3px 3px 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}form .formControl input[type=file]{padding:0;cursor:pointer}form .formControl input[type=file]::file-selector-button{background-color:var(--primaryDefault);color:#fff;border:0;border-right:5px solid var(--mutedBlack);padding:15px;margin-right:20px;-webkit-transition:all .5s;-moz-transition:all .5s;-ms-transition:all .5s;-o-transition:all .5s;transition:all .5s}form .formControl input[type=file]:hover::file-selector-button{background-color:var(--primaryHover)}section#about,section#curriculumVitae h1{padding:0 5rem}h1{text-transform:none}body,html{height:100%}main.login{height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;background-image:radial-gradient(var(--primaryDefault),#597226)}div.container{flex-direction:column;justify-content:center;align-items:center;background-color:#fff;padding:2em 5em;-webkit-border-radius:1em;-moz-border-radius:1em;border-radius:1em;box-shadow:0 6px 4px 0 var(--mutedBlack);-webkit-transform:translateX(-150vw);-moz-transform:translateX(-150vw);-ms-transform:translateX(-150vw);-o-transform:translateX(-150vw);transform:translateX(-150vw);-webkit-transition:transform .4s ease-in-out;-moz-transition:transform .4s ease-in-out;-ms-transition:transform .4s ease-in-out;-o-transition:transform .4s ease-in-out;transition:transform .4s ease-in-out;overflow:hidden}div.container.shown{-webkit-transform:translateX(0);-moz-transform:translateX(0);-ms-transform:translateX(0);-o-transform:translateX(0);transform:translateX(0)}div.container form{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:1em}div#login #password{font-family:Verdana,serif;letter-spacing:.125em}div#login input[type=submit]{margin:0}div.error,div.success{color:#fff;padding:.5em .8em;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;display:flex;justify-content:center;align-items:center;align-self:flex-start;flex-direction:row-reverse;position:relative;height:75px;visibility:visible;overflow:hidden;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;opacity:1;margin-top:1em}div.error{background:var(--errorDefault)}div.success{background-color:var(--primaryHover)}div.error button,div.success button{border:none;background:0 0;outline:0;cursor:pointer;color:#fff;font-size:1.25rem;margin-top:-5px;position:absolute;transform:translate(0,0);transform-origin:0 0;right:10px;top:10px}div.error.hidden,div.success.hidden{opacity:0;visibility:hidden;height:0;margin:0;padding:0}div.error button:hover,div.success button:hover{text-shadow:-1px 2px var(--mutedBlack)}div.btnContainer{width:100%;display:flex;flex-direction:row;justify-content:space-between;align-items:center}div.btnContainer a:not(.btn){color:#000}nav{font-size:var(--headingFS)}nav.sideNav{height:100%;width:250px;z-index:1;position:fixed;top:0;left:0;background-color:var(--primaryHover);overflow-x:hidden;-webkit-transition:.5s;-moz-transition:.5s;-ms-transition:.5s;-o-transition:.5s;transition:.5s;padding-top:60px}nav.sideNav ul li{list-style:none}nav.sideNav a{padding:8px 8px 8px 0;text-decoration:none;color:#fff;display:block;-webkit-transition:.3s;-moz-transition:.3s;-ms-transition:.3s;-o-transition:.3s;transition:.3s}nav.sideNav .closeBtn{position:absolute;top:0;right:25px;margin-left:50px;font-size:var(--titleFS);display:none}nav.sideNav ul li span{visibility:hidden}nav.sideNav ul li a.active span,nav.sideNav ul li a:hover span{visibility:visible}nav.sideNav ul li.dropdown ul{transition:max-height ease-out .4s;max-height:0;overflow:hidden}nav.sideNav ul li.dropdown ul.active{transition:max-height ease-in .4s;max-height:15rem}nav.sideNav ul li.dropdown ul li{margin-left:-1rem}span#navOpen{font-size:var(--titleFS);cursor:pointer}main.editor{margin-left:250px}.title{display:flex;flex-direction:column;justify-content:center;align-items:center}#navOpen{visibility:hidden;padding:.25em 0 0 .25em;align-self:flex-start}textarea{resize:none}main.editor section{margin:0 2em}input[type=submit]{margin-top:2em}.delete,.edit{border:none;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;outline:0;background-color:var(--primaryDefault);color:#fff;cursor:pointer}.timelineHeader{font-weight:400}div.editorContainer,div.projectsGrid{display:flex;flex-direction:row;justify-content:center;align-items:flex-start;gap:2em;margin-bottom:.5em}div.editorContainer>*,div.projectsGrid>*{width:45%}main.editor section{display:none}section#addPost{display:block}div.modifyBtnContainer{display:flex;flex-direction:row;justify-content:space-between;align-items:center;margin-bottom:.5em;width:100%}div.companyAreaContainer,div.dateContainer{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:1em;margin-bottom:.5em}section#curriculumVitae .timeline{position:relative;max-width:30em;gap:1em;display:flex;flex-direction:column;height:100%}section#curriculumVitae .timelineItem,section#projects .projItem{-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;padding:1rem;position:relative}section#curriculumVitae .timelineItem{border:2px solid var(--timelineItemBrdr);color:#fff;background-color:var(--primaryHover)}section#curriculumVitae .timelineItem.editing{color:#000;border:5px solid var(--primaryDefault);padding:.5em}section#curriculumVitae .timelineItem.editing{background-color:#fff}form div.gradeContainer.formControl{display:flex;flex-direction:row;justify-content:flex-start;align-items:center}section#curriculumVitae form.timelineItem:not(.editing) .delete,section#curriculumVitae form.timelineItem:not(.editing) .edit{color:var(--primaryHover);background-color:#fff}section#curriculumVitae form.timelineItem:not(.editing) div.dateContainer{display:none}section#curriculumVitae form.timelineItem.editing .timelineHeader{display:none}section#curriculumVitae form.timelineItem.editing div.gradeContainer.formControl{gap:1em;margin-bottom:.5em}section#curriculumVitae form.timelineItem:not(.editing) .formControl .courseText,section#curriculumVitae form.timelineItem:not(.editing) .formControl .jobTitleText,section#curriculumVitae form.timelineItem:not(.editing) div.companyAreaContainer.formControl input,section#curriculumVitae form.timelineItem:not(.editing) div.gradeContainer.formControl input,section#projects form.projItem:not(.editing) div.formControl.infoContainer textarea,section#projects form.projItem:not(.editing) div.formControl.projectTitleContainer input{outline:0;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;resize:none}section#curriculumVitae form.timelineItem:not(.editing) div.companyAreaContainer.formControl>*,section#curriculumVitae form.timelineItem:not(.editing) div.gradeContainer.formControl>*{color:#e5e5e5}section#curriculumVitae form.timelineItem:not(.editing) .formControl .courseText,section#curriculumVitae form.timelineItem:not(.editing) .formControl .jobTitleText{color:#fff}section#curriculumVitae form.timelineItem:not(.editing) div.gradeContainer.formControl input{padding:0 .25em}section#curriculumVitae form.timelineItem:not(.editing) .formControl .courseText,section#curriculumVitae form.timelineItem:not(.editing) div.formControl .courseText{padding:0}section#curriculumVitae form.timelineItem:not(.editing) input[type=submit]{display:none}.courseText{resize:none}section#curriculumVitae form.timelineItem:not(.editing) div.companyAreaContainer input{width:30%}section#projects #projList .projItem{display:flex;justify-content:center;align-items:center;flex-direction:column;margin:0 auto;width:90%;border:1px solid var(--grey);gap:1em;box-shadow:0 6px 4px 0 var(--mutedBlack)}section#projects #projList{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:1em}section#projects #projList .projItem img{max-width:15rem;width:100%;padding:0 1em}section#projects .projItem .linkContainer{display:flex;flex-direction:row;justify-content:flex-start;gap:3em;margin-left:2em}section#projects .linkContainer .btn{padding:.25em .5em}section#projects #isMainProject{width:auto}section#projects form.projItem.editing div.linkContainer,section#projects form.projItem.editing img.displayedImage,section#projects form.projItem:not(.editing) div.formControl.gitContainer,section#projects form.projItem:not(.editing) div.formControl.imageContainer,section#projects form.projItem:not(.editing) div.formControl.isMainProject,section#projects form.projItem:not(.editing) div.formControl.viewProjContainer,section#projects form.projItem:not(.editing) input[type=submit]{display:none}section#projects form.projItem:not(.editing) div.formControl.projectTitleContainer input{font-size:1.17em;font-weight:700}section#projects form.projItem:not(.editing) div.formControl.infoContainer textarea,section#projects form.projItem:not(.editing) div.formControl.projectTitleContainer input{color:#000}section#addPost form{margin:auto 4rem}form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar{border-bottom-right-radius:0;border-bottom-left-radius:0;border-bottom:2px solid var(--mutedGrey);box-shadow:0 3px 4px var(--mutedBlack)}form .formControl .ck.ck-editor__main .ck-content{border-top-right-radius:0;border-top-left-radius:0;border-top:inherit}form .formControl .ck-editor__editable{min-height:400px}@media only screen and (max-width:75em){nav.sideNav .closeBtn{display:block}}
\ No newline at end of file
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}:root{--mainHue:80;--mainSat:60%;--mainLight:50%;--primaryDefault:hsla(var(--mainHue), var(--mainSat), var(--mainLight), 1);--primaryHover:hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) - 10%), 1);--timelineItemBrdr:hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) - 20%), 1);--errorDefault:hsla(0, calc(var(--mainSat) + 10%), calc(var(--mainLight) + 10%), 1);--errorHover:hsla(0, calc(var(--mainSat) + 10%), calc(var(--mainLight) - 10%), 1);--grey:hsla(0, 0%, 39%, 1);--notAvailableDefault:hsla(0, 0%, 39%, 1);--notAvailableHover:hsla(0, 0%, 32%, 1);--mutedGrey:hsla(0, 0%, 78%, 1);--mutedBlack:hsla(0, 0%, 0%, 0.25);--mutedGreen:hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) + 20%), 0.5);--navBack:hsla(0, 0%, 30%, 1);--titleFS:2.25rem;--generalFS:1.125rem;--headingFS:1.5rem}*{box-sizing:border-box}html{scroll-behavior:smooth}body{font-family:Noto Sans KR,sans-serif;font-style:normal;font-weight:500;font-size:var(--generalFS);line-height:1.625rem}a:visited{color:inherit}h1,nav{font-family:Share Tech Mono,monospace;font-style:normal;font-weight:400;font-size:var(--titleFS);line-height:2.5625rem;text-transform:lowercase}h2{font-family:Noto Sans KR,sans-serif;font-style:normal;font-weight:500;font-size:var(--headingFS);line-height:2.1875rem}a.btn,form input[type=submit]{text-decoration:none;display:inline-flex;padding:1em 2em;border-radius:.625em;border:.3215em solid var(--primaryDefault);color:#fff;text-align:center;align-items:center;max-height:4em}form input[type=submit]{padding:1.1em 2em}a.btn:hover,form input[type=submit]:hover{border:.3215em solid var(--primaryHover)}a.btnPrimary,form input[type=submit]{background-color:var(--primaryDefault);cursor:pointer}a.btnOutline{background:#fff;color:var(--primaryDefault)}a.btnPrimary[disabled]{pointer-events:none;background:var(--notAvailableDefault);border:.3215em solid var(--notAvailableDefault)}a.btnPrimary[disabled]:hover{background:var(--notAvailableHover);border:.3215em solid var(--notAvailableHover)}a.btnPrimary:hover,form input[type=submit]:hover{background:var(--primaryHover)}a.btn:active,form input[type=submit]:active{padding:.8rem 1.8rem}.boxShadowOut:hover{box-shadow:0 6px 4px 0 var(--mutedBlack)}.boxShadowIn:active{box-shadow:inset 0 6px 4px 0 var(--mutedBlack)}.textShadow:hover{text-shadow:0 6px 4px var(--mutedBlack)}form .formControl input:not([type=submit]).invalid:invalid,form .formControl textarea.invalid:invalid{border:4px solid var(--errorDefault)}form .formControl input:not([type=submit]).invalid:invalid:focus,form .formControl textarea.invalid:invalid:focus{border:4px 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}form .formControl{width:100%;display:flex;flex-direction:column;justify-content:flex-start}form .formControl.passwordControl{display:block}form input[type=submit]{align-self:flex-start}form .formControl .ck.ck-editor__main .ck-content,form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar,form .formControl input:not([type=submit]),form .formControl textarea{width:100%;border:4px solid var(--primaryDefault);background:0 0;outline:0;-webkit-border-radius:1em;-moz-border-radius:1em;border-radius:.5em;padding:0 .5em}form .formControl textarea{padding:.5em}form .formControl input:not([type=submit]).invalid:invalid,form .formControl textarea.invalid:invalid{border:4px solid var(--errorDefault)}form .formControl input:not([type=submit]).invalid:invalid:focus,form .formControl textarea.invalid:invalid:focus{border:4px solid var(--errorHover);box-shadow:0 4px 2px 0 var(--mutedBlack)}form .formControl input:not([type=submit]):focus,form .formControl input:not([type=submit]):hover,form .formControl textarea:focus,form .formControl textarea:hover{border:4px solid var(--primaryHover)}form .formControl input:not([type=submit]){height:3em}form .formControl i.fa-eye,form .formControl i.fa-eye-slash{margin-left:-40px;cursor:pointer;color:var(--primaryDefault)}form .formControl input:not([type=submit]):focus+i.fa-eye,form .formControl input:not([type=submit]):focus+i.fa-eye-slash{color:var(--primaryHover)}form .formControl .checkContainer{display:block;position:relative;margin-bottom:1.25em;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}form .formControl .checkContainer input{position:absolute;opacity:0;cursor:pointer;height:0;width:0}form .formControl .checkContainer .checkmark{position:absolute;top:1.25em;left:0;height:25px;width:25px;background-color:var(--mutedGrey)}form .formControl .checkContainer:hover input~.checkmark{background-color:var(--grey)}form .formControl .checkContainer input:checked~.checkmark{background-color:var(--primaryDefault)}form .formControl .checkContainer input:checked:hover~.checkmark{background-color:var(--primaryHover)}form .formControl .checkContainer .checkmark:after{content:"";position:absolute;display:none}form .formControl .checkContainer input:checked~.checkmark:after{display:block}form .formControl .checkContainer .checkmark:after{left:9px;top:5px;width:5px;height:10px;border:solid #fff;border-width:0 3px 3px 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}form .formControl input[type=file]{padding:0;cursor:pointer}form .formControl input[type=file]::file-selector-button{background-color:var(--primaryDefault);color:#fff;border:0;border-right:5px solid var(--mutedBlack);padding:15px;margin-right:20px;-webkit-transition:all .5s;-moz-transition:all .5s;-ms-transition:all .5s;-o-transition:all .5s;transition:all .5s}form .formControl input[type=file]:hover::file-selector-button{background-color:var(--primaryHover)}section#about,section#curriculumVitae h1{padding:0 5rem}h1{text-transform:none}body,html{height:100%}main.login{height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;background-image:radial-gradient(var(--primaryDefault),#597226)}div.container{flex-direction:column;justify-content:center;align-items:center;background-color:#fff;padding:2em 5em;-webkit-border-radius:1em;-moz-border-radius:1em;border-radius:1em;box-shadow:0 6px 4px 0 var(--mutedBlack);-webkit-transform:translateX(-150vw);-moz-transform:translateX(-150vw);-ms-transform:translateX(-150vw);-o-transform:translateX(-150vw);transform:translateX(-150vw);-webkit-transition:transform .4s ease-in-out;-moz-transition:transform .4s ease-in-out;-ms-transition:transform .4s ease-in-out;-o-transition:transform .4s ease-in-out;transition:transform .4s ease-in-out;overflow:hidden}div.container.shown{-webkit-transform:translateX(0);-moz-transform:translateX(0);-ms-transform:translateX(0);-o-transform:translateX(0);transform:translateX(0)}div.container form{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:1em}div#login #password{font-family:Verdana,serif;letter-spacing:.125em}div#login input[type=submit]{margin:0}div.error,div.success{color:#fff;padding:.5em .8em;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;display:flex;justify-content:center;align-items:center;align-self:flex-start;flex-direction:row-reverse;position:relative;height:75px;visibility:visible;overflow:hidden;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;opacity:1;margin-top:1em}div.error{background:var(--errorDefault)}div.success{background-color:var(--primaryHover)}div.error button,div.success button{border:none;background:0 0;outline:0;cursor:pointer;color:#fff;font-size:1.25rem;margin-top:-5px;position:absolute;transform:translate(0,0);transform-origin:0 0;right:10px;top:10px}div.error.hidden,div.success.hidden{opacity:0;visibility:hidden;height:0;margin:0;padding:0}div.error button:hover,div.success button:hover{text-shadow:-1px 2px var(--mutedBlack)}div.btnContainer{width:100%;display:flex;flex-direction:row;justify-content:space-between;align-items:center}div.btnContainer a:not(.btn){color:#000}nav{font-size:var(--headingFS)}nav.sideNav{height:100%;width:250px;z-index:1;position:fixed;top:0;left:0;background-color:var(--primaryHover);overflow-x:hidden;-webkit-transition:.5s;-moz-transition:.5s;-ms-transition:.5s;-o-transition:.5s;transition:.5s;padding-top:60px}nav.sideNav ul li{list-style:none}nav.sideNav a{padding:8px 8px 8px 0;text-decoration:none;color:#fff;display:block;-webkit-transition:.3s;-moz-transition:.3s;-ms-transition:.3s;-o-transition:.3s;transition:.3s}nav.sideNav .closeBtn{position:absolute;top:0;right:25px;margin-left:50px;font-size:var(--titleFS);display:none}nav.sideNav ul li span{visibility:hidden}nav.sideNav ul li a.active span,nav.sideNav ul li a:hover span{visibility:visible}nav.sideNav ul li.dropdown ul{transition:max-height ease-out .4s;max-height:0;overflow:hidden}nav.sideNav ul li.dropdown ul.active{transition:max-height ease-in .4s;max-height:15rem}nav.sideNav ul li.dropdown ul li{margin-left:-1rem}span#navOpen{font-size:var(--titleFS);cursor:pointer}main.editor{margin-left:250px}.title{display:flex;flex-direction:column;justify-content:center;align-items:center}#navOpen{visibility:hidden;padding:.25em 0 0 .25em;align-self:flex-start}textarea{resize:none}main.editor section{margin:0 2em}input[type=submit]{margin-top:2em}.delete,.edit{border:none;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;outline:0;background-color:var(--primaryDefault);color:#fff;cursor:pointer}.timelineHeader{font-weight:400}div.editorContainer,div.projectsGrid{display:flex;flex-direction:row;justify-content:center;align-items:flex-start;gap:2em;margin-bottom:.5em}div.editorContainer>*,div.projectsGrid>*{width:45%}main.editor section{display:none;flex-direction:column}section#editPost{display:flex}div.modifyBtnContainer{display:flex;flex-direction:row;justify-content:space-between;align-items:center;margin-bottom:.5em;width:100%}div.companyAreaContainer,div.dateContainer{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:1em;margin-bottom:.5em}section#curriculumVitae .timeline{position:relative;max-width:30em;gap:1em;display:flex;flex-direction:column;height:100%}section#curriculumVitae .timelineItem,section#projects .projItem{-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;padding:1rem;position:relative}section#curriculumVitae .timelineItem{border:2px solid var(--timelineItemBrdr);color:#fff;background-color:var(--primaryHover)}section#curriculumVitae .timelineItem.editing{color:#000;border:5px solid var(--primaryDefault);padding:.5em}section#curriculumVitae .timelineItem.editing{background-color:#fff}form div.gradeContainer.formControl{display:flex;flex-direction:row;justify-content:flex-start;align-items:center}section#curriculumVitae form.timelineItem:not(.editing) .delete,section#curriculumVitae form.timelineItem:not(.editing) .edit{color:var(--primaryHover);background-color:#fff}section#curriculumVitae form.timelineItem:not(.editing) div.dateContainer{display:none}section#curriculumVitae form.timelineItem.editing .timelineHeader{display:none}section#curriculumVitae form.timelineItem.editing div.gradeContainer.formControl{gap:1em;margin-bottom:.5em}section#curriculumVitae form.timelineItem:not(.editing) .formControl .courseText,section#curriculumVitae form.timelineItem:not(.editing) .formControl .jobTitleText,section#curriculumVitae form.timelineItem:not(.editing) div.companyAreaContainer.formControl input,section#curriculumVitae form.timelineItem:not(.editing) div.gradeContainer.formControl input,section#projects form.projItem:not(.editing) div.formControl.infoContainer textarea,section#projects form.projItem:not(.editing) div.formControl.projectTitleContainer input{outline:0;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;resize:none}section#curriculumVitae form.timelineItem:not(.editing) div.companyAreaContainer.formControl>*,section#curriculumVitae form.timelineItem:not(.editing) div.gradeContainer.formControl>*{color:#e5e5e5}section#curriculumVitae form.timelineItem:not(.editing) .formControl .courseText,section#curriculumVitae form.timelineItem:not(.editing) .formControl .jobTitleText{color:#fff}section#curriculumVitae form.timelineItem:not(.editing) div.gradeContainer.formControl input{padding:0 .25em}section#curriculumVitae form.timelineItem:not(.editing) .formControl .courseText,section#curriculumVitae form.timelineItem:not(.editing) div.formControl .courseText{padding:0}section#curriculumVitae form.timelineItem:not(.editing) input[type=submit]{display:none}.courseText{resize:none}section#curriculumVitae form.timelineItem:not(.editing) div.companyAreaContainer input{width:30%}section#projects #projList .projItem{display:flex;justify-content:center;align-items:center;flex-direction:column;margin:0 auto;width:90%;border:1px solid var(--grey);gap:1em;box-shadow:0 6px 4px 0 var(--mutedBlack)}section#projects #projList{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:1em}section#projects #projList .projItem img{max-width:15rem;width:100%;padding:0 1em}section#projects .projItem .linkContainer{display:flex;flex-direction:row;justify-content:flex-start;gap:3em;margin-left:2em}section#projects .linkContainer .btn{padding:.25em .5em}section#projects #isMainProject{width:auto}section#projects form.projItem.editing div.linkContainer,section#projects form.projItem.editing img.displayedImage,section#projects form.projItem:not(.editing) div.formControl.gitContainer,section#projects form.projItem:not(.editing) div.formControl.imageContainer,section#projects form.projItem:not(.editing) div.formControl.isMainProject,section#projects form.projItem:not(.editing) div.formControl.viewProjContainer,section#projects form.projItem:not(.editing) input[type=submit]{display:none}section#projects form.projItem:not(.editing) div.formControl.projectTitleContainer input{font-size:1.17em;font-weight:700}section#projects form.projItem:not(.editing) div.formControl.infoContainer textarea,section#projects form.projItem:not(.editing) div.formControl.projectTitleContainer input{color:#000}section#addPost form,section#editPost form{margin:auto 4rem}form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar{border-bottom-right-radius:0;border-bottom-left-radius:0;border-bottom:2px solid var(--mutedGrey);box-shadow:0 3px 4px var(--mutedBlack)}form .formControl .ck.ck-editor__main .ck-content{border-top-right-radius:0;border-top-left-radius:0;border-top:inherit}form .formControl .ck-editor__editable{min-height:400px}section#editPost{justify-content:center}section#editPost h2{align-self:flex-start}section#editPost table{border-collapse:collapse;border-style:hidden;align-self:center;margin-bottom:5em}section#editPost table td,th{border:1px solid var(--mutedGrey);text-align:left;padding:8px;min-width:10rem}section#editPost form{margin-bottom:2em}@media only screen and (max-width:75em){nav.sideNav .closeBtn{display:block}}
\ No newline at end of file
diff --git a/dist/editor/editor.html b/dist/editor/editor.html
index 00eca0c..707026f 100644
--- a/dist/editor/editor.html
+++ b/dist/editor/editor.html
@@ -1 +1 @@
-
Editor × ☰
Editor
\ No newline at end of file
+Editor × ☰
Editor
\ No newline at end of file
diff --git a/dist/editor/js/editor.js b/dist/editor/js/editor.js
index fd54661..eff59c3 100644
--- a/dist/editor/js/editor.js
+++ b/dist/editor/js/editor.js
@@ -1 +1 @@
-let dateOptions={month:"short",year:"numeric"},textareaLoaded=!1,editor=null;function goToPage(e){document.querySelectorAll(".editor section").forEach((t=>{t.style.display="none",t.id===e&&(t.style.display="block")}))}function addActiveClass(e){document.querySelectorAll("nav.sideNav ul li a").forEach((t=>{t.classList.remove("active"),t.id===e&&t.classList.add("active")}))}function showErrorMessage(e,t){document.querySelector(`#${t}Error`).classList.remove("hidden"),document.querySelector(`#${t}Error div`).innerText=e}function showSuccessMessage(e,t){document.querySelector(`#${t}Success`).classList.remove("hidden"),document.querySelector(`#${t}Success div`).innerText=e}function editCVItem(e){if(textareaLoaded=!1,document.querySelector(`#timelineItem${e}`).classList.toggle("editing"),e.includes("e"))return document.querySelector(`#grade${e}`).toggleAttribute("disabled"),void document.querySelector(`#course${e}`).toggleAttribute("disabled");document.querySelector(`#companyName${e}`).toggleAttribute("disabled"),document.querySelector(`#area${e}`).toggleAttribute("disabled"),document.querySelector(`#jobTitle${e}`).toggleAttribute("disabled")}function addEduData(e,t,o,r,n,a=!1){let d=e+"e",i=document.createElement("form");i.id="timelineItem"+d,i.classList.add("timelineItem"),i.onsubmit=t=>updateEduItem(e,t),i.innerHTML=`\n \n \n \n
\n \n \n -\n \n
\n \n \n Grade: \n \n
\n \n ${n} \n
\n \n \n \n `,a?document.querySelector("#edu").prepend(i):document.getElementById("edu").appendChild(i)}function addWorkData(e,t,o,r,n,a,d=!1){let i=e+"w",c=document.createElement("form");c.id="timelineItem"+i,c.classList.add("timelineItem"),c.onsubmit=t=>updateWorkItem(e,t),c.innerHTML=`\n \n \n \n
\n \n \n -\n \n
\n \n \n \n -\n \n
\n \n ${a} \n
\n \n \n \n\t`,d?document.querySelector("#work").prepend(c):document.getElementById("work").appendChild(c)}function updateEduItem(e,t){t.preventDefault();let o={};o.dateFrom=document.querySelector(`#dateFrom${e}e`).value,o.dateTo=document.querySelector(`#dateTo${e}e`).value,o.grade=document.querySelector(`#grade${e}e`).value,o.course=document.querySelector(`#course${e}e`).value,fetch("/api/timelineData/edu/"+e,{method:"PATCH",body:JSON.stringify(o),headers:{"Content-Type":"application/json",Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{if(t.ok)return document.querySelector(`#timelineHeader${e}e`).innerHTML=new Date(document.querySelector(`#dateFrom${e}e`).value).toLocaleString("en-gb",dateOptions)+" - "+new Date(document.querySelector(`#dateTo${e}e`).value).toLocaleString("en-gb",dateOptions),document.querySelector(`#timelineItem${e}e`).classList.toggle("editing"),document.querySelector(`#grade${e}e`).setAttribute("disabled",""),void document.querySelector(`#course${e}e`).setAttribute("disabled","");401!==t.status?t.json().then((t=>{document.querySelector(`#eduError${e}e`).classList.remove("hidden"),document.querySelector(`#eduError${e}e div`).innerHTML=t.error})):window.location.href="./"}))}function updateWorkItem(e,t){t.preventDefault();let o={};o.dateFrom=document.querySelector(`#dateFrom${e}w`).value,o.dateTo=document.querySelector(`#dateTo${e}w`).value,o.companyName=document.querySelector(`#companyName${e}w`).value,o.area=document.querySelector(`#area${e}w`).value,o.title=document.querySelector(`#jobTitle${e}w`).value,fetch("/api/timelineData/work/"+e,{method:"PATCH",body:JSON.stringify(o),headers:{"Content-Type":"application/json",Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{if(t.ok)return document.querySelector(`#timelineHeader${e}w`).innerHTML=new Date(document.querySelector(`#dateFrom${e}w`).value).toLocaleString("en-gb",dateOptions)+" - "+new Date(document.querySelector(`#dateTo${e}w`).value).toLocaleString("en-gb",dateOptions),document.querySelector(`#timelineItem${e}w`).classList.toggle("editing"),document.querySelector(`#companyName${e}w`).setAttribute("disabled",""),document.querySelector(`#area${e}w`).setAttribute("disabled",""),void document.querySelector(`#jobTitle${e}w`).setAttribute("disabled","");401!==t.status?t.json().then((t=>{document.querySelector(`#workError${e}w`).classList.remove("hidden"),document.querySelector(`#workError${e}w div`).innerHTML=t.error})):window.location.href="./"}))}function deleteEduItem(e){fetch("/api/timelineData/edu/"+e,{method:"DELETE",headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{t.ok?document.querySelector(`#timelineItem${e}e`).remove():401!==t.status?t.json().then((e=>alert(e.error))):window.location.href="./"}))}function deleteWorkItem(e){fetch("/api/timelineData/work/"+e,{method:"DELETE",headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{t.ok?document.querySelector(`#timelineItem${e}w`).remove():401!==t.status?t.json().then((e=>alert(e.error))):window.location.href="./"}))}function updateProjectItem(e,t){t.preventDefault();let o={};o.title=document.querySelector(`#title${e}`).value,o.isMainProject=document.querySelector(`#isMainProject${e}`).checked?"true":"false",o.information=document.querySelector(`#info${e}`).value,o.projectLink=document.querySelector(`#viewProj${e}`).value,o.gitLink=document.querySelector(`#git${e}`).value;let r=new FormData;r.append("img",document.querySelector(`#img${e}`).files[0]),fetch("/api/projectData/"+e,{method:"PATCH",body:JSON.stringify(o),headers:{"Content-Type":"application/json",Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{if(t.ok)return"undefined"===r.get("img")?("true"===o.isMainProject&&(document.querySelectorAll(".isMainProject input").forEach((e=>e.checked=!1)),document.querySelector(`#isMainProject${e}`).checked=!0,document.querySelector("#projList").prepend(document.querySelector(`#projectItem${e}`))),document.querySelector(`#projectItem${e}`).classList.toggle("editing"),document.querySelector(`#title${e}`).setAttribute("disabled",""),void document.querySelector(`#info${e}`).setAttribute("disabled","")):(console.log("updating image"),fetch("/api/projectImage/"+e,{method:"POST",body:r,headers:{Authorization:"Bearer "+localStorage.getItem("token")}}));401!==t.status?t.json().then((t=>{document.querySelector(`#projError${e}`).classList.remove("hidden"),document.querySelector(`#projError${e} div`).innerHTML=t.error})):window.location.href="./"})).then((t=>t.json().then((o=>{if(t.ok)return"true"===o.isMainProject&&(document.querySelectorAll(".isMainProject input").forEach((e=>e.checked=!1)),document.querySelector(`#isMainProject${e}`).checked=!0,document.querySelector("#projList").prepend(document.querySelector(`#projectItem${e}`))),document.querySelector(`#projectItem${e}`).classList.toggle("editing"),document.querySelector(`#title${e}`).setAttribute("disabled",""),document.querySelector(`#info${e}`).setAttribute("disabled",""),void(document.querySelector(`#projectImage${e}`).src=o.imgLocation);401!==t.status?(document.querySelector(`#projError${e}`).classList.remove("hidden"),document.querySelector(`#projError${e} div`).innerHTML=o.error):window.location.href="./"}))))}function editProjectItem(e){document.querySelector(`#projectItem${e}`).classList.toggle("editing"),document.querySelector(`#title${e}`).removeAttribute("disabled"),document.querySelector(`#info${e}`).removeAttribute("disabled")}function deleteProjectItem(e){fetch("/api/projectData/"+e,{method:"DELETE",headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{t.ok?document.querySelector(`#projectItem${e}`).remove():401!==t.status?t.json().then((e=>alert(e.error))):window.location.href="./"}))}function addProject(e,t,o,r,n,a,d){let i=document.createElement("form");if(i.id="projectItem"+e,i.classList.add("projItem"),i.onsubmit=t=>updateProjectItem(e,t),i.innerHTML=`\n \n \n \n
\n \n \n \n
\n \n \n
\n \n Is It The Main Project\n \n \n \n
\n \n ${n} \n
\n \n \n
\n \n \n
\n \n \n \n `,"true"===t)return document.querySelectorAll(".isMainProject input").forEach((e=>e.checked=!1)),void document.querySelector("#projList").prepend(i);document.querySelector("#projList").appendChild(i)}document.addEventListener("DOMContentLoaded",(()=>{fetch("/api/user/isLoggedIn").then((e=>{e.ok||(window.location.href="./")})),document.querySelector("#dateFromE").max=(new Date).toISOString().split("T")[0],document.querySelector("#dateFromW").max=(new Date).toISOString().split("T")[0],fetch("/api/timelineData/edu").then((e=>{e.json().then((t=>{if(e.ok)for(let e=0;e{e.json().then((t=>{if(e.ok)for(let e=0;e{e.json().then((t=>{e.ok?t.forEach((e=>{addProject(e.ID,1===e.isMainProject?"true":"false",""===e.imgLocation?"../imgs/placeholder.png":e.imgLocation,e.title,e.information,e.projectLink,e.gitLink)})):document.querySelector("#projList").innerHTML="No project data found"}))})),ClassicEditor.create(document.querySelector("#CKEditor"),{placeholder:"Write something amazing...",simpleUpload:{uploadUrl:"/api/blog/uploadPostImage",headers:{Authorization:"Bearer "+localStorage.getItem("token")}}}).then((e=>{editor=e})).catch((e=>{console.error("Oops, something went wrong!"),console.error("Please, report the following error on https://github.com/ckeditor/ckeditor5/issues with the build id and the error stack trace:"),console.warn("Build id: 1eo8ioyje2om-vgar4aghypdm"),console.error(e)}))})),document.querySelector("body").addEventListener("click",(()=>{if(textareaLoaded)return;const e=document.querySelectorAll("main.editor textarea");console.log(e);for(let t=0;t{e.target.style.height="0",e.target.style.height=e.target.scrollHeight+"px"};textareaLoaded=!0})),document.querySelector("#navOpen").addEventListener("click",(e=>{document.querySelector("nav.sideNav").style.removeProperty("width"),document.querySelector("main.editor").style.removeProperty("margin-left"),e.target.style.removeProperty("visibility")})),document.querySelector("#navClose").addEventListener("click",(()=>{document.querySelector("nav.sideNav").style.width="0",document.querySelector("main.editor").style.marginLeft="0",document.querySelector("#navOpen").style.visibility="visible"})),document.querySelector("#addEdu").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;t.append("dateFrom",document.querySelector("#dateFromE").value),t.append("dateTo",document.querySelector("#dateToE").value),t.append("grade",document.querySelector("#grade").value),t.append("course",document.querySelector("#courseTitle").value),fetch("/api/timelineData/edu",{method:"POST",body:t,headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((e=>e.json().then((o=>{if(e.ok)return addEduData(o.ID,t.get("dateFrom"),t.get("dateTo"),t.get("grade"),t.get("course"),!0),void document.querySelector("#addEdu").reset();401!==e.status?showErrorMessage(o.error,"edu"):window.location.href="./"}))))})),document.querySelector("#addWork").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;t.append("dateFrom",document.querySelector("#dateFromW").value),t.append("dateTo",document.querySelector("#dateToW").value),t.append("companyName",document.querySelector("#company").value),t.append("area",document.querySelector("#area").value),t.append("title",document.querySelector("#jobTitle").value),fetch("/api/timelineData/work",{method:"POST",body:t,headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((e=>e.json().then((o=>{if(e.ok){let e=""===t.get("dateTo")?"Present":t.get("dateTo ");return addWorkData(o.ID,t.get("dateFrom"),e,t.get("companyName"),t.get("area"),t.get("title"),!0),void document.querySelector("#addWork").reset()}401!==e.status?showErrorMessage(o.error,"work"):window.location.href="./"}))))})),document.querySelector("#addProj").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;t.append("title",document.querySelector("#projTitle").value),t.append("isMainProject",document.querySelector("#isMainProject").checked?"true":"false"),t.append("information",document.querySelector("#projInfo").value),t.append("projectLink",document.querySelector("#projLink").value?document.querySelector("#projLink").value:"N/A"),t.append("gitLink",document.querySelector("#gitLink").value);let o=new FormData;o.append("img",document.querySelector("#projImg").files[0]);let r=0;fetch("/api/projectData",{method:"POST",body:t,headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((e=>e.json().then((n=>{if(e.ok)return"undefined"===o.get("img")?(addProject(n.ID,t.get("isMainProject"),"../imgs/placeholder.png",t.get("title"),t.get("information"),t.get("projectLink"),t.get("gitLink")),void document.querySelector("#addProj").reset()):(r=n.ID,fetch("/api/projectImage/"+n.ID,{method:"POST",body:o,headers:{Authorization:"Bearer "+localStorage.getItem("token")}}));401!==e.status?showErrorMessage(n.error,"proj"):window.location.href="./"})).then((e=>e.json().then((o=>{if(e.ok)return addProject(r,t.get("isMainProject"),o.imgLocation,t.get("title"),t.get("information"),t.get("projectLink"),t.get("gitLink")),void document.querySelector("#addProj").reset();401!==e.status?showErrorMessage(o.error,"proj"):window.location.href="./"}))))))})),document.querySelector("#addPostForm").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;t.append("title",document.querySelector("#postTitle").value),t.append("body",editor.getData()),t.append("dateCreated",(new Date).toISOString().slice(0,19).replace("T"," ")),t.append("featured",document.querySelector("#isFeatured").checked?"true":"false"),t.append("categories",document.querySelector("#postCategories").value),t.append("headerImg",document.querySelector("#headerImg").files[0]),fetch("/api/blog/post",{method:"POST",body:t,headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((e=>{if(e.ok)return document.querySelector("#addPostForm").reset(),editor.setData(""),void showSuccessMessage("Post added successfully","addPost");401!==e.status?e.json().then((e=>showErrorMessage(e.error,"addPost"))):window.location.href="./"}))})),document.querySelector("#goToCV").addEventListener("click",(()=>{textareaLoaded=!1,addActiveClass("goToCV"),goToPage("curriculumVitae")})),document.querySelector("#goToProjects").addEventListener("click",(()=>{textareaLoaded=!1,addActiveClass("goToProjects"),goToPage("projects")})),document.querySelector("#blog").addEventListener("click",(()=>{document.querySelector("nav.sideNav ul li.dropdown ul").classList.toggle("active"),document.querySelector("#blog i.fa").classList.toggle("fa-caret-down"),document.querySelector("#blog i.fa").classList.toggle("fa-caret-right")})),document.querySelector("#goToAddPost").addEventListener("click",(()=>{textareaLoaded=!1,addActiveClass("goToAddPost"),goToPage("addPost"),document.querySelector("#blog").classList.add("active")})),document.querySelector("#logout").addEventListener("click",(()=>{fetch("/api/user/logout").then((e=>{e.ok&&window.location.reload()}))})),document.querySelector("#eduError .close").addEventListener("click",(()=>document.querySelector("#eduError").classList.toggle("hidden"))),document.querySelector("#workError .close").addEventListener("click",(()=>document.querySelector("#workError").classList.toggle("hidden"))),document.querySelector("#projError .close").addEventListener("click",(()=>document.querySelector("#projError").classList.toggle("hidden"))),document.querySelector("#addPostError .close").addEventListener("click",(()=>document.querySelector("#addPostError").classList.toggle("hidden"))),document.querySelector("#addPostSuccess .close").addEventListener("click",(()=>document.querySelector("#addPostSuccess").classList.toggle("hidden")));
\ No newline at end of file
+let dateOptions={month:"short",year:"numeric"},textareaLoaded=!1,editors={},posts=null;function goToPage(e){document.querySelectorAll(".editor section").forEach((t=>{t.style.display="none",t.id===e&&(t.style.display="flex")}))}function addActiveClass(e){document.querySelectorAll("nav.sideNav ul li a").forEach((t=>{t.classList.remove("active"),t.id===e&&t.classList.add("active")}))}function editProjectItem(e){document.querySelector(`#projectItem${e}`).classList.toggle("editing"),document.querySelector(`#title${e}proj`).toggleAttribute("disabled"),document.querySelector(`#info${e}proj`).toggleAttribute("disabled")}function createEditors(...e){e.forEach((e=>{ClassicEditor.create(document.querySelector(`#${e}`),{placeholder:"Write something amazing...",simpleUpload:{uploadUrl:"/api/blog/uploadPostImage",headers:{Authorization:"Bearer "+localStorage.getItem("token")}}}).then((t=>{editors[e]=t})).catch((e=>{console.error("Oops, something went wrong!"),console.error("Please, report the following error on https://github.com/ckeditor/ckeditor5/issues with the build id and the error stack trace:"),console.warn("Build id: 1eo8ioyje2om-vgar4aghypdm"),console.error(e)}))}))}function editPostItem(e){posts.forEach((t=>{t.ID===e&&(document.querySelector("#editPostTitle").value=t.title,document.querySelector("#editIsFeatured").checked=1===t.featured,document.querySelector("#editPostCategories").value=t.categories,editors.CKEditorEditPost.setData(t.body),document.querySelector("#editPostForm input[type='submit']").id=e)}))}function showErrorMessage(e,t){document.querySelector(`#${t}Error`).classList.remove("hidden"),document.querySelector(`#${t}Error div`).innerText=e}function showSuccessMessage(e,t){document.querySelector(`#${t}Success`).classList.remove("hidden"),document.querySelector(`#${t}Success div`).innerText=e}function editCVItem(e){if(textareaLoaded=!1,document.querySelector(`#timelineItem${e}`).classList.toggle("editing"),e.includes("e"))return document.querySelector(`#grade${e}`).toggleAttribute("disabled"),void document.querySelector(`#course${e}`).toggleAttribute("disabled");document.querySelector(`#companyName${e}`).toggleAttribute("disabled"),document.querySelector(`#area${e}`).toggleAttribute("disabled"),document.querySelector(`#jobTitle${e}`).toggleAttribute("disabled")}function addEduData(e,t,o,r,n,d=!1){let a=e+"e",i=document.createElement("form");i.id="timelineItem"+a,i.classList.add("timelineItem"),i.onsubmit=t=>updateEduItem(e,t),i.innerHTML=`\n \n \n \n
\n \n \n -\n \n
\n \n \n Grade: \n \n
\n \n ${n} \n
\n \n \n \n `,d?document.querySelector("#edu").prepend(i):document.getElementById("edu").appendChild(i)}function addWorkData(e,t,o,r,n,d,a=!1){let i=e+"w",c=document.createElement("form");c.id="timelineItem"+i,c.classList.add("timelineItem"),c.onsubmit=t=>updateWorkItem(e,t),c.innerHTML=`\n \n \n \n
\n \n \n -\n \n
\n \n \n \n -\n \n
\n \n ${d} \n
\n \n \n \n\t`,a?document.querySelector("#work").prepend(c):document.getElementById("work").appendChild(c)}function updateEduItem(e,t){t.preventDefault();let o={};o.dateFrom=document.querySelector(`#dateFrom${e}e`).value,o.dateTo=document.querySelector(`#dateTo${e}e`).value,o.grade=document.querySelector(`#grade${e}e`).value,o.course=document.querySelector(`#course${e}e`).value,fetch("/api/timelineData/edu/"+e,{method:"PATCH",body:JSON.stringify(o),headers:{"Content-Type":"application/json",Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{if(t.ok)return document.querySelector(`#timelineHeader${e}e`).innerHTML=new Date(document.querySelector(`#dateFrom${e}e`).value).toLocaleString("en-gb",dateOptions)+" - "+new Date(document.querySelector(`#dateTo${e}e`).value).toLocaleString("en-gb",dateOptions),document.querySelector(`#timelineItem${e}e`).classList.toggle("editing"),document.querySelector(`#grade${e}e`).setAttribute("disabled",""),void document.querySelector(`#course${e}e`).setAttribute("disabled","");401!==t.status?t.json().then((t=>{document.querySelector(`#eduError${e}e`).classList.remove("hidden"),document.querySelector(`#eduError${e}e div`).innerHTML=t.error})):window.location.href="./"}))}function updateWorkItem(e,t){t.preventDefault();let o={};o.dateFrom=document.querySelector(`#dateFrom${e}w`).value,o.dateTo=document.querySelector(`#dateTo${e}w`).value,o.companyName=document.querySelector(`#companyName${e}w`).value,o.area=document.querySelector(`#area${e}w`).value,o.title=document.querySelector(`#jobTitle${e}w`).value,fetch("/api/timelineData/work/"+e,{method:"PATCH",body:JSON.stringify(o),headers:{"Content-Type":"application/json",Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{if(t.ok)return document.querySelector(`#timelineHeader${e}w`).innerHTML=new Date(document.querySelector(`#dateFrom${e}w`).value).toLocaleString("en-gb",dateOptions)+" - "+new Date(document.querySelector(`#dateTo${e}w`).value).toLocaleString("en-gb",dateOptions),document.querySelector(`#timelineItem${e}w`).classList.toggle("editing"),document.querySelector(`#companyName${e}w`).setAttribute("disabled",""),document.querySelector(`#area${e}w`).setAttribute("disabled",""),void document.querySelector(`#jobTitle${e}w`).setAttribute("disabled","");401!==t.status?t.json().then((t=>{document.querySelector(`#workError${e}w`).classList.remove("hidden"),document.querySelector(`#workError${e}w div`).innerHTML=t.error})):window.location.href="./"}))}function deleteEduItem(e){fetch("/api/timelineData/edu/"+e,{method:"DELETE",headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{t.ok?document.querySelector(`#timelineItem${e}e`).remove():401!==t.status?t.json().then((e=>alert(e.error))):window.location.href="./"}))}function deleteWorkItem(e){fetch("/api/timelineData/work/"+e,{method:"DELETE",headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{t.ok?document.querySelector(`#timelineItem${e}w`).remove():401!==t.status?t.json().then((e=>alert(e.error))):window.location.href="./"}))}function updateProjectItem(e,t){t.preventDefault();let o={};o.title=document.querySelector(`#title${e}`).value,o.isMainProject=document.querySelector(`#isMainProject${e}`).checked?"true":"false",o.information=document.querySelector(`#info${e}`).value,o.projectLink=document.querySelector(`#viewProj${e}`).value,o.gitLink=document.querySelector(`#git${e}`).value;let r=new FormData;r.append("img",document.querySelector(`#img${e}`).files[0]),fetch("/api/projectData/"+e,{method:"PATCH",body:JSON.stringify(o),headers:{"Content-Type":"application/json",Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{if(t.ok)return"undefined"===r.get("img")?("true"===o.isMainProject&&(document.querySelectorAll(".isMainProject input").forEach((e=>e.checked=!1)),document.querySelector(`#isMainProject${e}`).checked=!0,document.querySelector("#projList").prepend(document.querySelector(`#projectItem${e}`))),document.querySelector(`#projectItem${e}`).classList.toggle("editing"),document.querySelector(`#title${e}`).setAttribute("disabled",""),void document.querySelector(`#info${e}`).setAttribute("disabled","")):(console.log("updating image"),fetch("/api/projectImage/"+e,{method:"POST",body:r,headers:{Authorization:"Bearer "+localStorage.getItem("token")}}));401!==t.status?t.json().then((t=>{document.querySelector(`#projError${e}`).classList.remove("hidden"),document.querySelector(`#projError${e} div`).innerHTML=t.error})):window.location.href="./"})).then((t=>t.json().then((o=>{if(t.ok)return"true"===o.isMainProject&&(document.querySelectorAll(".isMainProject input").forEach((e=>e.checked=!1)),document.querySelector(`#isMainProject${e}`).checked=!0,document.querySelector("#projList").prepend(document.querySelector(`#projectItem${e}`))),document.querySelector(`#projectItem${e}`).classList.toggle("editing"),document.querySelector(`#title${e}`).setAttribute("disabled",""),document.querySelector(`#info${e}`).setAttribute("disabled",""),void(document.querySelector(`#projectImage${e}`).src=o.imgLocation);401!==t.status?(document.querySelector(`#projError${e}`).classList.remove("hidden"),document.querySelector(`#projError${e} div`).innerHTML=o.error):window.location.href="./"}))))}function deleteProjectItem(e){fetch("/api/projectData/"+e,{method:"DELETE",headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{t.ok?document.querySelector(`#projectItem${e}`).remove():401!==t.status?t.json().then((e=>alert(e.error))):window.location.href="./"}))}function addProject(e,t,o,r,n,d,a){let i=document.createElement("form"),c=e+"proj";if(i.id="projectItem"+e,i.classList.add("projItem"),i.onsubmit=e=>updateProjectItem(c,e),i.innerHTML=`\n \n \n \n
\n \n \n \n
\n \n \n
\n \n Is It The Main Project\n \n \n \n
\n \n ${n} \n
\n \n \n
\n \n \n
\n \n \n \n `,"true"===t)return document.querySelectorAll(".isMainProject input").forEach((e=>e.checked=!1)),void document.querySelector("#projList").prepend(i);document.querySelector("#projList").appendChild(i)}function deletePostItem(e){fetch("/api/blog/post/"+e,{method:"DELETE",headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((t=>{t.ok?document.querySelector(`#postInfo${e}`).remove():401!==t.status?t.json().then((e=>alert(e.error))):window.location.href="./"}))}function addPostInfo(e,t,o,r){let n=document.createElement("tr"),d=e+"post";n.id="postInfo"+e,n.innerHTML=`\n \n ${t}\n \n \n ${new Date(o).toLocaleDateString()}\n \n \n ${new Date(r).toLocaleDateString()}\n \n \n \n \n \n `,document.querySelector("#editPost table tbody").appendChild(n)}document.addEventListener("DOMContentLoaded",(()=>{fetch("/api/user/isLoggedIn").then((e=>{e.ok||(window.location.href="./")})),document.querySelector("#dateFromE").max=(new Date).toISOString().split("T")[0],document.querySelector("#dateFromW").max=(new Date).toISOString().split("T")[0],fetch("/api/timelineData/edu").then((e=>e.json().then((t=>{if(e.ok)for(let e=0;ee.json().then((t=>{if(e.ok)for(let e=0;ee.json().then((t=>{e.ok?t.forEach((e=>{addProject(e.ID,1===e.isMainProject?"true":"false",""===e.imgLocation?"../imgs/placeholder.png":e.imgLocation,e.title,e.information,e.projectLink,e.gitLink)})):document.querySelector("#projList").innerHTML="No project data found"})))),fetch("/api/blog/post").then((e=>e.json().then((t=>{e.ok&&(posts=t,t.forEach((e=>{addPostInfo(e.ID,e.title,e.dateCreated,e.dateModified)})))})))),createEditors("CKEditorAddPost","CKEditorEditPost")})),document.querySelector("body").addEventListener("click",(()=>{if(textareaLoaded)return;const e=document.querySelectorAll("main.editor textarea");console.log(e);for(let t=0;t{e.target.style.height="0",e.target.style.height=e.target.scrollHeight+"px"};textareaLoaded=!0})),document.querySelector("#navOpen").addEventListener("click",(e=>{document.querySelector("nav.sideNav").style.removeProperty("width"),document.querySelector("main.editor").style.removeProperty("margin-left"),e.target.style.removeProperty("visibility")})),document.querySelector("#navClose").addEventListener("click",(()=>{document.querySelector("nav.sideNav").style.width="0",document.querySelector("main.editor").style.marginLeft="0",document.querySelector("#navOpen").style.visibility="visible"})),document.querySelector("#addEdu").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;t.append("dateFrom",document.querySelector("#dateFromE").value),t.append("dateTo",document.querySelector("#dateToE").value),t.append("grade",document.querySelector("#grade").value),t.append("course",document.querySelector("#courseTitle").value),fetch("/api/timelineData/edu",{method:"POST",body:t,headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((e=>e.json().then((o=>{if(e.ok)return addEduData(o.ID,t.get("dateFrom"),t.get("dateTo"),t.get("grade"),t.get("course"),!0),void document.querySelector("#addEdu").reset();401!==e.status?showErrorMessage(o.error,"edu"):window.location.href="./"}))))})),document.querySelector("#addWork").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;t.append("dateFrom",document.querySelector("#dateFromW").value),t.append("dateTo",document.querySelector("#dateToW").value),t.append("companyName",document.querySelector("#company").value),t.append("area",document.querySelector("#area").value),t.append("title",document.querySelector("#jobTitle").value),fetch("/api/timelineData/work",{method:"POST",body:t,headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((e=>e.json().then((o=>{if(e.ok){let e=""===t.get("dateTo")?"Present":t.get("dateTo ");return addWorkData(o.ID,t.get("dateFrom"),e,t.get("companyName"),t.get("area"),t.get("title"),!0),void document.querySelector("#addWork").reset()}401!==e.status?showErrorMessage(o.error,"work"):window.location.href="./"}))))})),document.querySelector("#addProj").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;t.append("title",document.querySelector("#projTitle").value),t.append("isMainProject",document.querySelector("#isMainProject").checked?"true":"false"),t.append("information",document.querySelector("#projInfo").value),t.append("projectLink",document.querySelector("#projLink").value?document.querySelector("#projLink").value:"N/A"),t.append("gitLink",document.querySelector("#gitLink").value);let o=new FormData;o.append("img",document.querySelector("#projImg").files[0]);let r=0;fetch("/api/projectData",{method:"POST",body:t,headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((e=>e.json().then((n=>{if(e.ok)return"undefined"===o.get("img")?(addProject(n.ID,t.get("isMainProject"),"../imgs/placeholder.png",t.get("title"),t.get("information"),t.get("projectLink"),t.get("gitLink")),void document.querySelector("#addProj").reset()):(r=n.ID,fetch("/api/projectImage/"+n.ID,{method:"POST",body:o,headers:{Authorization:"Bearer "+localStorage.getItem("token")}}));401!==e.status?showErrorMessage(n.error,"proj"):window.location.href="./"})).then((e=>e.json().then((o=>{if(e.ok)return addProject(r,t.get("isMainProject"),o.imgLocation,t.get("title"),t.get("information"),t.get("projectLink"),t.get("gitLink")),void document.querySelector("#addProj").reset();401!==e.status?showErrorMessage(o.error,"proj"):window.location.href="./"}))))))})),document.querySelector("#addPostForm").addEventListener("submit",(e=>{if(e.preventDefault(),""===editors.CKEditorAddPost.getData())return void showErrorMessage("Post body cannot be empty","addPost");let t=new FormData;t.append("title",document.querySelector("#postTitle").value),t.append("featured",document.querySelector("#isFeatured").checked?"1":"0"),t.append("body",editors.CKEditorAddPost.getData()),t.append("dateCreated",(new Date).toISOString().slice(0,19).replace("T"," ")),t.append("categories",document.querySelector("#postCategories").value),t.append("headerImg",document.querySelector("#headerImg").files[0]),fetch("/api/blog/post",{method:"POST",body:t,headers:{Authorization:"Bearer "+localStorage.getItem("token")}}).then((e=>e.json().then((o=>{if(e.ok)return document.querySelector("#addPostForm").reset(),editors.CKEditorAddPost.setData(""),addPostInfo(o.ID,t.get("title"),t.get("dateCreated"),t.get("dateModified")),void showSuccessMessage("Post added successfully","addPost");401!==e.status?e.json().then((e=>showErrorMessage(e.error,"addPost"))):window.location.href="./"}))))})),document.querySelector("#editPostForm").addEventListener("submit",(e=>{e.preventDefault();let t=document.querySelector("#editPostForm input[type='submit']").id;if(""===t)return void showErrorMessage("Currently not editing any post","editPost");if(""===editors.CKEditorEditPost.getData())return void showErrorMessage("Post body cannot be empty","editPost");let o={};o.title=document.querySelector("#editPostTitle").value,o.featured=document.querySelector("#editIsFeatured").checked?"1":"0",o.body=editors.CKEditorEditPost.getData(),o.dateModified=(new Date).toISOString().slice(0,19).replace("T"," "),o.categories=document.querySelector("#editPostCategories").value;let r=new FormData;r.append("headerImg",document.querySelector("#editHeaderImg").files[0]),fetch("/api/blog/post/"+t,{method:"PATCH",body:JSON.stringify(o),headers:{"Content-Type":"application/json",Authorization:"Bearer "+localStorage.getItem("token")}}).then((e=>{if(e.ok)return"undefined"===r.get("headerImg")?(document.querySelector("#editPostForm").reset(),document.querySelector("#editPostForm input[type='submit']").id="",editors.CKEditorEditPost.setData(""),void showSuccessMessage("Post edited successfully","editPost")):fetch("/api/blog/headerImage/"+t,{method:"POST",body:r,headers:{Authorization:"Bearer "+localStorage.getItem("token")}});401!==e.status?e.json().then((e=>showErrorMessage(e.error,"editPost"))):window.location.href="./"})).then((e=>e.json().then((t=>{if(e.ok)return document.querySelector("#editPostForm").reset(),document.querySelector("#editPostForm input[type='submit']").id="",editors.CKEditorEditPost.setData(""),void showSuccessMessage("Post edited successfully","editPost");401!==e.status?showErrorMessage(t.error.message,"editPost"):window.location.href="./"}))))})),document.querySelector("#goToCV").addEventListener("click",(()=>{textareaLoaded=!1,addActiveClass("goToCV"),goToPage("curriculumVitae")})),document.querySelector("#goToProjects").addEventListener("click",(()=>{textareaLoaded=!1,addActiveClass("goToProjects"),goToPage("projects")})),document.querySelector("#blog").addEventListener("click",(()=>{document.querySelector("nav.sideNav ul li.dropdown ul").classList.toggle("active"),document.querySelector("#blog i.fa").classList.toggle("fa-caret-down"),document.querySelector("#blog i.fa").classList.toggle("fa-caret-right")})),document.querySelector("#goToAddPost").addEventListener("click",(()=>{textareaLoaded=!1,addActiveClass("goToAddPost"),goToPage("addPost"),document.querySelector("#blog").classList.add("active")})),document.querySelector("#goToEditPost").addEventListener("click",(()=>{textareaLoaded=!1,addActiveClass("goToEditPost"),goToPage("editPost"),document.querySelector("#blog").classList.add("active")})),document.querySelector("#logout").addEventListener("click",(()=>{fetch("/api/user/logout").then((e=>{e.ok&&window.location.reload()}))})),document.querySelector("#eduError .close").addEventListener("click",(()=>document.querySelector("#eduError").classList.toggle("hidden"))),document.querySelector("#workError .close").addEventListener("click",(()=>document.querySelector("#workError").classList.toggle("hidden"))),document.querySelector("#projError .close").addEventListener("click",(()=>document.querySelector("#projError").classList.toggle("hidden"))),document.querySelector("#addPostError .close").addEventListener("click",(()=>document.querySelector("#addPostError").classList.toggle("hidden"))),document.querySelector("#addPostSuccess .close").addEventListener("click",(()=>document.querySelector("#addPostSuccess").classList.toggle("hidden"))),document.querySelector("#editPostError .close").addEventListener("click",(()=>document.querySelector("#editPostError").classList.toggle("hidden"))),document.querySelector("#editPostSuccess .close").addEventListener("click",(()=>document.querySelector("#editPostSuccess").classList.toggle("hidden")));
\ No newline at end of file
diff --git a/src/api/blog/blogData.php b/src/api/blog/blogData.php
index e0eb5cd..951bda2 100644
--- a/src/api/blog/blogData.php
+++ b/src/api/blog/blogData.php
@@ -21,7 +21,8 @@ class blogData
public function getBlogPosts(): array
{
$conn = dbConn();
- $stmt = $conn->prepare("SELECT ID, title, dateCreated, dateModified, body, categories FROM blog ORDER BY dateCreated DESC;");
+ $stmt = $conn->prepare("SELECT ID, title, dateCreated, dateModified, body, categories, featured
+ FROM blog ORDER BY dateCreated;");
$stmt->execute();
// set the resulting array to associative
@@ -99,6 +100,164 @@ class blogData
return array("errorMessage" => "Error, blog post could not found");
}
+ /**
+ * Delete a blog post with the given ID
+ * @param int $ID - ID of the blog post to delete
+ * @return string - Success or error message
+ */
+ public function deletePost(int $ID): string
+ {
+ $conn = dbConn();
+
+ $stmtCheckPost = $conn->prepare("SELECT * FROM blog WHERE ID = :ID");
+ $stmtCheckPost->bindParam(":ID", $ID);
+ $stmtCheckPost->execute();
+ $result = $stmtCheckPost->fetch(PDO::FETCH_ASSOC);
+
+ if (!$result)
+ {
+ return "post not found";
+ }
+
+ if ($result["featured"] === 1)
+ {
+ return "cannot delete";
+ }
+
+ $stmt = $conn->prepare("DELETE FROM blog WHERE ID = :ID");
+ $stmt->bindParam(":ID", $ID);
+
+ if ($stmt->execute())
+ {
+ $imagUtils = new imgUtils();
+ $imagUtils->deleteDirectory("../blog/imgs/" . $result["title"] . "_" . $result["folderID"] . "/");
+ return "success";
+ }
+
+ return "error";
+ }
+
+ /**
+ * Update the blog post with the given ID
+ * @param int $ID - ID of the blog post to update
+ * @param string $title - Title of the blog post
+ * @param bool $featured - Whether the blog post is featured or not
+ * @param string $body - Body of the blog post
+ * @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 $body, string $dateModified, string $categories): bool|string
+ {
+ $conn = dbConn();
+
+ $stmtCheckPost = $conn->prepare("SELECT * FROM blog WHERE ID = :ID");
+ $stmtCheckPost->bindParam(":ID", $ID);
+ $stmtCheckPost->execute();
+ $result = $stmtCheckPost->fetch(PDO::FETCH_ASSOC);
+
+ if (!$result)
+ {
+ return "post not found";
+ }
+
+ if (!$featured && $result["featured"] === 1)
+ {
+ return "unset feature";
+ }
+
+ if ($featured)
+ {
+ $stmtUnsetFeatured = $conn->prepare("UPDATE blog SET featured = 0 WHERE featured = 1;");
+ $stmtUnsetFeatured->execute();
+ }
+
+ $to = "../blog/imgs/" . $title . "_" . $result["folderID"] . "/";
+ if ($result["title"] !== $title)
+ {
+ $from = "../blog/imgs/" . $result["title"] . "_" . $result["folderID"] . "/";
+ mkdir($to, 0777, true);
+ rename($result["headerImg"], $to . basename($result["headerImg"]));
+ $body = $this->changeHTMLSrc($body, $to, $from);
+ rmdir($from);
+ }
+
+ $from = "../blog/imgs/tmp/";
+ $newBody = $this->changeHTMLSrc($body, $to, $from);
+
+ $stmt = $conn->prepare("UPDATE blog SET title = :title, featured = :featured, body = :body, dateModified = :dateModified, categories = :categories WHERE ID = :ID;");
+ $stmt->bindParam(":ID", $ID);
+ $stmt->bindParam(":title", $title);
+ $stmt->bindParam(":featured", $featured);
+ $stmt->bindParam(":body", $newBody);
+ $stmt->bindParam(":dateModified", $dateModified);
+ $stmt->bindParam(":categories", $categories);
+
+ return $stmt->execute();
+ }
+
+ /**
+ * Creates a new post di rectory, uploads the header image and moves the images from the
+ * temp folder to the new folder, then updates the post html to point to the new images, finally
+ * it creates the post in the database
+ * @param string $title - Title of the blog post
+ * @param string $body - Body of the blog post
+ * @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 $body, string $dateCreated, bool $featured, string $categories, UploadedFileInterface $headerImg): int|string
+ {
+ $conn = dbConn();
+ $folderID = uniqid();
+ $targetFile = array("imgLocation" => "../blog/imgs/placeholder.png");
+
+ $targetDir = "../blog/imgs/" . $title . "_" . $folderID . "/";
+ mkdir($targetDir, 0777, true);
+
+ if ($headerImg !== null)
+ {
+ $imagUtils = new imgUtils();
+ $targetFile = $imagUtils->uploadFile($targetDir, $headerImg);
+ }
+
+
+ if (!is_array($targetFile))
+ {
+ return $targetFile;
+ }
+
+ $newBody = $this->changeHTMLSrc($body, $targetDir, "../blog/imgs/tmp/");
+
+
+ if ($featured)
+ {
+ $stmtMainProject = $conn->prepare("UPDATE blog SET featured = 0 WHERE featured = 1;");
+ $stmtMainProject->execute();
+ }
+
+ $stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, body, categories, folderID)
+ VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :body, :categories, :folderID);");
+ $stmt->bindParam(":title", $title);
+ $stmt->bindParam(":dateCreated", $dateCreated);
+ $stmt->bindParam(":dateModified", $dateCreated);
+ $isFeatured = $featured ? 1 : 0;
+ $stmt->bindParam(":featured", $isFeatured);
+ $stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
+ $stmt->bindParam(":body", $newBody);
+ $stmt->bindParam(":categories", $categories);
+ $stmt->bindParam(":folderID", $folderID);
+
+ if ($stmt->execute())
+ {
+ return intval($conn->lastInsertId());
+ }
+
+ return "Error, couldn't create post";
+ }
+
/**
* Upload the images in the post to temp folder and return image location
* @param UploadedFileInterface $img - Image to upload
@@ -131,37 +290,60 @@ class blogData
}
/**
- * Creates a new post directory, uploads the header image and moves the images from the
- * temp folder to the new folder, then updates the post html to point to the new images, finally
- * it creates the post in the database
- * @param string $title - Title of the blog post
- * @param string $body - Body of the blog post
- * @param string $dateCreated - Date the blog post was created
- * @param string $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
+ * Upload the header image of the post and update the database
+ * @param int $ID - ID of the post
+ * @param UploadedFileInterface $img - Image to upload
+ * @return string|array - String with error message or array with the location of the uploaded file
*/
- public function createPost(string $title, string $body, string $dateCreated, string $featured, string $categories, UploadedFileInterface $headerImg): int|string
+ public function uploadHeaderImage(int $ID, UploadedFileInterface $img): string|array
{
$conn = dbConn();
- $targetFile = "";
- $folderID = uniqid();
- if ($headerImg !== null)
+ $stmt = $conn->prepare("SELECT * FROM blog WHERE ID = :ID;");
+ $stmt->bindParam(":ID", $ID);
+ $stmt->execute();
+ $result = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (!$result)
{
- $targetDir = "../blog/imgs/" . $title . "_" . $folderID . "/";
- mkdir($targetDir, 0777, true);
- $imagUtils = new imgUtils();
- $targetFile = $imagUtils->uploadFile($targetDir, $headerImg);
+ return "Couldn't find the post";
}
- $targetFile = array("imgLocation" => ".../blog/imgs/placeholder.png");
+ $targetDir = "../blog/imgs/" . $result["title"] . "_" . $result["folderID"] . "/";
+ $imagUtils = new imgUtils();
+ $targetFile = $imagUtils->uploadFile($targetDir, $img);
if (!is_array($targetFile))
{
return $targetFile;
}
+ if (file_exists($targetFile["imgLocation"]))
+ {
+ unlink($result["headerImg"]);
+ $stmt = $conn->prepare("UPDATE blog SET headerImg = :headerImg WHERE ID = :ID;");
+ $stmt->bindParam(":ID", $ID);
+ $stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
+ $stmt->execute();
+ if ($stmt->rowCount() > 0)
+ {
+ return $targetFile;
+ }
+
+ return "Couldn't update the post";
+ }
+
+ return "Couldn't upload the image";
+ }
+
+ /**
+ * Change the HTML src of the images in the post to point to the new location
+ * @param string $body - Body of the post
+ * @param string $to - New location of the images
+ * @param string $from - Old location of the images
+ * @return string - Body of the post with the new image locations
+ */
+ public function changeHTMLSrc(string $body, string $to, string $from): string
+ {
$htmlDoc = new DOMDocument();
$htmlDoc->loadHTML($body, LIBXML_NOERROR);
$doc = $htmlDoc->getElementsByTagName('body')->item(0);
@@ -172,24 +354,25 @@ class blogData
foreach ($imgs as $img)
{
$src = $img->getAttribute("src");
+ $src = urldecode($src);
$srcList[] = $src;
$fileName = basename($src);
- $img->setAttribute("src", $targetDir . $fileName);
+ $img->setAttribute("src", $to . $fileName);
}
- $files = scandir("../blog/imgs/tmp/");
+ $files = scandir($from);
foreach ($files as $file)
{
if ($file != "." && $file != "..")
{
- if (!in_array("../blog/imgs/tmp/" . $file, $srcList))
+ if (!in_array($from . $file, $srcList))
{
- unlink("../blog/imgs/tmp/" . $file);
+ unlink($from . $file);
}
else
{
- rename("../blog/imgs/tmp/" . $file, $targetDir . $file);
+ rename($from . $file, $to . $file);
}
}
}
@@ -199,23 +382,6 @@ class blogData
{
$newBody .= $htmlDoc->saveHTML($node);
}
-
- $stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, body, categories, folderID)
- VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :body, :categories, :folderID);");
- $stmt->bindParam(":title", $title);
- $stmt->bindParam(":dateCreated", $dateCreated);
- $stmt->bindParam(":dateModified", $dateCreated);
- $stmt->bindParam(":featured", $featured);
- $stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
- $stmt->bindParam(":body", $newBody);
- $stmt->bindParam(":categories", $categories);
- $stmt->bindParam(":folderID", $folderID);
-
- if ($stmt->execute())
- {
- return intval($conn->lastInsertId());
- }
-
- return "Error, couldn't create post";
+ return $newBody;
}
}
\ No newline at end of file
diff --git a/src/api/blog/blogRoutes.php b/src/api/blog/blogRoutes.php
index a12075a..a0770a9 100644
--- a/src/api/blog/blogRoutes.php
+++ b/src/api/blog/blogRoutes.php
@@ -29,12 +29,122 @@ class blogRoutes implements routesInterface
*/
public function createRoutes(App $app): void
{
- $app->post("/blog/post", function (Request $request, Response $response, array $args)
+ $app->get("/blog/post", function (Request $request, Response $response)
+ {
+ $posts = $this->blogData->getBlogPosts();
+
+ $json = json_encode($posts);
+
+ $response->getBody()->write($json);
+
+ if (array_key_exists("errorMessage", $posts))
+ {
+ $response->withStatus(404);
+ }
+
+ return $response;
+ });
+
+ $app->get("/blog/post/{id}", function (Request $request, Response $response, $args)
+ {
+ if ($args["id"] != null)
+ {
+ $post = $this->blogData->getBlogPost($args["id"]);
+ if (array_key_exists("errorMessage", $post))
+ {
+ $response->getBody()->write(json_encode($post));
+ return $response->withStatus(404);
+ }
+
+ $response->getBody()->write(json_encode($post));
+ return $response;
+ }
+
+ $response->getBody()->write(json_encode(array("error" => "Please provide an ID")));
+ 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"]))
+ {
+ // uh oh sent some empty data
+ $response->getBody()->write(json_encode(array("error" => "Only some of the data was sent")));
+ return $response->withStatus(400);
+ }
+
+ $message = $this->blogData->updatePost($args["id"], $data["title"], intval($data["featured"]), $data["body"], $data["dateModified"], $data["categories"]);
+
+ if ($message === "post not found")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Error, post not found")));
+ return $response->withStatus(404);
+ }
+
+ if ($message === "unset featured")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Error, cannot unset featured post, try updating another post to be featured first")));
+ return $response->withStatus(409);
+ }
+
+ if (!is_bool($message) || $message === false)
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => $message)));
+ return $response->withStatus(500);
+ }
+
+ return $response;
+ }
+
+ $response->getBody()->write(json_encode(array("error" => "Please provide an ID")));
+ return $response->withStatus(400);
+ });
+
+ $app->delete("/blog/post/{id}", function (Request $request, Response $response, $args)
+ {
+ if ($args["id"] != null)
+ {
+ $message = $this->blogData->deletePost($args["id"]);
+
+ if ($message === "post not found")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Error, post not found")));
+ return $response->withStatus(404);
+ }
+
+ if ($message === "error")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Error, something went wrong")));
+ return $response->withStatus(500);
+ }
+
+ if ($message === "cannot delete")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Error, cannot delete featured post")));
+ return $response->withStatus(409);
+ }
+ return $response;
+ }
+
+ $response->getBody()->write(json_encode(array("error" => "Please provide an ID")));
+ return $response->withStatus(400);
+ });
+
+ $app->post("/blog/post", function (Request $request, Response $response)
{
$data = $request->getParsedBody();
$files = $request->getUploadedFiles();
$headerImg = $files["headerImg"];
- if (empty($data["title"]) || empty($data["body"]) || empty($data["dateCreated"]) || empty($data["featured"]) || empty($data["categories"]))
+ if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["dateCreated"]) || empty($data["categories"]))
{
// uh oh sent some empty data
$response->getBody()->write(json_encode(array("error" => "Error, empty data sent")));
@@ -46,7 +156,8 @@ class blogRoutes implements routesInterface
$headerImg = null;
}
- $insertedID = $this->blogData->createPost($data["title"], $data["body"], $data["dateCreated"], $data["featured"], $data["categories"], $headerImg);
+ $featured = $data["featured"] === "true";
+ $insertedID = $this->blogData->createPost($data["title"], $data["body"], $data["dateCreated"], $featured, $data["categories"], $headerImg);
if (!is_int($insertedID))
{
// uh oh something went wrong
@@ -74,9 +185,36 @@ class blogRoutes implements routesInterface
return $response->withStatus(500);
}
-
$response->getBody()->write(json_encode($message));
return $response->withStatus(201);
});
+
+ $app->post("/blog/headerImage/{id}", function (Request $request, Response $response, $args)
+ {
+ $files = $request->getUploadedFiles();
+
+ if ($args["id"] != null)
+ {
+ if (empty($files))
+ {
+ // uh oh sent some empty data
+ $response->getBody()->write(json_encode(array("error" => array("message" => "Error, empty data sent"))));
+ return $response->withStatus(400);
+ }
+
+ $message = $this->blogData->uploadHeaderImage($args["id"], $files["headerImg"]);
+ if (!is_array($message))
+ {
+ $response->getBody()->write(json_encode(array("error" => array("message" => $message))));
+ return $response->withStatus(500);
+ }
+
+ $response->getBody()->write(json_encode($message));
+ return $response->withStatus(201);
+ }
+
+ $response->getBody()->write(json_encode(array("error" => "Please provide an ID")));
+ return $response->withStatus(400);
+ });
}
}
\ No newline at end of file
diff --git a/src/api/project/projectData.php b/src/api/project/projectData.php
index 77a65a6..a69b22f 100644
--- a/src/api/project/projectData.php
+++ b/src/api/project/projectData.php
@@ -39,30 +39,32 @@ class projectData
* Update project data in the database with the given ID
* @param string $ID - ID of the project in the database to update
* @param string $title - Title of the project
- * @param string $isMainProject - Is the project a main project or not
+ * @param bool $isMainProject - Is the project a main project or not
* @param string $information - Information about the project
* @param string $projectLink - Link to the project
* @param string $gitLink - Link to the git repository
* @return bool|string - True if project was updated, false if not and there was an error, or an error string
*/
- public function updateProjectData(string $ID, string $title, string $isMainProject, string $information, string $projectLink, string $gitLink): bool|string
+ public function updateProjectData(string $ID, string $title, bool $isMainProject, string $information, string $projectLink, string $gitLink): bool|string
{
$conn = dbConn();
- if ($isMainProject === "false")
- {
- $stmtMainProject = $conn->prepare("SELECT isMainProject FROM projects WHERE ID = :ID");
- $stmtMainProject->bindParam(":ID", $ID);
- $stmtMainProject->execute();
- $result = $stmtMainProject->fetch(PDO::FETCH_ASSOC);
+ $stmtMainProject = $conn->prepare("SELECT isMainProject FROM projects WHERE ID = :ID");
+ $stmtMainProject->bindParam(":ID", $ID);
+ $stmtMainProject->execute();
+ $result = $stmtMainProject->fetch(PDO::FETCH_ASSOC);
- if ($result["isMainProject"] === "1")
- {
- return "unset main project";
- }
+ if (!$result)
+ {
+ return "project not found";
}
- if ($isMainProject === "true")
+ if (!$isMainProject && $result["isMainProject"] === "1")
+ {
+ return "unset main project";
+ }
+
+ if ($isMainProject)
{
$stmtMainProject = $conn->prepare("UPDATE projects SET isMainProject = 0 WHERE isMainProject = 1;");
$stmtMainProject->execute();
@@ -70,7 +72,7 @@ class projectData
$stmt = $conn->prepare("UPDATE projects SET title = :title, isMainProject = :isMainProject, information = :information, projectLink = :projectLink, gitLink = :gitLink WHERE ID = :ID");
$stmt->bindParam(":title", $title);
- $isMainProj = ($isMainProject === "true") ? 1 : 0;
+ $isMainProj = $isMainProject ? 1 : 0;
$stmt->bindParam(":isMainProject", $isMainProj);
$stmt->bindParam(":information", $information);
$stmt->bindParam(":projectLink", $projectLink);
@@ -89,12 +91,16 @@ class projectData
$conn = dbConn();
// check if the project is a main project if it is return false
-
$stmtMainProject = $conn->prepare("SELECT isMainProject FROM projects WHERE ID = :ID");
$stmtMainProject->bindParam(":ID", $ID);
$stmtMainProject->execute();
$result = $stmtMainProject->fetch(PDO::FETCH_ASSOC);
+ if (!$result)
+ {
+ return "project not found";
+ }
+
if ($result["isMainProject"] === "1")
{
return "cannot delete";
@@ -158,6 +164,20 @@ class projectData
*/
public function uploadImage(int $ID, UploadedFileInterface $img): string | array
{
+
+ $conn = dbConn();
+
+ $stmt = $conn->prepare("SELECT ID FROM projects WHERE ID = :ID");
+ $stmt->bindParam(":ID", $ID);
+ $stmt->execute();
+ $result = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (!$result)
+ {
+ return "Project with ID $ID not found";
+ }
+
+
$targetDir = "../imgs/projects/";
$imgUtils = new imgUtils();
$targetFile = $imgUtils->uploadFile($targetDir, $img);
@@ -171,7 +191,6 @@ class projectData
{
$this->deleteImage($ID);
// update the database with the new image location
- $conn = dbConn();
$stmt = $conn->prepare("UPDATE projects SET imgLocation = :imgLocation WHERE ID = :ID");
$stmt->bindParam(":imgLocation", $targetFile["imgLocation"]);
$stmt->bindParam(":ID", $ID);
diff --git a/src/api/project/projectRoutes.php b/src/api/project/projectRoutes.php
index 1b471cb..a7a332e 100644
--- a/src/api/project/projectRoutes.php
+++ b/src/api/project/projectRoutes.php
@@ -39,7 +39,7 @@ class projectRoutes implements routesInterface
if(array_key_exists("errorMessage", $result))
{
- $response = $response->withStatus(404);
+ $response->withStatus(404);
}
//use content type json to indicate json data on frontend.
@@ -49,7 +49,7 @@ class projectRoutes implements routesInterface
$app->patch("/projectData/{id}", function (Request $request, Response $response, array $args)
{
$data = $request->getParsedBody();
- if ($args["id"] != "undefined")
+ if ($args["id"] != null)
{
if (empty($data["title"]) || empty($data["isMainProject"]) || empty($data["information"]) || empty($data["gitLink"]))
{
@@ -58,7 +58,15 @@ class projectRoutes implements routesInterface
return $response->withStatus(400);
}
- $update = $this->projectData->updateProjectData($args["id"], $data["title"], $data["isMainProject"], $data["information"], $data["projectLink"], $data["gitLink"]);
+ $isMainProject = $data["isMainProject"] === "true";
+ $update = $this->projectData->updateProjectData($args["id"], $data["title"], $isMainProject, $data["information"], $data["projectLink"], $data["gitLink"]);
+
+ if ($update === "project not found")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Project with ID " . $args["id"] . " not found")));
+ return $response->withStatus(404);
+ }
if ($update === "unset main project")
{
@@ -73,6 +81,7 @@ class projectRoutes implements routesInterface
$response->getBody()->write(json_encode(array("error" => "Something went wrong")));
return $response->withStatus(500);
}
+
return $response;
}
@@ -85,11 +94,12 @@ class projectRoutes implements routesInterface
if ($args["id"] != null)
{
$message = $this->projectData->deleteProjectData($args["id"]);
- if ($message === "error")
+
+ if ($message === "project not found")
{
// uh oh something went wrong
- $response->getBody()->write(json_encode(array("error" => "Something went wrong or the project with ID ".$args["id"]."does not exist")));
- return $response->withStatus(500);
+ $response->getBody()->write(json_encode(array("error" => "Project with ID " . $args["id"] . " not found")));
+ return $response->withStatus(404);
}
if ($message === "cannot delete")
@@ -99,6 +109,13 @@ class projectRoutes implements routesInterface
return $response->withStatus(409);
}
+ if ($message === "error")
+ {
+ // uh oh something went wrong
+ $response->getBody()->write(json_encode(array("error" => "Something went wrong")));
+ return $response->withStatus(500);
+ }
+
return $response;
}
diff --git a/src/api/timeline/timelineData.php b/src/api/timeline/timelineData.php
index b72dbdf..9c1df75 100644
--- a/src/api/timeline/timelineData.php
+++ b/src/api/timeline/timelineData.php
@@ -58,19 +58,33 @@ class timelineData
* @param string $dateTo - End date
* @param string $grade - Grade
* @param string $course - Course
- * @param string $id - ID of the education data
- * @return bool - True if successful, false if not
+ * @param string $ID - ID of the education data
+ * @return string - "not found" if the ID is not found, "ok" if successful, "error" if not
*/
- public function updateEduData(string $dateFrom, string $dateTo, string $grade, string $course, string $id): bool
+ public function updateEduData(string $dateFrom, string $dateTo, string $grade, string $course, string $ID): string
{
$conn = dbConn();
+ $chkStmt = $conn->prepare("SELECT ID FROM edu WHERE ID = :id;");
+ $chkStmt->bindParam(":id", $ID);
+ $chkStmt->execute();
+ $result = $chkStmt->fetch(PDO::FETCH_ASSOC);
+ if (!$result)
+ {
+ return "not found";
+ }
+
$stmt = $conn->prepare("UPDATE edu SET startPeriod = :dateFrom, endPeriod = :dateTo, grade = :grade, course = :course WHERE ID = :id;");
$stmt->bindParam(":dateFrom", $dateFrom);
$stmt->bindParam(":dateTo", $dateTo);
$stmt->bindParam(":grade", $grade);
$stmt->bindParam(":course", $course);
- $stmt->bindParam(":id", $id);
- return $stmt->execute();
+ $stmt->bindParam(":id", $ID);
+ if ($stmt->execute())
+ {
+ return "ok";
+ }
+
+ return "error";
}
/**
@@ -80,11 +94,21 @@ class timelineData
* @param string $companyName - Company name
* @param string $area - Area
* @param string $title - Title
- * @param string $id - ID of the work data
- * @return bool - True if successful, false if not
+ * @param string $ID - ID of the work data
+ * @return string - "not found" if the ID is not found, "ok" if successful, "error" if not
*/
- public function updateWorkData(string $dateFrom, string $dateTo, string $companyName, string $area, string $title, string $id): bool
+ public function updateWorkData(string $dateFrom, string $dateTo, string $companyName, string $area, string $title, string $ID): string
{
+ $conn = dbConn();
+ $chkStmt = $conn->prepare("SELECT ID FROM work WHERE ID = :id;");
+ $chkStmt->bindParam(":id", $ID);
+ $chkStmt->execute();
+ $result = $chkStmt->fetch(PDO::FETCH_ASSOC);
+ if (!$result)
+ {
+ return "not found";
+ }
+
$conn = dbConn();
$stmt = $conn->prepare("UPDATE work SET startPeriod = :dateFrom, endPeriod = :dateTo, companyName = :companyName, area = :area, title = :title WHERE ID = :id;");
$stmt->bindParam(":dateFrom", $dateFrom);
@@ -92,34 +116,67 @@ class timelineData
$stmt->bindParam(":companyName", $companyName);
$stmt->bindParam(":area", $area);
$stmt->bindParam(":title", $title);
- $stmt->bindParam(":id", $id);
- return $stmt->execute();
+ $stmt->bindParam(":id", $ID);
+ if ($stmt->execute())
+ {
+ return "ok";
+ }
+
+ return "error";
}
/**
* Delete education data by ID
- * @param int $id
- * @return bool - True if successful, false if not
+ * @param int $ID
+ * @return string - "not found" if the ID is not found, "ok" if successful, "error" if not
*/
- public function deleteEduData(int $id): bool
+ public function deleteEduData(int $ID): string
{
$conn = dbConn();
+ $chkStmt = $conn->prepare("SELECT ID FROM edu WHERE ID = :id;");
+ $chkStmt->bindParam(":id", $ID);
+ $chkStmt->execute();
+ $result = $chkStmt->fetch(PDO::FETCH_ASSOC);
+ if (!$result)
+ {
+ return "not found";
+ }
+
$stmt = $conn->prepare("DELETE FROM edu WHERE ID = :id;");
- $stmt->bindParam(":id", $id);
- return $stmt->execute();
+ $stmt->bindParam(":id", $ID);
+ if ($stmt->execute())
+ {
+ return "ok";
+ }
+
+ return "error";
}
/**
* Delete work data by ID
- * @param int $id
- * @return bool - True if successful, false if not
+ * @param int $ID
+ * @return string - "not found" if the ID is not found, "ok" if successful, "error" if not
*/
- function deleteWorkData(int $id): bool
+ function deleteWorkData(int $ID): string
{
$conn = dbConn();
+ $chkStmt = $conn->prepare("SELECT ID FROM work WHERE ID = :id;");
+ $chkStmt->bindParam(":id", $ID);
+ $chkStmt->execute();
+ $result = $chkStmt->fetch(PDO::FETCH_ASSOC);
+ if (!$result)
+ {
+ return "not found";
+ }
+
$stmt = $conn->prepare("DELETE FROM work WHERE ID = :id;");
- $stmt->bindParam(":id", $id);
- return $stmt->execute();
+ $stmt->bindParam(":id", $ID);
+ if ($stmt->execute())
+ {
+ return "ok";
+ }
+
+ return "error";
}
/**
diff --git a/src/api/timeline/timelineRoutes.php b/src/api/timeline/timelineRoutes.php
index d6ab9d6..72ea6f0 100644
--- a/src/api/timeline/timelineRoutes.php
+++ b/src/api/timeline/timelineRoutes.php
@@ -53,7 +53,7 @@ class timelineRoutes implements routesInterface
$app->patch("/timelineData/{timeline}/{id}", function (Request $request, Response $response, array $args)
{
$data = $request->getParsedBody();
- if ($args["timeline"] == "edu" && $args["id"] != "undefined")
+ if ($args["timeline"] == "edu" && $args["id"] != null)
{
if (empty($data["dateFrom"]) || empty($data["dateTo"]) || empty($data["grade"]) || empty($data["course"]))
{
@@ -61,8 +61,16 @@ class timelineRoutes implements routesInterface
$response->getBody()->write(json_encode(array("error" => "Only some of the data was sent")));
return $response->withStatus(400);
}
+ $message = $this->timelineData->updateEduData($data["dateFrom"], $data["dateTo"], $data["grade"], $data["course"], $args["id"]);
- if (!$this->timelineData->updateEduData($data["dateFrom"], $data["dateTo"], $data["grade"], $data["course"], $args["id"]))
+ if ($message == "not found")
+ {
+ // uh oh sent some empty data
+ $response->getBody()->write(json_encode(array("error" => "Edu data with ID " . $args["id"] . " was not found")));
+ return $response->withStatus(404);
+ }
+
+ if ($message == "error")
{
// uh oh something went wrong
$response->getBody()->write(json_encode(array("error" => "Something went wrong")));
@@ -82,7 +90,16 @@ class timelineRoutes implements routesInterface
return $response->withStatus(400);
}
- if (!$this->timelineData->updateWorkData($data["dateFrom"], $data["dateTo"], $data["companyName"], $data["area"], $data["title"], $args["id"]))
+ $message = $this->timelineData->updateWorkData($data["dateFrom"], $data["dateTo"], $data["companyName"], $data["area"], $data["title"], $args["id"]);
+
+ if ($message == "not found")
+ {
+ // uh oh sent some empty data
+ $response->getBody()->write(json_encode(array("error" => "Work data with ID " . $args["id"] . " was not found")));
+ return $response->withStatus(404);
+ }
+
+ if ($message == "error")
{
// uh oh something went wrong
$response->getBody()->write(json_encode(array("error" => "Something went wrong")));
@@ -101,7 +118,16 @@ class timelineRoutes implements routesInterface
{
if ($args["timeline"] == "edu" && $args["id"] != null)
{
- if (!$this->timelineData->deleteEduData($args["id"]))
+ $message = $this->timelineData->deleteEduData($args["id"]);
+
+ if ($message == "not found")
+ {
+ // uh oh sent some empty data
+ $response->getBody()->write(json_encode(array("error" => "Edu data with ID " . $args["id"] . " was not found")));
+ return $response->withStatus(404);
+ }
+
+ if ($message == "error")
{
// uh oh something went wrong
$response->getBody()->write(json_encode(array("error" => "Something went wrong")));
@@ -113,7 +139,16 @@ class timelineRoutes implements routesInterface
if ($args["timeline"] == "work" && $args["id"] != null)
{
- if (!$this->timelineData->deleteWorkData($args["id"]))
+ $message = $this->timelineData->deleteWorkData($args["id"]);
+
+ if ($message == "not found")
+ {
+ // uh oh sent some empty data
+ $response->getBody()->write(json_encode(array("error" => "Work data with ID " . $args["id"] . " was not found")));
+ return $response->withStatus(404);
+ }
+
+ if ($message == "error")
{
// uh oh something went wrong
$response->getBody()->write(json_encode(array("error" => "Something went wrong")));
diff --git a/src/api/user/userData.php b/src/api/user/userData.php
index 0aa11b2..493b6db 100644
--- a/src/api/user/userData.php
+++ b/src/api/user/userData.php
@@ -46,7 +46,7 @@ class userData
public function createToken(string $username): string
{
$now = time();
- $future = strtotime('+6 hour', $now);
+ $future = strtotime('+2 day', $now);
$secretKey = getSecretKey();
$payload = [
"jti" => $username,
diff --git a/src/api/user/userRoutes.php b/src/api/user/userRoutes.php
index 2bde66c..465ca7a 100644
--- a/src/api/user/userRoutes.php
+++ b/src/api/user/userRoutes.php
@@ -36,15 +36,19 @@ class userRoutes implements routesInterface
if (empty($data["username"]) || empty($data["password"]))
{
- // uh oh userData sent empty data
+ // uh oh user sent empty data
return $response->withStatus(400);
}
if ($this->user->checkUser($data["username"], $data["password"]))
{
- // yay, userData is logged in
+ // yay, user is logged in
$_SESSION["token"] = $this->user->createToken($data["username"]);
$_SESSION["username"] = $data["username"];
+
+ $inactive = 60 * 60 * 48; // 2 days
+ $_SESSION["timeout"] = time() + $inactive;
+
$response->getBody()->write(json_encode(array("token" => $_SESSION["token"])));
return $response;
}
@@ -62,15 +66,24 @@ class userRoutes implements routesInterface
{
if (empty($_SESSION["token"]) && empty($_SESSION["username"]))
{
- // uh oh userData not logged in
+ // uh oh user not logged in
+ return $response->withStatus(401);
+ }
+
+ $inactive = 60 * 60 * 48; // 2 days
+ $sessionLife = time() - $_SESSION["timeout"];
+ if ($sessionLife > $inactive)
+ {
+ // uh oh user session expired
+ session_destroy();
return $response->withStatus(401);
}
if (empty($_SESSION["token"]))
{
- // userData is logged in but no token was created
+ // user is logged in but no token was created
$_SESSION["token"] = $this->user->createToken($_SESSION["username"]);
- return $response;
+ return $response->withStatus(201);
}
$response->getBody()->write(json_encode(array("token" => $_SESSION["token"])));
diff --git a/src/api/utils/imgUtils.php b/src/api/utils/imgUtils.php
index 40f9487..1deab53 100644
--- a/src/api/utils/imgUtils.php
+++ b/src/api/utils/imgUtils.php
@@ -3,6 +3,8 @@
namespace api\utils;
use Psr\Http\Message\UploadedFileInterface;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
class imgUtils
{
@@ -40,4 +42,29 @@ class imgUtils
return array("imgLocation" => $targetFile);
}
+
+ /**
+ * Deletes a directory and all its contents
+ * @param string $path - Path to the directory to delete
+ */
+ public function deleteDirectory(string $path): void
+ {
+ $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path,
+ RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
+
+ foreach ($iterator as $file)
+ {
+ if ($file->isDir())
+ {
+ rmdir($file->getPathname());
+ }
+ else
+ {
+ unlink($file->getPathname());
+ }
+ }
+
+ rmdir($path);
+ }
+
}
\ No newline at end of file
diff --git a/src/blog/index.js b/src/blog/js/index.js
similarity index 95%
rename from src/blog/index.js
rename to src/blog/js/index.js
index 2beb7a8..fd6b514 100644
--- a/src/blog/index.js
+++ b/src/blog/js/index.js
@@ -4,7 +4,7 @@ document.addEventListener('DOMContentLoaded', () =>
goToURL(window.location.pathname);
});
-window.addEventListener('popstate', e =>
+window.addEventListener('popstate', _ =>
{
goToURL(window.history.state);
});
diff --git a/src/css/templateStyles.css b/src/css/templateStyles.css
index 85d8459..b26db45 100644
--- a/src/css/templateStyles.css
+++ b/src/css/templateStyles.css
@@ -10,13 +10,14 @@
--primaryDefault: hsla(var(--mainHue), var(--mainSat), var(--mainLight), 1);
--primaryHover: hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) - 10%), 1);
--timelineItemBrdr: hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) - 20%), 1);
- --errorDefault: hsla(0, calc(var(--mainSat) + 10%),calc(var(--mainLight) + 10%), 1);
+ --errorDefault: hsla(0, calc(var(--mainSat) + 10%), calc(var(--mainLight) + 10%), 1);
--errorHover: hsla(0, calc(var(--mainSat) + 10%), calc(var(--mainLight) - 10%), 1);
--grey: hsla(0, 0%, 39%, 1);
--notAvailableDefault: hsla(0, 0%, 39%, 1);
- --notAvailableHover: hsla(0, 0%,32%, 1);
+ --notAvailableHover: hsla(0, 0%, 32%, 1);
--mutedGrey: hsla(0, 0%, 78%, 1);
--mutedBlack: hsla(0, 0%, 0%, 0.25);
+ --mutedGreen: hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) + 20%), 0.5);
--navBack: hsla(0, 0%, 30%, 1);
/* Font Sizes */
diff --git a/src/editor/css/editor.css b/src/editor/css/editor.css
index c7a92ad..ad15e90 100644
--- a/src/editor/css/editor.css
+++ b/src/editor/css/editor.css
@@ -42,10 +42,11 @@ div.editorContainer > *, div.projectsGrid > * {
main.editor section {
display: none;
+ flex-direction: column;
}
-section#addPost {
- display: block;
+section#editPost {
+ display: flex;
}
div.modifyBtnContainer {
@@ -232,7 +233,7 @@ section#projects form.projItem:not(.editing) div.formControl.infoContainer texta
color: #000000;
}
-section#addPost form {
+section#addPost form, section#editPost form {
margin: auto 4rem;
}
@@ -251,4 +252,30 @@ form .formControl .ck.ck-editor__main .ck-content {
form .formControl .ck-editor__editable {
min-height: 400px;
+}
+
+section#editPost {
+ justify-content: center;
+}
+
+section#editPost h2 {
+ align-self: flex-start;
+}
+
+section#editPost table {
+ border-collapse: collapse;
+ border-style: hidden;
+ align-self: center;
+ margin-bottom: 5em;
+}
+
+section#editPost table td, th {
+ border: 1px solid var(--mutedGrey);
+ text-align: left;
+ padding: 8px;
+ min-width: 10rem;
+}
+
+section#editPost form {
+ margin-bottom: 2em;
}
\ No newline at end of file
diff --git a/src/editor/editor.html b/src/editor/editor.html
index 8a3fa53..6888395 100644
--- a/src/editor/editor.html
+++ b/src/editor/editor.html
@@ -15,11 +15,11 @@
< CV>
< Projects>
< Blog>
+ class="fa fa-caret-right">
+
@@ -189,8 +189,9 @@
-
Post
-
@@ -207,6 +208,64 @@
+
diff --git a/src/editor/js/editor.js b/src/editor/js/editor.js
index 0ecd259..2129fe6 100644
--- a/src/editor/js/editor.js
+++ b/src/editor/js/editor.js
@@ -1,6 +1,7 @@
let dateOptions = {month: 'short', year: 'numeric'};
let textareaLoaded = false;
-let editor = null;
+let editors = {};
+let posts = null;
document.addEventListener('DOMContentLoaded', () =>
{
// check if the userData is logged in, if not redirect to log in
@@ -15,76 +16,61 @@ document.addEventListener('DOMContentLoaded', () =>
document.querySelector("#dateFromE").max = new Date().toISOString().split("T")[0];
document.querySelector("#dateFromW").max = new Date().toISOString().split("T")[0];
- fetch("/api/timelineData/edu").then(res =>
+ fetch("/api/timelineData/edu").then(res => res.json().then(json =>
{
- res.json().then(json =>
+ if (res.ok)
{
- if (res.ok)
+ for (let i = 0; i < json.length; i++)
{
- for (let i = 0; i < json.length; i++)
- {
- addEduData(json[i].ID, json[i].startPeriod, json[i].endPeriod, json[i].grade, json[i].course);
- }
- return;
+ addEduData(json[i].ID, json[i].startPeriod, json[i].endPeriod, json[i].grade, json[i].course);
}
- document.querySelector("#edu").innerHTML = "No education data found";
- })
- });
-
- fetch("/api/timelineData/work").then(res =>
- {
- res.json().then(json =>
- {
- if (res.ok)
- {
- for (let i = 0; i < json.length; i++)
- {
- let endPeriod = json[i].endPeriod === "0000-00-00" ? "Present" : json[i].endPeriod;
- addWorkData(json[i].ID, json[i].startPeriod, endPeriod, json[i].companyName, json[i].area, json[i].title);
- }
- return;
- }
- document.querySelector("#edu").innerHTML = "No education data found";
- })
- });
+ return;
+ }
+ document.querySelector("#edu").innerHTML = "No education data found";
+ }));
- fetch("/api/projectData").then(res =>
+ fetch("/api/timelineData/work").then(res => res.json().then(json =>
{
- res.json().then(json =>
+ if (res.ok)
{
- if (res.ok)
+ for (let i = 0; i < json.length; i++)
{
- json.forEach(item =>
- {
- addProject(item["ID"], (item["isMainProject"] === 1 ? "true" : "false"), (item["imgLocation"] === "") ? "../imgs/placeholder.png" : item["imgLocation"], item["title"], item["information"], item["projectLink"], item["gitLink"]);
- })
- return;
+ let endPeriod = json[i].endPeriod === "0000-00-00" ? "Present" : json[i].endPeriod;
+ addWorkData(json[i].ID, json[i].startPeriod, endPeriod, json[i].companyName, json[i].area, json[i].title);
}
- document.querySelector("#projList").innerHTML = "No project data found";
- })
- })
+ return;
+ }
+ document.querySelector("#edu").innerHTML = "No education data found";
+ }));
+ fetch("/api/projectData").then(res => res.json().then(json =>
+ {
+ if (res.ok)
+ {
+ json.forEach(item =>
+ {
+ addProject(item["ID"], (item["isMainProject"] === 1 ? "true" : "false"), (item["imgLocation"] === "") ? "../imgs/placeholder.png" : item["imgLocation"], item["title"], item["information"], item["projectLink"], item["gitLink"]);
+ })
+ return;
+ }
+ document.querySelector("#projList").innerHTML = "No project data found";
+ }))
+
+ fetch("/api/blog/post").then(res => res.json().then(json =>
+ {
+ if (res.ok)
+ {
+ posts = json;
+ json.forEach(item =>
+ {
+ addPostInfo(item["ID"], item["title"], item["dateCreated"], item["dateModified"]);
+ })
+ }
+ }));
// CKEditor stuff
- ClassicEditor.create(document.querySelector("#CKEditor"), {
- placeholder: "Write something amazing...",
- simpleUpload: {
- uploadUrl: '/api/blog/uploadPostImage',
- headers: {
- Authorization: "Bearer " + localStorage.getItem("token")
- }
- }
- }).then(CKEditor =>
- {
- editor = CKEditor;
- }).catch(error =>
- {
- console.error('Oops, something went wrong!');
- console.error('Please, report the following error on https://github.com/ckeditor/ckeditor5/issues with the build id and the error stack trace:');
- console.warn('Build id: 1eo8ioyje2om-vgar4aghypdm');
- console.error(error);
- });
-})
+ createEditors("CKEditorAddPost", "CKEditorEditPost");
+});
document.querySelector("body").addEventListener("click", () =>
{
@@ -265,11 +251,17 @@ document.querySelector("#addProj").addEventListener("submit", e =>
document.querySelector("#addPostForm").addEventListener("submit", e =>
{
e.preventDefault();
+ if (editors["CKEditorAddPost"].getData() === "")
+ {
+ showErrorMessage("Post body cannot be empty", "addPost");
+ return;
+ }
+
let data = new FormData();
data.append("title", document.querySelector("#postTitle").value);
- data.append("body", editor.getData());
+ data.append("featured", document.querySelector("#isFeatured").checked ? "1" : "0");
+ data.append("body", editors["CKEditorAddPost"].getData());
data.append("dateCreated", new Date().toISOString().slice(0, 19).replace('T', ' '));
- data.append("featured", document.querySelector("#isFeatured").checked ? "true" : "false");
data.append("categories", document.querySelector("#postCategories").value);
data.append("headerImg", document.querySelector("#headerImg").files[0]);
@@ -279,12 +271,13 @@ document.querySelector("#addPostForm").addEventListener("submit", e =>
headers: {
"Authorization": "Bearer " + localStorage.getItem("token")
}
- }).then(res =>
+ }).then(res => res.json().then(json =>
{
if (res.ok)
{
document.querySelector("#addPostForm").reset();
- editor.setData("");
+ editors["CKEditorAddPost"].setData("");
+ addPostInfo(json.ID, data.get("title"), data.get("dateCreated"), data.get("dateModified"));
showSuccessMessage("Post added successfully", "addPost");
return;
}
@@ -296,7 +289,92 @@ document.querySelector("#addPostForm").addEventListener("submit", e =>
}
res.json().then(json => showErrorMessage(json.error, "addPost"));
- });
+ }));
+
+});
+
+document.querySelector("#editPostForm").addEventListener("submit", e =>
+{
+ e.preventDefault();
+ let id = document.querySelector("#editPostForm input[type='submit']").id;
+ if (id === "")
+ {
+ showErrorMessage("Currently not editing any post", "editPost");
+ return;
+ }
+
+ if (editors["CKEditorEditPost"].getData() === "")
+ {
+ showErrorMessage("Post body cannot be empty", "editPost");
+ return;
+ }
+
+ let data = {};
+ data["title"] = document.querySelector("#editPostTitle").value;
+ data["featured"] = document.querySelector("#editIsFeatured").checked ? "1" : "0";
+ data["body"] = editors["CKEditorEditPost"].getData();
+ data["dateModified"] = new Date().toISOString().slice(0, 19).replace('T', ' ');
+ data["categories"] = document.querySelector("#editPostCategories").value;
+
+ let imgData = new FormData();
+ imgData.append("headerImg", document.querySelector("#editHeaderImg").files[0]);
+
+ fetch("/api/blog/post/" + id, {
+ method: "PATCH",
+ body: JSON.stringify(data),
+ headers: {
+ "Content-Type": "application/json",
+ "Authorization": "Bearer " + localStorage.getItem("token")
+ }
+ }).then(res =>
+ {
+ if (res.ok)
+ {
+ if (imgData.get("headerImg") === "undefined")
+ {
+ document.querySelector("#editPostForm").reset();
+ document.querySelector("#editPostForm input[type='submit']").id = "";
+ editors["CKEditorEditPost"].setData("");
+ showSuccessMessage("Post edited successfully", "editPost");
+ return;
+ }
+
+ return fetch("/api/blog/headerImage/" + id, {
+ method: "POST",
+ body: imgData,
+ headers: {
+ "Authorization": "Bearer " + localStorage.getItem("token")
+ }
+ });
+
+ }
+
+ if (res.status === 401)
+ {
+ window.location.href = "./";
+ return;
+ }
+
+ res.json().then(json => showErrorMessage(json.error, "editPost"));
+ }).then(res => res.json().then(json =>
+ {
+ if (res.ok)
+ {
+ document.querySelector("#editPostForm").reset();
+ document.querySelector("#editPostForm input[type='submit']").id = "";
+ editors["CKEditorEditPost"].setData("");
+ showSuccessMessage("Post edited successfully", "editPost");
+ return;
+ }
+
+ if (res.status === 401)
+ {
+ window.location.href = "./";
+ return;
+ }
+
+ showErrorMessage(json.error.message, "editPost");
+ }));
});
@@ -330,6 +408,14 @@ document.querySelector("#goToAddPost").addEventListener("click", () =>
document.querySelector("#blog").classList.add("active");
});
+document.querySelector("#goToEditPost").addEventListener("click", () =>
+{
+ textareaLoaded = false;
+ addActiveClass("goToEditPost");
+ goToPage("editPost");
+ document.querySelector("#blog").classList.add("active");
+});
+
document.querySelector("#logout").addEventListener("click", () =>
{
fetch("/api/user/logout").then(res =>
@@ -356,9 +442,16 @@ document.querySelector("#addPostError .close").addEventListener("click", () =>
document.querySelector("#addPostSuccess .close").addEventListener("click", () =>
document.querySelector("#addPostSuccess").classList.toggle("hidden"));
+document.querySelector("#editPostError .close").addEventListener("click", () =>
+ document.querySelector("#editPostError").classList.toggle("hidden"));
+
+document.querySelector("#editPostSuccess .close").addEventListener("click", () =>
+ document.querySelector("#editPostSuccess").classList.toggle("hidden"));
+
+
/**
* Goes to the page with the given id of the section
- * @param {string} id The id of the section to go to
+ * @param {string} id - The id of the section to go to
*/
function goToPage(id)
{
@@ -367,7 +460,7 @@ function goToPage(id)
element.style.display = "none";
if (element.id === id)
{
- element.style.display = "block";
+ element.style.display = "flex";
}
});
@@ -375,7 +468,7 @@ function goToPage(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
*/
function addActiveClass(id)
{
@@ -389,10 +482,70 @@ function addActiveClass(id)
});
}
+
+/**
+ * Toggles the editing mode of the project item with the given id
+ * @param id {number} - the id of the project item from the database
+ */
+function editProjectItem(id)
+{
+ document.querySelector(`#projectItem${id}`).classList.toggle("editing");
+ document.querySelector(`#title${id}proj`).toggleAttribute("disabled");
+ document.querySelector(`#info${id}proj`).toggleAttribute("disabled");
+}
+
+/**
+ * Creates new CKEditor instances
+ * @param ids {string[]} - the ids of the divs to create editors for
+ */
+function createEditors(...ids)
+{
+ ids.forEach(id =>
+ {
+ ClassicEditor.create(document.querySelector(`#${id}`), {
+ placeholder: "Write something amazing...",
+ simpleUpload: {
+ uploadUrl: '/api/blog/uploadPostImage',
+ headers: {
+ Authorization: "Bearer " + localStorage.getItem("token")
+ }
+ }
+ }).then(CKEditor =>
+ {
+ editors[id] = CKEditor;
+ }).catch(error =>
+ {
+ console.error('Oops, something went wrong!');
+ console.error('Please, report the following error on https://github.com/ckeditor/ckeditor5/issues with the build id and the error stack trace:');
+ console.warn('Build id: 1eo8ioyje2om-vgar4aghypdm');
+ console.error(error);
+ });
+ })
+}
+
+/**
+ * Edits the post with the given id
+ * @param {number} id - the id of the post from the database
+ */
+function editPostItem(id)
+{
+ posts.forEach(post =>
+ {
+ if (post.ID === id)
+ {
+ document.querySelector("#editPostTitle").value = post.title;
+ document.querySelector("#editIsFeatured").checked = (post.featured === 1);
+ document.querySelector("#editPostCategories").value = post.categories;
+ editors["CKEditorEditPost"].setData(post.body);
+ document.querySelector("#editPostForm input[type='submit']").id = id;
+ }
+ })
+}
+
/**
* Shows respective error message for form
- * @param {string} message The error message to show
- * @param {string} form The form to show the error message for
+ * @param {string} message - The error message to show
+ * @param {string} form - The form to show the error message for
*/
function showErrorMessage(message, form)
{
@@ -413,7 +566,7 @@ function showSuccessMessage(message, form)
/**
* Switches the timeline item between edit and view mode
- * @param id the id of the timeline item from the database
+ * @param id - the id of the timeline item from the database
*/
function editCVItem(id)
{
@@ -534,8 +687,8 @@ function addWorkData(ID, startPeriod, endPeriod, companyName, area, jobTitle, pr
/**
* Updates the edu timeline item with the given id
* and data from the form
- * @param {number} id the id of the edu timeline item from the database
- * @param {SubmitEvent} e the event that triggered the function
+ * @param {number} id - the id of the edu timeline item from the database
+ * @param {SubmitEvent} e - the event that triggered the function
*/
function updateEduItem(id, e)
{
@@ -582,8 +735,8 @@ function updateEduItem(id, e)
/**
* Updates the work timeline item with the given id
* and data from the form
- * @param {number} id the id of the work timeline item from the database
- * @param {SubmitEvent} e the event that triggered the function
+ * @param {number} id - the id of the work timeline item from the database
+ * @param {SubmitEvent} e - the event that triggered the function
*/
function updateWorkItem(id, e)
{
@@ -630,7 +783,7 @@ function updateWorkItem(id, e)
/**
* Deletes the timeline item with the given id
- * @param {number} id the id of the timeline item
+ * @param {number} id - the id of the timeline item
*/
function deleteEduItem(id)
{
@@ -658,7 +811,7 @@ function deleteEduItem(id)
/**
* Updates the timeline item with the given id
- * @param {number} id the id of the timeline item from the database
+ * @param {number} id - the id of the timeline item from the database
*/
function deleteWorkItem(id)
{
@@ -688,8 +841,8 @@ function deleteWorkItem(id)
/**
* Updates the project item with the given id
* and data from the form
- * @param {number} id the id from the database
- * @param {SubmitEvent} e the event of the form that was submitted
+ * @param {string} id - the base id of the element
+ * @param {SubmitEvent} e - the event of the form that was submitted
*/
function updateProjectItem(id, e)
{
@@ -785,20 +938,9 @@ function updateProjectItem(id, e)
}
-/**
- * Toggles the editing mode of the project item with the given id
- * @param id {number} the id of the project item from the database
- */
-function editProjectItem(id)
-{
- document.querySelector(`#projectItem${id}`).classList.toggle("editing");
- document.querySelector(`#title${id}`).removeAttribute("disabled");
- document.querySelector(`#info${id}`).removeAttribute("disabled");
-}
-
/**
* Deletes the project item with the given id
- * @param id {number} the id of the project item from the database
+ * @param id {number} - the id of the project item from the database
*/
function deleteProjectItem(id)
{
@@ -827,24 +969,25 @@ function deleteProjectItem(id)
/**
* Adds a new project to the page
- * @param {number} id the id of the project from the database
- * @param {string} isMainProject is it the main project
- * @param {string} imgLocation the relative path of the image
- * @param {string} title the title of the project
- * @param {string} information the information about the project
- * @param {string} projectLink the link to the project
- * @param {string} gitLink the link to the git repository
+ * @param {number} ID - the id of the project from the database
+ * @param {string} isMainProject - is it the main project
+ * @param {string} imgLocation - the relative path of the image
+ * @param {string} title - the title of the project
+ * @param {string} information - the information about the project
+ * @param {string} projectLink - the link to the project
+ * @param {string} gitLink - the link to the git repository
*/
-function addProject(id , isMainProject, imgLocation, title, information, projectLink, gitLink)
+function addProject(ID, isMainProject, imgLocation, title, information, projectLink, gitLink)
{
let projectItem = document.createElement("form");
- projectItem.id = "projectItem" + id;
+ let id = ID + "proj";
+ projectItem.id = "projectItem" + ID;
projectItem.classList.add("projItem");
projectItem.onsubmit = e => updateProjectItem(id, e);
projectItem.innerHTML = `
-
-
+
+
@@ -889,3 +1032,63 @@ function addProject(id , isMainProject, imgLocation, title, information, project
document.querySelector("#projList").appendChild(projectItem);
}
+
+
+/**
+ * Deletes the post item with the given id
+ * @param {number} id - the id of the post item from the database
+ */
+function deletePostItem(id)
+{
+ fetch("/api/blog/post/" + id, {
+ method: "DELETE",
+ headers: {
+ "Authorization": "Bearer " + localStorage.getItem("token")
+ }
+ }).then(res =>
+ {
+ if (res.ok)
+ {
+ document.querySelector(`#postInfo${id}`).remove();
+ return;
+ }
+
+ if (res.status === 401)
+ {
+ window.location.href = "./";
+ return;
+ }
+
+ res.json().then(json => alert(json.error));
+ });
+}
+
+/**
+ * Adds a new post info to the edit post table
+ * @param {number} ID - the id of the post
+ * @param {string} title - the title of the post
+ * @param {string} dateCreated - the date the post was created
+ * @param {string} dateModified - the date the post was modified
+ */
+function addPostInfo(ID, title, dateCreated, dateModified)
+{
+ let postInfo = document.createElement("tr");
+ let id = ID + "post";
+ postInfo.id = "postInfo" + ID;
+ postInfo.innerHTML = `
+
+ ${title}
+
+
+ ${new Date(dateCreated).toLocaleDateString()}
+
+
+ ${new Date(dateModified).toLocaleDateString()}
+
+
+
+
+
+ `;
+ document.querySelector("#editPost table tbody").appendChild(postInfo);
+}
\ No newline at end of file