newsletter-and-cookies #51

Merged
rodude123 merged 2 commits from newsletter-and-cookies into master 2023-12-06 23:32:09 +00:00
37 changed files with 2672 additions and 154 deletions

View File

@ -16,7 +16,8 @@
"tuupola/slim-jwt-auth": "^3.6", "tuupola/slim-jwt-auth": "^3.6",
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*", "ext-libxml": "*",
"donatello-za/rake-php-plus": "^1.0" "donatello-za/rake-php-plus": "^1.0",
"phpmailer/phpmailer": "^6.9"
}, },
"repositories": [ "repositories": [
{ {

83
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "f675ad7eec6390b82dca0b13dec49e5b", "content-hash": "f156a57e5e895727417d4274c8ad414c",
"packages": [ "packages": [
{ {
"name": "donatello-za/rake-php-plus", "name": "donatello-za/rake-php-plus",
@ -1046,6 +1046,87 @@
}, },
"time": "2020-10-12T12:39:22+00:00" "time": "2020-10-12T12:39:22+00:00"
}, },
{
"name": "phpmailer/phpmailer",
"version": "v6.9.1",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "039de174cd9c17a8389754d3b877a2ed22743e18"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/039de174cd9c17a8389754d3b877a2ed22743e18",
"reference": "039de174cd9c17a8389754d3b877a2ed22743e18",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-filter": "*",
"ext-hash": "*",
"php": ">=5.5.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.7.2",
"yoast/phpunit-polyfills": "^1.0.4"
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-only"
],
"authors": [
{
"name": "Marcus Bointon",
"email": "phpmailer@synchromedia.co.uk"
},
{
"name": "Jim Jagielski",
"email": "jimjag@gmail.com"
},
{
"name": "Andy Prevost",
"email": "codeworxtech@users.sourceforge.net"
},
{
"name": "Brent R. Matzelle"
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.1"
},
"funding": [
{
"url": "https://github.com/Synchro",
"type": "github"
}
],
"time": "2023-11-25T22:23:28+00:00"
},
{ {
"name": "psr/container", "name": "psr/container",
"version": "1.1.2", "version": "1.1.2",

View File

@ -8,11 +8,15 @@ use DOMDocument;
use PDO; use PDO;
use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\UploadedFileInterface;
use DonatelloZa\RakePlus\RakePlus; use DonatelloZa\RakePlus\RakePlus;
use function DI\string; use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use function api\utils\dbConn;
use function api\utils\getEmailPassword;
use const api\utils\feedGenerator\ATOM; use const api\utils\feedGenerator\ATOM;
use const api\utils\feedGenerator\RSS2; use const api\utils\feedGenerator\RSS2;
require_once __DIR__ . "/../utils/config.php"; require_once __DIR__ . "/../utils/config.php";
require_once __DIR__ . "/../utils/imgUtils.php"; require_once __DIR__ . "/../utils/imgUtils.php";
require_once __DIR__ . "/../utils/feedGenerator/FeedWriter.php"; require_once __DIR__ . "/../utils/feedGenerator/FeedWriter.php";
@ -254,10 +258,10 @@ class blogData
* @param string $dateCreated - Date the blog post was created * @param string $dateCreated - Date the blog post was created
* @param bool $featured - Whether the blog post is featured or not * @param bool $featured - Whether the blog post is featured or not
* @param string $categories - Categories of the blog post * @param string $categories - Categories of the blog post
* @param UploadedFileInterface $headerImg - Header image of the blog post * @param UploadedFileInterface|null $headerImg - Header image of the blog post
* @return int|string - ID of the blog post or error message * @return int|string - ID of the blog post or error message
*/ */
public function createPost(string $title, string $abstract, string $body, string $bodyText, string $dateCreated, bool $featured, string $categories, UploadedFileInterface $headerImg): int|string public function createPost(string $title, string $abstract, string $body, string $bodyText, string $dateCreated, bool $featured, string $categories, UploadedFileInterface|null $headerImg): int|string
{ {
$conn = dbConn(); $conn = dbConn();
$folderID = uniqid(); $folderID = uniqid();
@ -289,6 +293,13 @@ class blogData
$keywords = implode(", ", RakePlus::create($bodyText)->keywords()); $keywords = implode(", ", RakePlus::create($bodyText)->keywords());
$latest = $this->getLatestBlogPost();
$prevTitle = $latest["title"];
$prevAbstract = $latest["abstract"];
$prevHeaderImage = $latest["headerImg"];
$headerImage = $targetFile["imgLocation"];
$stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, abstract, body, bodyText, categories, keywords, folderID) $stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, abstract, body, bodyText, categories, keywords, folderID)
VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :abstract, :body, :bodyText, :categories, :keywords, :folderID);"); VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :abstract, :body, :bodyText, :categories, :keywords, :folderID);");
$stmt->bindParam(":title", $title); $stmt->bindParam(":title", $title);
@ -296,7 +307,7 @@ class blogData
$stmt->bindParam(":dateModified", $dateCreated); $stmt->bindParam(":dateModified", $dateCreated);
$isFeatured = $featured ? 1 : 0; $isFeatured = $featured ? 1 : 0;
$stmt->bindParam(":featured", $isFeatured); $stmt->bindParam(":featured", $isFeatured);
$stmt->bindParam(":headerImg", $targetFile["imgLocation"]); $stmt->bindParam(":headerImg", $headerImage);
$stmt->bindParam(":abstract", $abstract); $stmt->bindParam(":abstract", $abstract);
$stmt->bindParam(":body", $newBody); $stmt->bindParam(":body", $newBody);
$stmt->bindParam(":bodyText", $bodyText); $stmt->bindParam(":bodyText", $bodyText);
@ -304,12 +315,267 @@ class blogData
$stmt->bindParam(":keywords", $keywords); $stmt->bindParam(":keywords", $keywords);
$stmt->bindParam(":folderID", $folderID); $stmt->bindParam(":folderID", $folderID);
if ($stmt->execute()) if (!$stmt->execute())
{ {
return intval($conn->lastInsertId()); return "Error, couldn't create post";
} }
return "Error, couldn't create post"; $stmtEmails = $conn->prepare("SELECT email FROM newsletter;");
$stmtEmails->execute();
$emails = $stmtEmails->fetchAll(PDO::FETCH_ASSOC);
$emailBody = <<<EOD
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rohit Pai's blog</title>
<style>
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/notosans/v34/o-0NIpQlx3QUlC5A4PNjXhFVZNyB.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Share Tech Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
*{
box-sizing: border-box;
}
body, html {
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: Noto Sans KR, sans-serif;
font-style: normal;
font-weight: 500;
font-size: 22px;
line-height: 1.625rem;
min-height: 100%;
}
main, header, footer {
max-width: 768px;
margin: 0 auto;
}
a {
text-decoration: none;
text-transform: lowercase;
}
a:visited {
color: inherit;
}
h1, h2 {
font-family: Share Tech Mono, monospace;
font-style: normal;
font-weight: normal;
line-height: 2.5625rem;
text-transform: lowercase;
}
h1, nav {
font-size: 40px;
}
h2 {
font-size: 28px;
}
h3 {
font-size: 22px;
}
header {
background: hsla(80, 60%, 50%, 1);
color: #FFFFFF;
border-bottom: 5px solid hsla(0, 0%, 75%, 1);
}
header div.title, header div.byLine {
padding: 0 3em 1em;
}
header div.title h1 {
margin: 0;
padding: 1em 0 0;
font-size: 42px;
}
header div.byLine {
border-top: 5px solid hsla(80, 60%, 30%, 1);
}
a.btn {
background-color: hsla(80, 60%, 50%, 1);
text-decoration: none;
display: inline-flex;
padding: 1em 2em;
border-radius: 0.625em;
border: 0.3215em solid hsla(80, 60%, 50%, 1);
color: #FFFFFF;
text-align: center;
align-items: center;
max-height: 4em;
}
div.postContainer, div.container {
padding: 1em 2em 0;
margin: 0 auto 1em;
max-width: 768px;
}
div.postContainer ~ div.postContainer {
border-top: 5px solid hsla(0, 0%, 75%, 1);
}
div.postContainer > *, div.container > * {
margin: 0 auto;
max-width: 768px;
}
div.postContainer div.post h2, div.container div.content h2 {
margin: 0;
padding: 0;
}
div.postContainer div.image, div.container div.image {
width: 50%;
max-width: 100%;
height: auto;
}
div.postContainer div.image img {
-webkit-border-radius: 0.5em;
-moz-border-radius: 0.5em;
border-radius: 0.5em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.container div.image img {
-webkit-border-radius: 10em;
-moz-border-radius: 10em;
border-radius: 10em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.postContainer div.image img, div.container div.image img {
min-width: 100%;
width: 100%;
}
footer {
background-color: hsla(80, 60%, 50%, 1);
margin-top: auto;
padding: 3em;
display: block;
color: #FFFFFF;
}
footer .nav {
width: 75%;
float: left;
}
footer .nav ul {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
footer .nav ul li {
display: inline-block;
margin: 0 1em;
}
footer .nav ul a {
color: #FFFFFF;
}
footer .date {
width: 25%;
float: right;
}
</style>
</head>
<body>
<header>
<div class="title">
<h1>Hey, I've got a new post!</h1>
</div>
<div class="byLine">
<p>Hello I'm Rohit an avid full-stack developer and home lab enthusiast. I love anything and everything
to do with full stack development, home labs, coffee and generally anything to do with tech.</p>
</div>
</header>
<main>
<div class="postContainer">
<h2>latest post</h2>
<div class="image">
<img src="$headerImage" alt="header image of the latest post">
</div>
<div class="post">
<h3>$title</h3>
<p>$abstract</p>
<a href="https://rohitpai.co.uk/blog/post/$title" class="btn">See Post</a>
</div>
</div>
<div class="postContainer">
<h2>in case you missed the previous post</h2>
<div class="image">
<img src="$prevHeaderImage" alt="header image of the previous post">
</div>
<div class="post">
<h3>$prevTitle</h3>
<p>$prevAbstract</p>
<a href="https://rohitpai.co.uk/blog/post/$prevTitle" class="btn">See Post</a>
</div>
</div>
</main>
<footer>
<div class="nav">
<ul>
<li><a href="https://rohitpai.co.uk/blog">&lt;https://rohitpai.co.uk/blog&gt;</a></li>
<li><a href="https://rohitpai.co.uk/blog/unsubscribe">&lt;Unsubscribe&gt;</a></li>
</ul>
</div>
<div class="date">2023</div>
</footer>
</body>
</html>
EOD;
foreach ($emails as $email)
{
$this->sendMail($email["email"], $emailBody, "Hey, Rohit's blog has a new post!");
}
return intval($conn->lastInsertId());
} }
/** /**
@ -616,12 +882,12 @@ class blogData
foreach ($posts as $post) foreach ($posts as $post)
{ {
$items[] = array( $items[] = array(
"id" => string($post["ID"]), "id" => strval($post["ID"]),
"url" => "https://rohitpai.co.uk/blog/post/" . rawurlencode($post["title"]) . "#disqus_thread", "url" => "https://rohitpai.co.uk/blog/post/" . rawurlencode($post["title"]) . "#disqus_thread",
"title" => $post["title"], "title" => $post["title"],
"date_published" => date($post["dateCreated"]), "date_published" => date($post["dateCreated"]),
"date_modified" => date($post["dateModified"]), "date_modified" => date($post["dateModified"]),
// "description" => $post["abstract"], "description" => $post["abstract"],
"banner_image" => "https://rohitpai.co.uk/" . rawurlencode($post["headerImg"]), "banner_image" => "https://rohitpai.co.uk/" . rawurlencode($post["headerImg"]),
"content_html" => $post["body"] "content_html" => $post["body"]
); );
@ -655,4 +921,572 @@ class blogData
return $feed; return $feed;
} }
/**
* Add an email to the newsletter and send welcome email
* @param string $email - Email to add to the newsletter
* @return string|array - Success or error message
*/
public function addNewsletterEmail(string $email): string|array
{
$conn = dbConn();
$stmtCheckEmail = $conn->prepare("SELECT * FROM newsletter WHERE email = :email;");
$stmtCheckEmail->bindParam(":email", $email);
$stmtCheckEmail->execute();
$result = $stmtCheckEmail->fetch(PDO::FETCH_ASSOC);
if ($result)
{
return "Email already exists";
}
$stmt = $conn->prepare("INSERT INTO newsletter (email) VALUES (:email);");
$stmt->bindParam(":email", $email);
$stmt->execute();
$body = <<<EOD
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rohit Pai's blog</title>
<style>
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/notosans/v34/o-0NIpQlx3QUlC5A4PNjXhFVZNyB.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Share Tech Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
*{
box-sizing: border-box;
}
body, html {
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: Noto Sans KR, sans-serif;
font-style: normal;
font-weight: 500;
font-size: 22px;
line-height: 1.625rem;
min-height: 100%;
}
main, header, footer {
max-width: 768px;
margin: 0 auto;
}
a {
text-decoration: none;
text-transform: lowercase;
}
a:visited {
color: inherit;
}
h1, h2 {
font-family: Share Tech Mono, monospace;
font-style: normal;
font-weight: normal;
line-height: 2.5625rem;
text-transform: lowercase;
}
h1, nav {
font-size: 40px;
}
h2 {
font-size: 28px;
}
h3 {
font-size: 22px;
}
header {
background: hsla(80, 60%, 50%, 1);
color: #FFFFFF;
border-bottom: 5px solid hsla(0, 0%, 75%, 1);
}
header div.title, header div.byLine {
padding: 0 3em 1em;
}
header div.title h1 {
margin: 0;
padding: 1em 0 0;
font-size: 42px;
}
header div.byLine {
border-top: 5px solid hsla(80, 60%, 30%, 1);
}
a.btn {
background-color: hsla(80, 60%, 50%, 1);
text-decoration: none;
display: inline-flex;
padding: 1em 2em;
border-radius: 0.625em;
border: 0.3215em solid hsla(80, 60%, 50%, 1);
color: #FFFFFF;
text-align: center;
align-items: center;
max-height: 4em;
}
div.postContainer, div.container {
padding: 1em 2em 0;
margin: 0 auto 1em;
max-width: 768px;
}
div.postContainer ~ div.postContainer {
border-top: 5px solid hsla(0, 0%, 75%, 1);
}
div.postContainer > *, div.container > * {
margin: 0 auto;
max-width: 768px;
}
div.postContainer div.post h2, div.container div.content h2 {
margin: 0;
padding: 0;
}
div.postContainer div.image, div.container div.image {
width: 50%;
max-width: 100%;
height: auto;
}
div.postContainer div.image img {
-webkit-border-radius: 0.5em;
-moz-border-radius: 0.5em;
border-radius: 0.5em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.container div.image img {
-webkit-border-radius: 50em;
-moz-border-radius: 50em;
border-radius: 50em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.postContainer div.image img, div.container div.image img {
min-width: 100%;
width: 100%;
}
footer {
background-color: hsla(80, 60%, 50%, 1);
margin-top: auto;
padding: 3em;
display: block;
color: #FFFFFF;
}
footer .nav {
width: 75%;
float: left;
}
footer .nav ul {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
footer .nav ul li {
display: inline-block;
margin: 0 1em;
}
footer .nav ul a {
color: #FFFFFF;
}
footer .date {
width: 25%;
float: right;
}
</style>
</head>
<body>
<header>
<div class="title">
<h1>hello from rohit</h1>
</div>
<div class="byLine">
<p>Hello I'm Rohit an avid full-stack developer and home lab enthusiast. I love anything and everything
to do with full stack development, home labs, coffee and generally anything to do with tech.</p>
</div>
</header>
<main>
<div class="container">
<h2>hey there, i'm rohit!</h2>
<div class="image"><img src="https://rohitpai.co.uk/imgs/profile.jpg" alt=""></div>
<div class="content">
<h3>What to Expect</h3>
<p>You'll get an email from me everytime I make a new post to my blog. Sometimes, you may get a special
email on occasion, where I talk about something interesting that's not worth a full on post but I
Still want to tell you about it.</p>
<p>Don't worry, I won't spam you with emails. Well, thanks for signing up to my newsletter, hopefully
you'll hear from me soon!</p>
<p>P.S. Please consider adding this email address rohit@rohitpai.co.uk to your emails
contact list so that my emails won't get sent to span, thanks.</p>
</div>
</div>
</main>
<footer>
<div class="nav">
<ul>
<li><a href="https://rohitpai.co.uk/blog">&lt;https://rohitpai.co.uk/blog&gt;</a></li>
<li><a href="https://rohitpai.co.uk/blog/unsubscribe">&lt;Unsubscribe&gt;</a></li>
</ul>
</div>
<div class="date">2023</div>
</footer>
</body>
</html>
EOD;
return $this->sendMail($email, $body, "Hello from Rohit!");
}
/**
* Send an email to the given email address
* @param string $email - Email address to send the email to
* @param string $body - Body of the email
* @param string $subject - Subject of the email
* @return string|string[]
*/
public function sendMail(string $email, string $body, string $subject): string|array
{
$mail = new PHPMailer(true);
try
{
$mail->isSMTP();
$mail->Host = "smtp.hostinger.com";
$mail->SMTPAuth = true;
$mail->Username = "rohit@rohitpai.co.uk";
$mail->Password = getEmailPassword();
$mail->SMTPSecure = "tls";
$mail->Port = 587;
$mail->setFrom("rohit@rohitpai.co.uk", "Rohit Pai");
$mail->addAddress($email);
$mail->isHTML();
$mail->Subject = $subject;
$mail->Body = $body;
$mail->send();
return "success";
}
catch (Exception $e)
{
return array("errorMessage" => "Error, couldn't send email because of " . $e);
}
}
/**
* @param string $email - Email to delete from the newsletter
* @return string - Success or error message
*/
public function deleteNewsletterEmail(string $email): string
{
$conn = dbConn();
$stmtCheckEmail = $conn->prepare("SELECT * FROM newsletter WHERE email = :email;");
$stmtCheckEmail->bindParam(":email", $email);
$stmtCheckEmail->execute();
$result = $stmtCheckEmail->fetch(PDO::FETCH_ASSOC);
if (!$result)
{
return "email not found";
}
$stmt = $conn->prepare("DELETE FROM newsletter WHERE email = :email;");
$stmt->bindParam(":email", $email);
$stmt->execute();;
return "success";
}
/**
* @param string $subject - Subject of the newsletter
* @param string $message - Message content
* @return array|string - Success or error message
*/
public function sendNewsletter(string $subject, string $message): array|string
{
$conn = dbConn();
$stmtEmails = $conn->prepare("SELECT email FROM newsletter;");
$stmtEmails->execute();
$emails = $stmtEmails->fetchAll(PDO::FETCH_ASSOC);
$msg = "";
$body = <<<EOD
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rohit Pai's blog</title>
<style>
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/notosans/v34/o-0NIpQlx3QUlC5A4PNjXhFVZNyB.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Share Tech Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
*{
box-sizing: border-box;
}
body, html {
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: Noto Sans KR, sans-serif;
font-style: normal;
font-weight: 500;
font-size: 22px;
line-height: 1.625rem;
min-height: 100%;
}
main, header, footer {
max-width: 768px;
margin: 0 auto;
}
a {
text-decoration: none;
text-transform: lowercase;
}
a:visited {
color: inherit;
}
h1, h2 {
font-family: Share Tech Mono, monospace;
font-style: normal;
font-weight: normal;
line-height: 2.5625rem;
text-transform: lowercase;
}
h1, nav {
font-size: 40px;
}
h2 {
font-size: 28px;
}
h3 {
font-size: 22px;
}
header {
background: hsla(80, 60%, 50%, 1);
color: #FFFFFF;
border-bottom: 5px solid hsla(0, 0%, 75%, 1);
}
header div.title, header div.byLine {
padding: 0 3em 1em;
}
header div.title h1 {
margin: 0;
padding: 1em 0 0;
font-size: 42px;
}
header div.byLine {
border-top: 5px solid hsla(80, 60%, 30%, 1);
}
a.btn {
background-color: hsla(80, 60%, 50%, 1);
text-decoration: none;
display: inline-flex;
padding: 1em 2em;
border-radius: 0.625em;
border: 0.3215em solid hsla(80, 60%, 50%, 1);
color: #FFFFFF;
text-align: center;
align-items: center;
max-height: 4em;
}
div.postContainer, div.container {
padding: 1em 2em 0;
margin: 0 auto 1em;
max-width: 768px;
}
div.postContainer ~ div.postContainer {
border-top: 5px solid hsla(0, 0%, 75%, 1);
}
div.postContainer > *, div.container > * {
margin: 0 auto;
max-width: 768px;
}
div.postContainer div.post h2, div.container div.content h2 {
margin: 0;
padding: 0;
}
div.postContainer div.image, div.container div.image {
width: 50%;
max-width: 100%;
height: auto;
}
div.postContainer div.image img {
-webkit-border-radius: 0.5em;
-moz-border-radius: 0.5em;
border-radius: 0.5em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.container div.image img {
-webkit-border-radius: 50em;
-moz-border-radius: 50em;
border-radius: 50em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.postContainer div.image img, div.container div.image img {
min-width: 100%;
width: 100%;
}
footer {
background-color: hsla(80, 60%, 50%, 1);
margin-top: auto;
padding: 3em;
display: block;
color: #FFFFFF;
}
footer .nav {
width: 75%;
float: left;
}
footer .nav ul {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
footer .nav ul li {
display: inline-block;
margin: 0 1em;
}
footer .nav ul a {
color: #FFFFFF;
}
footer .date {
width: 25%;
float: right;
}
</style>
</head>
<body>
<header>
<div class="title">
<h1>a surprise hello from rohit</h1>
</div>
<div class="byLine">
<p>Hello I'm Rohit an avid full-stack developer and home lab enthusiast. I love anything and everything
to do with full stack development, home labs, coffee and generally anything to do with tech.</p>
</div>
</header>
<main>
<div class="container">
<h2>$subject</h2>
<div class="content">
$message
</div>
</div>
</main>
<footer>
<div class="nav">
<ul>
<li><a href="https://rohitpai.co.uk/blog">&lt;https://rohitpai.co.uk/blog&gt;</a></li>
<li><a href="https://rohitpai.co.uk/blog/unsubscribe">&lt;Unsubscribe&gt;</a></li>
</ul>
</div>
<div class="date">2023</div>
</footer>
</body>
</html>
EOD;
foreach ($emails as $email)
{
$msg = $this->sendMail($email["email"], $body, $subject);
if (is_array($msg))
{
return $msg;
}
}
return $msg;
}
} }

View File

@ -269,12 +269,38 @@ class blogRoutes implements routesInterface
return $response; return $response;
}); });
$app->delete("/blog/newsletter/{email}", function (Request $request, Response $response, $args)
{
if ($args["email"] == null)
{
$response->getBody()->write(json_encode(array("error" => "Please provide an email")));
return $response->withStatus(400);
}
$message = $this->blogData->deleteNewsletterEmail($args["email"]);
if ($message === "email not found")
{
// uh oh something went wrong
$response->getBody()->write(json_encode(array("error" => "Error, email 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);
}
return $response;
});
$app->post("/blog/post", function (Request $request, Response $response) $app->post("/blog/post", function (Request $request, Response $response)
{ {
$data = $request->getParsedBody(); $data = $request->getParsedBody();
$files = $request->getUploadedFiles(); $files = $request->getUploadedFiles();
$headerImg = $files["headerImg"]; if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["bodyText"]) || empty($data["abstract"]) || empty($data["dateCreated"]) || empty($data["categories"]))
if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["abstract"]) || empty($data["dateCreated"]) || empty($data["categories"]))
{ {
// uh oh sent some empty data // uh oh sent some empty data
$response->getBody()->write(json_encode(array("error" => "Error, empty data sent"))); $response->getBody()->write(json_encode(array("error" => "Error, empty data sent")));
@ -288,6 +314,11 @@ class blogRoutes implements routesInterface
return $response->withStatus(400); return $response->withStatus(400);
} }
if (array_key_exists("headerImg", $files))
{
$headerImg = $files["headerImg"];
}
if (empty($files["headerImg"])) if (empty($files["headerImg"]))
{ {
$headerImg = null; $headerImg = null;
@ -339,19 +370,66 @@ class blogRoutes implements routesInterface
if (empty($files)) if (empty($files))
{ {
// uh oh sent some empty data // uh oh sent some empty data
$response->getBody()->write(json_encode(array("error" => array("message" => "Error, empty data sent")))); $response->getBody()->write(json_encode(array("error" => "Error, empty data sent")));
return $response->withStatus(400); return $response->withStatus(400);
} }
$message = $this->blogData->uploadHeaderImage($args["id"], $files["headerImg"]); $message = $this->blogData->uploadHeaderImage($args["id"], $files["headerImg"]);
if (!is_array($message)) if (!is_array($message))
{ {
$response->getBody()->write(json_encode(array("error" => array("message" => $message)))); $response->getBody()->write(json_encode(array("error" => $message)));
return $response->withStatus(500); return $response->withStatus(500);
} }
$response->getBody()->write(json_encode($message)); $response->getBody()->write(json_encode($message));
return $response->withStatus(201); return $response->withStatus(201);
}); });
$app->post("/blog/newsletter", function (Request $request, Response $response)
{
$data = $request->getParsedBody();
if (empty($data["subject"]) || empty($data["message"]))
{
// uh oh sent some empty data
$response->getBody()->write(json_encode(array("error" => "Error, empty data sent")));
return $response->withStatus(400);
}
$message = $this->blogData->sendNewsletter(strtolower($data["subject"]), $data["message"]);
if (is_array($message))
{
$response->getBody()->write(json_encode(array("error" => "Error, something went wrong")));
return $response->withStatus(500);
}
$response->getBody()->write(json_encode(array("message" => "Message sent")));
return $response->withStatus(201);
});
$app->post("/blog/newsletter/{email}", function (Request $request, Response $response, $args)
{
if ($args["email"] == null)
{
$response->getBody()->write(json_encode(array("error" => "Please provide an email")));
return $response->withStatus(400);
}
$message = $this->blogData->addNewsletterEmail($args["email"]);
if ($message === "Email already exists")
{
$response->getBody()->write(json_encode(array("message" => "exists")));
return $response->withStatus(409);
}
if (is_array($message) || !$message || $message === "error")
{
$response->getBody()->write(json_encode(array("message" => "Something went wrong")));
return $response->withStatus(500);
}
$response->getBody()->write(json_encode(array("message" => "Thanks for signing up!")));
return $response->withStatus(201);
});
} }
} }

View File

@ -5,6 +5,7 @@ namespace api\project;
use api\utils\imgUtils; use api\utils\imgUtils;
use PDO; use PDO;
use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\UploadedFileInterface;
use function api\utils\dbConn;
require_once __DIR__ . "/../utils/config.php"; require_once __DIR__ . "/../utils/config.php";
require_once __DIR__ . "/../utils/imgUtils.php"; require_once __DIR__ . "/../utils/imgUtils.php";

View File

@ -3,6 +3,7 @@
namespace api\timeline; namespace api\timeline;
use PDO; use PDO;
use function api\utils\dbConn;
require_once __DIR__ . "/../utils/config.php"; require_once __DIR__ . "/../utils/config.php";

View File

@ -4,6 +4,8 @@ namespace api\user;
use Firebase\JWT\JWT; use Firebase\JWT\JWT;
use PDO; use PDO;
use function api\utils\dbConn;
use function api\utils\getSecretKey;
require_once __DIR__ . "/../utils/config.php"; require_once __DIR__ . "/../utils/config.php";

View File

@ -13,6 +13,7 @@ use Slim\Exception\HttpInternalServerErrorException;
use Slim\Exception\HttpMethodNotAllowedException; use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException; use Slim\Exception\HttpNotFoundException;
use Slim\Psr7\Response; use Slim\Psr7\Response;
use Throwable;
use Tuupola\Middleware\JwtAuthentication; use Tuupola\Middleware\JwtAuthentication;
use Tuupola\Middleware\JwtAuthentication\RequestMethodRule; use Tuupola\Middleware\JwtAuthentication\RequestMethodRule;
use Tuupola\Middleware\JwtAuthentication\RequestPathRule; use Tuupola\Middleware\JwtAuthentication\RequestPathRule;
@ -84,8 +85,8 @@ class middleware
$app->add(new JwtAuthentication([ $app->add(new JwtAuthentication([
"rules" => [ "rules" => [
new RequestPathRule([ new RequestPathRule([
"path" => ["/api/projectData", "/api/timelineData/[a-z]*", "/api/projectImage/[0-9]*", "/api/logout"], "path" => ["/api/projectData", "/api/timelineData/[a-z]*", "/api/projectImage/[0-9]*", "/api/logout", "/api/blog/[a-z]*"],
"ignore" => ["/api/contact", "/api/userData/login", "/api/userData/changePassword"] "ignore" => ["/api/contact", "/api/userData/login", "/api/userData/changePassword", "/api/blog/newsletter/\S*", "/api/blog/newsletter/unsubscribe/\S*"]
]), ]),
new RequestMethodRule([ new RequestMethodRule([
"ignore" => ["OPTIONS", "GET"] "ignore" => ["OPTIONS", "GET"]
@ -133,8 +134,27 @@ class middleware
return $response; return $response;
} }
}); });
$app->addErrorMiddleware(true, true, true);
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorHandler = $errorMiddleware->getDefaultErrorHandler();
$errorMiddleware->setDefaultErrorHandler(function (ServerRequestInterface $request, Throwable $exception,
bool $displayErrorDetails,
bool $logErrors,
bool $logErrorDetails
) use ($app, $errorHandler)
{
$statusCode = $exception->getCode() ?: 500;
// Create a JSON response with the error message
$response = $app->getResponseFactory()->createResponse($statusCode);
$response->getBody()->write(json_encode(['error' => $exception->getMessage()]));
return $response;
});
} }
} }

142
dist/api/utils/user/userData.php vendored Normal file
View File

@ -0,0 +1,142 @@
<?php
namespace api\user;
use Firebase\JWT\JWT;
use PDO;
use function api\utils\dbConn;
use function api\utils\getSecretKey;
require_once __DIR__ . "/../utils/config.php";
/**
* User Class
* Define all functions which either check, update or delete userData data
*/
class userData
{
/**
* Check if userData exists and can be logged in
* @param $username string - Username
* @param $password string - Password
* @return bool - True if logged in, false if not
*/
public function checkUser(string $username, string $password): bool
{
$conn = dbConn();
$stmt = $conn->prepare("SELECT * FROM users WHERE username = :username");
$stmt->bindParam(":username", $username);
$stmt->execute();
// set the resulting array to associative
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($result)
{
if (password_verify($password, $result[0]["password"]))
{
return true;
}
return false;
}
return false;
}
/**
* Create a JWT token
* @param $username string - Username
* @return string - JWT token
*/
public function createToken(string $username): string
{
$now = time();
$future = strtotime('+2 day', $now);
$secretKey = getSecretKey();
$payload = [
"jti" => $username,
"iat" => $now,
"exp" => $future
];
return JWT::encode($payload, $secretKey, "HS256");
}
/**
* Check if email is already in use
* @param string $email - Email to check
* @return bool - True if email exists, false if not
*/
public function checkEmail(string $email): bool
{
$conn = dbConn();
$stmt = $conn->prepare("SELECT * FROM users WHERE email = :email");
$stmt->bindParam(":email", $email);
$stmt->execute();
// set the resulting array to associative
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($result)
{
return true;
}
return false;
}
/**
* Send a verification email to the userData
* @param $email - email address of the userData
* @return string - verification code
*/
public function sendResetEmail($email): string
{
//generate a random token and email the address
$token = uniqid("rpe-");
$headers1 = "From: noreply@rohitpai.co.uk\r\n";
$headers1 .= "MIME-Version: 1.0\r\n";
$headers1 .= "Content-Type: text/html; charset=UTF-8\r\n";
$message = "
<!doctype html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, userData-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0'>
<meta http-equiv='X-UA-Compatible' content='ie=edge'>
<title>Document</title>
</head>
<body>
<h1>Reset Password Verification Code</h1>
<br>
<p>Please enter the following code to reset your password: $token</p>
</body>
</html>
";
mail($email, "Reset Password Verification Code", $message, $headers1);
return $token;
}
/**
* Change password for an email with new password
* @param $email string Email
* @param $password string Password
* @return bool - true if the password was changed, false if not
*/
public function changePassword(string $email, string $password): bool
{
$conn = dbConn();
$stmt = $conn->prepare("UPDATE users SET password = :password WHERE email = :email");
$newPwd = password_hash($password, PASSWORD_BCRYPT);
$stmt->bindParam(":password", $newPwd);
$stmt->bindParam(":email", $email);
if ($stmt->execute())
{
return true;
}
return false;
}
}

168
dist/api/utils/user/userRoutes.php vendored Normal file
View File

@ -0,0 +1,168 @@
<?php
namespace api\user;
require_once __DIR__ . "/../utils/routesInterface.php";
require_once "userData.php";
use api\utils\routesInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\App;
class userRoutes implements routesInterface
{
private userData $user;
/**
* constructor used to instantiate a base user routes, to be used in the index.php file.
* @param App $app - the slim app used to create the routes
*/
public function __construct(App $app)
{
$this->user = new userData();
$this->createRoutes($app);
}
/**
* creates the routes for the user
* @param App $app - the slim app used to create the routes
* @return void - returns nothing
*/
public function createRoutes(App $app): void
{
$app->post("/user/login", function (Request $request, Response $response)
{
// get request data
$data = $request->getParsedBody();
if (empty($data["username"]) || empty($data["password"]))
{
// uh oh user sent empty data
return $response->withStatus(400);
}
if ($this->user->checkUser($data["username"], $data["password"]))
{
// 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;
}
$response->getBody()->write(json_encode(array("error" => "Unauthorised")));
return $response->withStatus(401);
});
$app->get("/user/logout", function (Request $request, Response $response)
{
session_unset();
return $response;
});
$app->get("/user/isLoggedIn", function (Request $request, Response $response)
{
if (empty($_SESSION["token"]) && empty($_SESSION["username"]))
{
// 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"]))
{
// user is logged in but no token was created
$_SESSION["token"] = $this->user->createToken($_SESSION["username"]);
return $response->withStatus(201);
}
$response->getBody()->write(json_encode(array("token" => $_SESSION["token"])));
return $response;
});
$app->get("/user/checkResetEmail/{email}", function (Request $request, Response $response, array $args)
{
if (empty($args["email"]))
{
// uh oh sent empty data
return $response->withStatus(400);
}
if ($this->user->checkEmail($args["email"]))
{
// yay email does exist
$_SESSION["resetToken"] = $this->user->sendResetEmail($args["email"]);
$_SESSION["resetEmail"] = $args["email"];
return $response;
}
return $response->withStatus(404);
});
$app->get("/user/resendEmail", function (Request $request, Response $response)
{
if (empty($_SESSION["resetToken"]))
{
// uh oh not authorized to resend email
return $response->withStatus(401);
}
$_SESSION["resetToken"] = $this->user->sendResetEmail($_SESSION["resetEmail"]);
return $response;
});
$app->get("/user/checkResetCode/{code}", function (Request $request, Response $response, array $args)
{
if (empty($args["code"]))
{
// uh oh sent empty data
return $response->withStatus(400);
}
if ($_SESSION["resetToken"] === $args["code"])
{
// yay, code code matches
return $response;
}
return $response->withStatus(401);
});
$app->post("/user/changePassword", function (Request $request, Response $response)
{
if (empty($_SESSION["resetToken"]) && empty($_SESSION["resetEmail"]))
{
// uh oh not authorized to change password
return $response->withStatus(401);
}
$data = $request->getParsedBody();
if (empty($data["password"]))
{
// uh oh sent empty data
return $response->withStatus(400);
}
if ($this->user->changePassword($_SESSION["resetEmail"], $data["password"]))
{
// yay, password changed
unset($_SESSION["resetToken"]);
unset($_SESSION["resetEmail"]);
return $response->withStatus(201);
}
return $response->withStatus(500);
});
}
}

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Rohit Pai - Blog</title><meta name="title" content="Rohit Pai - Blog"><meta name="description" content="This is all the blog posts that Rohit Pai has posted. You'll find posts on various topics, mostly on tech but some on various other random topics."><meta name="keywords" content="Blog, all posts, rohit, pai, rohit pai, tech, web development, self-hosting, hosting"><meta name="robots" content="index, follow"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta name="language" content="English"><meta name="author" content="Rohit Pai"><link rel="stylesheet" href="/blog/css/main.css"><script src="https://kit.fontawesome.com/ed3c25598e.js" crossorigin="anonymous"></script><script type="text/javascript" src="https://platform-api.sharethis.com/js/sharethis.js#property=6550cdc47a115e0012964576&product=sop" async="async"></script></head><body><nav><input type="checkbox" id="nav-check"><h1><a href="/" class="link">rohit pai</a></h1><div class="nav-btn"><label for="nav-check"><span></span> <span></span> <span></span></label></div><ul><li><a href="/#about" class="textShadow link">about</a></li><li><a href="/#curriculumVitae" class="textShadow link">cv</a></li><li><a href="/#projects" class="textShadow link">projects</a></li><li><a href="/#contact" class="textShadow link">contact</a></li><li><a href="/blog" class="textShadow link active">blog</a></li></ul></nav><header><div><h1>full stack developer</h1><a href="/#sayHello" class="btn btnPrimary boxShadowIn boxShadowOut">Contact Me</a> <a href="" id="arrow"><i class="fa-solid fa-chevron-down"></i></a></div></header><div class="menuBar"><div class="menu"><ul><li><a href="/blog" class="link active">All posts</a></li><li><a href="/blog/category" class="link">categories</a></li><li><label for="searchField" aria-hidden="true" hidden>search</label> <input type="search" name="search" id="searchField" placeholder="Search..."> <button type="submit" id="searchBtn" class="btn btnPrimary"><i class="fa fa-search"></i></button></li></ul></div></div><main id="main"></main><footer class="flexRow"><div class="nav"><ul><li><a href="/blog/policy/privacy" class="link">privacy policy</a></li><li><a href="/blog/policy/cookie" class="link">cookie policy</a></li></ul></div><p>&copy; <span id="year"></span> Rohit Pai all rights reserved</p><div class="button"><button id="goBackToTop"><i class="fa-solid fa-chevron-up"></i></button></div></footer><script src="/js/typewriter.js"></script><script src="/blog/js/index.js"></script><script id="dsq-count-scr" src="https://rohitpaiportfolio.disqus.com/count.js" async></script></body></html> <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Rohit Pai - Blog</title><meta name="title" content="Rohit Pai - Blog"><meta name="description" content="This is all the blog posts that Rohit Pai has posted. You'll find posts on various topics, mostly on tech but some on various other random topics."><meta name="keywords" content="Blog, all posts, rohit, pai, rohit pai, tech, web development, self-hosting, hosting"><meta name="robots" content="index, follow"><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><meta name="language" content="English"><meta name="author" content="Rohit Pai"><link rel="stylesheet" href="/blog/css/main.css"><script src="https://kit.fontawesome.com/ed3c25598e.js" crossorigin="anonymous"></script><script type="text/javascript" src="https://platform-api.sharethis.com/js/sharethis.js#property=6550cdc47a115e0012964576&product=sop" async="async"></script></head><body><nav><input type="checkbox" id="nav-check"><h1><a href="/" class="link">rohit pai</a></h1><div class="nav-btn"><label for="nav-check"><span></span> <span></span> <span></span></label></div><ul><li><a href="/#about" class="textShadow link">about</a></li><li><a href="/#curriculumVitae" class="textShadow link">cv</a></li><li><a href="/#projects" class="textShadow link">projects</a></li><li><a href="/#contact" class="textShadow link">contact</a></li><li><a href="/blog" class="textShadow link active">blog</a></li></ul></nav><header><div><h1>full stack developer</h1><a href="/#sayHello" class="btn btnPrimary boxShadowIn boxShadowOut">Contact Me</a> <a href="" id="arrow"><i class="fa-solid fa-chevron-down"></i></a></div></header><div class="menuBar"><div class="menu"><ul><li><a href="/blog" class="link active">All posts</a></li><li><a href="/blog/category" class="link">categories</a></li><li><label for="searchField" aria-hidden="true" hidden>search</label> <input type="search" name="search" id="searchField" placeholder="Search..."> <button type="submit" id="searchBtn" class="btn btnPrimary"><i class="fa fa-search"></i></button></li></ul></div></div><main id="main"></main><div class="modal-container" id="cookiePopup"><div class="modal"><div class="modal-content"><h2><i class="fas fa-cookie-bite"></i> Hey I use cookies btw</h2><p>Just to let you know, I use cookies to give you the best experience on my blog. By clicking agree I'll assume that you are happy with it. <a href="/blog/policy/cookie" class="link">Read more</a></p><div class="flexRow"><button class="btn btnPrimary" id="cookieAccept">agree</button> <a href="https://google.co.uk" class="btn btnPrimary">disagree</a></div></div></div></div><footer class="flexRow"><div class="nav"><ul><li><a href="/blog/policy/privacy" class="link">privacy policy</a></li><li><a href="/blog/policy/cookie" class="link">cookie policy</a></li></ul></div><p>&copy; <span id="year"></span> Rohit Pai all rights reserved</p><div class="button"><button id="goBackToTop"><i class="fa-solid fa-chevron-up"></i></button></div></footer><script src="/js/typewriter.js"></script><script src="/blog/js/index.js"></script><script id="dsq-count-scr" src="https://rohitpaiportfolio.disqus.com/count.js" async></script></body></html>

File diff suppressed because one or more lines are too long

2
dist/css/main.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
function showErrorMessage(e,t){document.querySelector(`#${t}Error`).classList.remove("hidden"),document.querySelector(`#${t}Error div`).innerText=e}function switchView(e,t){document.querySelector(e).classList.toggle("shown"),setTimeout((()=>document.querySelector(e).style.transform="translateX(150vw)"),500),setTimeout((()=>document.querySelector(e).style.display="none"),500),setTimeout((()=>document.querySelector(t).style.removeProperty("display")),200),setTimeout((()=>document.querySelector(t).classList.toggle("shown")),300),setTimeout((()=>document.querySelector(t).style.removeProperty("transform")),400)}document.addEventListener("DOMContentLoaded",(e=>{fetch("/api/user/isLoggedIn").then((e=>{e.ok&&(window.location.href="./editor.html")}))})),document.querySelector("#login form").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;if(e.target.username.value.length>0&&e.target.password.value.length>0)return t.append("username",e.target.username.value),t.append("password",e.target.password.value),void fetch("/api/user/login",{method:"POST",body:t}).then((e=>e.json().then((t=>{if(e.ok)return localStorage.setItem("token",t.token),void(window.location.href="./editor.html");400!==e.status?showErrorMessage("Invalid username or password.","login"):showErrorMessage("Please type in a username and password.","login")}))));showErrorMessage("Please type in a username and password.","login")})),document.querySelector("#loginError .close").addEventListener("click",(()=>document.querySelector("#loginError").classList.toggle("hidden"))),document.querySelector("#resetError .close").addEventListener("click",(()=>document.querySelector("#resetError").classList.toggle("hidden"))),document.querySelector("#changeError .close").addEventListener("click",(()=>document.querySelector("#changeError").classList.toggle("hidden"))),document.querySelectorAll("form i.fa-eye").forEach((e=>{e.addEventListener("click",(e=>{if("password"===e.target.previousElementSibling.type)return e.target.previousElementSibling.type="text",e.target.classList.remove("fa-eye"),void e.target.classList.add("fa-eye-slash");e.target.previousElementSibling.type="password",e.target.classList.remove("fa-eye-slash"),e.target.classList.add("fa-eye")}))})),document.querySelector("#resetPwd").addEventListener("click",(()=>{switchView("#login","#resetPassword")})),document.querySelector("#loginBtn").addEventListener("click",(()=>{switchView("#resetPassword","#login")})),document.querySelector("#resetPassword form").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;""!==e.target.email?(window.email=e.target.email.value,t.append("email",e.target.email.value),fetch(`/api/user/checkResetEmail/${e.target.email.value}`).then((e=>{e.ok&&switchView("#resetPassword","#checkResetCode"),showErrorMessage("Invalid email.","reset")}))):showErrorMessage("Please type in your email.","reset")})),document.querySelector("#checkResetCode form").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;""!==e.target.code.value?(t.append("code",e.target.code.value),fetch(`/api/user/checkResetCode/${e.target.code.value}`).then((e=>{e.ok&&switchView("#checkResetCode","#changePassword"),showErrorMessage("Invalid code.","resetCode")}))):showErrorMessage("Please type in your reset code.","check")})),document.querySelector("#changePassword form").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;""!==e.target.pass.value||""!==e.target.rePass.value?e.target.pass.value===e.target.rePass.value?(t.append("password",e.target.pass.value),fetch("/api/user/changePassword",{method:"POST",body:t}).then((e=>{e.ok&&switchView("#changePassword","#login"),showErrorMessage("Something went wrong.","change")}))):showErrorMessage("Passwords do not match.","change"):showErrorMessage("Please type in a new password.","change")})); function showErrorMessage(e,t){document.querySelector(`#${t}Error`).classList.remove("hidden"),document.querySelector(`#${t}Error div`).innerText=e}function switchView(e,t){document.querySelector(e).classList.toggle("shown"),setTimeout((()=>document.querySelector(e).style.transform="translateX(150vw)"),500),setTimeout((()=>document.querySelector(e).style.display="none"),500),setTimeout((()=>document.querySelector(t).style.removeProperty("display")),200),setTimeout((()=>document.querySelector(t).classList.toggle("shown")),300),setTimeout((()=>document.querySelector(t).style.removeProperty("transform")),400)}document.addEventListener("DOMContentLoaded",(e=>{fetch("/api/user/isLoggedIn").then((e=>{e.ok&&(window.location.href="./editor.html")}))})),document.querySelector("#login form").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;if(e.target.username.value.length>0&&e.target.password.value.length>0)return t.append("username",e.target.username.value),t.append("password",e.target.password.value),void fetch("/api/user/login",{method:"POST",body:t}).then((e=>e.json().then((t=>{if(e.ok)return localStorage.setItem("token",t.token),void(window.location.href="./editor.html");400!==e.status?401!==e.status?showErrorMessage(t.error,"login"):showErrorMessage("Invalid username or password.","login"):showErrorMessage("Please type in a username and password.","login")}))));showErrorMessage("Please type in a username and password.","login")})),document.querySelector("#loginError .close").addEventListener("click",(()=>document.querySelector("#loginError").classList.toggle("hidden"))),document.querySelector("#resetError .close").addEventListener("click",(()=>document.querySelector("#resetError").classList.toggle("hidden"))),document.querySelector("#changeError .close").addEventListener("click",(()=>document.querySelector("#changeError").classList.toggle("hidden"))),document.querySelectorAll("form i.fa-eye").forEach((e=>{e.addEventListener("click",(e=>{if("password"===e.target.previousElementSibling.type)return e.target.previousElementSibling.type="text",e.target.classList.remove("fa-eye"),void e.target.classList.add("fa-eye-slash");e.target.previousElementSibling.type="password",e.target.classList.remove("fa-eye-slash"),e.target.classList.add("fa-eye")}))})),document.querySelector("#resetPwd").addEventListener("click",(()=>{switchView("#login","#resetPassword")})),document.querySelector("#loginBtn").addEventListener("click",(()=>{switchView("#resetPassword","#login")})),document.querySelector("#resetPassword form").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;""!==e.target.email?(window.email=e.target.email.value,t.append("email",e.target.email.value),fetch(`/api/user/checkResetEmail/${e.target.email.value}`).then((e=>{e.ok&&switchView("#resetPassword","#checkResetCode"),showErrorMessage("Invalid email.","reset")}))):showErrorMessage("Please type in your email.","reset")})),document.querySelector("#checkResetCode form").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;""!==e.target.code.value?(t.append("code",e.target.code.value),fetch(`/api/user/checkResetCode/${e.target.code.value}`).then((e=>{e.ok&&switchView("#checkResetCode","#changePassword"),showErrorMessage("Invalid code.","resetCode")}))):showErrorMessage("Please type in your reset code.","check")})),document.querySelector("#changePassword form").addEventListener("submit",(e=>{e.preventDefault();let t=new FormData;""!==e.target.pass.value||""!==e.target.rePass.value?e.target.pass.value===e.target.rePass.value?(t.append("password",e.target.pass.value),fetch("/api/user/changePassword",{method:"POST",body:t}).then((e=>{e.ok&&switchView("#changePassword","#login"),showErrorMessage("Something went wrong.","change")}))):showErrorMessage("Passwords do not match.","change"):showErrorMessage("Please type in a new password.","change")}));

