Added in a cookie popup and proper newsletter functionality
All checks were successful
🚀 Deploy website on push / 🎉 Deploy (push) Successful in 21s
All checks were successful
🚀 Deploy website on push / 🎉 Deploy (push) Successful in 21s
Signed-off-by: rodude123 <rodude123@gmail.com>
This commit is contained in:
parent
e6522fb05e
commit
52614e5835
@ -16,7 +16,8 @@
|
||||
"tuupola/slim-jwt-auth": "^3.6",
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"donatello-za/rake-php-plus": "^1.0"
|
||||
"donatello-za/rake-php-plus": "^1.0",
|
||||
"phpmailer/phpmailer": "^6.9"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
|
83
composer.lock
generated
83
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f675ad7eec6390b82dca0b13dec49e5b",
|
||||
"content-hash": "f156a57e5e895727417d4274c8ad414c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "donatello-za/rake-php-plus",
|
||||
@ -1046,6 +1046,87 @@
|
||||
},
|
||||
"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",
|
||||
"version": "1.1.2",
|
||||
|
852
dist/api/blog/blogData.php
vendored
852
dist/api/blog/blogData.php
vendored
@ -8,11 +8,15 @@ use DOMDocument;
|
||||
use PDO;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
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\RSS2;
|
||||
|
||||
|
||||
|
||||
require_once __DIR__ . "/../utils/config.php";
|
||||
require_once __DIR__ . "/../utils/imgUtils.php";
|
||||
require_once __DIR__ . "/../utils/feedGenerator/FeedWriter.php";
|
||||
@ -254,10 +258,10 @@ class blogData
|
||||
* @param string $dateCreated - Date the blog post was created
|
||||
* @param bool $featured - Whether the blog post is featured or not
|
||||
* @param string $categories - Categories of the blog post
|
||||
* @param UploadedFileInterface $headerImg - Header image of the blog post
|
||||
* @param UploadedFileInterface|null $headerImg - Header image of the blog post
|
||||
* @return int|string - ID of the blog post or error message
|
||||
*/
|
||||
public function createPost(string $title, string $abstract, string $body, string $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();
|
||||
$folderID = uniqid();
|
||||
@ -289,6 +293,13 @@ class blogData
|
||||
|
||||
$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)
|
||||
VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :abstract, :body, :bodyText, :categories, :keywords, :folderID);");
|
||||
$stmt->bindParam(":title", $title);
|
||||
@ -296,7 +307,7 @@ class blogData
|
||||
$stmt->bindParam(":dateModified", $dateCreated);
|
||||
$isFeatured = $featured ? 1 : 0;
|
||||
$stmt->bindParam(":featured", $isFeatured);
|
||||
$stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
|
||||
$stmt->bindParam(":headerImg", $headerImage);
|
||||
$stmt->bindParam(":abstract", $abstract);
|
||||
$stmt->bindParam(":body", $newBody);
|
||||
$stmt->bindParam(":bodyText", $bodyText);
|
||||
@ -304,12 +315,267 @@ class blogData
|
||||
$stmt->bindParam(":keywords", $keywords);
|
||||
$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"><https://rohitpai.co.uk/blog></a></li>
|
||||
<li><a href="https://rohitpai.co.uk/blog/unsubscribe"><Unsubscribe></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)
|
||||
{
|
||||
$items[] = array(
|
||||
"id" => string($post["ID"]),
|
||||
"id" => strval($post["ID"]),
|
||||
"url" => "https://rohitpai.co.uk/blog/post/" . rawurlencode($post["title"]) . "#disqus_thread",
|
||||
"title" => $post["title"],
|
||||
"date_published" => date($post["dateCreated"]),
|
||||
"date_modified" => date($post["dateModified"]),
|
||||
// "description" => $post["abstract"],
|
||||
"description" => $post["abstract"],
|
||||
"banner_image" => "https://rohitpai.co.uk/" . rawurlencode($post["headerImg"]),
|
||||
"content_html" => $post["body"]
|
||||
);
|
||||
@ -655,4 +921,572 @@ class blogData
|
||||
|
||||
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"><https://rohitpai.co.uk/blog></a></li>
|
||||
<li><a href="https://rohitpai.co.uk/blog/unsubscribe"><Unsubscribe></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"><https://rohitpai.co.uk/blog></a></li>
|
||||
<li><a href="https://rohitpai.co.uk/blog/unsubscribe"><Unsubscribe></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;
|
||||
}
|
||||
}
|
86
dist/api/blog/blogRoutes.php
vendored
86
dist/api/blog/blogRoutes.php
vendored
@ -269,12 +269,38 @@ class blogRoutes implements routesInterface
|
||||
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)
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$files = $request->getUploadedFiles();
|
||||
$headerImg = $files["headerImg"];
|
||||
if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["abstract"]) || empty($data["dateCreated"]) || empty($data["categories"]))
|
||||
if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["bodyText"]) || empty($data["abstract"]) || empty($data["dateCreated"]) || empty($data["categories"]))
|
||||
{
|
||||
// uh oh sent some empty data
|
||||
$response->getBody()->write(json_encode(array("error" => "Error, empty data sent")));
|
||||
@ -288,6 +314,11 @@ class blogRoutes implements routesInterface
|
||||
return $response->withStatus(400);
|
||||
}
|
||||
|
||||
if (array_key_exists("headerImg", $files))
|
||||
{
|
||||
$headerImg = $files["headerImg"];
|
||||
}
|
||||
|
||||
if (empty($files["headerImg"]))
|
||||
{
|
||||
$headerImg = null;
|
||||
@ -339,19 +370,66 @@ class blogRoutes implements routesInterface
|
||||
if (empty($files))
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
$message = $this->blogData->uploadHeaderImage($args["id"], $files["headerImg"]);
|
||||
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);
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode($message));
|
||||
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);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
1
dist/api/project/projectData.php
vendored
1
dist/api/project/projectData.php
vendored
@ -5,6 +5,7 @@ namespace api\project;
|
||||
use api\utils\imgUtils;
|
||||
use PDO;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use function api\utils\dbConn;
|
||||
|
||||
require_once __DIR__ . "/../utils/config.php";
|
||||
require_once __DIR__ . "/../utils/imgUtils.php";
|
||||
|
1
dist/api/timeline/timelineData.php
vendored
1
dist/api/timeline/timelineData.php
vendored
@ -3,6 +3,7 @@
|
||||
namespace api\timeline;
|
||||
|
||||
use PDO;
|
||||
use function api\utils\dbConn;
|
||||
|
||||
require_once __DIR__ . "/../utils/config.php";
|
||||
|
||||
|
2
dist/api/user/userData.php
vendored
2
dist/api/user/userData.php
vendored
@ -4,6 +4,8 @@ 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";
|
||||
|
||||
|
26
dist/api/utils/middleware.php
vendored
26
dist/api/utils/middleware.php
vendored
@ -13,6 +13,7 @@ use Slim\Exception\HttpInternalServerErrorException;
|
||||
use Slim\Exception\HttpMethodNotAllowedException;
|
||||
use Slim\Exception\HttpNotFoundException;
|
||||
use Slim\Psr7\Response;
|
||||
use Throwable;
|
||||
use Tuupola\Middleware\JwtAuthentication;
|
||||
use Tuupola\Middleware\JwtAuthentication\RequestMethodRule;
|
||||
use Tuupola\Middleware\JwtAuthentication\RequestPathRule;
|
||||
@ -84,8 +85,8 @@ class middleware
|
||||
$app->add(new JwtAuthentication([
|
||||
"rules" => [
|
||||
new RequestPathRule([
|
||||
"path" => ["/api/projectData", "/api/timelineData/[a-z]*", "/api/projectImage/[0-9]*", "/api/logout"],
|
||||
"ignore" => ["/api/contact", "/api/userData/login", "/api/userData/changePassword"]
|
||||
"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", "/api/blog/newsletter/\S*", "/api/blog/newsletter/unsubscribe/\S*"]
|
||||
]),
|
||||
new RequestMethodRule([
|
||||
"ignore" => ["OPTIONS", "GET"]
|
||||
@ -133,8 +134,27 @@ class middleware
|
||||
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
142
dist/api/utils/user/userData.php
vendored
Normal 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
168
dist/api/utils/user/userRoutes.php
vendored
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
2
dist/blog/css/main.css
vendored
2
dist/blog/css/main.css
vendored
File diff suppressed because one or more lines are too long
2
dist/blog/index.html
vendored
2
dist/blog/index.html
vendored
@ -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>© <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><!-- cookie info text and 2 links disagree and agree, clicking disagree should redirect to google.co.uk --><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>© <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>
|
2
dist/blog/js/index.js
vendored
2
dist/blog/js/index.js
vendored
File diff suppressed because one or more lines are too long
2
dist/css/main.css
vendored
2
dist/css/main.css
vendored
File diff suppressed because one or more lines are too long
2
dist/editor/css/main.css
vendored
2
dist/editor/css/main.css
vendored
File diff suppressed because one or more lines are too long
2
dist/editor/editor.html
vendored
2
dist/editor/editor.html
vendored
File diff suppressed because one or more lines are too long
2
dist/editor/js/editor.js
vendored
2
dist/editor/js/editor.js
vendored
File diff suppressed because one or more lines are too long
2
dist/editor/js/index.js
vendored
2
dist/editor/js/index.js
vendored
@ -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")}));
|
@ -8,11 +8,15 @@ use DOMDocument;
|
||||
use PDO;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
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\RSS2;
|
||||
|
||||
|
||||
|
||||
require_once __DIR__ . "/../utils/config.php";
|
||||
require_once __DIR__ . "/../utils/imgUtils.php";
|
||||
require_once __DIR__ . "/../utils/feedGenerator/FeedWriter.php";
|
||||
@ -254,10 +258,10 @@ class blogData
|
||||
* @param string $dateCreated - Date the blog post was created
|
||||
* @param bool $featured - Whether the blog post is featured or not
|
||||
* @param string $categories - Categories of the blog post
|
||||
* @param UploadedFileInterface $headerImg - Header image of the blog post
|
||||
* @param UploadedFileInterface|null $headerImg - Header image of the blog post
|
||||
* @return int|string - ID of the blog post or error message
|
||||
*/
|
||||
public function createPost(string $title, string $abstract, string $body, string $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();
|
||||
$folderID = uniqid();
|
||||
@ -289,6 +293,13 @@ class blogData
|
||||
|
||||
$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)
|
||||
VALUES (:title, :dateCreated, :dateModified, :featured, :headerImg, :abstract, :body, :bodyText, :categories, :keywords, :folderID);");
|
||||
$stmt->bindParam(":title", $title);
|
||||
@ -296,7 +307,7 @@ class blogData
|
||||
$stmt->bindParam(":dateModified", $dateCreated);
|
||||
$isFeatured = $featured ? 1 : 0;
|
||||
$stmt->bindParam(":featured", $isFeatured);
|
||||
$stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
|
||||
$stmt->bindParam(":headerImg", $headerImage);
|
||||
$stmt->bindParam(":abstract", $abstract);
|
||||
$stmt->bindParam(":body", $newBody);
|
||||
$stmt->bindParam(":bodyText", $bodyText);
|
||||
@ -304,12 +315,267 @@ class blogData
|
||||
$stmt->bindParam(":keywords", $keywords);
|
||||
$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"><https://rohitpai.co.uk/blog></a></li>
|
||||
<li><a href="https://rohitpai.co.uk/blog/unsubscribe"><Unsubscribe></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)
|
||||
{
|
||||
$items[] = array(
|
||||
"id" => string($post["ID"]),
|
||||
"id" => strval($post["ID"]),
|
||||
"url" => "https://rohitpai.co.uk/blog/post/" . rawurlencode($post["title"]) . "#disqus_thread",
|
||||
"title" => $post["title"],
|
||||
"date_published" => date($post["dateCreated"]),
|
||||
"date_modified" => date($post["dateModified"]),
|
||||
// "description" => $post["abstract"],
|
||||
"description" => $post["abstract"],
|
||||
"banner_image" => "https://rohitpai.co.uk/" . rawurlencode($post["headerImg"]),
|
||||
"content_html" => $post["body"]
|
||||
);
|
||||
@ -655,4 +921,572 @@ class blogData
|
||||
|
||||
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"><https://rohitpai.co.uk/blog></a></li>
|
||||
<li><a href="https://rohitpai.co.uk/blog/unsubscribe"><Unsubscribe></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"><https://rohitpai.co.uk/blog></a></li>
|
||||
<li><a href="https://rohitpai.co.uk/blog/unsubscribe"><Unsubscribe></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;
|
||||
}
|
||||
}
|
@ -269,12 +269,38 @@ class blogRoutes implements routesInterface
|
||||
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)
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$files = $request->getUploadedFiles();
|
||||
$headerImg = $files["headerImg"];
|
||||
if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["abstract"]) || empty($data["dateCreated"]) || empty($data["categories"]))
|
||||
if (empty($data["title"]) || strlen($data["featured"]) == 0 || empty($data["body"]) || empty($data["bodyText"]) || empty($data["abstract"]) || empty($data["dateCreated"]) || empty($data["categories"]))
|
||||
{
|
||||
// uh oh sent some empty data
|
||||
$response->getBody()->write(json_encode(array("error" => "Error, empty data sent")));
|
||||
@ -288,6 +314,11 @@ class blogRoutes implements routesInterface
|
||||
return $response->withStatus(400);
|
||||
}
|
||||
|
||||
if (array_key_exists("headerImg", $files))
|
||||
{
|
||||
$headerImg = $files["headerImg"];
|
||||
}
|
||||
|
||||
if (empty($files["headerImg"]))
|
||||
{
|
||||
$headerImg = null;
|
||||
@ -339,19 +370,66 @@ class blogRoutes implements routesInterface
|
||||
if (empty($files))
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
$message = $this->blogData->uploadHeaderImage($args["id"], $files["headerImg"]);
|
||||
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);
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode($message));
|
||||
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);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ namespace api\project;
|
||||
use api\utils\imgUtils;
|
||||
use PDO;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use function api\utils\dbConn;
|
||||
|
||||
require_once __DIR__ . "/../utils/config.php";
|
||||
require_once __DIR__ . "/../utils/imgUtils.php";
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace api\timeline;
|
||||
|
||||
use PDO;
|
||||
use function api\utils\dbConn;
|
||||
|
||||
require_once __DIR__ . "/../utils/config.php";
|
||||
|
||||
|
@ -4,6 +4,8 @@ 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";
|
||||
|
||||
|
@ -13,6 +13,7 @@ use Slim\Exception\HttpInternalServerErrorException;
|
||||
use Slim\Exception\HttpMethodNotAllowedException;
|
||||
use Slim\Exception\HttpNotFoundException;
|
||||
use Slim\Psr7\Response;
|
||||
use Throwable;
|
||||
use Tuupola\Middleware\JwtAuthentication;
|
||||
use Tuupola\Middleware\JwtAuthentication\RequestMethodRule;
|
||||
use Tuupola\Middleware\JwtAuthentication\RequestPathRule;
|
||||
@ -84,8 +85,8 @@ class middleware
|
||||
$app->add(new JwtAuthentication([
|
||||
"rules" => [
|
||||
new RequestPathRule([
|
||||
"path" => ["/api/projectData", "/api/timelineData/[a-z]*", "/api/projectImage/[0-9]*", "/api/logout"],
|
||||
"ignore" => ["/api/contact", "/api/userData/login", "/api/userData/changePassword"]
|
||||
"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", "/api/blog/newsletter/\S*", "/api/blog/newsletter/unsubscribe/\S*"]
|
||||
]),
|
||||
new RequestMethodRule([
|
||||
"ignore" => ["OPTIONS", "GET"]
|
||||
@ -133,8 +134,27 @@ class middleware
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -151,7 +151,7 @@ div.otherPosts a, div.feeds a {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
div.newsletter form input[type="submit"] {
|
||||
div.newsletter div.form input[type="submit"] {
|
||||
margin-top: 1em;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
@ -12,11 +12,12 @@ section.catPosts .largePost {
|
||||
section.categories {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
margin-bottom: 5em;
|
||||
row-gap: 1em;
|
||||
}
|
||||
|
||||
section.categories .btnContainer {
|
||||
|
@ -108,7 +108,7 @@ section.largePost .outerContent .postContent a {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
#main .error {
|
||||
#main .errorFof {
|
||||
display: table;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
@ -23,6 +23,44 @@
|
||||
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 */
|
||||
}
|
||||
|
||||
.modal-container.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 40%;
|
||||
max-width: 550px;
|
||||
height: auto;
|
||||
max-height: 70vh;
|
||||
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);
|
||||
}
|
||||
|
||||
/**** Media Queries *****/
|
||||
|
||||
@media screen and (max-width: 90em) {
|
||||
|
@ -72,6 +72,22 @@
|
||||
|
||||
</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>
|
||||
<!-- cookie info text and 2 links disagree and agree, clicking disagree should redirect to google.co.uk -->
|
||||
<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>
|
||||
|
@ -33,6 +33,11 @@ function goToURL(url)
|
||||
// Get the current URL and split it into an array
|
||||
let urlArray = url.split('/');
|
||||
|
||||
if (localStorage.getItem('cookiePopup') === 'accepted')
|
||||
{
|
||||
document.querySelector('#cookiePopup').classList.add('hidden');
|
||||
}
|
||||
|
||||
if (url === '/blog/' || url === '/blog')
|
||||
{
|
||||
loadHomeContent();
|
||||
@ -84,7 +89,6 @@ function goToURL(url)
|
||||
}
|
||||
|
||||
show404();
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
* @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>
|
||||
</div>
|
||||
<div class="newsletter">
|
||||
<h3>Sign up to the newsletter</h3>
|
||||
<form action="newsletter.html">
|
||||
<h3>Sign up to the newsletter to never miss a new post!</h3>
|
||||
<div id="newsletterForm" class="form">
|
||||
<div class="formControl">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" placeholder="Email" required>
|
||||
</div>
|
||||
<input type="submit" value="Sign Up">
|
||||
</form>
|
||||
<div class="success hidden" id="newsletterMessage">
|
||||
<button class="close" type="button" onclick="this.parentElement.classList.toggle('hidden')">×</button>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Sign Up" onclick="submitNewsletter()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="feeds">
|
||||
<h2>feeds</h2>
|
||||
<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/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/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/json" class="btn btnPrimary" title="JSON"><img class="json" src="/blog/imgs/jsonFeed.svg" alt="JSON"></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="categories">
|
||||
@ -384,6 +435,12 @@ async function createSideContent()
|
||||
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)
|
||||
{
|
||||
let existingTag = document.querySelector(`meta[name="${nameOrProperty}"], meta[property="${nameOrProperty}"]`);
|
||||
@ -760,7 +817,7 @@ function loadCookiePolicy()
|
||||
document.querySelector('#main').innerHTML = `
|
||||
<div class="policy">
|
||||
<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>
|
||||
<a href="mailto:rohit@rohitpai.co.uk" class="link">rohit@rohitpai.co.uk</a>
|
||||
<br>
|
||||
@ -775,7 +832,7 @@ function loadCookiePolicy()
|
||||
function show404()
|
||||
{
|
||||
document.querySelector('#main').innerHTML = `
|
||||
<div class="error">
|
||||
<div class="errorFof">
|
||||
<div class="fof">
|
||||
<h1>Blog post, Category or page not found</h1>
|
||||
<a href="/blog/" class="btn btnPrimary">See all blog posts</a>
|
||||
|
@ -15,9 +15,9 @@
|
||||
--grey: hsla(0, 0%, 39%, 1);
|
||||
--notAvailableDefault: hsla(0, 0%, 39%, 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);
|
||||
--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);
|
||||
|
||||
/* Font Sizes */
|
||||
@ -63,7 +63,7 @@ h2 {
|
||||
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;
|
||||
display: inline-flex;
|
||||
padding: 1em 2em;
|
||||
@ -75,11 +75,15 @@ a.btn, button.btn, form input[type="submit"] {
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -87,7 +91,7 @@ a.btn:hover::before, a.btn:hover::after {
|
||||
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);
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -108,12 +112,12 @@ a.btnPrimary[disabled]:hover, button.btnPrimary[disabled]:hover {
|
||||
border: 0.3215em solid var(--notAvailableHover);
|
||||
}
|
||||
|
||||
a.btnPrimary:hover, button.btnPrimary:hover, form input[type="submit"]:hover {
|
||||
a.btnPrimary:hover, button.btnPrimary:hover, form input[type="submit"]:hover, div.form input[type="submit"]:hover {
|
||||
background: var(--primaryHover);
|
||||
border: 0.3215em solid var(--primaryHover);
|
||||
}
|
||||
|
||||
a.btn:active, button.btn:active, form input[type="submit"]:active {
|
||||
a.btn:active, button.btn:active, form input[type="submit"]:active, div.form input[type="submit"]:active {
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
form .formControl {
|
||||
form .formControl,
|
||||
div.form .formControl {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
form .formControl.passwordControl {
|
||||
form .formControl.passwordControl,
|
||||
div.form .formControl.passwordControl {
|
||||
display: block;
|
||||
}
|
||||
|
||||
form input[type="submit"] {
|
||||
form input[type="submit"],
|
||||
div.form input[type="submit"] {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
form .formControl input:not([type="submit"]), form .formControl textarea,
|
||||
form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar,
|
||||
form .formControl .ck.ck-editor__main .ck-content, 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%;
|
||||
border: 0.3125em solid var(--primaryDefault);
|
||||
background: none;
|
||||
@ -170,30 +183,37 @@ form .formControl .ck.ck-editor__main .ck-content, div.menu input:not([type="sub
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
form .formControl textarea {
|
||||
form .formControl textarea,
|
||||
div.form .formControl textarea {
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
box-shadow: 0 4px 2px 0 var(--mutedBlack);
|
||||
}
|
||||
|
||||
form .formControl input:not([type="submit"]):focus, form .formControl textarea:focus,
|
||||
form .formControl input:not([type="submit"]):hover, form .formControl textarea:hover,
|
||||
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);
|
||||
}
|
||||
|
||||
form .formControl input:not([type="submit"]) {
|
||||
form .formControl input:not([type="submit"]),
|
||||
div.form .formControl input:not([type="submit"]) {
|
||||
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;
|
||||
cursor: pointer;
|
||||
color: var(--primaryDefault);
|
||||
@ -204,7 +224,8 @@ form .formControl input:not([type="submit"]):focus + i.fa-eye-slash {
|
||||
color: var(--primaryHover);
|
||||
}
|
||||
|
||||
form .formControl .checkContainer {
|
||||
form .formControl .checkContainer,
|
||||
div.form .formControl .checkContainer {
|
||||
display: block;
|
||||
position: relative;
|
||||
margin-bottom: 1.25em;
|
||||
@ -215,7 +236,8 @@ form .formControl .checkContainer {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
form .formControl .checkContainer input {
|
||||
form .formControl .checkContainer input,
|
||||
div.form .formControl .checkContainer input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
@ -223,7 +245,8 @@ form .formControl .checkContainer input {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
form .formControl .checkContainer .checkmark {
|
||||
form .formControl .checkContainer .checkmark,
|
||||
div.form .formControl .checkContainer .checkmark {
|
||||
position: absolute;
|
||||
top: 1.25em;
|
||||
left: 0;
|
||||
@ -232,29 +255,35 @@ form .formControl .checkContainer .checkmark {
|
||||
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);
|
||||
}
|
||||
|
||||
form .formControl .checkContainer input:checked ~ .checkmark {
|
||||
form .formControl .checkContainer input:checked ~ .checkmark,
|
||||
div.form .formControl .checkContainer input:checked ~ .checkmark {
|
||||
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);
|
||||
}
|
||||
|
||||
form .formControl .checkContainer .checkmark:after {
|
||||
form .formControl .checkContainer .checkmark:after,
|
||||
div.form .formControl .checkContainer .checkmark:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
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;
|
||||
}
|
||||
|
||||
form .formControl .checkContainer .checkmark:after {
|
||||
form .formControl .checkContainer .checkmark:after,
|
||||
div.form .formControl .checkContainer .checkmark:after {
|
||||
left: 9px;
|
||||
top: 5px;
|
||||
width: 5px;
|
||||
@ -324,10 +353,61 @@ a.link:hover::after {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/*.link span {*/
|
||||
/* visibility: hidden;*/
|
||||
/*}*/
|
||||
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;
|
||||
}
|
||||
|
||||
/*.link:hover span {*/
|
||||
/* visibility: visible;*/
|
||||
/*}*/
|
||||
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);
|
||||
}
|
@ -279,3 +279,7 @@ section#editPost table td, th {
|
||||
section#editPost form {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
section#newsletter form {
|
||||
margin: 0 5em;
|
||||
}
|
@ -66,64 +66,7 @@ div#login input[type=submit]{
|
||||
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 {
|
||||
width: 100%;
|
||||
|
@ -61,7 +61,7 @@ nav.sideNav ul li.dropdown ul {
|
||||
|
||||
nav.sideNav ul li.dropdown ul.active {
|
||||
transition: max-height ease-in 400ms;
|
||||
max-height: 15rem;
|
||||
max-height: 20rem;
|
||||
}
|
||||
|
||||
nav.sideNav ul li.dropdown ul li {
|
||||
|
@ -7,7 +7,6 @@
|
||||
<script src="https://kit.fontawesome.com/ed3c25598e.js" crossorigin="anonymous"></script>
|
||||
<script src="js/CKEditor/ckeditor.js"></script>
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<nav class="sideNav">
|
||||
@ -40,6 +39,11 @@
|
||||
Edit Blog Post
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" id="goToNewsletter" class="link">
|
||||
Send Newsletter
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#" id="logout">Logout</a></li>
|
||||
@ -288,6 +292,33 @@
|
||||
</form>
|
||||
</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">×</button>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<div class="success hidden" id="newsletterSuccess">
|
||||
<button class="close" type="button">×</button>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<input type="submit" class="btn btnPrimary boxShadowIn boxShadowOut" value="Send newsletter">
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="js/editor.js"></script>
|
||||
|
@ -70,7 +70,7 @@ document.addEventListener('DOMContentLoaded', () =>
|
||||
}));
|
||||
|
||||
// CKEditor stuff
|
||||
createEditors("CKEditorAddPost", "CKEditorEditPost");
|
||||
createEditors('CKEditorAddPost', 'CKEditorEditPost', 'CKEditorNewsletter');
|
||||
|
||||
});
|
||||
|
||||
@ -379,11 +379,44 @@ document.querySelector("#editPostForm").addEventListener("submit", e =>
|
||||
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", () =>
|
||||
{
|
||||
textareaLoaded = false;
|
||||
@ -422,6 +455,13 @@ document.querySelector("#goToEditPost").addEventListener("click", () =>
|
||||
document.querySelector("#blog").classList.add("active");
|
||||
});
|
||||
|
||||
document.querySelector('#goToNewsletter').addEventListener('click', () =>
|
||||
{
|
||||
textareaLoaded = false;
|
||||
addActiveClass('goToNewsletter');
|
||||
goToPage('newsletter');
|
||||
});
|
||||
|
||||
document.querySelector("#logout").addEventListener("click", () =>
|
||||
{
|
||||
fetch("/api/user/logout").then(res =>
|
||||
|
@ -63,7 +63,13 @@ document.querySelector("#login form").addEventListener("submit", e =>
|
||||
showErrorMessage("Please type in a username and password.", "login");
|
||||
return;
|
||||
}
|
||||
showErrorMessage("Invalid username or password.", "login");
|
||||
if (res.status === 401)
|
||||
{
|
||||
showErrorMessage('Invalid username or password.', 'login');
|
||||
return;
|
||||
}
|
||||
|
||||
showErrorMessage(json.error, 'login');
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user