View File

@ -8,11 +8,15 @@ use DOMDocument;
use PDO; use PDO;
use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\UploadedFileInterface;
use DonatelloZa\RakePlus\RakePlus; use DonatelloZa\RakePlus\RakePlus;
use function DI\string; use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use function api\utils\dbConn;
use function api\utils\getEmailPassword;
use const api\utils\feedGenerator\ATOM; use const api\utils\feedGenerator\ATOM;
use const api\utils\feedGenerator\RSS2; use const api\utils\feedGenerator\RSS2;
require_once __DIR__ . "/../utils/config.php"; require_once __DIR__ . "/../utils/config.php";
require_once __DIR__ . "/../utils/imgUtils.php"; require_once __DIR__ . "/../utils/imgUtils.php";
require_once __DIR__ . "/../utils/feedGenerator/FeedWriter.php"; require_once __DIR__ . "/../utils/feedGenerator/FeedWriter.php";
@ -254,10 +258,10 @@ class blogData
* @param string $dateCreated - Date the blog post was created * @param string $dateCreated - Date the blog post was created
* @param bool $featured - Whether the blog post is featured or not * @param bool $featured - Whether the blog post is featured or not
* @param string $categories - Categories of the blog post * @param string $categories - Categories of the blog post
* @param UploadedFileInterface $headerImg - Header image of the blog post * @param UploadedFileInterface|null $headerImg - Header image of the blog post
* @return int|string - ID of the blog post or error message * @return int|string - ID of the blog post or error message
*/ */
public function createPost(string $title, string $abstract, string $body, string $bodyText, string $dateCreated, bool $featured, string $categories, UploadedFileInterface $headerImg): int|string public function createPost(string $title, string $abstract, string $body, string $bodyText, string $dateCreated, bool $featured, string $categories, UploadedFileInterface|null $headerImg): int|string
{ {
$conn = dbConn(); $conn = dbConn();
$folderID = uniqid(); $folderID = uniqid();
@ -289,6 +293,13 @@ class blogData
$keywords = implode(", ", RakePlus::create($bodyText)->keywords()); $keywords = implode(", ", RakePlus::create($bodyText)->keywords());
$latest = $this->getLatestBlogPost();
$prevTitle = $latest["title"];
$prevAbstract = $latest["abstract"];
$prevHeaderImage = $latest["headerImg"];
$headerImage = $targetFile["imgLocation"];
$stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, abstract, body, bodyText, categories, keywords, folderID) $stmt = $conn->prepare("INSERT INTO blog (title, dateCreated, dateModified, featured, headerImg, abstract, body, bodyText, categories, keywords, folderID)
VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :abstract, :body, :bodyText, :categories, :keywords, :folderID);"); VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :abstract, :body, :bodyText, :categories, :keywords, :folderID);");
$stmt->bindParam(":title", $title); $stmt->bindParam(":title", $title);
@ -296,7 +307,7 @@ class blogData
$stmt->bindParam(":dateModified", $dateCreated); $stmt->bindParam(":dateModified", $dateCreated);
$isFeatured = $featured ? 1 : 0; $isFeatured = $featured ? 1 : 0;
$stmt->bindParam(":featured", $isFeatured); $stmt->bindParam(":featured", $isFeatured);
$stmt->bindParam(":headerImg", $targetFile["imgLocation"]); $stmt->bindParam(":headerImg", $headerImage);
$stmt->bindParam(":abstract", $abstract); $stmt->bindParam(":abstract", $abstract);
$stmt->bindParam(":body", $newBody); $stmt->bindParam(":body", $newBody);
$stmt->bindParam(":bodyText", $bodyText); $stmt->bindParam(":bodyText", $bodyText);
@ -304,12 +315,267 @@ class blogData
$stmt->bindParam(":keywords", $keywords); $stmt->bindParam(":keywords", $keywords);
$stmt->bindParam(":folderID", $folderID); $stmt->bindParam(":folderID", $folderID);
if ($stmt->execute()) if (!$stmt->execute())
{ {
return intval($conn->lastInsertId()); return "Error, couldn't create post";
} }
return "Error, couldn't create post"; $stmtEmails = $conn->prepare("SELECT email FROM newsletter;");
$stmtEmails->execute();
$emails = $stmtEmails->fetchAll(PDO::FETCH_ASSOC);
$emailBody = <<<EOD
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rohit Pai's blog</title>
<style>
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/notosans/v34/o-0NIpQlx3QUlC5A4PNjXhFVZNyB.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Share Tech Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
*{
box-sizing: border-box;
}
body, html {
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: Noto Sans KR, sans-serif;
font-style: normal;
font-weight: 500;
font-size: 22px;
line-height: 1.625rem;
min-height: 100%;
}
main, header, footer {
max-width: 768px;
margin: 0 auto;
}
a {
text-decoration: none;
text-transform: lowercase;
}
a:visited {
color: inherit;
}
h1, h2 {
font-family: Share Tech Mono, monospace;
font-style: normal;
font-weight: normal;
line-height: 2.5625rem;
text-transform: lowercase;
}
h1, nav {
font-size: 40px;
}
h2 {
font-size: 28px;
}
h3 {
font-size: 22px;
}
header {
background: hsla(80, 60%, 50%, 1);
color: #FFFFFF;
border-bottom: 5px solid hsla(0, 0%, 75%, 1);
}
header div.title, header div.byLine {
padding: 0 3em 1em;
}
header div.title h1 {
margin: 0;
padding: 1em 0 0;
font-size: 42px;
}
header div.byLine {
border-top: 5px solid hsla(80, 60%, 30%, 1);
}
a.btn {
background-color: hsla(80, 60%, 50%, 1);
text-decoration: none;
display: inline-flex;
padding: 1em 2em;
border-radius: 0.625em;
border: 0.3215em solid hsla(80, 60%, 50%, 1);
color: #FFFFFF;
text-align: center;
align-items: center;
max-height: 4em;
}
div.postContainer, div.container {
padding: 1em 2em 0;
margin: 0 auto 1em;
max-width: 768px;
}
div.postContainer ~ div.postContainer {
border-top: 5px solid hsla(0, 0%, 75%, 1);
}
div.postContainer > *, div.container > * {
margin: 0 auto;
max-width: 768px;
}
div.postContainer div.post h2, div.container div.content h2 {
margin: 0;
padding: 0;
}
div.postContainer div.image, div.container div.image {
width: 50%;
max-width: 100%;
height: auto;
}
div.postContainer div.image img {
-webkit-border-radius: 0.5em;
-moz-border-radius: 0.5em;
border-radius: 0.5em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.container div.image img {
-webkit-border-radius: 10em;
-moz-border-radius: 10em;
border-radius: 10em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.postContainer div.image img, div.container div.image img {
min-width: 100%;
width: 100%;
}
footer {
background-color: hsla(80, 60%, 50%, 1);
margin-top: auto;
padding: 3em;
display: block;
color: #FFFFFF;
}
footer .nav {
width: 75%;
float: left;
}
footer .nav ul {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
footer .nav ul li {
display: inline-block;
margin: 0 1em;
}
footer .nav ul a {
color: #FFFFFF;
}
footer .date {
width: 25%;
float: right;
}
</style>
</head>
<body>
<header>
<div class="title">
<h1>Hey, I've got a new post!</h1>
</div>
<div class="byLine">
<p>Hello I'm Rohit an avid full-stack developer and home lab enthusiast. I love anything and everything
to do with full stack development, home labs, coffee and generally anything to do with tech.</p>
</div>
</header>
<main>
<div class="postContainer">
<h2>latest post</h2>
<div class="image">
<img src="$headerImage" alt="header image of the latest post">
</div>
<div class="post">
<h3>$title</h3>
<p>$abstract</p>
<a href="https://rohitpai.co.uk/blog/post/$title" class="btn">See Post</a>
</div>
</div>
<div class="postContainer">
<h2>in case you missed the previous post</h2>
<div class="image">
<img src="$prevHeaderImage" alt="header image of the previous post">
</div>
<div class="post">
<h3>$prevTitle</h3>
<p>$prevAbstract</p>
<a href="https://rohitpai.co.uk/blog/post/$prevTitle" class="btn">See Post</a>
</div>
</div>
</main>
<footer>
<div class="nav">
<ul>
<li><a href="https://rohitpai.co.uk/blog">&lt;https://rohitpai.co.uk/blog&gt;</a></li>
<li><a href="https://rohitpai.co.uk/blog/unsubscribe">&lt;Unsubscribe&gt;</a></li>
</ul>
</div>
<div class="date">2023</div>
</footer>
</body>
</html>
EOD;
foreach ($emails as $email)
{
$this->sendMail($email["email"], $emailBody, "Hey, Rohit's blog has a new post!");
}
return intval($conn->lastInsertId());
} }
/** /**
@ -616,12 +882,12 @@ class blogData
foreach ($posts as $post) foreach ($posts as $post)
{ {
$items[] = array( $items[] = array(
"id" => string($post["ID"]), "id" => strval($post["ID"]),
"url" => "https://rohitpai.co.uk/blog/post/" . rawurlencode($post["title"]) . "#disqus_thread", "url" => "https://rohitpai.co.uk/blog/post/" . rawurlencode($post["title"]) . "#disqus_thread",
"title" => $post["title"], "title" => $post["title"],
"date_published" => date($post["dateCreated"]), "date_published" => date($post["dateCreated"]),
"date_modified" => date($post["dateModified"]), "date_modified" => date($post["dateModified"]),
// "description" => $post["abstract"], "description" => $post["abstract"],
"banner_image" => "https://rohitpai.co.uk/" . rawurlencode($post["headerImg"]), "banner_image" => "https://rohitpai.co.uk/" . rawurlencode($post["headerImg"]),
"content_html" => $post["body"] "content_html" => $post["body"]
); );
@ -655,4 +921,572 @@ class blogData
return $feed; return $feed;
} }
/**
* Add an email to the newsletter and send welcome email
* @param string $email - Email to add to the newsletter
* @return string|array - Success or error message
*/
public function addNewsletterEmail(string $email): string|array
{
$conn = dbConn();
$stmtCheckEmail = $conn->prepare("SELECT * FROM newsletter WHERE email = :email;");
$stmtCheckEmail->bindParam(":email", $email);
$stmtCheckEmail->execute();
$result = $stmtCheckEmail->fetch(PDO::FETCH_ASSOC);
if ($result)
{
return "Email already exists";
}
$stmt = $conn->prepare("INSERT INTO newsletter (email) VALUES (:email);");
$stmt->bindParam(":email", $email);
$stmt->execute();
$body = <<<EOD
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rohit Pai's blog</title>
<style>
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/notosans/v34/o-0NIpQlx3QUlC5A4PNjXhFVZNyB.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Share Tech Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
*{
box-sizing: border-box;
}
body, html {
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: Noto Sans KR, sans-serif;
font-style: normal;
font-weight: 500;
font-size: 22px;
line-height: 1.625rem;
min-height: 100%;
}
main, header, footer {
max-width: 768px;
margin: 0 auto;
}
a {
text-decoration: none;
text-transform: lowercase;
}
a:visited {
color: inherit;
}
h1, h2 {
font-family: Share Tech Mono, monospace;
font-style: normal;
font-weight: normal;
line-height: 2.5625rem;
text-transform: lowercase;
}
h1, nav {
font-size: 40px;
}
h2 {
font-size: 28px;
}
h3 {
font-size: 22px;
}
header {
background: hsla(80, 60%, 50%, 1);
color: #FFFFFF;
border-bottom: 5px solid hsla(0, 0%, 75%, 1);
}
header div.title, header div.byLine {
padding: 0 3em 1em;
}
header div.title h1 {
margin: 0;
padding: 1em 0 0;
font-size: 42px;
}
header div.byLine {
border-top: 5px solid hsla(80, 60%, 30%, 1);
}
a.btn {
background-color: hsla(80, 60%, 50%, 1);
text-decoration: none;
display: inline-flex;
padding: 1em 2em;
border-radius: 0.625em;
border: 0.3215em solid hsla(80, 60%, 50%, 1);
color: #FFFFFF;
text-align: center;
align-items: center;
max-height: 4em;
}
div.postContainer, div.container {
padding: 1em 2em 0;
margin: 0 auto 1em;
max-width: 768px;
}
div.postContainer ~ div.postContainer {
border-top: 5px solid hsla(0, 0%, 75%, 1);
}
div.postContainer > *, div.container > * {
margin: 0 auto;
max-width: 768px;
}
div.postContainer div.post h2, div.container div.content h2 {
margin: 0;
padding: 0;
}
div.postContainer div.image, div.container div.image {
width: 50%;
max-width: 100%;
height: auto;
}
div.postContainer div.image img {
-webkit-border-radius: 0.5em;
-moz-border-radius: 0.5em;
border-radius: 0.5em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.container div.image img {
-webkit-border-radius: 50em;
-moz-border-radius: 50em;
border-radius: 50em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.postContainer div.image img, div.container div.image img {
min-width: 100%;
width: 100%;
}
footer {
background-color: hsla(80, 60%, 50%, 1);
margin-top: auto;
padding: 3em;
display: block;
color: #FFFFFF;
}
footer .nav {
width: 75%;
float: left;
}
footer .nav ul {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
footer .nav ul li {
display: inline-block;
margin: 0 1em;
}
footer .nav ul a {
color: #FFFFFF;
}
footer .date {
width: 25%;
float: right;
}
</style>
</head>
<body>
<header>
<div class="title">
<h1>hello from rohit</h1>
</div>
<div class="byLine">
<p>Hello I'm Rohit an avid full-stack developer and home lab enthusiast. I love anything and everything
to do with full stack development, home labs, coffee and generally anything to do with tech.</p>
</div>
</header>
<main>
<div class="container">
<h2>hey there, i'm rohit!</h2>
<div class="image"><img src="https://rohitpai.co.uk/imgs/profile.jpg" alt=""></div>
<div class="content">
<h3>What to Expect</h3>
<p>You'll get an email from me everytime I make a new post to my blog. Sometimes, you may get a special
email on occasion, where I talk about something interesting that's not worth a full on post but I
Still want to tell you about it.</p>
<p>Don't worry, I won't spam you with emails. Well, thanks for signing up to my newsletter, hopefully
you'll hear from me soon!</p>
<p>P.S. Please consider adding this email address rohit@rohitpai.co.uk to your emails
contact list so that my emails won't get sent to span, thanks.</p>
</div>
</div>
</main>
<footer>
<div class="nav">
<ul>
<li><a href="https://rohitpai.co.uk/blog">&lt;https://rohitpai.co.uk/blog&gt;</a></li>
<li><a href="https://rohitpai.co.uk/blog/unsubscribe">&lt;Unsubscribe&gt;</a></li>
</ul>
</div>
<div class="date">2023</div>
</footer>
</body>
</html>
EOD;
return $this->sendMail($email, $body, "Hello from Rohit!");
}
/**
* Send an email to the given email address
* @param string $email - Email address to send the email to
* @param string $body - Body of the email
* @param string $subject - Subject of the email
* @return string|string[]
*/
public function sendMail(string $email, string $body, string $subject): string|array
{
$mail = new PHPMailer(true);
try
{
$mail->isSMTP();
$mail->Host = "smtp.hostinger.com";
$mail->SMTPAuth = true;
$mail->Username = "rohit@rohitpai.co.uk";
$mail->Password = getEmailPassword();
$mail->SMTPSecure = "tls";
$mail->Port = 587;
$mail->setFrom("rohit@rohitpai.co.uk", "Rohit Pai");
$mail->addAddress($email);
$mail->isHTML();
$mail->Subject = $subject;
$mail->Body = $body;
$mail->send();
return "success";
}
catch (Exception $e)
{
return array("errorMessage" => "Error, couldn't send email because of " . $e);
}
}
/**
* @param string $email - Email to delete from the newsletter
* @return string - Success or error message
*/
public function deleteNewsletterEmail(string $email): string
{
$conn = dbConn();
$stmtCheckEmail = $conn->prepare("SELECT * FROM newsletter WHERE email = :email;");
$stmtCheckEmail->bindParam(":email", $email);
$stmtCheckEmail->execute();
$result = $stmtCheckEmail->fetch(PDO::FETCH_ASSOC);
if (!$result)
{
return "email not found";
}
$stmt = $conn->prepare("DELETE FROM newsletter WHERE email = :email;");
$stmt->bindParam(":email", $email);
$stmt->execute();;
return "success";
}
/**
* @param string $subject - Subject of the newsletter
* @param string $message - Message content
* @return array|string - Success or error message
*/
public function sendNewsletter(string $subject, string $message): array|string
{
$conn = dbConn();
$stmtEmails = $conn->prepare("SELECT email FROM newsletter;");
$stmtEmails->execute();
$emails = $stmtEmails->fetchAll(PDO::FETCH_ASSOC);
$msg = "";
$body = <<<EOD
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rohit Pai's blog</title>
<style>
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/notosans/v34/o-0NIpQlx3QUlC5A4PNjXhFVZNyB.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Share Tech Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/sharetechmono/v15/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
*{
box-sizing: border-box;
}
body, html {
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
}
body {
font-family: Noto Sans KR, sans-serif;
font-style: normal;
font-weight: 500;
font-size: 22px;
line-height: 1.625rem;
min-height: 100%;
}
main, header, footer {
max-width: 768px;
margin: 0 auto;
}
a {
text-decoration: none;
text-transform: lowercase;
}
a:visited {
color: inherit;
}
h1, h2 {
font-family: Share Tech Mono, monospace;
font-style: normal;
font-weight: normal;
line-height: 2.5625rem;
text-transform: lowercase;
}
h1, nav {
font-size: 40px;
}
h2 {
font-size: 28px;
}
h3 {
font-size: 22px;
}
header {
background: hsla(80, 60%, 50%, 1);
color: #FFFFFF;
border-bottom: 5px solid hsla(0, 0%, 75%, 1);
}
header div.title, header div.byLine {
padding: 0 3em 1em;
}
header div.title h1 {
margin: 0;
padding: 1em 0 0;
font-size: 42px;
}
header div.byLine {
border-top: 5px solid hsla(80, 60%, 30%, 1);
}
a.btn {
background-color: hsla(80, 60%, 50%, 1);
text-decoration: none;
display: inline-flex;
padding: 1em 2em;
border-radius: 0.625em;
border: 0.3215em solid hsla(80, 60%, 50%, 1);
color: #FFFFFF;
text-align: center;
align-items: center;
max-height: 4em;
}
div.postContainer, div.container {
padding: 1em 2em 0;
margin: 0 auto 1em;
max-width: 768px;
}
div.postContainer ~ div.postContainer {
border-top: 5px solid hsla(0, 0%, 75%, 1);
}
div.postContainer > *, div.container > * {
margin: 0 auto;
max-width: 768px;
}
div.postContainer div.post h2, div.container div.content h2 {
margin: 0;
padding: 0;
}
div.postContainer div.image, div.container div.image {
width: 50%;
max-width: 100%;
height: auto;
}
div.postContainer div.image img {
-webkit-border-radius: 0.5em;
-moz-border-radius: 0.5em;
border-radius: 0.5em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.container div.image img {
-webkit-border-radius: 50em;
-moz-border-radius: 50em;
border-radius: 50em;
border: 4px solid hsla(0, 0%, 75%, 1);
}
div.postContainer div.image img, div.container div.image img {
min-width: 100%;
width: 100%;
}
footer {
background-color: hsla(80, 60%, 50%, 1);
margin-top: auto;
padding: 3em;
display: block;
color: #FFFFFF;
}
footer .nav {
width: 75%;
float: left;
}
footer .nav ul {
list-style: none;
margin: 0;
padding: 0;
width: 100%;
}
footer .nav ul li {
display: inline-block;
margin: 0 1em;
}
footer .nav ul a {
color: #FFFFFF;
}
footer .date {
width: 25%;
float: right;
}
</style>
</head>
<body>
<header>
<div class="title">
<h1>a surprise hello from rohit</h1>
</div>
<div class="byLine">
<p>Hello I'm Rohit an avid full-stack developer and home lab enthusiast. I love anything and everything
to do with full stack development, home labs, coffee and generally anything to do with tech.</p>
</div>
</header>
<main>
<div class="container">
<h2>$subject</h2>
<div class="content">
$message
</div>
</div>
</main>
<footer>
<div class="nav">
<ul>
<li><a href="https://rohitpai.co.uk/blog">&lt;https://rohitpai.co.uk/blog&gt;</a></li>
<li><a href="https://rohitpai.co.uk/blog/unsubscribe">&lt;Unsubscribe&gt;</a></li>
</ul>
</div>
<div class="date">2023</div>
</footer>
</body>
</html>
EOD;
foreach ($emails as $email)
{
$msg = $this->sendMail($email["email"], $body, $subject);
if (is_array($msg))
{
return $msg;
}
}
return $msg;
}
} }

View File

@ -269,12 +269,38 @@ class blogRoutes implements routesInterface
return $response; return $response;
}); });
$app->delete("/blog/newsletter/{email}", function (Request $request, Response $response, $args)
{
if ($args["email"] == null)
{
$response->getBody()->write(json_encode(array("error" => "Please provide an email")));
return $response->withStatus(400);
}
$message = $this->blogData->deleteNewsletterEmail($args["email"]);
if ($message === "email not found")
{
// uh oh something went wrong
$response->getBody()->write(json_encode(array("error" => "Error, email 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);
}
return $response;
});
$app->post("/blog/post", function (Request $request, Response $response) $app->post("/blog/post", function (Request $request, Response $response)
{ {
$data = $request->getParsedBody(); $data = $request->getParsedBody();
$files = $request->getUploadedFiles(); $files = $request->getUploadedFiles();
$headerImg = $files["headerImg"]; if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["bodyText"]) || empty($data["abstract"]) || empty($data["dateCreated"]) || empty($data["categories"]))
if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["abstract"]) || empty($data["dateCreated"]) || empty($data["categories"]))
{ {
// uh oh sent some empty data // uh oh sent some empty data
$response->getBody()->write(json_encode(array("error" => "Error, empty data sent"))); $response->getBody()->write(json_encode(array("error" => "Error, empty data sent")));
@ -288,6 +314,11 @@ class blogRoutes implements routesInterface
return $response->withStatus(400); return $response->withStatus(400);
} }
if (array_key_exists("headerImg", $files))
{
$headerImg = $files["headerImg"];
}
if (empty($files["headerImg"])) if (empty($files["headerImg"]))
{ {
$headerImg = null; $headerImg = null;
@ -339,19 +370,66 @@ class blogRoutes implements routesInterface
if (empty($files)) if (empty($files))
{ {
// uh oh sent some empty data // uh oh sent some empty data
$response->getBody()->write(json_encode(array("error" => array("message" => "Error, empty data sent")))); $response->getBody()->write(json_encode(array("error" => "Error, empty data sent")));
return $response->withStatus(400); return $response->withStatus(400);
} }
$message = $this->blogData->uploadHeaderImage($args["id"], $files["headerImg"]); $message = $this->blogData->uploadHeaderImage($args["id"], $files["headerImg"]);
if (!is_array($message)) if (!is_array($message))
{ {
$response->getBody()->write(json_encode(array("error" => array("message" => $message)))); $response->getBody()->write(json_encode(array("error" => $message)));
return $response->withStatus(500); return $response->withStatus(500);
} }
$response->getBody()->write(json_encode($message)); $response->getBody()->write(json_encode($message));
return $response->withStatus(201); return $response->withStatus(201);
}); });
$app->post("/blog/newsletter", function (Request $request, Response $response)
{
$data = $request->getParsedBody();
if (empty($data["subject"]) || empty($data["message"]))
{
// uh oh sent some empty data
$response->getBody()->write(json_encode(array("error" => "Error, empty data sent")));
return $response->withStatus(400);
}
$message = $this->blogData->sendNewsletter(strtolower($data["subject"]), $data["message"]);
if (is_array($message))
{
$response->getBody()->write(json_encode(array("error" => "Error, something went wrong")));
return $response->withStatus(500);
}
$response->getBody()->write(json_encode(array("message" => "Message sent")));
return $response->withStatus(201);
});
$app->post("/blog/newsletter/{email}", function (Request $request, Response $response, $args)
{
if ($args["email"] == null)
{
$response->getBody()->write(json_encode(array("error" => "Please provide an email")));
return $response->withStatus(400);
}
$message = $this->blogData->addNewsletterEmail($args["email"]);
if ($message === "Email already exists")
{
$response->getBody()->write(json_encode(array("message" => "exists")));
return $response->withStatus(409);
}
if (is_array($message) || !$message || $message === "error")
{
$response->getBody()->write(json_encode(array("message" => "Something went wrong")));
return $response->withStatus(500);
}
$response->getBody()->write(json_encode(array("message" => "Thanks for signing up!")));
return $response->withStatus(201);
});
} }
} }

View File

@ -5,6 +5,7 @@ namespace api\project;
use api\utils\imgUtils; use api\utils\imgUtils;
use PDO; use PDO;
use Psr\Http\Message\UploadedFileInterface; use Psr\Http\Message\UploadedFileInterface;
use function api\utils\dbConn;
require_once __DIR__ . "/../utils/config.php"; require_once __DIR__ . "/../utils/config.php";
require_once __DIR__ . "/../utils/imgUtils.php"; require_once __DIR__ . "/../utils/imgUtils.php";

View File

@ -3,6 +3,7 @@
namespace api\timeline; namespace api\timeline;
use PDO; use PDO;
use function api\utils\dbConn;
require_once __DIR__ . "/../utils/config.php"; require_once __DIR__ . "/../utils/config.php";

View File

@ -4,6 +4,8 @@ namespace api\user;
use Firebase\JWT\JWT; use Firebase\JWT\JWT;
use PDO; use PDO;
use function api\utils\dbConn;
use function api\utils\getSecretKey;
require_once __DIR__ . "/../utils/config.php"; require_once __DIR__ . "/../utils/config.php";

View File

@ -13,6 +13,7 @@ use Slim\Exception\HttpInternalServerErrorException;
use Slim\Exception\HttpMethodNotAllowedException; use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException; use Slim\Exception\HttpNotFoundException;
use Slim\Psr7\Response; use Slim\Psr7\Response;
use Throwable;
use Tuupola\Middleware\JwtAuthentication; use Tuupola\Middleware\JwtAuthentication;
use Tuupola\Middleware\JwtAuthentication\RequestMethodRule; use Tuupola\Middleware\JwtAuthentication\RequestMethodRule;
use Tuupola\Middleware\JwtAuthentication\RequestPathRule; use Tuupola\Middleware\JwtAuthentication\RequestPathRule;
@ -84,8 +85,8 @@ class middleware
$app->add(new JwtAuthentication([ $app->add(new JwtAuthentication([
"rules" => [ "rules" => [
new RequestPathRule([ new RequestPathRule([
"path" => ["/api/projectData", "/api/timelineData/[a-z]*", "/api/projectImage/[0-9]*", "/api/logout"], "path" => ["/api/projectData", "/api/timelineData/[a-z]*", "/api/projectImage/[0-9]*", "/api/logout", "/api/blog/[a-z]*"],
"ignore" => ["/api/contact", "/api/userData/login", "/api/userData/changePassword"] "ignore" => ["/api/contact", "/api/userData/login", "/api/userData/changePassword", "/api/blog/newsletter/\S*", "/api/blog/newsletter/unsubscribe/\S*"]
]), ]),
new RequestMethodRule([ new RequestMethodRule([
"ignore" => ["OPTIONS", "GET"] "ignore" => ["OPTIONS", "GET"]
@ -133,8 +134,27 @@ class middleware
return $response; return $response;
} }
}); });
$app->addErrorMiddleware(true, true, true);
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorHandler = $errorMiddleware->getDefaultErrorHandler();
$errorMiddleware->setDefaultErrorHandler(function (ServerRequestInterface $request, Throwable $exception,
bool $displayErrorDetails,
bool $logErrors,
bool $logErrorDetails
) use ($app, $errorHandler)
{
$statusCode = $exception->getCode() ?: 500;
// Create a JSON response with the error message
$response = $app->getResponseFactory()->createResponse($statusCode);
$response->getBody()->write(json_encode(['error' => $exception->getMessage()]));
return $response;
});
} }
} }

View File

@ -151,7 +151,7 @@ div.otherPosts a, div.feeds a {
padding: 0.5em 1em; padding: 0.5em 1em;
} }
div.newsletter form input[type="submit"] { div.newsletter div.form input[type="submit"] {
margin-top: 1em; margin-top: 1em;
padding: 0.5em 1em; padding: 0.5em 1em;
} }

View File

@ -12,11 +12,12 @@ section.catPosts .largePost {
section.categories { section.categories {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: flex-start;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
width: 100%; width: 100%;
margin-bottom: 5em; margin-bottom: 5em;
row-gap: 1em;
} }
section.categories .btnContainer { section.categories .btnContainer {

View File

@ -108,7 +108,7 @@ section.largePost .outerContent .postContent a {
align-self: flex-end; align-self: flex-end;
} }
#main .error { #main .errorFof {
display: table; display: table;
width: 100%; width: 100%;
height: 100vh; height: 100vh;

View File

@ -23,6 +23,53 @@
font-weight: bold; font-weight: bold;
} }
.modal-container {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4); /* Background color for the entire screen */
box-sizing: border-box; /* Include padding and border in the element's total width and height */
z-index: 9999999;
}
.modal-container.hidden {
display: none;
}
.modal {
position: fixed;
right: 0;
bottom: 0;
width: 40%;
height: auto;
max-height: 90dvh;
overflow: auto;
margin: 1.25em;
padding: 1.25em;
box-sizing: border-box; /* Include padding and border in the element's total width and height */
}
.modal-content {
background-color: #DDDDDD;
margin: auto;
padding: 20px;
border: 5px solid var(--primaryHover);
width: 100%;
box-shadow: 0 6px 4px 0 var(--mutedBlack);
}
.modal-content .flexRow {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
gap: 1em;
flex-wrap: wrap;
}
/**** Media Queries *****/ /**** Media Queries *****/
@media screen and (max-width: 90em) { @media screen and (max-width: 90em) {
@ -52,6 +99,12 @@
width: 100%; width: 100%;
} }
/*** Cookie popup ***/
.modal-content .flexRow {
width: 50%;
flex-direction: column;
}
/*** Large Post for Home and Category ***/ /*** Large Post for Home and Category ***/
section.largePost { section.largePost {
padding: 1em; padding: 1em;
@ -150,6 +203,18 @@
@media screen and (max-width: 55em) { @media screen and (max-width: 55em) {
/*** Cookie Popup ***/
.modal {
width: 90%;
}
.modal-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/*** Large Post for Home and Category ***/ /*** Large Post for Home and Category ***/
.banner { .banner {
max-width: 75%; max-width: 75%;
@ -168,6 +233,18 @@
} }
@media screen and (max-height: 38em) and (orientation: landscape) {
/*** cookie popup ***/
.modal {
width: 90%;
}
.modal-content .flexRow {
flex-direction: row;
flex-wrap: nowrap;
}
}
@media screen and (max-width: 30em) { @media screen and (max-width: 30em) {
/***** Individual Blog Posts ***/ /***** Individual Blog Posts ***/

View File

@ -72,6 +72,21 @@
</main> </main>
<div class="modal-container" id="cookiePopup">
<div class="modal">
<div class="modal-content">
<h2><i class="fas fa-cookie-bite"></i> Hey I use cookies btw</h2>
<p>Just to let you know, I use cookies to give you the best experience on my blog. By clicking agree
I'll assume that you are happy with it. <a href="/blog/policy/cookie" class="link">Read more</a>
</p>
<div class="flexRow">
<button class="btn btnPrimary" id="cookieAccept">agree</button>
<a href="https://google.co.uk" class="btn btnPrimary">disagree</a>
</div>
</div>
</div>
</div>
<footer class="flexRow"> <footer class="flexRow">
<div class="nav"> <div class="nav">
<ul> <ul>

View File

@ -33,6 +33,11 @@ function goToURL(url)
// Get the current URL and split it into an array // Get the current URL and split it into an array
let urlArray = url.split('/'); let urlArray = url.split('/');
if (localStorage.getItem('cookiePopup') === 'accepted')
{
document.querySelector('#cookiePopup').classList.add('hidden');
}
if (url === '/blog/' || url === '/blog') if (url === '/blog/' || url === '/blog')
{ {
loadHomeContent(); loadHomeContent();
@ -84,7 +89,6 @@ function goToURL(url)
} }
show404(); show404();
} }
document.querySelector('#searchBtn').addEventListener('click', _ => document.querySelector('#searchBtn').addEventListener('click', _ =>
@ -107,6 +111,48 @@ document.querySelector('#searchField').addEventListener('keyup', e =>
} }
}); });
document.querySelector('#cookieAccept').addEventListener('click', _ =>
{
document.querySelector('#cookiePopup').classList.add('hidden');
localStorage.setItem('cookiePopup', 'accepted');
});
/**
* Submits the newsletter form
*/
function submitNewsletter()
{
fetch(`/api/blog/newsletter/${document.querySelector('#email').value}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}).then(res => res.json().then(json =>
{
document.querySelector('#newsletterMessage').classList.remove('hidden');
if (json.message.includes('exists'))
{
document.querySelector('#newsletterMessage').classList.add('error');
document.querySelector('#newsletterMessage').classList.remove('success');
document.querySelector('#newsletterMessage div').innerHTML = 'You\'ve already signed up you silly goose!';
return;
}
if (!res.ok)
{
document.querySelector('#newsletterMessage').classList.add('error');
document.querySelector('#newsletterMessage').classList.remove('success');
document.querySelector('#newsletterMessage div').innerHTML = json.error;
return;
}
document.querySelector('#newsletterMessage div').innerHTML = json.message;
document.querySelector('#newsletterMessage').classList.add('success');
}));
}
/** /**
* Creates a formatted date * Creates a formatted date
* @param {string} dateString - the date string * @param {string} dateString - the date string
@ -358,21 +404,26 @@ async function createSideContent()
<a href="/blog/post/${featuredPost.title}" class="btn btnPrimary boxShadowIn boxShadowOut">See Post</a> <a href="/blog/post/${featuredPost.title}" class="btn btnPrimary boxShadowIn boxShadowOut">See Post</a>
</div> </div>
<div class="newsletter"> <div class="newsletter">
<h3>Sign up to the newsletter</h3> <h3>Sign up to the newsletter to never miss a new post!</h3>
<form action="newsletter.html"> <div id="newsletterForm" class="form">
<div class="formControl"> <div class="formControl">
<label for="email">Email</label> <label for="email">Email</label>
<input type="email" id="email" name="email" placeholder="Email" required> <input type="email" id="email" name="email" placeholder="Email" required>
</div> </div>
<input type="submit" value="Sign Up"> <div class="success hidden" id="newsletterMessage">
</form> <button class="close" type="button" onclick="this.parentElement.classList.toggle('hidden')">&times;</button>
<div></div>
</div>
<input type="submit" value="Sign Up" onclick="submitNewsletter()">
</div>
</div> </div>
<div class="feeds"> <div class="feeds">
<h2>feeds</h2> <h2>feeds</h2>
<div class="icons"> <div class="icons">
<a href="https://rohitpai.co.uk/api/blog/feed/rss" class="btn btnPrimary" title="RSS"><i class="fa-solid fa-rss"></i></a> <a href="https://rohitpai.co.uk/api/blog/feed/rss" class="btn btnPrimary" title="RSS"><i class="fa-solid fa-rss"></i></a>
<a href="https://rohitpai.co.uk/blog/feed/atom" class="btn btnPrimary" title="Atom"><img class="atom" src="/blog/imgs/atomFeed.svg" alt="Atom"></a> <a href="https://rohitpai.co.uk/api/blog/feed/atom" class="btn btnPrimary" title="Atom"><img class="atom" src="/blog/imgs/atomFeed.svg" alt="Atom"></a>
<a href="https://rohitpai.co.uk/blog/feed/json" class="btn btnPrimary" title="JSON"><img class="json" src="/blog/imgs/jsonFeed.svg" alt="JSON"></a> <a href="https://rohitpai.co.uk/api/blog/feed/json" class="btn btnPrimary" title="JSON"><img class="json" src="/blog/imgs/jsonFeed.svg" alt="JSON"></a>
</div> </div>
</div> </div>
<div class="categories"> <div class="categories">
@ -384,6 +435,12 @@ async function createSideContent()
return sideContent; return sideContent;
} }
/**
* Create the meta tags
* @param nameOrProperty - the name or property
* @param attribute - the attribute
* @param value - the value
*/
function createMetaTag(nameOrProperty, attribute, value) function createMetaTag(nameOrProperty, attribute, value)
{ {
let existingTag = document.querySelector(`meta[name="${nameOrProperty}"], meta[property="${nameOrProperty}"]`); let existingTag = document.querySelector(`meta[name="${nameOrProperty}"], meta[property="${nameOrProperty}"]`);
@ -760,7 +817,7 @@ function loadCookiePolicy()
document.querySelector('#main').innerHTML = ` document.querySelector('#main').innerHTML = `
<div class="policy"> <div class="policy">
<h3>Cookies Policy</h3> <h3>Cookies Policy</h3>
<p>I only use functional cookies for the blog which includes PHP Session ID, disqus and maybe share this. I think that these are functional cookies, if you don't, you're welcome to exit the site or tell me by emailing me through the email address below, or the contact form on the contact page.</p> <p>I only use functional cookies for the blog which includes PHP Session ID, disqus. a cookie to disable the cookie popup, and maybe share this. I think that these are functional cookies, if you don't, you're welcome to exit the site or tell me by emailing me through the email address below, or the contact form on the contact section of my main website.</p>
<br> <br>
<a href="mailto:rohit@rohitpai.co.uk" class="link">rohit@rohitpai.co.uk</a> <a href="mailto:rohit@rohitpai.co.uk" class="link">rohit@rohitpai.co.uk</a>
<br> <br>
@ -775,7 +832,7 @@ function loadCookiePolicy()
function show404() function show404()
{ {
document.querySelector('#main').innerHTML = ` document.querySelector('#main').innerHTML = `
<div class="error"> <div class="errorFof">
<div class="fof"> <div class="fof">
<h1>Blog post, Category or page not found</h1> <h1>Blog post, Category or page not found</h1>
<a href="/blog/" class="btn btnPrimary">See all blog posts</a> <a href="/blog/" class="btn btnPrimary">See all blog posts</a>

View File

@ -15,9 +15,9 @@
--grey: hsla(0, 0%, 39%, 1); --grey: hsla(0, 0%, 39%, 1);
--notAvailableDefault: 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); --mutedGrey: hsla(0, 0%, 75%, 1);
--mutedBlack: hsla(0, 0%, 0%, 0.25); --mutedBlack: hsla(0, 0%, 0%, 0.25);
--mutedGreen: hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) + 20%), 0.5); --mutedGreen: hsla(var(--mainHue), var(--mainSat), calc(var(--mainLight) + 20%), 1);
--navBack: hsla(0, 0%, 30%, 1); --navBack: hsla(0, 0%, 30%, 1);
/* Font Sizes */ /* Font Sizes */
@ -63,7 +63,7 @@ h2 {
line-height: 2.1875rem; line-height: 2.1875rem;
} }
a.btn, button.btn, form input[type="submit"] { a.btn, button.btn, form input[type="submit"], div.form input[type="submit"] {
text-decoration: none; text-decoration: none;
display: inline-flex; display: inline-flex;
padding: 1em 2em; padding: 1em 2em;
@ -75,11 +75,15 @@ a.btn, button.btn, form input[type="submit"] {
max-height: 4em; max-height: 4em;
} }
form input[type="submit"] { button.btn {
padding: 1.2em 2.2em;
}
form input[type="submit"], div.form input[type="submit"] {
padding: 1.1em 2em; padding: 1.1em 2em;
} }
a.btn:hover, button.btn:hover form input[type="submit"]:hover { a.btn:hover, button.btn:hover, form input[type="submit"]:hover, div.form input[type="submit"]:hover {
border: 0.3215em solid var(--primaryHover); border: 0.3215em solid var(--primaryHover);
} }
@ -87,7 +91,7 @@ a.btn:hover::before, a.btn:hover::after {
visibility: hidden; visibility: hidden;
} }
a.btnPrimary, button.btnPrimary, form input[type="submit"] { a.btnPrimary, button.btnPrimary, form input[type="submit"], div.form input[type="submit"] {
background-color: var(--primaryDefault); background-color: var(--primaryDefault);
cursor: pointer; cursor: pointer;
} }
@ -108,12 +112,12 @@ a.btnPrimary[disabled]:hover, button.btnPrimary[disabled]:hover {
border: 0.3215em solid var(--notAvailableHover); border: 0.3215em solid var(--notAvailableHover);
} }
a.btnPrimary:hover, button.btnPrimary:hover, form input[type="submit"]:hover { a.btnPrimary:hover, button.btnPrimary:hover, form input[type="submit"]:hover, div.form input[type="submit"]:hover {
background: var(--primaryHover); background: var(--primaryHover);
border: 0.3215em solid var(--primaryHover); border: 0.3215em solid var(--primaryHover);
} }
a.btn:active, button.btn:active, form input[type="submit"]:active { a.btn:active, button.btn:active, form input[type="submit"]:active, div.form input[type="submit"]:active {
padding: 0.8rem 1.8rem; padding: 0.8rem 1.8rem;
} }
@ -129,37 +133,46 @@ a.btn:active, button.btn:active, form input[type="submit"]:active {
text-shadow: 0 6px 4px var(--mutedBlack); text-shadow: 0 6px 4px var(--mutedBlack);
} }
form .formControl input:not([type="submit"]).invalid:invalid, form .formControl textarea.invalid:invalid { form .formControl input:not([type="submit"]).invalid:invalid, form .formControl textarea.invalid:invalid,
div.form .formControl input:not([type="submit"]).invalid:invalid, form .formControl textarea.invalid:invalid {
border: 0.3125em solid var(--errorDefault); border: 0.3125em solid var(--errorDefault);
} }
form .formControl input:not([type="submit"]).invalid:invalid:focus, form .formControl textarea.invalid:invalid:focus { form .formControl input:not([type="submit"]).invalid:invalid:focus, form .formControl textarea.invalid:invalid:focus,
div.form .formControl input:not([type="submit"]).invalid:invalid:focus, div.form .formControl textarea.invalid:invalid:focus {
border: 0.3125em solid var(--errorHover); border: 0.3125em solid var(--errorHover);
box-shadow: 0 4px 2px 0 var(--mutedBlack); box-shadow: 0 4px 2px 0 var(--mutedBlack);
} }
form .formControl input:not([type="submit"]) { form .formControl input:not([type="submit"]),
div.form .formControl input:not([type="submit"]) {
height: 3em; height: 3em;
} }
form .formControl { form .formControl,
div.form .formControl {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
} }
form .formControl.passwordControl { form .formControl.passwordControl,
div.form .formControl.passwordControl {
display: block; display: block;
} }
form input[type="submit"] { form input[type="submit"],
div.form input[type="submit"] {
align-self: flex-start; align-self: flex-start;
} }
form .formControl input:not([type="submit"]), form .formControl textarea, form .formControl input:not([type="submit"]), form .formControl textarea,
form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar, form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar,
form .formControl .ck.ck-editor__main .ck-content, div.menu input:not([type="submit"]) { form .formControl .ck.ck-editor__main .ck-content, div.menu input:not([type="submit"]),
div.form .formControl input:not([type="submit"]), form .formControl textarea,
div.form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar,
div.form .formControl .ck.ck-editor__main .ck-content, div.menu input:not([type="submit"]) {
width: 100%; width: 100%;
border: 0.3125em solid var(--primaryDefault); border: 0.3125em solid var(--primaryDefault);
background: none; background: none;
@ -170,30 +183,37 @@ form .formControl .ck.ck-editor__main .ck-content, div.menu input:not([type="sub
padding: 0 0.5em; padding: 0 0.5em;
} }
form .formControl textarea { form .formControl textarea,
div.form .formControl textarea {
padding: 0.5em; padding: 0.5em;
} }
form .formControl input:not([type="submit"]).invalid:invalid, form .formControl textarea.invalid:invalid { form .formControl input:not([type="submit"]).invalid:invalid, form .formControl textarea.invalid:invalid,
div.form .formControl input:not([type="submit"]).invalid:invalid, form .formControl textarea.invalid:invalid {
border: 0.3125em solid var(--errorDefault); border: 0.3125em solid var(--errorDefault);
} }
form .formControl input:not([type="submit"]).invalid:invalid:focus, form .formControl textarea.invalid:invalid:focus { form .formControl input:not([type="submit"]).invalid:invalid:focus, form .formControl textarea.invalid:invalid:focus,
div.form .formControl input:not([type="submit"]).invalid:invalid:focus, form .formControl textarea.invalid:invalid:focus {
border: 0.3125em solid var(--errorHover); border: 0.3125em solid var(--errorHover);
box-shadow: 0 4px 2px 0 var(--mutedBlack); box-shadow: 0 4px 2px 0 var(--mutedBlack);
} }
form .formControl input:not([type="submit"]):focus, form .formControl textarea:focus, form .formControl input:not([type="submit"]):focus, form .formControl textarea:focus,
form .formControl input:not([type="submit"]):hover, form .formControl textarea:hover, form .formControl input:not([type="submit"]):hover, form .formControl textarea:hover,
div.menu input:not([type="submit"]):focus, div.menu input:not([type="submit"]):hover { div.menu input:not([type="submit"]):focus, div.menu input:not([type="submit"]):hover,
div.form .formControl input:not([type="submit"]):focus, form .formControl textarea:focus,
div.form .formControl input:not([type="submit"]):hover, form .formControl textarea:hover {
border: 0.3125em solid var(--primaryHover); border: 0.3125em solid var(--primaryHover);
} }
form .formControl input:not([type="submit"]) { form .formControl input:not([type="submit"]),
div.form .formControl input:not([type="submit"]) {
height: 3em; height: 3em;
} }
form .formControl i.fa-eye, form .formControl i.fa-eye-slash { form .formControl i.fa-eye, form .formControl i.fa-eye-slash,
div.form .formControl i.fa-eye, form .formControl i.fa-eye-slash {
margin-left: -40px; margin-left: -40px;
cursor: pointer; cursor: pointer;
color: var(--primaryDefault); color: var(--primaryDefault);
@ -204,7 +224,8 @@ form .formControl input:not([type="submit"]):focus + i.fa-eye-slash {
color: var(--primaryHover); color: var(--primaryHover);
} }
form .formControl .checkContainer { form .formControl .checkContainer,
div.form .formControl .checkContainer {
display: block; display: block;
position: relative; position: relative;
margin-bottom: 1.25em; margin-bottom: 1.25em;
@ -215,7 +236,8 @@ form .formControl .checkContainer {
user-select: none; user-select: none;
} }
form .formControl .checkContainer input { form .formControl .checkContainer input,
div.form .formControl .checkContainer input {
position: absolute; position: absolute;
opacity: 0; opacity: 0;
cursor: pointer; cursor: pointer;
@ -223,7 +245,8 @@ form .formControl .checkContainer input {
width: 0; width: 0;
} }
form .formControl .checkContainer .checkmark { form .formControl .checkContainer .checkmark,
div.form .formControl .checkContainer .checkmark {
position: absolute; position: absolute;
top: 1.25em; top: 1.25em;
left: 0; left: 0;
@ -232,29 +255,35 @@ form .formControl .checkContainer .checkmark {
background-color: var(--mutedGrey); background-color: var(--mutedGrey);
} }
form .formControl .checkContainer:hover input ~ .checkmark { form .formControl .checkContainer:hover input ~ .checkmark,
div.form .formControl .checkContainer:hover input ~ .checkmark {
background-color: var(--grey); background-color: var(--grey);
} }
form .formControl .checkContainer input:checked ~ .checkmark { form .formControl .checkContainer input:checked ~ .checkmark,
div.form .formControl .checkContainer input:checked ~ .checkmark {
background-color: var(--primaryDefault); background-color: var(--primaryDefault);
} }
form .formControl .checkContainer input:checked:hover ~ .checkmark { form .formControl .checkContainer input:checked:hover ~ .checkmark,
div.form .formControl .checkContainer input:checked:hover ~ .checkmark {
background-color: var(--primaryHover); background-color: var(--primaryHover);
} }
form .formControl .checkContainer .checkmark:after { form .formControl .checkContainer .checkmark:after,
div.form .formControl .checkContainer .checkmark:after {
content: ""; content: "";
position: absolute; position: absolute;
display: none; display: none;
} }
form .formControl .checkContainer input:checked ~ .checkmark:after { form .formControl .checkContainer input:checked ~ .checkmark:after,
div.form .formControl .checkContainer input:checked ~ .checkmark:after {
display: block; display: block;
} }
form .formControl .checkContainer .checkmark:after { form .formControl .checkContainer .checkmark:after,
div.form .formControl .checkContainer .checkmark:after {
left: 9px; left: 9px;
top: 5px; top: 5px;
width: 5px; width: 5px;
@ -324,10 +353,61 @@ a.link:hover::after {
visibility: visible; visibility: visible;
} }
/*.link span {*/ div.error, div.success {
/* visibility: hidden;*/ color: #FFFFFF;
/*}*/ padding: 0.5em 0.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 0.5s ease-in-out;
-moz-transition: all 0.5s ease-in-out;
-ms-transition: all 0.5s ease-in-out;
-o-transition: all 0.5s ease-in-out;
transition: all 0.5s ease-in-out;
opacity: 1;
margin-top: 1em;
}
/*.link:hover span {*/ div.error {
/* visibility: visible;*/ background: var(--errorDefault);
/*}*/ }
div.success {
background-color: var(--primaryHover);
}
div.error button, div.success button {
border: none;
background: none;
outline: none;
cursor: pointer;
color: #FFFFFF;
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);
}

View File

@ -279,3 +279,7 @@ section#editPost table td, th {
section#editPost form { section#editPost form {
margin-bottom: 2em; margin-bottom: 2em;
} }
section#newsletter form {
margin: 0 5em;
}

View File

@ -66,64 +66,7 @@ div#login input[type=submit]{
margin: 0; margin: 0;
} }
div.error, div.success {
color: #FFFFFF;
padding: 0.5em 0.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 0.5s ease-in-out;
-moz-transition: all 0.5s ease-in-out;
-ms-transition: all 0.5s ease-in-out;
-o-transition: all 0.5s ease-in-out;
transition: all 0.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: none;
outline: none;
cursor: pointer;
color: #FFFFFF;
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 { div.btnContainer {
width: 100%; width: 100%;

View File

@ -61,7 +61,7 @@ nav.sideNav ul li.dropdown ul {
nav.sideNav ul li.dropdown ul.active { nav.sideNav ul li.dropdown ul.active {
transition: max-height ease-in 400ms; transition: max-height ease-in 400ms;
max-height: 15rem; max-height: 20rem;
} }
nav.sideNav ul li.dropdown ul li { nav.sideNav ul li.dropdown ul li {

View File

@ -7,7 +7,6 @@
<script src="https://kit.fontawesome.com/ed3c25598e.js" crossorigin="anonymous"></script> <script src="https://kit.fontawesome.com/ed3c25598e.js" crossorigin="anonymous"></script>
<script src="js/CKEditor/ckeditor.js"></script> <script src="js/CKEditor/ckeditor.js"></script>
<link rel="stylesheet" href="css/main.css"> <link rel="stylesheet" href="css/main.css">
</head> </head>
<body> <body>
<nav class="sideNav"> <nav class="sideNav">
@ -40,6 +39,11 @@
Edit Blog Post Edit Blog Post
</a> </a>
</li> </li>
<li>
<a href="#" id="goToNewsletter" class="link">
Send Newsletter
</a>
</li>
</ul> </ul>
</li> </li>
<li><a href="#" id="logout">Logout</a></li> <li><a href="#" id="logout">Logout</a></li>
@ -288,6 +292,33 @@
</form> </form>
</section> </section>
<section id="newsletter">
<h2>newsletter</h2>
<form action="" id="sendNewsletterForm" method="POST">
<div class="formControl">
<label for="newsletterSubject">Subject</label>
<input type="text" id="newsletterSubject" name="newsletterSubject" required>
</div>
<div class="formControl">
<label for="CKEditorNewsletter">Message</label>
<div id="CKEditorNewsletter">
</div>
</div>
<div class="error hidden" id="newsletterError">
<button class="close" type="button">&times;</button>
<div></div>
</div>
<div class="success hidden" id="newsletterSuccess">
<button class="close" type="button">&times;</button>
<div></div>
</div>
<input type="submit" class="btn btnPrimary boxShadowIn boxShadowOut" value="Send newsletter">
</form>
</section>
</main> </main>
<script src="js/editor.js"></script> <script src="js/editor.js"></script>

View File

@ -70,7 +70,7 @@ document.addEventListener('DOMContentLoaded', () =>
})); }));
// CKEditor stuff // CKEditor stuff
createEditors("CKEditorAddPost", "CKEditorEditPost"); createEditors('CKEditorAddPost', 'CKEditorEditPost', 'CKEditorNewsletter');
}); });
@ -379,11 +379,44 @@ document.querySelector("#editPostForm").addEventListener("submit", e =>
return; return;
} }
showErrorMessage(json.error.message, "editPost"); showErrorMessage(json.error, 'editPost');
})); }));
}); });
document.querySelector('#sendNewsletterForm').addEventListener('submit', e =>
{
e.preventDefault();
let data = new FormData();
data.append('subject', document.querySelector('#newsletterSubject').value);
data.append('message', editors['CKEditorNewsletter'].getData());
fetch('/api/blog/newsletter', {
method: 'POST',
body: data,
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token'),
},
}).then(res => res.json().then(json =>
{
if (res.ok)
{
document.querySelector('#sendNewsletterForm').reset();
editors['CKEditorNewsletter'].setData('');
showSuccessMessage('Newsletter sent successfully', 'newsletter');
return;
}
if (res.status === 401)
{
window.location.href = './';
return;
}
showErrorMessage(json.error, 'newsletter');
}));
});
document.querySelector("#goToCV").addEventListener("click", () => document.querySelector("#goToCV").addEventListener("click", () =>
{ {
textareaLoaded = false; textareaLoaded = false;
@ -422,6 +455,13 @@ document.querySelector("#goToEditPost").addEventListener("click", () =>
document.querySelector("#blog").classList.add("active"); document.querySelector("#blog").classList.add("active");
}); });
document.querySelector('#goToNewsletter').addEventListener('click', () =>
{
textareaLoaded = false;
addActiveClass('goToNewsletter');
goToPage('newsletter');
});
document.querySelector("#logout").addEventListener("click", () => document.querySelector("#logout").addEventListener("click", () =>
{ {
fetch("/api/user/logout").then(res => fetch("/api/user/logout").then(res =>

View File

@ -63,7 +63,13 @@ document.querySelector("#login form").addEventListener("submit", e =>
showErrorMessage("Please type in a username and password.", "login"); showErrorMessage("Please type in a username and password.", "login");
return; return;
} }
showErrorMessage("Invalid username or password.", "login"); if (res.status === 401)
{
showErrorMessage('Invalid username or password.', 'login');
return;
}
showErrorMessage(json.error, 'login');
})); }));
return; return;
} }