Compare commits
18 Commits
e6522fb05e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 00851d37b8 | |||
| a55ba6ce2f | |||
| c2e01dd1e8 | |||
| 860c52e829 | |||
| b28e7b2da5 | |||
| 7d6eeb2310 | |||
| 558ac03fbb | |||
| 646cfa6561 | |||
| 591db4dfa3 | |||
| 7f96aa9277 | |||
| 7b8e81e1f7 | |||
| 430e1c65ca | |||
| 364e2d2675 | |||
| 804d8a9390 | |||
| a5f17a70ed | |||
| 62f871f4ca | |||
| 0cb57d0813 | |||
| 52614e5835 |
+4
-1
@@ -16,7 +16,10 @@
|
||||
"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",
|
||||
"onelogin/php-saml": "^4.1",
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
|
||||
Generated
+180
-1
@@ -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": "a0850a84ff9d5a207b7b0724563015c7",
|
||||
"packages": [
|
||||
{
|
||||
"name": "donatello-za/rake-php-plus",
|
||||
@@ -873,6 +873,62 @@
|
||||
],
|
||||
"time": "2023-11-08T09:30:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "onelogin/php-saml",
|
||||
"version": "4.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/onelogin/php-saml.git",
|
||||
"reference": "b22a57ebd13e838b90df5d3346090bc37056409d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/onelogin/php-saml/zipball/b22a57ebd13e838b90df5d3346090bc37056409d",
|
||||
"reference": "b22a57ebd13e838b90df5d3346090bc37056409d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.3",
|
||||
"robrichards/xmlseclibs": ">=3.1.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"pdepend/pdepend": "^2.8.0",
|
||||
"php-coveralls/php-coveralls": "^2.0",
|
||||
"phploc/phploc": "^4.0 || ^5.0 || ^6.0 || ^7.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"sebastian/phpcpd": "^4.0 || ^5.0 || ^6.0 ",
|
||||
"squizlabs/php_codesniffer": "^3.5.8"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-curl": "Install curl lib to be able to use the IdPMetadataParser for parsing remote XMLs",
|
||||
"ext-dom": "Install xml lib",
|
||||
"ext-openssl": "Install openssl lib in order to handle with x509 certs (require to support sign and encryption)",
|
||||
"ext-zlib": "Install zlib"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"OneLogin\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "OneLogin PHP SAML Toolkit",
|
||||
"homepage": "https://developers.onelogin.com/saml/php",
|
||||
"keywords": [
|
||||
"SAML2",
|
||||
"onelogin",
|
||||
"saml"
|
||||
],
|
||||
"support": {
|
||||
"email": "sixto.garcia@onelogin.com",
|
||||
"issues": "https://github.com/onelogin/php-saml/issues",
|
||||
"source": "https://github.com/onelogin/php-saml/"
|
||||
},
|
||||
"time": "2022-07-15T20:44:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "php-di/invoker",
|
||||
"version": "2.3.4",
|
||||
@@ -1046,6 +1102,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",
|
||||
@@ -1533,6 +1670,48 @@
|
||||
},
|
||||
"time": "2021-07-12T10:12:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "robrichards/xmlseclibs",
|
||||
"version": "3.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/robrichards/xmlseclibs.git",
|
||||
"reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/f8f19e58f26cdb42c54b214ff8a820760292f8df",
|
||||
"reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-openssl": "*",
|
||||
"php": ">= 5.4"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"RobRichards\\XMLSecLibs\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"description": "A PHP library for XML Security",
|
||||
"homepage": "https://github.com/robrichards/xmlseclibs",
|
||||
"keywords": [
|
||||
"security",
|
||||
"signature",
|
||||
"xml",
|
||||
"xmldsig"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/robrichards/xmlseclibs/issues",
|
||||
"source": "https://github.com/robrichards/xmlseclibs/tree/3.1.1"
|
||||
},
|
||||
"time": "2020-09-05T13:00:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "selective/samesite-cookie",
|
||||
"version": "0.3.0",
|
||||
|
||||
Vendored
+885
-14
@@ -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,14 +293,22 @@ class blogData
|
||||
|
||||
$keywords = implode(", ", RakePlus::create($bodyText)->keywords());
|
||||
|
||||
$latest = $this->getLatestBlogPost();
|
||||
$prevTitle = $latest["title"];
|
||||
$prevAbstract = $latest["abstract"];
|
||||
$prevHeaderImage = substr($latest["headerImg"], 10);
|
||||
$prevHeaderImage = str_ireplace("%2F", "/", $prevHeaderImage);
|
||||
|
||||
$headerImage = rawurlencode("../" . $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);
|
||||
$stmt->bindParam(":dateCreated", $dateCreated);
|
||||
$stmt->bindParam(":dateModified", $dateCreated);
|
||||
$isFeatured = $featured ? 1 : 0;
|
||||
$stmt->bindParam(":featured", $isFeatured);
|
||||
$stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
|
||||
// $isFeatured = $featured ? 1 : 0;
|
||||
$stmt->bindParam(":featured", $featured);
|
||||
$stmt->bindParam(":headerImg", $headerImage);
|
||||
$stmt->bindParam(":abstract", $abstract);
|
||||
$stmt->bindParam(":body", $newBody);
|
||||
$stmt->bindParam(":bodyText", $bodyText);
|
||||
@@ -304,12 +316,274 @@ 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);
|
||||
$headerImage = substr($headerImage, 10);
|
||||
$headerImage = str_ireplace("%2F", "/", $headerImage);
|
||||
|
||||
$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="https://rohitpai.co.uk/$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="https://rohitpai.co.uk/$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>
|
||||
|
||||
EOD;
|
||||
|
||||
foreach ($emails as $email)
|
||||
{
|
||||
$emailFooter = <<<EOD
|
||||
<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/$email"><Unsubscribe></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="date">2023</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
EOD;
|
||||
$emailBody .= $emailFooter;
|
||||
|
||||
$this->sendMail($email["email"], $emailBody, "Hey, Rohit's blog has a new post!");
|
||||
}
|
||||
|
||||
return intval($conn->lastInsertId());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,7 +650,8 @@ class blogData
|
||||
unlink($result["headerImg"]);
|
||||
$stmt = $conn->prepare("UPDATE blog SET headerImg = :headerImg WHERE ID = :ID;");
|
||||
$stmt->bindParam(":ID", $ID);
|
||||
$stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
|
||||
$location = urldecode("../" . $targetFile["imgLocation"]);
|
||||
$stmt->bindParam(":headerImg", $location);
|
||||
$stmt->execute();
|
||||
if ($stmt->rowCount() > 0)
|
||||
{
|
||||
@@ -399,7 +674,11 @@ class blogData
|
||||
public function changeHTMLSrc(string $body, string $to, string $from): string
|
||||
{
|
||||
$htmlDoc = new DOMDocument();
|
||||
$htmlDoc->loadHTML($body, LIBXML_NOERROR);
|
||||
|
||||
// Load the raw HTML content into DOMDocument
|
||||
@$htmlDoc->loadHTML($body, LIBXML_NOERROR);
|
||||
|
||||
// Get the body and process images
|
||||
$doc = $htmlDoc->getElementsByTagName('body')->item(0);
|
||||
$imgs = $doc->getElementsByTagName('img');
|
||||
|
||||
@@ -412,9 +691,11 @@ class blogData
|
||||
$srcList[] = $src;
|
||||
$fileName = basename($src);
|
||||
|
||||
// Update the src attribute to the new location
|
||||
$img->setAttribute("src", substr($to, 2) . $fileName);
|
||||
}
|
||||
|
||||
// Rename files and clean up old ones
|
||||
$files = scandir($from);
|
||||
foreach ($files as $file)
|
||||
{
|
||||
@@ -430,14 +711,36 @@ class blogData
|
||||
}
|
||||
}
|
||||
|
||||
// Process the HTML content for output
|
||||
$newBody = '';
|
||||
foreach ($doc->childNodes as $node)
|
||||
{
|
||||
$newBody .= $htmlDoc->saveHTML($node);
|
||||
// Only convert text nodes to HTML entities
|
||||
if ($node->nodeType === XML_TEXT_NODE)
|
||||
{
|
||||
$newBody .= $this->convertToHtmlEntities($node->nodeValue); // Convert text nodes
|
||||
}
|
||||
else
|
||||
{
|
||||
$newBody .= $htmlDoc->saveHTML($node); // Keep HTML tags intact
|
||||
}
|
||||
}
|
||||
|
||||
return $newBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all characters in a string to HTML entities while leaving HTML tags intact.
|
||||
* @param string $text - The text to convert
|
||||
* @return string - The converted text with HTML entities
|
||||
*/
|
||||
private function convertToHtmlEntities(string $text): string
|
||||
{
|
||||
// Convert characters to HTML entities using mb_encode_numericentity
|
||||
return htmlentities($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all posts with the given category
|
||||
* @param string $category - Category of the post
|
||||
@@ -616,12 +919,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 +958,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/$email"><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;
|
||||
}
|
||||
}
|
||||
Vendored
+85
-6
@@ -269,12 +269,39 @@ 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("message" => "Woah, you're already trying to leave without signing up?")));
|
||||
return $response->withStatus(404);
|
||||
}
|
||||
|
||||
if ($message === "error")
|
||||
{
|
||||
// uh oh something went wrong
|
||||
$response->getBody()->write(json_encode(array("message" => "Error, something went wrong")));
|
||||
return $response->withStatus(500);
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode(array("message" => "Sorry to see you go! You'll no longer receive any emails from me. If you change your mind, you can always sign up again.")));
|
||||
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,13 +315,18 @@ class blogRoutes implements routesInterface
|
||||
return $response->withStatus(400);
|
||||
}
|
||||
|
||||
if (array_key_exists("headerImg", $files))
|
||||
{
|
||||
$headerImg = $files["headerImg"];
|
||||
}
|
||||
|
||||
if (empty($files["headerImg"]))
|
||||
{
|
||||
$headerImg = null;
|
||||
}
|
||||
|
||||
$featured = $data["featured"] === "true";
|
||||
$insertedID = $this->blogData->createPost($data["title"], $data["abstract"], $data["body"], $data["bodyText"], $data["dateCreated"], $featured, $data["categories"], $headerImg);
|
||||
// $featured = $data["featured"] === "true";
|
||||
$insertedID = $this->blogData->createPost($data["title"], $data["abstract"], $data["body"], $data["bodyText"], $data["dateCreated"], intval($data["featured"]), $data["categories"], $headerImg);
|
||||
if (!is_int($insertedID))
|
||||
{
|
||||
// uh oh something went wrong
|
||||
@@ -339,19 +371,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);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -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";
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@
|
||||
namespace api\timeline;
|
||||
|
||||
use PDO;
|
||||
use function api\utils\dbConn;
|
||||
|
||||
require_once __DIR__ . "/../utils/config.php";
|
||||
|
||||
|
||||
Vendored
-138
@@ -1,138 +0,0 @@
|
||||
<?php
|
||||
namespace api\user;
|
||||
use Firebase\JWT\JWT;
|
||||
use PDO;
|
||||
|
||||
require_once __DIR__ . "/../utils/config.php";
|
||||
|
||||
/**
|
||||
* User Class
|
||||
* Define all functions which either check, update or delete user data
|
||||
*/
|
||||
class user
|
||||
{
|
||||
/**
|
||||
* Check if user exists and can be logged in
|
||||
* @param $username string - Username
|
||||
* @param $password string - Password
|
||||
* @return bool - True if logged in, false if not
|
||||
*/
|
||||
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
|
||||
*/
|
||||
function createToken(string $username): string
|
||||
{
|
||||
$now = time();
|
||||
$future = strtotime('+6 hour',$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
|
||||
*/
|
||||
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 user
|
||||
* @param $email - email address of the user
|
||||
* @return string - verification code
|
||||
*/
|
||||
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, user-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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Vendored
+36
@@ -4,6 +4,9 @@ namespace api\user;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use PDO;
|
||||
use function api\utils\dbConn;
|
||||
use function api\utils\getSAMLSettings;
|
||||
use function api\utils\getSecretKey;
|
||||
|
||||
require_once __DIR__ . "/../utils/config.php";
|
||||
|
||||
@@ -136,5 +139,38 @@ class userData
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SAML settings
|
||||
* @return array - SAML settings
|
||||
*/
|
||||
public function getSamlConf(): array
|
||||
{
|
||||
return getSAMLSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the SAML user exists
|
||||
* @param string $username - Username
|
||||
* @param string $email - Email
|
||||
* @return bool - True if the user exists, false if not
|
||||
*/
|
||||
public function checkSAMLUser(string $username, string $email): bool
|
||||
{
|
||||
$conn = dbConn();
|
||||
$stmt = $conn->prepare("SELECT * FROM users WHERE username = :username AND email = :email");
|
||||
$stmt->bindParam(":username", $username);
|
||||
$stmt->bindParam(":email", $email);
|
||||
$stmt->execute();
|
||||
|
||||
// set the resulting array to associative
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($result)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Vendored
+74
-25
@@ -5,6 +5,8 @@ require_once __DIR__ . "/../utils/routesInterface.php";
|
||||
require_once "userData.php";
|
||||
|
||||
use api\utils\routesInterface;
|
||||
use OneLogin\Saml2\Auth;
|
||||
use OneLogin\Saml2\Error;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\App;
|
||||
@@ -12,14 +14,17 @@ use Slim\App;
|
||||
class userRoutes implements routesInterface
|
||||
{
|
||||
private userData $user;
|
||||
private Auth $samlAuth;
|
||||
|
||||
/**
|
||||
* constructor used to instantiate a base user routes, to be used in the index.php file.
|
||||
* constructor used to instantiate base user routes, to be used in the index.php file.
|
||||
* @param App $app - the slim app used to create the routes
|
||||
* @throws Error
|
||||
*/
|
||||
public function __construct(App $app)
|
||||
{
|
||||
$this->user = new userData();
|
||||
$this->samlAuth = new Auth($this->user->getSamlConf());
|
||||
$this->createRoutes($app);
|
||||
}
|
||||
|
||||
@@ -30,31 +35,9 @@ class userRoutes implements routesInterface
|
||||
*/
|
||||
public function createRoutes(App $app): void
|
||||
{
|
||||
$app->post("/user/login", function (Request $request, Response $response)
|
||||
$app->get("/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);
|
||||
$this->samlAuth->login();
|
||||
});
|
||||
|
||||
$app->get("/user/logout", function (Request $request, Response $response)
|
||||
@@ -92,6 +75,20 @@ class userRoutes implements routesInterface
|
||||
|
||||
});
|
||||
|
||||
$app->get("/user/metadata", function (Request $request, Response $response)
|
||||
{
|
||||
$settings = $this->samlAuth->getSettings();
|
||||
$metadata = $settings->getSPMetadata();
|
||||
$errors = $settings->validateMetadata($metadata);
|
||||
if (empty($errors))
|
||||
{
|
||||
$response->getBody()->write($metadata);
|
||||
return $response->withHeader("Content-Type", "text/xml");
|
||||
}
|
||||
$response->getBody()->write(json_encode(array("error" => $errors)));
|
||||
return $response->withStatus(500);
|
||||
});
|
||||
|
||||
$app->get("/user/checkResetEmail/{email}", function (Request $request, Response $response, array $args)
|
||||
{
|
||||
if (empty($args["email"]))
|
||||
@@ -139,6 +136,58 @@ class userRoutes implements routesInterface
|
||||
return $response->withStatus(401);
|
||||
});
|
||||
|
||||
$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->post("/user/acs", function (Request $request, Response $response)
|
||||
{
|
||||
$this->samlAuth->processResponse();
|
||||
|
||||
$attributes = $this->samlAuth->getAttributes();
|
||||
$username = $attributes["username"][0];
|
||||
$email = $attributes["email"][0];
|
||||
|
||||
if ($this->user->checkSAMLUser($username, $email))
|
||||
{
|
||||
// yay, user is logged in
|
||||
$_SESSION["token"] = $this->user->createToken($username);
|
||||
$_SESSION["username"] = $username;
|
||||
$_SESSION["email"] = $email;
|
||||
|
||||
$inactive = 60 * 60 * 48; // 2 days
|
||||
$_SESSION["timeout"] = time() + $inactive;
|
||||
|
||||
return $response->withHeader("Location", "https://rohitpai.co.uk/editor/")->withStatus(302);
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode(array("error" => "Unauthorised")));
|
||||
return $response->withStatus(401);
|
||||
});
|
||||
|
||||
$app->post("/user/changePassword", function (Request $request, Response $response)
|
||||
{
|
||||
if (empty($_SESSION["resetToken"]) && empty($_SESSION["resetEmail"]))
|
||||
|
||||
Vendored
+23
-3
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
/* PrismJS 1.29.0
|
||||
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+abap+abnf+actionscript+ada+agda+al+antlr4+apacheconf+apex+apl+applescript+aql+arduino+arff+armasm+arturo+asciidoc+aspnet+asm6502+asmatmel+autohotkey+autoit+avisynth+avro-idl+awk+bash+basic+batch+bbcode+bbj+bicep+birb+bison+bnf+bqn+brainfuck+brightscript+bro+bsl+c+csharp+cpp+cfscript+chaiscript+cil+cilkc+cilkcpp+clojure+cmake+cobol+coffeescript+concurnas+csp+cooklang+coq+crystal+css-extras+csv+cue+cypher+d+dart+dataweave+dax+dhall+diff+django+dns-zone-file+docker+dot+ebnf+editorconfig+eiffel+ejs+elixir+elm+etlua+erb+erlang+excel-formula+fsharp+factor+false+firestore-security-rules+flow+fortran+ftl+gml+gap+gcode+gdscript+gedcom+gettext+gherkin+git+glsl+gn+linker-script+go+go-module+gradle+graphql+groovy+haml+handlebars+haskell+haxe+hcl+hlsl+hoon+http+hpkp+hsts+ichigojam+icon+icu-message-format+idris+ignore+inform7+ini+io+j+java+javadoc+javadoclike+javastacktrace+jexl+jolie+jq+jsdoc+js-extras+json+json5+jsonp+jsstacktrace+js-templates+julia+keepalived+keyman+kotlin+kumir+kusto+latex+latte+less+lilypond+liquid+lisp+livescript+llvm+log+lolcode+lua+magma+makefile+markdown+markup-templating+mata+matlab+maxscript+mel+mermaid+metafont+mizar+mongodb+monkey+moonscript+n1ql+n4js+nand2tetris-hdl+naniscript+nasm+neon+nevod+nginx+nim+nix+nsis+objectivec+ocaml+odin+opencl+openqasm+oz+parigp+parser+pascal+pascaligo+psl+pcaxis+peoplecode+perl+php+phpdoc+php-extras+plant-uml+plsql+powerquery+powershell+processing+prolog+promql+properties+protobuf+pug+puppet+pure+purebasic+purescript+python+qsharp+q+qml+qore+r+racket+cshtml+jsx+tsx+reason+regex+rego+renpy+rescript+rest+rip+roboconf+robotframework+ruby+rust+sas+sass+scss+scala+scheme+shell-session+smali+smalltalk+smarty+sml+solidity+solution-file+soy+sparql+splunk-spl+sqf+sql+squirrel+stan+stata+iecst+stylus+supercollider+swift+systemd+t4-templating+t4-cs+t4-vb+tap+tcl+tt2+textile+toml+tremor+turtle+twig+typescript+typoscript+unrealscript+uorazor+uri+v+vala+vbnet+velocity+verilog+vhdl+vim+visual-basic+warpscript+wasm+web-idl+wgsl+wiki+wolfram+wren+xeora+xml-doc+xojo+xquery+yaml+yang+zig&plugins=line-numbers+show-language+inline-color+previewers+unescaped-markup+toolbar+copy-to-clipboard+download-button+match-braces */
|
||||
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
|
||||
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
|
||||
div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}
|
||||
span.inline-color-wrapper{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyIDIiPjxwYXRoIGZpbGw9ImdyYXkiIGQ9Ik0wIDBoMnYySDB6Ii8+PHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0wIDBoMXYxSDB6TTEgMWgxdjFIMXoiLz48L3N2Zz4=);background-position:center;background-size:110%;display:inline-block;height:1.333ch;width:1.333ch;margin:0 .333ch;box-sizing:border-box;border:1px solid #fff;outline:1px solid rgba(0,0,0,.5);overflow:hidden}span.inline-color{display:block;height:120%;width:120%}
|
||||
.prism-previewer,.prism-previewer:after,.prism-previewer:before{position:absolute;pointer-events:none}.prism-previewer,.prism-previewer:after{left:50%}.prism-previewer{margin-top:-48px;width:32px;height:32px;margin-left:-16px;z-index:10;opacity:0;-webkit-transition:opacity .25s;-o-transition:opacity .25s;transition:opacity .25s}.prism-previewer.flipped{margin-top:0;margin-bottom:-48px}.prism-previewer:after,.prism-previewer:before{content:'';position:absolute;pointer-events:none}.prism-previewer:before{top:-5px;right:-5px;left:-5px;bottom:-5px;border-radius:10px;border:5px solid #fff;box-shadow:0 0 3px rgba(0,0,0,.5) inset,0 0 10px rgba(0,0,0,.75)}.prism-previewer:after{top:100%;width:0;height:0;margin:5px 0 0 -7px;border:7px solid transparent;border-color:rgba(255,0,0,0);border-top-color:#fff}.prism-previewer.flipped:after{top:auto;bottom:100%;margin-top:0;margin-bottom:5px;border-top-color:rgba(255,0,0,0);border-bottom-color:#fff}.prism-previewer.active{opacity:1}.prism-previewer-angle:before{border-radius:50%;background:#fff}.prism-previewer-angle:after{margin-top:4px}.prism-previewer-angle svg{width:32px;height:32px;-webkit-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.prism-previewer-angle[data-negative] svg{-webkit-transform:scaleX(-1) rotate(-90deg);-moz-transform:scaleX(-1) rotate(-90deg);-ms-transform:scaleX(-1) rotate(-90deg);-o-transform:scaleX(-1) rotate(-90deg);transform:scaleX(-1) rotate(-90deg)}.prism-previewer-angle circle{fill:transparent;stroke:#2d3438;stroke-opacity:.9;stroke-width:32;stroke-dasharray:0,500}.prism-previewer-gradient{background-image:linear-gradient(45deg,#bbb 25%,transparent 25%,transparent 75%,#bbb 75%,#bbb),linear-gradient(45deg,#bbb 25%,#eee 25%,#eee 75%,#bbb 75%,#bbb);background-size:10px 10px;background-position:0 0,5px 5px;width:64px;margin-left:-32px}.prism-previewer-gradient:before{content:none}.prism-previewer-gradient div{position:absolute;top:-5px;left:-5px;right:-5px;bottom:-5px;border-radius:10px;border:5px solid #fff;box-shadow:0 0 3px rgba(0,0,0,.5) inset,0 0 10px rgba(0,0,0,.75)}.prism-previewer-color{background-image:linear-gradient(45deg,#bbb 25%,transparent 25%,transparent 75%,#bbb 75%,#bbb),linear-gradient(45deg,#bbb 25%,#eee 25%,#eee 75%,#bbb 75%,#bbb);background-size:10px 10px;background-position:0 0,5px 5px}.prism-previewer-color:before{background-color:inherit;background-clip:padding-box}.prism-previewer-easing{margin-top:-76px;margin-left:-30px;width:60px;height:60px;background:#333}.prism-previewer-easing.flipped{margin-bottom:-116px}.prism-previewer-easing svg{width:60px;height:60px}.prism-previewer-easing circle{fill:#2d3438;stroke:#fff}.prism-previewer-easing path{fill:none;stroke:#fff;stroke-linecap:round;stroke-width:4}.prism-previewer-easing line{stroke:#fff;stroke-opacity:.5;stroke-width:2}@-webkit-keyframes prism-previewer-time{0%{stroke-dasharray:0,500;stroke-dashoffset:0}50%{stroke-dasharray:100,500;stroke-dashoffset:0}100%{stroke-dasharray:0,500;stroke-dashoffset:-100}}@-o-keyframes prism-previewer-time{0%{stroke-dasharray:0,500;stroke-dashoffset:0}50%{stroke-dasharray:100,500;stroke-dashoffset:0}100%{stroke-dasharray:0,500;stroke-dashoffset:-100}}@-moz-keyframes prism-previewer-time{0%{stroke-dasharray:0,500;stroke-dashoffset:0}50%{stroke-dasharray:100,500;stroke-dashoffset:0}100%{stroke-dasharray:0,500;stroke-dashoffset:-100}}@keyframes prism-previewer-time{0%{stroke-dasharray:0,500;stroke-dashoffset:0}50%{stroke-dasharray:100,500;stroke-dashoffset:0}100%{stroke-dasharray:0,500;stroke-dashoffset:-100}}.prism-previewer-time:before{border-radius:50%;background:#fff}.prism-previewer-time:after{margin-top:4px}.prism-previewer-time svg{width:32px;height:32px;-webkit-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.prism-previewer-time circle{fill:transparent;stroke:#2d3438;stroke-opacity:.9;stroke-width:32;stroke-dasharray:0,500;stroke-dashoffset:0;-webkit-animation:prism-previewer-time linear infinite 3s;-moz-animation:prism-previewer-time linear infinite 3s;-o-animation:prism-previewer-time linear infinite 3s;animation:prism-previewer-time linear infinite 3s}
|
||||
[class*=lang-] script[type='text/plain'],[class*=language-] script[type='text/plain'],script[type='text/plain'][class*=lang-],script[type='text/plain'][class*=language-]{display:block;font:100% Consolas,Monaco,monospace;white-space:pre;overflow:auto}
|
||||
.token.punctuation.brace-hover,.token.punctuation.brace-selected{outline:solid 1px}.rainbow-braces .token.punctuation.brace-level-1,.rainbow-braces .token.punctuation.brace-level-5,.rainbow-braces .token.punctuation.brace-level-9{color:#e50;opacity:1}.rainbow-braces .token.punctuation.brace-level-10,.rainbow-braces .token.punctuation.brace-level-2,.rainbow-braces .token.punctuation.brace-level-6{color:#0b3;opacity:1}.rainbow-braces .token.punctuation.brace-level-11,.rainbow-braces .token.punctuation.brace-level-3,.rainbow-braces .token.punctuation.brace-level-7{color:#26f;opacity:1}.rainbow-braces .token.punctuation.brace-level-12,.rainbow-braces .token.punctuation.brace-level-4,.rainbow-braces .token.punctuation.brace-level-8{color:#e0e;opacity:1}
|
||||
|
||||
/*gruvbox light*/
|
||||
code[class*=language-],pre[class*=language-]{color:#3c3836;font-family:Consolas,Monaco,"Andale Mono",monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{color:#282828;background:#a89984}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{color:#282828;background:#a89984}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f9f5d7}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em}.token.attr-name,.token.attr-value,.token.attr-value .punctuation,.token.cdata,.token.comment,.token.operator,.token.prolog,.token.punctuation{color:#7c6f64}.token.atrule,.token.boolean,.token.constant,.token.delimiter,.token.important,.token.keyword,.token.property,.token.selector,.token.variable{color:#9d0006}.token.builtin,.token.doctype,.token.function,.token.tag,.token.tag .punctuation{color:#b57614}.token.entity,.token.number,.token.symbol{color:#8f3f71}.token.char,.token.string,.token.url{color:#797403}.token.url{text-decoration:underline}.token.regex{background:#797403}.token.bold{font-weight:700}.token.italic{font-style:italic}.token.inserted{background:#7c6f64}.token.deleted{background:#9d0006}
|
||||
Vendored
+1
-1
@@ -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/prism.css"><link rel="stylesheet" href="/blog/css/main.css"><script src="https://kit.fontawesome.com/ed3c25598e.js" crossorigin="anonymous"></script><script type="text/javascript" src="https://platform-api.sharethis.com/js/sharethis.js#property=6550cdc47a115e0012964576&product=sop" async="async"></script></head><body><nav><input type="checkbox" id="nav-check"><h1><a href="/" class="link">rohit pai</a></h1><div class="nav-btn"><label for="nav-check"><span></span> <span></span> <span></span></label></div><ul><li><a href="/#about" class="textShadow link">about</a></li><li><a href="/#curriculumVitae" class="textShadow link">cv</a></li><li><a href="/#projects" class="textShadow link">projects</a></li><li><a href="/#contact" class="textShadow link">contact</a></li><li><a href="/blog" class="textShadow link active">blog</a></li></ul></nav><header><div><h1>full stack developer</h1><a href="/#sayHello" class="btn btnPrimary boxShadowIn boxShadowOut">Contact Me</a> <a href="" id="arrow"><i class="fa-solid fa-chevron-down"></i></a></div></header><div class="menuBar"><div class="menu"><ul><li><a href="/blog" class="link active">All posts</a></li><li><a href="/blog/category" class="link">categories</a></li><li><label for="searchField" aria-hidden="true" hidden>search</label> <input type="search" name="search" id="searchField" placeholder="Search..."> <button type="submit" id="searchBtn" class="btn btnPrimary"><i class="fa fa-search"></i></button></li></ul></div></div><main id="main"></main><div class="modal-container" id="cookiePopup"><div class="modal"><div class="modal-content"><h2><i class="fas fa-cookie-bite"></i> Hey I use cookies btw</h2><p>Just to let you know, I use cookies to give you the best experience on my blog. By clicking agree I'll assume that you are happy with it. <a href="/blog/policy/cookie" class="link">Read more</a></p><div class="flexRow"><button class="btn btnPrimary" id="cookieAccept">agree</button></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/prism.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>
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Editor</title><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="stylesheet" href="css/main.css"><script src="https://kit.fontawesome.com/ed3c25598e.js" crossorigin="anonymous"></script></head><body><main class="login"><div id="login" class="container shown"><h1>Login To Editor</h1><form action="" method="POST"><div class="formControl"><label for="username">Username</label> <input type="text" id="username" name="username" required></div><div class="formControl passwordControl"><label for="password">Password</label> <input type="password" id="password" name="password" required> <i class="fa-solid fa-eye"></i></div><div class="error hidden" id="loginError"><button class="close" type="button">×</button><div></div></div><div class="btnContainer"><input type="submit" value="Submit" class="btn btnPrimary boxShadowIn boxShadowOut"> <a href="#" id="resetPwd">Reset Password</a></div></form></div><div id="resetPassword" class="container" style="display: none; transform: translateX(150vw)"><h1>Reset Password</h1><form action="#" method="POST"><div class="formControl"><label for="email">Email</label> <input type="email" id="email" name="email"></div><div class="error hidden" id="resetError"><button class="close" type="button">×</button><div></div></div><div class="btnContainer"><input type="submit" value="Submit" class="btn btnPrimary boxShadowIn boxShadowOut"> <a href="#" class="loginBtn btn btnPrimary boxShadowIn boxShadowOut" id="loginBtn">Login</a></div></form></div><div id="checkResetCode" class="container" style="display: none; transform: translateX(150vw)"><h1>Check Reset Code</h1><form action="#" method="POST"><div class="formControl"><label for="code">Code</label> <input type="text" id="code" name="code"></div><div class="error hidden" id="resetCodeError"><button class="close" type="button">×</button><div></div></div><div class="btnContainer"><input type="submit" value="Submit" class="btn btnPrimary boxShadowIn boxShadowOut"> <a href="#" class="loginBtn btn btnPrimary boxShadowIn boxShadowOut" id="resendEmail">Resend Email</a></div></form></div><div id="changePassword" class="container" style="display: none; transform: translateX(150vw)"><h1>Reset Password</h1><form action="" method="POST"><div class="formControl"><label for="pass">Password</label> <input type="password" name="pass" id="pass"> <i class="fa-solid fa-eye"></i></div><div class="formControl"><label for="rePass">Password</label> <input type="password" name="rePass" id="rePass"> <i class="fa-solid fa-eye"></i></div><div class="error hidden" id="changeError"><button class="close" type="button">×</button><div></div></div><div class="btnContainer"><input type="submit" value="Submit" class="btn btnPrimary boxShadowIn boxShadowOut"> <a href="#" class="loginBtn btn btnPrimary boxShadowIn boxShadowOut">Login</a></div></form></div></main><script src="js/index.js"></script></body></html>
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Editor</title><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="stylesheet" href="css/main.css"><script src="https://kit.fontawesome.com/ed3c25598e.js" crossorigin="anonymous"></script></head><body><main class="login"><div id="login" class="container shown"><h1>Login To Editor</h1><form action="" method="POST"><div class="formControl"><label for="username">Username</label> <input type="text" id="username" name="username" required></div><div class="formControl passwordControl"><label for="password">Password</label> <input type="password" id="password" name="password" required> <i class="fa-solid fa-eye"></i></div><div class="error hidden" id="loginError"><button class="close" type="button">×</button><div></div></div><div class="btnContainer"><input type="submit" value="Submit" class="btn btnPrimary boxShadowIn boxShadowOut"> <a href="/api/user/login" class="btn btnPrimary boxShadowIn boxShadowOut">Login with Jump Cloud</a> <button type="button" id="resetPwd" class="btn btnPrimary boxShadowIn boxShadowOut">Reset Password</button></div></form></div><div id="resetPassword" class="container" style="display: none; transform: translateX(150vw)"><h1>Reset Password</h1><form action="#" method="POST"><div class="formControl"><label for="email">Email</label> <input type="email" id="email" name="email"></div><div class="error hidden" id="resetError"><button class="close" type="button">×</button><div></div></div><div class="btnContainer"><input type="submit" value="Submit" class="btn btnPrimary boxShadowIn boxShadowOut"> <button type="button" class="loginBtn btn btnPrimary boxShadowIn boxShadowOut" id="loginBtn">Login</button></div></form></div><div id="checkResetCode" class="container" style="display: none; transform: translateX(150vw)"><h1>Check Reset Code</h1><form action="#" method="POST"><div class="formControl"><label for="code">Code</label> <input type="text" id="code" name="code"></div><div class="error hidden" id="resetCodeError"><button class="close" type="button">×</button><div></div></div><div class="btnContainer"><input type="submit" value="Submit" class="btn btnPrimary boxShadowIn boxShadowOut"> <button type="button" class="loginBtn btn btnPrimary boxShadowIn boxShadowOut" id="resendEmail">Resend Email</button></div></form></div><div id="changePassword" class="container" style="display: none; transform: translateX(150vw)"><h1>Reset Password</h1><form action="" method="POST"><div class="formControl"><label for="pass">Password</label> <input type="password" name="pass" id="pass"> <i class="fa-solid fa-eye"></i></div><div class="formControl"><label for="rePass">Password</label> <input type="password" name="rePass" id="rePass"> <i class="fa-solid fa-eye"></i></div><div class="error hidden" id="changeError"><button class="close" type="button">×</button><div></div></div><div class="btnContainer"><input type="submit" value="Submit" class="btn btnPrimary boxShadowIn boxShadowOut"> <button type="button" class="loginBtn btn btnPrimary boxShadowIn boxShadowOut">Login</button></div></form></div></main><script src="js/index.js"></script></body></html>
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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")}));
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Generated
+1866
-920
File diff suppressed because it is too large
Load Diff
+885
-14
@@ -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,14 +293,22 @@ class blogData
|
||||
|
||||
$keywords = implode(", ", RakePlus::create($bodyText)->keywords());
|
||||
|
||||
$latest = $this->getLatestBlogPost();
|
||||
$prevTitle = $latest["title"];
|
||||
$prevAbstract = $latest["abstract"];
|
||||
$prevHeaderImage = substr($latest["headerImg"], 10);
|
||||
$prevHeaderImage = str_ireplace("%2F", "/", $prevHeaderImage);
|
||||
|
||||
$headerImage = rawurlencode("../" . $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);
|
||||
$stmt->bindParam(":dateCreated", $dateCreated);
|
||||
$stmt->bindParam(":dateModified", $dateCreated);
|
||||
$isFeatured = $featured ? 1 : 0;
|
||||
$stmt->bindParam(":featured", $isFeatured);
|
||||
$stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
|
||||
// $isFeatured = $featured ? 1 : 0;
|
||||
$stmt->bindParam(":featured", $featured);
|
||||
$stmt->bindParam(":headerImg", $headerImage);
|
||||
$stmt->bindParam(":abstract", $abstract);
|
||||
$stmt->bindParam(":body", $newBody);
|
||||
$stmt->bindParam(":bodyText", $bodyText);
|
||||
@@ -304,12 +316,274 @@ 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);
|
||||
$headerImage = substr($headerImage, 10);
|
||||
$headerImage = str_ireplace("%2F", "/", $headerImage);
|
||||
|
||||
$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="https://rohitpai.co.uk/$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="https://rohitpai.co.uk/$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>
|
||||
|
||||
EOD;
|
||||
|
||||
foreach ($emails as $email)
|
||||
{
|
||||
$emailFooter = <<<EOD
|
||||
<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/$email"><Unsubscribe></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="date">2023</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
EOD;
|
||||
$emailBody .= $emailFooter;
|
||||
|
||||
$this->sendMail($email["email"], $emailBody, "Hey, Rohit's blog has a new post!");
|
||||
}
|
||||
|
||||
return intval($conn->lastInsertId());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,7 +650,8 @@ class blogData
|
||||
unlink($result["headerImg"]);
|
||||
$stmt = $conn->prepare("UPDATE blog SET headerImg = :headerImg WHERE ID = :ID;");
|
||||
$stmt->bindParam(":ID", $ID);
|
||||
$stmt->bindParam(":headerImg", $targetFile["imgLocation"]);
|
||||
$location = urldecode("../" . $targetFile["imgLocation"]);
|
||||
$stmt->bindParam(":headerImg", $location);
|
||||
$stmt->execute();
|
||||
if ($stmt->rowCount() > 0)
|
||||
{
|
||||
@@ -399,7 +674,11 @@ class blogData
|
||||
public function changeHTMLSrc(string $body, string $to, string $from): string
|
||||
{
|
||||
$htmlDoc = new DOMDocument();
|
||||
$htmlDoc->loadHTML($body, LIBXML_NOERROR);
|
||||
|
||||
// Load the raw HTML content into DOMDocument
|
||||
@$htmlDoc->loadHTML($body, LIBXML_NOERROR);
|
||||
|
||||
// Get the body and process images
|
||||
$doc = $htmlDoc->getElementsByTagName('body')->item(0);
|
||||
$imgs = $doc->getElementsByTagName('img');
|
||||
|
||||
@@ -412,9 +691,11 @@ class blogData
|
||||
$srcList[] = $src;
|
||||
$fileName = basename($src);
|
||||
|
||||
// Update the src attribute to the new location
|
||||
$img->setAttribute("src", substr($to, 2) . $fileName);
|
||||
}
|
||||
|
||||
// Rename files and clean up old ones
|
||||
$files = scandir($from);
|
||||
foreach ($files as $file)
|
||||
{
|
||||
@@ -430,14 +711,36 @@ class blogData
|
||||
}
|
||||
}
|
||||
|
||||
// Process the HTML content for output
|
||||
$newBody = '';
|
||||
foreach ($doc->childNodes as $node)
|
||||
{
|
||||
$newBody .= $htmlDoc->saveHTML($node);
|
||||
// Only convert text nodes to HTML entities
|
||||
if ($node->nodeType === XML_TEXT_NODE)
|
||||
{
|
||||
$newBody .= $this->convertToHtmlEntities($node->nodeValue); // Convert text nodes
|
||||
}
|
||||
else
|
||||
{
|
||||
$newBody .= $htmlDoc->saveHTML($node); // Keep HTML tags intact
|
||||
}
|
||||
}
|
||||
|
||||
return $newBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all characters in a string to HTML entities while leaving HTML tags intact.
|
||||
* @param string $text - The text to convert
|
||||
* @return string - The converted text with HTML entities
|
||||
*/
|
||||
private function convertToHtmlEntities(string $text): string
|
||||
{
|
||||
// Convert characters to HTML entities using mb_encode_numericentity
|
||||
return htmlentities($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all posts with the given category
|
||||
* @param string $category - Category of the post
|
||||
@@ -616,12 +919,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 +958,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/$email"><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,39 @@ 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("message" => "Woah, you're already trying to leave without signing up?")));
|
||||
return $response->withStatus(404);
|
||||
}
|
||||
|
||||
if ($message === "error")
|
||||
{
|
||||
// uh oh something went wrong
|
||||
$response->getBody()->write(json_encode(array("message" => "Error, something went wrong")));
|
||||
return $response->withStatus(500);
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode(array("message" => "Sorry to see you go! You'll no longer receive any emails from me. If you change your mind, you can always sign up again.")));
|
||||
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,13 +315,18 @@ class blogRoutes implements routesInterface
|
||||
return $response->withStatus(400);
|
||||
}
|
||||
|
||||
if (array_key_exists("headerImg", $files))
|
||||
{
|
||||
$headerImg = $files["headerImg"];
|
||||
}
|
||||
|
||||
if (empty($files["headerImg"]))
|
||||
{
|
||||
$headerImg = null;
|
||||
}
|
||||
|
||||
$featured = $data["featured"] === "true";
|
||||
$insertedID = $this->blogData->createPost($data["title"], $data["abstract"], $data["body"], $data["bodyText"], $data["dateCreated"], $featured, $data["categories"], $headerImg);
|
||||
// $featured = $data["featured"] === "true";
|
||||
$insertedID = $this->blogData->createPost($data["title"], $data["abstract"], $data["body"], $data["bodyText"], $data["dateCreated"], intval($data["featured"]), $data["categories"], $headerImg);
|
||||
if (!is_int($insertedID))
|
||||
{
|
||||
// uh oh something went wrong
|
||||
@@ -339,19 +371,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,9 @@ namespace api\user;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use PDO;
|
||||
use function api\utils\dbConn;
|
||||
use function api\utils\getSAMLSettings;
|
||||
use function api\utils\getSecretKey;
|
||||
|
||||
require_once __DIR__ . "/../utils/config.php";
|
||||
|
||||
@@ -136,5 +139,38 @@ class userData
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SAML settings
|
||||
* @return array - SAML settings
|
||||
*/
|
||||
public function getSamlConf(): array
|
||||
{
|
||||
return getSAMLSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the SAML user exists
|
||||
* @param string $username - Username
|
||||
* @param string $email - Email
|
||||
* @return bool - True if the user exists, false if not
|
||||
*/
|
||||
public function checkSAMLUser(string $username, string $email): bool
|
||||
{
|
||||
$conn = dbConn();
|
||||
$stmt = $conn->prepare("SELECT * FROM users WHERE username = :username AND email = :email");
|
||||
$stmt->bindParam(":username", $username);
|
||||
$stmt->bindParam(":email", $email);
|
||||
$stmt->execute();
|
||||
|
||||
// set the resulting array to associative
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($result)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+74
-25
@@ -5,6 +5,8 @@ require_once __DIR__ . "/../utils/routesInterface.php";
|
||||
require_once "userData.php";
|
||||
|
||||
use api\utils\routesInterface;
|
||||
use OneLogin\Saml2\Auth;
|
||||
use OneLogin\Saml2\Error;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\App;
|
||||
@@ -12,14 +14,17 @@ use Slim\App;
|
||||
class userRoutes implements routesInterface
|
||||
{
|
||||
private userData $user;
|
||||
private Auth $samlAuth;
|
||||
|
||||
/**
|
||||
* constructor used to instantiate a base user routes, to be used in the index.php file.
|
||||
* constructor used to instantiate base user routes, to be used in the index.php file.
|
||||
* @param App $app - the slim app used to create the routes
|
||||
* @throws Error
|
||||
*/
|
||||
public function __construct(App $app)
|
||||
{
|
||||
$this->user = new userData();
|
||||
$this->samlAuth = new Auth($this->user->getSamlConf());
|
||||
$this->createRoutes($app);
|
||||
}
|
||||
|
||||
@@ -30,31 +35,9 @@ class userRoutes implements routesInterface
|
||||
*/
|
||||
public function createRoutes(App $app): void
|
||||
{
|
||||
$app->post("/user/login", function (Request $request, Response $response)
|
||||
$app->get("/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);
|
||||
$this->samlAuth->login();
|
||||
});
|
||||
|
||||
$app->get("/user/logout", function (Request $request, Response $response)
|
||||
@@ -92,6 +75,20 @@ class userRoutes implements routesInterface
|
||||
|
||||
});
|
||||
|
||||
$app->get("/user/metadata", function (Request $request, Response $response)
|
||||
{
|
||||
$settings = $this->samlAuth->getSettings();
|
||||
$metadata = $settings->getSPMetadata();
|
||||
$errors = $settings->validateMetadata($metadata);
|
||||
if (empty($errors))
|
||||
{
|
||||
$response->getBody()->write($metadata);
|
||||
return $response->withHeader("Content-Type", "text/xml");
|
||||
}
|
||||
$response->getBody()->write(json_encode(array("error" => $errors)));
|
||||
return $response->withStatus(500);
|
||||
});
|
||||
|
||||
$app->get("/user/checkResetEmail/{email}", function (Request $request, Response $response, array $args)
|
||||
{
|
||||
if (empty($args["email"]))
|
||||
@@ -139,6 +136,58 @@ class userRoutes implements routesInterface
|
||||
return $response->withStatus(401);
|
||||
});
|
||||
|
||||
$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->post("/user/acs", function (Request $request, Response $response)
|
||||
{
|
||||
$this->samlAuth->processResponse();
|
||||
|
||||
$attributes = $this->samlAuth->getAttributes();
|
||||
$username = $attributes["username"][0];
|
||||
$email = $attributes["email"][0];
|
||||
|
||||
if ($this->user->checkSAMLUser($username, $email))
|
||||
{
|
||||
// yay, user is logged in
|
||||
$_SESSION["token"] = $this->user->createToken($username);
|
||||
$_SESSION["username"] = $username;
|
||||
$_SESSION["email"] = $email;
|
||||
|
||||
$inactive = 60 * 60 * 48; // 2 days
|
||||
$_SESSION["timeout"] = time() + $inactive;
|
||||
|
||||
return $response->withHeader("Location", "https://rohitpai.co.uk/editor/")->withStatus(302);
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode(array("error" => "Unauthorised")));
|
||||
return $response->withStatus(401);
|
||||
});
|
||||
|
||||
$app->post("/user/changePassword", function (Request $request, Response $response)
|
||||
{
|
||||
if (empty($_SESSION["resetToken"]) && empty($_SESSION["resetEmail"]))
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -79,11 +79,16 @@ article a {
|
||||
|
||||
article a::before,
|
||||
article a::after {
|
||||
visibility: hidden;
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
article a.btn::before,
|
||||
article a.btn::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
article a::before {
|
||||
content: ' <';
|
||||
margin-left: -0.5em;
|
||||
@@ -93,9 +98,9 @@ article a::after {
|
||||
content: '>';
|
||||
}
|
||||
|
||||
article a:hover::before,
|
||||
article a:hover::after {
|
||||
visibility: visible;
|
||||
article a:hover,
|
||||
article a:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
article h1 {
|
||||
@@ -106,6 +111,31 @@ article h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
article h3:not(div.byLine > h3), .otherPosts h3 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
article .media {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
article table td, article table th {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
article table tr:nth-child(even) {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
article table tr:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
article .table {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
aside.sideContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -151,7 +181,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;
|
||||
}
|
||||
@@ -186,6 +216,10 @@ div.categories {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.form input[type="submit"] {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.image img, .image_resized img {
|
||||
max-width: 100%;
|
||||
-webkit-border-radius: 10px;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+106
-3
@@ -108,14 +108,117 @@ section.largePost .outerContent .postContent a {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
#main .error {
|
||||
#main .errorFof, #main .unsubscribe {
|
||||
display: table;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fof {
|
||||
#main .unsubscribe {
|
||||
height: 50dvh;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
section#olderPosts {
|
||||
/*max-width: 90%;*/
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
section#olderPosts .carousel {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
section#olderPosts .arrow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
display: flex;
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
font-size: 1.625em;
|
||||
color: white;
|
||||
background: var(--mutedBlack);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
section#olderPosts .arrow:hover {
|
||||
background: var(--grey);
|
||||
}
|
||||
|
||||
section#olderPosts #prev {
|
||||
left: -4em;
|
||||
}
|
||||
|
||||
section#olderPosts #next {
|
||||
right: -4em;
|
||||
}
|
||||
|
||||
section#olderPosts .carouselOuter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
section#olderPosts #allCarouselItems {
|
||||
display: none;
|
||||
}
|
||||
|
||||
section#olderPosts #carouselInner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
gap: 1em;
|
||||
transition: transform 0.5s ease-in-out;
|
||||
left: 0;
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
section#olderPosts #carouselInner .cardItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 2px solid var(--primaryDefault);
|
||||
-webkit-border-radius: 0.625rem;
|
||||
-moz-border-radius: 0.625rem;
|
||||
border-radius: 0.625rem;
|
||||
width: 30em;
|
||||
height: 40rem;
|
||||
}
|
||||
|
||||
section#olderPosts #carouselInner .cardItem img {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
object-fit: cover;
|
||||
object-position: left;
|
||||
-webkit-border-radius: 0.625rem;
|
||||
-moz-border-radius: 0.625rem;
|
||||
border-radius: 0.625rem;
|
||||
color: #FFFFFF;
|
||||
|
||||
}
|
||||
|
||||
section#olderPosts #carouselInner .cardItem .content {
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
background: #FFFFFF;
|
||||
margin: 0 2em 1.5em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
+125
-1
@@ -8,8 +8,13 @@
|
||||
@import "blogPosts.css";
|
||||
@import "home.css";
|
||||
@import "category.css";
|
||||
@import "prism.css";
|
||||
|
||||
div.menuBar a.link::before, main a.link::before,
|
||||
div.menuBar a.link::after, main a.link::after {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
/* Modal Styling */
|
||||
.policy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -23,10 +28,62 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.4); /* Background color for the entire screen */
|
||||
box-sizing: border-box; /* Include padding and border in the element's total width and height */
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
.modal-container.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 40%;
|
||||
height: auto;
|
||||
max-height: 90dvh;
|
||||
overflow: auto;
|
||||
margin: 1.25em;
|
||||
padding: 1.25em;
|
||||
box-sizing: border-box; /* Include padding and border in the element's total width and height */
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #DDDDDD;
|
||||
margin: auto;
|
||||
padding: 20px;
|
||||
border: 5px solid var(--primaryHover);
|
||||
width: 100%;
|
||||
box-shadow: 0 6px 4px 0 var(--mutedBlack);
|
||||
}
|
||||
|
||||
.modal-content .flexRow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: 1em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/**** Media Queries *****/
|
||||
|
||||
@media screen and (max-width: 90em) {
|
||||
|
||||
|
||||
section#olderPosts #carouselInner .cardItem {
|
||||
width: 25em;
|
||||
}
|
||||
|
||||
/***** Individual Blog Posts ***/
|
||||
|
||||
div.mainContent {
|
||||
@@ -52,6 +109,12 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*** Cookie popup ***/
|
||||
.modal-content .flexRow {
|
||||
width: 50%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/*** Large Post for Home and Category ***/
|
||||
section.largePost {
|
||||
padding: 1em;
|
||||
@@ -72,6 +135,25 @@
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
section#olderPosts .arrow {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
|
||||
section#olderPosts #prev {
|
||||
left: -3em;
|
||||
}
|
||||
|
||||
section#olderPosts #next {
|
||||
right: -3em;
|
||||
}
|
||||
|
||||
section#olderPosts #carouselInner .cardItem {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
/***** Individual Blog Posts ***/
|
||||
section#individualPost {
|
||||
flex-direction: column-reverse;
|
||||
@@ -150,11 +232,41 @@
|
||||
|
||||
@media screen and (max-width: 55em) {
|
||||
|
||||
/*** Cookie Popup ***/
|
||||
.modal {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/*** Large Post for Home and Category ***/
|
||||
.banner {
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
section#olderPosts .arrow {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
section#olderPosts #prev {
|
||||
left: -1.75em;
|
||||
}
|
||||
|
||||
section#olderPosts #next {
|
||||
right: -1.75em;
|
||||
}
|
||||
|
||||
section#olderPosts #carouselInner .cardItem {
|
||||
width: 15em;
|
||||
}
|
||||
|
||||
/***** Individual Blog Posts ***/
|
||||
|
||||
aside.sideContent > div.authorInfo {
|
||||
@@ -168,6 +280,18 @@
|
||||
|
||||
}
|
||||
|
||||
@media screen and (max-height: 38em) and (orientation: landscape) {
|
||||
/*** cookie popup ***/
|
||||
.modal {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.modal-content .flexRow {
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 30em) {
|
||||
|
||||
/***** Individual Blog Posts ***/
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
/* PrismJS 1.29.0
|
||||
https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clike+javascript+abap+abnf+actionscript+ada+agda+al+antlr4+apacheconf+apex+apl+applescript+aql+arduino+arff+armasm+arturo+asciidoc+aspnet+asm6502+asmatmel+autohotkey+autoit+avisynth+avro-idl+awk+bash+basic+batch+bbcode+bbj+bicep+birb+bison+bnf+bqn+brainfuck+brightscript+bro+bsl+c+csharp+cpp+cfscript+chaiscript+cil+cilkc+cilkcpp+clojure+cmake+cobol+coffeescript+concurnas+csp+cooklang+coq+crystal+css-extras+csv+cue+cypher+d+dart+dataweave+dax+dhall+diff+django+dns-zone-file+docker+dot+ebnf+editorconfig+eiffel+ejs+elixir+elm+etlua+erb+erlang+excel-formula+fsharp+factor+false+firestore-security-rules+flow+fortran+ftl+gml+gap+gcode+gdscript+gedcom+gettext+gherkin+git+glsl+gn+linker-script+go+go-module+gradle+graphql+groovy+haml+handlebars+haskell+haxe+hcl+hlsl+hoon+http+hpkp+hsts+ichigojam+icon+icu-message-format+idris+ignore+inform7+ini+io+j+java+javadoc+javadoclike+javastacktrace+jexl+jolie+jq+jsdoc+js-extras+json+json5+jsonp+jsstacktrace+js-templates+julia+keepalived+keyman+kotlin+kumir+kusto+latex+latte+less+lilypond+liquid+lisp+livescript+llvm+log+lolcode+lua+magma+makefile+markdown+markup-templating+mata+matlab+maxscript+mel+mermaid+metafont+mizar+mongodb+monkey+moonscript+n1ql+n4js+nand2tetris-hdl+naniscript+nasm+neon+nevod+nginx+nim+nix+nsis+objectivec+ocaml+odin+opencl+openqasm+oz+parigp+parser+pascal+pascaligo+psl+pcaxis+peoplecode+perl+php+phpdoc+php-extras+plant-uml+plsql+powerquery+powershell+processing+prolog+promql+properties+protobuf+pug+puppet+pure+purebasic+purescript+python+qsharp+q+qml+qore+r+racket+cshtml+jsx+tsx+reason+regex+rego+renpy+rescript+rest+rip+roboconf+robotframework+ruby+rust+sas+sass+scss+scala+scheme+shell-session+smali+smalltalk+smarty+sml+solidity+solution-file+soy+sparql+splunk-spl+sqf+sql+squirrel+stan+stata+iecst+stylus+supercollider+swift+systemd+t4-templating+t4-cs+t4-vb+tap+tcl+tt2+textile+toml+tremor+turtle+twig+typescript+typoscript+unrealscript+uorazor+uri+v+vala+vbnet+velocity+verilog+vhdl+vim+visual-basic+warpscript+wasm+web-idl+wgsl+wiki+wolfram+wren+xeora+xml-doc+xojo+xquery+yaml+yang+zig&plugins=line-numbers+autolinker+show-language+previewers+toolbar+match-braces+diff-highlight */
|
||||
code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
|
||||
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+abap+abnf+actionscript+ada+agda+al+antlr4+apacheconf+apex+apl+applescript+aql+arduino+arff+armasm+arturo+asciidoc+aspnet+asm6502+asmatmel+autohotkey+autoit+avisynth+avro-idl+awk+bash+basic+batch+bbcode+bbj+bicep+birb+bison+bnf+bqn+brainfuck+brightscript+bro+bsl+c+csharp+cpp+cfscript+chaiscript+cil+cilkc+cilkcpp+clojure+cmake+cobol+coffeescript+concurnas+csp+cooklang+coq+crystal+css-extras+csv+cue+cypher+d+dart+dataweave+dax+dhall+diff+django+dns-zone-file+docker+dot+ebnf+editorconfig+eiffel+ejs+elixir+elm+etlua+erb+erlang+excel-formula+fsharp+factor+false+firestore-security-rules+flow+fortran+ftl+gml+gap+gcode+gdscript+gedcom+gettext+gherkin+git+glsl+gn+linker-script+go+go-module+gradle+graphql+groovy+haml+handlebars+haskell+haxe+hcl+hlsl+hoon+http+hpkp+hsts+ichigojam+icon+icu-message-format+idris+ignore+inform7+ini+io+j+java+javadoc+javadoclike+javastacktrace+jexl+jolie+jq+jsdoc+js-extras+json+json5+jsonp+jsstacktrace+js-templates+julia+keepalived+keyman+kotlin+kumir+kusto+latex+latte+less+lilypond+liquid+lisp+livescript+llvm+log+lolcode+lua+magma+makefile+markdown+markup-templating+mata+matlab+maxscript+mel+mermaid+metafont+mizar+mongodb+monkey+moonscript+n1ql+n4js+nand2tetris-hdl+naniscript+nasm+neon+nevod+nginx+nim+nix+nsis+objectivec+ocaml+odin+opencl+openqasm+oz+parigp+parser+pascal+pascaligo+psl+pcaxis+peoplecode+perl+php+phpdoc+php-extras+plant-uml+plsql+powerquery+powershell+processing+prolog+promql+properties+protobuf+pug+puppet+pure+purebasic+purescript+python+qsharp+q+qml+qore+r+racket+cshtml+jsx+tsx+reason+regex+rego+renpy+rescript+rest+rip+roboconf+robotframework+ruby+rust+sas+sass+scss+scala+scheme+shell-session+smali+smalltalk+smarty+sml+solidity+solution-file+soy+sparql+splunk-spl+sqf+sql+squirrel+stan+stata+iecst+stylus+supercollider+swift+systemd+t4-templating+t4-cs+t4-vb+tap+tcl+tt2+textile+toml+tremor+turtle+twig+typescript+typoscript+unrealscript+uorazor+uri+v+vala+vbnet+velocity+verilog+vhdl+vim+visual-basic+warpscript+wasm+web-idl+wgsl+wiki+wolfram+wren+xeora+xml-doc+xojo+xquery+yaml+yang+zig&plugins=line-numbers+show-language+inline-color+previewers+unescaped-markup+toolbar+copy-to-clipboard+download-button+match-braces */
|
||||
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
|
||||
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
|
||||
.token a{color:inherit}
|
||||
div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}
|
||||
span.inline-color-wrapper{background:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyIDIiPjxwYXRoIGZpbGw9ImdyYXkiIGQ9Ik0wIDBoMnYySDB6Ii8+PHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0wIDBoMXYxSDB6TTEgMWgxdjFIMXoiLz48L3N2Zz4=);background-position:center;background-size:110%;display:inline-block;height:1.333ch;width:1.333ch;margin:0 .333ch;box-sizing:border-box;border:1px solid #fff;outline:1px solid rgba(0,0,0,.5);overflow:hidden}span.inline-color{display:block;height:120%;width:120%}
|
||||
.prism-previewer,.prism-previewer:after,.prism-previewer:before{position:absolute;pointer-events:none}.prism-previewer,.prism-previewer:after{left:50%}.prism-previewer{margin-top:-48px;width:32px;height:32px;margin-left:-16px;z-index:10;opacity:0;-webkit-transition:opacity .25s;-o-transition:opacity .25s;transition:opacity .25s}.prism-previewer.flipped{margin-top:0;margin-bottom:-48px}.prism-previewer:after,.prism-previewer:before{content:'';position:absolute;pointer-events:none}.prism-previewer:before{top:-5px;right:-5px;left:-5px;bottom:-5px;border-radius:10px;border:5px solid #fff;box-shadow:0 0 3px rgba(0,0,0,.5) inset,0 0 10px rgba(0,0,0,.75)}.prism-previewer:after{top:100%;width:0;height:0;margin:5px 0 0 -7px;border:7px solid transparent;border-color:rgba(255,0,0,0);border-top-color:#fff}.prism-previewer.flipped:after{top:auto;bottom:100%;margin-top:0;margin-bottom:5px;border-top-color:rgba(255,0,0,0);border-bottom-color:#fff}.prism-previewer.active{opacity:1}.prism-previewer-angle:before{border-radius:50%;background:#fff}.prism-previewer-angle:after{margin-top:4px}.prism-previewer-angle svg{width:32px;height:32px;-webkit-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.prism-previewer-angle[data-negative] svg{-webkit-transform:scaleX(-1) rotate(-90deg);-moz-transform:scaleX(-1) rotate(-90deg);-ms-transform:scaleX(-1) rotate(-90deg);-o-transform:scaleX(-1) rotate(-90deg);transform:scaleX(-1) rotate(-90deg)}.prism-previewer-angle circle{fill:transparent;stroke:#2d3438;stroke-opacity:.9;stroke-width:32;stroke-dasharray:0,500}.prism-previewer-gradient{background-image:linear-gradient(45deg,#bbb 25%,transparent 25%,transparent 75%,#bbb 75%,#bbb),linear-gradient(45deg,#bbb 25%,#eee 25%,#eee 75%,#bbb 75%,#bbb);background-size:10px 10px;background-position:0 0,5px 5px;width:64px;margin-left:-32px}.prism-previewer-gradient:before{content:none}.prism-previewer-gradient div{position:absolute;top:-5px;left:-5px;right:-5px;bottom:-5px;border-radius:10px;border:5px solid #fff;box-shadow:0 0 3px rgba(0,0,0,.5) inset,0 0 10px rgba(0,0,0,.75)}.prism-previewer-color{background-image:linear-gradient(45deg,#bbb 25%,transparent 25%,transparent 75%,#bbb 75%,#bbb),linear-gradient(45deg,#bbb 25%,#eee 25%,#eee 75%,#bbb 75%,#bbb);background-size:10px 10px;background-position:0 0,5px 5px}.prism-previewer-color:before{background-color:inherit;background-clip:padding-box}.prism-previewer-easing{margin-top:-76px;margin-left:-30px;width:60px;height:60px;background:#333}.prism-previewer-easing.flipped{margin-bottom:-116px}.prism-previewer-easing svg{width:60px;height:60px}.prism-previewer-easing circle{fill:#2d3438;stroke:#fff}.prism-previewer-easing path{fill:none;stroke:#fff;stroke-linecap:round;stroke-width:4}.prism-previewer-easing line{stroke:#fff;stroke-opacity:.5;stroke-width:2}@-webkit-keyframes prism-previewer-time{0%{stroke-dasharray:0,500;stroke-dashoffset:0}50%{stroke-dasharray:100,500;stroke-dashoffset:0}100%{stroke-dasharray:0,500;stroke-dashoffset:-100}}@-o-keyframes prism-previewer-time{0%{stroke-dasharray:0,500;stroke-dashoffset:0}50%{stroke-dasharray:100,500;stroke-dashoffset:0}100%{stroke-dasharray:0,500;stroke-dashoffset:-100}}@-moz-keyframes prism-previewer-time{0%{stroke-dasharray:0,500;stroke-dashoffset:0}50%{stroke-dasharray:100,500;stroke-dashoffset:0}100%{stroke-dasharray:0,500;stroke-dashoffset:-100}}@keyframes prism-previewer-time{0%{stroke-dasharray:0,500;stroke-dashoffset:0}50%{stroke-dasharray:100,500;stroke-dashoffset:0}100%{stroke-dasharray:0,500;stroke-dashoffset:-100}}.prism-previewer-time:before{border-radius:50%;background:#fff}.prism-previewer-time:after{margin-top:4px}.prism-previewer-time svg{width:32px;height:32px;-webkit-transform:rotate(-90deg);-moz-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.prism-previewer-time circle{fill:transparent;stroke:#2d3438;stroke-opacity:.9;stroke-width:32;stroke-dasharray:0,500;stroke-dashoffset:0;-webkit-animation:prism-previewer-time linear infinite 3s;-moz-animation:prism-previewer-time linear infinite 3s;-o-animation:prism-previewer-time linear infinite 3s;animation:prism-previewer-time linear infinite 3s}
|
||||
[class*=lang-] script[type='text/plain'],[class*=language-] script[type='text/plain'],script[type='text/plain'][class*=lang-],script[type='text/plain'][class*=language-]{display:block;font:100% Consolas,Monaco,monospace;white-space:pre;overflow:auto}
|
||||
.token.punctuation.brace-hover,.token.punctuation.brace-selected{outline:solid 1px}.rainbow-braces .token.punctuation.brace-level-1,.rainbow-braces .token.punctuation.brace-level-5,.rainbow-braces .token.punctuation.brace-level-9{color:#e50;opacity:1}.rainbow-braces .token.punctuation.brace-level-10,.rainbow-braces .token.punctuation.brace-level-2,.rainbow-braces .token.punctuation.brace-level-6{color:#0b3;opacity:1}.rainbow-braces .token.punctuation.brace-level-11,.rainbow-braces .token.punctuation.brace-level-3,.rainbow-braces .token.punctuation.brace-level-7{color:#26f;opacity:1}.rainbow-braces .token.punctuation.brace-level-12,.rainbow-braces .token.punctuation.brace-level-4,.rainbow-braces .token.punctuation.brace-level-8{color:#e0e;opacity:1}
|
||||
pre.diff-highlight>code .token.deleted:not(.prefix),pre>code.diff-highlight .token.deleted:not(.prefix){background-color:rgba(255,0,0,.1);color:inherit;display:block}pre.diff-highlight>code .token.inserted:not(.prefix),pre>code.diff-highlight .token.inserted:not(.prefix){background-color:rgba(0,255,128,.1);color:inherit;display:block}
|
||||
|
||||
/*gruvbox light*/
|
||||
code[class*=language-],pre[class*=language-]{color:#3c3836;font-family:Consolas,Monaco,"Andale Mono",monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{color:#282828;background:#a89984}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{color:#282828;background:#a89984}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f9f5d7}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em}.token.attr-name,.token.attr-value,.token.attr-value .punctuation,.token.cdata,.token.comment,.token.operator,.token.prolog,.token.punctuation{color:#7c6f64}.token.atrule,.token.boolean,.token.constant,.token.delimiter,.token.important,.token.keyword,.token.property,.token.selector,.token.variable{color:#9d0006}.token.builtin,.token.doctype,.token.function,.token.tag,.token.tag .punctuation{color:#b57614}.token.entity,.token.number,.token.symbol{color:#8f3f71}.token.char,.token.string,.token.url{color:#797403}.token.url{text-decoration:underline}.token.regex{background:#797403}.token.bold{font-weight:700}.token.italic{font-style:italic}.token.inserted{background:#7c6f64}.token.deleted{background:#9d0006}
|
||||
@@ -15,6 +15,7 @@
|
||||
<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/prism.css">
|
||||
<link rel="stylesheet" href="/blog/css/main.css">
|
||||
<script src="https://kit.fontawesome.com/ed3c25598e.js" crossorigin="anonymous"></script>
|
||||
<script type='text/javascript'
|
||||
@@ -72,6 +73,20 @@
|
||||
|
||||
</main>
|
||||
|
||||
<div class="modal-container" id="cookiePopup">
|
||||
<div class="modal">
|
||||
<div class="modal-content">
|
||||
<h2><i class="fas fa-cookie-bite"></i> Hey I use cookies btw</h2>
|
||||
<p>Just to let you know, I use cookies to give you the best experience on my blog. By clicking agree
|
||||
I'll assume that you are happy with it. <a href="/blog/policy/cookie" class="link">Read more</a>
|
||||
</p>
|
||||
<div class="flexRow">
|
||||
<button class="btn btnPrimary" id="cookieAccept">agree</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="flexRow">
|
||||
<div class="nav">
|
||||
<ul>
|
||||
@@ -86,6 +101,7 @@
|
||||
</footer>
|
||||
|
||||
<script src="/js/typewriter.js"></script>
|
||||
<script src="/blog/js/prism.js"></script>
|
||||
<script src="/blog/js/index.js"></script>
|
||||
<script id="dsq-count-scr" src="https://rohitpaiportfolio.disqus.com/count.js" async></script>
|
||||
</body>
|
||||
|
||||
+308
-19
@@ -33,10 +33,14 @@ function goToURL(url)
|
||||
// Get the current URL and split it into an array
|
||||
let urlArray = url.split('/');
|
||||
|
||||
if (url === '/blog/' || url === '/blog')
|
||||
if (localStorage.getItem('cookiePopup') === 'accepted')
|
||||
{
|
||||
document.querySelector('#cookiePopup').classList.add('hidden');
|
||||
}
|
||||
|
||||
if (url === '/blog' || url === 'blog' || url === '/blog/')
|
||||
{
|
||||
loadHomeContent();
|
||||
// window.history.pushState(null, null, url);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -83,8 +87,16 @@ function goToURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
show404();
|
||||
if (urlArray[2] === 'unsubscribe')
|
||||
{
|
||||
if (urlArray[3])
|
||||
{
|
||||
unsubscribe(urlArray[urlArray.length - 1]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
show404();
|
||||
}
|
||||
|
||||
document.querySelector('#searchBtn').addEventListener('click', _ =>
|
||||
@@ -107,6 +119,77 @@ 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');
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* unsubscribe by email
|
||||
* @param email the email to unsubscribe
|
||||
*/
|
||||
function unsubscribe(email)
|
||||
{
|
||||
fetch(`/api/blog/newsletter/${email}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(res => res.json().then(json =>
|
||||
{
|
||||
document.querySelector('#main').innerHTML = '';
|
||||
let post = document.createElement('section');
|
||||
post.classList.add('unsubscribe');
|
||||
post.id = 'unsubscribe';
|
||||
let mainContent = document.createElement('div');
|
||||
mainContent.classList.add('centered');
|
||||
mainContent.innerHTML = `
|
||||
<h1>Unsubscribe</h1>
|
||||
<p>${json.message}</p>
|
||||
<a href="/blog/" class="btn btnPrimary">See all blog posts</a>
|
||||
`;
|
||||
post.appendChild(mainContent);
|
||||
document.querySelector('#main').appendChild(post);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted date
|
||||
* @param {string} dateString - the date string
|
||||
@@ -129,7 +212,7 @@ function createLargePost(post)
|
||||
outerContent.classList.add('outerContent');
|
||||
let img = document.createElement('img');
|
||||
img.className = 'banner';
|
||||
img.src = post.headerImg;
|
||||
img.src = post.headerImg.replaceAll('%2F', '/');
|
||||
img.alt = post.title;
|
||||
outerContent.appendChild(img);
|
||||
let content = document.createElement('div');
|
||||
@@ -163,6 +246,32 @@ function createLargePost(post)
|
||||
return outerContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a card post element
|
||||
* @param post the object
|
||||
* @returns {HTMLDivElement} the outer content of the post
|
||||
*/
|
||||
function createCardPost(post)
|
||||
{
|
||||
let cardItem = document.createElement('div');
|
||||
cardItem.classList.add('cardItem');
|
||||
cardItem.id = 'post' + post.ID;
|
||||
let img = document.createElement('img');
|
||||
img.className = 'cardImg';
|
||||
img.src = post.headerImg.replaceAll('%2F', '/');
|
||||
img.alt = post.title;
|
||||
cardItem.appendChild(img);
|
||||
let content = document.createElement('div');
|
||||
content.classList.add('content');
|
||||
content.innerHTML = `
|
||||
<h2>${post.title}</h2>
|
||||
<h3>Last updated: ${createFormattedDate(post.dateModified)}</h3>
|
||||
<a href="/blog/post/${post.title}" class="btn btnPrimary">See Post</a>
|
||||
`;
|
||||
cardItem.appendChild(content);
|
||||
return cardItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the home content
|
||||
*/
|
||||
@@ -170,6 +279,30 @@ function loadHomeContent()
|
||||
{
|
||||
fetch('/api/blog/post').then(res => res.json().then(json =>
|
||||
{
|
||||
// older posts outside the carousel and loop
|
||||
let olderPosts = document.createElement('section');
|
||||
olderPosts.classList.add('largePost');
|
||||
olderPosts.id = 'olderPosts';
|
||||
let h1 = document.createElement('h1');
|
||||
h1.innerHTML = 'older posts';
|
||||
olderPosts.appendChild(h1);
|
||||
|
||||
let carousel = document.createElement('div');
|
||||
carousel.classList.add('carousel');
|
||||
carousel.innerHTML += `<div class="arrow" id="prev"><i class="fa-solid fa-chevron-left"></i></div>
|
||||
<div class="arrow" id="next"><i class="fa-solid fa-chevron-right"></i></div>
|
||||
`;
|
||||
let carouselOuter = document.createElement('div');
|
||||
let carouselInner = document.createElement('div');
|
||||
let allCarouselItems = document.createElement('div');
|
||||
carouselOuter.classList.add('carouselOuter');
|
||||
carouselInner.id = 'carouselInner';
|
||||
allCarouselItems.id = 'allCarouselItems';
|
||||
carouselOuter.appendChild(allCarouselItems);
|
||||
carouselOuter.appendChild(carouselInner);
|
||||
carousel.appendChild(carouselOuter);
|
||||
olderPosts.appendChild(carousel);
|
||||
|
||||
for (let i = 0; i < json.length; i++)
|
||||
{
|
||||
if (json[i].featured === 1)
|
||||
@@ -198,10 +331,150 @@ function loadHomeContent()
|
||||
document.querySelector('#main').appendChild(latestPost);
|
||||
}
|
||||
|
||||
if (i > 1)
|
||||
{
|
||||
allCarouselItems.appendChild(createCardPost(json[i]));
|
||||
}
|
||||
}
|
||||
document.querySelector('#main').appendChild(olderPosts);
|
||||
//carousel loop
|
||||
carouselLoop(carouselInner, allCarouselItems);
|
||||
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the loop for the carousel
|
||||
* @param {HTMLDivElement} carouselInner
|
||||
* @param {HTMLDivElement} allItems
|
||||
*/
|
||||
function carouselLoop(carouselInner, allItems)
|
||||
{
|
||||
const prev = document.querySelector('#prev');
|
||||
const next = document.querySelector('#next');
|
||||
const mediaBig = window.matchMedia('(max-width: 75em)');
|
||||
const mediaSmall = window.matchMedia('(max-width: 30em)');
|
||||
let cards = document.querySelectorAll('#allCarouselItems .cardItem');
|
||||
let visibleCardsCount = 3;
|
||||
|
||||
if (mediaBig.matches)
|
||||
{
|
||||
visibleCardsCount = 2; // only show 2 cards if on a slightly smaller screen e.g. tablet/laptop
|
||||
}
|
||||
|
||||
if (mediaSmall.matches)
|
||||
{
|
||||
visibleCardsCount = 1; // only show 1 card if on a mobile, although it'll only work on portrait
|
||||
}
|
||||
|
||||
let visibleCards = [];
|
||||
|
||||
// put the first n (3, 2, or 1) cards in the carousel
|
||||
for (let i = 0; i < visibleCardsCount; i++)
|
||||
{
|
||||
carouselInner.appendChild(cards[i]);
|
||||
visibleCards.push(cards[i]);
|
||||
}
|
||||
|
||||
if (allItems.children.length === 0)
|
||||
{
|
||||
// if there are no cards in the carousel, don't show the arrows
|
||||
prev.style.visibility = 'hidden';
|
||||
next.style.visibility = 'hidden';
|
||||
return;
|
||||
}
|
||||
|
||||
next.addEventListener('click', () =>
|
||||
{
|
||||
const firstCard = visibleCards.shift();
|
||||
firstCard.style.visibility = 'hidden'; // hide the first card
|
||||
|
||||
// add the next card to the end and off the screen
|
||||
const nextCard = allItems.querySelector('.cardItem');
|
||||
nextCard.style.transform = 'translateX(100%)';
|
||||
nextCard.style.transition = 'none';
|
||||
carouselInner.appendChild(nextCard);
|
||||
visibleCards.push(nextCard);
|
||||
|
||||
// transition all the cards to the left after 50ms
|
||||
// i.e. after the next card is added hence the setTimeout
|
||||
setTimeout(() =>
|
||||
{
|
||||
// move all the cards to the left
|
||||
for (let i = 0; i < carouselInner.children.length; i++)
|
||||
{
|
||||
carouselInner.children[i].style.transition = 'transform 0.5s ease-out';
|
||||
carouselInner.children[i].style.transform = 'translateX(-100%)';
|
||||
}
|
||||
}, 50);
|
||||
|
||||
// after 500ms move all cards back to the center and move
|
||||
// the first card to the hidden div
|
||||
setTimeout(() =>
|
||||
{
|
||||
// move the first card back to the center
|
||||
// instantly and move it to the hidden div
|
||||
firstCard.style.transition = 'none';
|
||||
firstCard.style.transform = 'translateX(0)';
|
||||
allItems.appendChild(firstCard);
|
||||
firstCard.style.visibility = 'visible'; // make it visible again
|
||||
|
||||
// for the remaining cards, reset them
|
||||
for (let i = 0; i < carouselInner.children.length; i++)
|
||||
{
|
||||
carouselInner.children[i].style.transition = 'none';
|
||||
carouselInner.children[i].style.transform = 'translateX(0%)';
|
||||
}
|
||||
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// everything is done in reverse
|
||||
prev.addEventListener('click', () =>
|
||||
{
|
||||
const lastCard = visibleCards.pop();
|
||||
lastCard.style.visibility = 'hidden'; // hide the last card
|
||||
|
||||
// add the previous card to the beginning and off the screen
|
||||
const prevCard = allItems.querySelector('.cardItem:last-child');
|
||||
prevCard.style.transform = 'translateX(-100%)';
|
||||
prevCard.style.transition = 'none';
|
||||
carouselInner.insertBefore(prevCard, carouselInner.firstChild);
|
||||
visibleCards.unshift(prevCard);
|
||||
|
||||
// transition all the cards to the right after 50ms
|
||||
// i.e. after the previous card is added hence the setTimeout
|
||||
setTimeout(() =>
|
||||
{
|
||||
// move all the cards to the right
|
||||
for (let i = 0; i < carouselInner.children.length; i++)
|
||||
{
|
||||
carouselInner.children[i].style.transition = 'transform 0.5s ease-out';
|
||||
carouselInner.children[i].style.transform = 'translateX(100%)';
|
||||
}
|
||||
}, 50);
|
||||
|
||||
// after 500ms move all cards back to the center and move
|
||||
// the last card to the hidden div
|
||||
setTimeout(() =>
|
||||
{
|
||||
// move the last card back to the center
|
||||
// instantly and move it to the hidden div
|
||||
lastCard.style.transition = 'none';
|
||||
lastCard.style.transform = 'translateX(0)';
|
||||
allItems.insertBefore(lastCard, allItems.firstChild);
|
||||
lastCard.style.visibility = 'visible';
|
||||
|
||||
// for the remaining cards reset them
|
||||
for (let i = 0; i < carouselInner.children.length; i++)
|
||||
{
|
||||
carouselInner.children[i].style.transition = 'none';
|
||||
carouselInner.children[i].style.transform = 'translateX(0%)';
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the latest and featured posts
|
||||
* @returns {Promise<any[]>} the latest and featured posts
|
||||
@@ -358,21 +631,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 +662,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}"]`);
|
||||
@@ -407,7 +691,7 @@ function createMetaTag(nameOrProperty, attribute, value)
|
||||
*/
|
||||
function insertMetaTags(json)
|
||||
{
|
||||
let metaDesc = document.querySelector('meta[name="description"]');
|
||||
let metaDesc = document.querySelector('meta[name=\'description\']');
|
||||
metaDesc.setAttribute('content', json.abstract);
|
||||
|
||||
// Twitter meta tags
|
||||
@@ -421,11 +705,11 @@ function insertMetaTags(json)
|
||||
createMetaTag('og:image', 'content', json.headerImg);
|
||||
createMetaTag('og:url', 'content', window.location.href);
|
||||
createMetaTag('og:type', 'content', 'blog');
|
||||
createMetaTag('og:site_name', 'content', 'Rohit Pai\'s Blog');
|
||||
createMetaTag('og:site_name', 'content', 'Rohit Pai"s Blog');
|
||||
createMetaTag('og:locale', 'content', 'en_GB');
|
||||
|
||||
//Keywords
|
||||
let metKeywords = document.querySelector('meta[name="keywords"]');
|
||||
let metKeywords = document.querySelector('meta[name=\'keywords\']');
|
||||
let keywords = metKeywords.getAttribute('content');
|
||||
keywords += `, ${json.keywords}`;
|
||||
metKeywords.setAttribute('content', keywords);
|
||||
@@ -458,6 +742,7 @@ async function loadIndividualPost(title)
|
||||
let mainContent = document.createElement('div');
|
||||
mainContent.classList.add('mainContent');
|
||||
let article = document.createElement('article');
|
||||
let headerImg = json.headerImg.replaceAll('%2F', '/');
|
||||
article.innerHTML = `
|
||||
<h1>${json.title}</h1>
|
||||
<div class="byLine">
|
||||
@@ -465,9 +750,9 @@ async function loadIndividualPost(title)
|
||||
<h3>${createButtonCategories([csvToArray(json.categories.replace(/\s*,\s*/g, ','))])}</h3>
|
||||
<div class="sharethis-inline-share-buttons" data-url="https://rohitpai.co.uk/blog/post/${title}"
|
||||
data-title="${json.title}" data-description="${json.abstract}"
|
||||
data-image="https://rohitpai.co.uk/${json.headerImg}" data-username="@rohitpai123"></div>
|
||||
data-image="https://rohitpai.co.uk/${headerImg}" data-username="@rohitpai123"></div>
|
||||
</div>
|
||||
<div class="cover" style="background-image: url('${json.headerImg}')"></div>
|
||||
<div class="cover" style="background-image: url('${headerImg}')"></div>
|
||||
${json.body}
|
||||
`;
|
||||
let comments = document.createElement('section');
|
||||
@@ -497,6 +782,7 @@ async function loadIndividualPost(title)
|
||||
s.setAttribute('data-timestamp', +new Date());
|
||||
d.body.appendChild(s);
|
||||
})();
|
||||
Prism.highlightAll();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -605,7 +891,7 @@ function loadPrivacyPolicy()
|
||||
<h2>Privacy Policy</h2>
|
||||
<p>Last Updated: Nov 12, 2023</p>
|
||||
<p>
|
||||
Thank you for visiting the Privacy Policy of Rohit Pai's Blog. This Privacy Policy explains how I, Rohit Pai, collect, use, and share information about you (“you”, “yours” or “user”) when you access or use my website (“Services”). You are responsible for any third-party data you provide or share through the Services and confirm that you have the third party's consent to provide such data to me.
|
||||
Thank you for visiting the Privacy Policy of Rohit Pai"s Blog. This Privacy Policy explains how I, Rohit Pai, collect, use, and share information about you (“you”, “yours” or “user”) when you access or use my website (“Services”). You are responsible for any third-party data you provid'e or share through the Services and confirm that you have the third partys consent to provide such data to me.
|
||||
</p>
|
||||
<br>
|
||||
|
||||
@@ -755,12 +1041,15 @@ function loadPrivacyPolicy()
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the cookie policy
|
||||
*/
|
||||
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,8 +1064,8 @@ function loadCookiePolicy()
|
||||
function show404()
|
||||
{
|
||||
document.querySelector('#main').innerHTML = `
|
||||
<div class="error">
|
||||
<div class="fof">
|
||||
<div class="errorFof">
|
||||
<div class="centered">
|
||||
<h1>Blog post, Category or page not found</h1>
|
||||
<a href="/blog/" class="btn btnPrimary">See all blog posts</a>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
+134
-42
@@ -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,41 @@ 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"] {
|
||||
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 +178,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 +219,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 +231,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 +240,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 +250,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;
|
||||
@@ -305,11 +329,18 @@ a.link {
|
||||
|
||||
a.link::before,
|
||||
a.link::after {
|
||||
visibility: hidden;
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
nav a.link::before,
|
||||
nav a.link::after,
|
||||
.nav a.link::before,
|
||||
.nav a.link::after {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
a.link::before {
|
||||
content: ' <';
|
||||
margin-left: -0.5em;
|
||||
@@ -319,15 +350,76 @@ a.link::after {
|
||||
content: '> ';
|
||||
}
|
||||
|
||||
a.link:hover::before,
|
||||
a.link:hover::after {
|
||||
a.link:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
nav a.link:hover::before,
|
||||
nav a.link:hover::after,
|
||||
.nav a.link:hover::before,
|
||||
.nav a.link:hover::after {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/*.link span {*/
|
||||
/* visibility: hidden;*/
|
||||
/*}*/
|
||||
nav a.link:hover, .nav a.link:hover {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/*.link:hover span {*/
|
||||
/* visibility: visible;*/
|
||||
/*}*/
|
||||
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);
|
||||
}
|
||||
@@ -234,7 +234,7 @@ section#projects form.projItem:not(.editing) div.formControl.infoContainer texta
|
||||
}
|
||||
|
||||
section#addPost form, section#editPost form {
|
||||
margin: auto 4rem;
|
||||
margin: auto 4rem 4rem;
|
||||
}
|
||||
|
||||
form .formControl .ck.ck-editor__top .ck-sticky-panel .ck-toolbar {
|
||||
@@ -276,6 +276,6 @@ section#editPost table td, th {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
section#editPost form {
|
||||
margin-bottom: 2em;
|
||||
section#newsletter form {
|
||||
margin: 0 5em;
|
||||
}
|
||||
+14
-63
@@ -66,71 +66,22 @@ 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%;
|
||||
width: 60%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
div.btnContainer a {
|
||||
justify-content: center;
|
||||
text-transform: unset;
|
||||
}
|
||||
|
||||
form div.btnContainer input[type="submit"] {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
div.btnContainer a:not(.btn) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+32
-1
@@ -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>
|
||||
|
||||
@@ -32,8 +32,9 @@
|
||||
|
||||
<div class="btnContainer">
|
||||
<input type="submit" value="Submit" class="btn btnPrimary boxShadowIn boxShadowOut">
|
||||
|
||||
<a href="#" id="resetPwd">Reset Password</a>
|
||||
<a href="/api/user/login" class="btn btnPrimary boxShadowIn boxShadowOut">Login with Jump Cloud</a>
|
||||
<button type="button" id="resetPwd" class="btn btnPrimary boxShadowIn boxShadowOut">Reset Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -54,7 +55,8 @@
|
||||
|
||||
<div class="btnContainer">
|
||||
<input type="submit" value="Submit" class="btn btnPrimary boxShadowIn boxShadowOut">
|
||||
<a href="#" class="loginBtn btn btnPrimary boxShadowIn boxShadowOut" id="loginBtn">Login</a>
|
||||
<button type="button" class="loginBtn btn btnPrimary boxShadowIn boxShadowOut" id="loginBtn">Login
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -75,7 +77,9 @@
|
||||
|
||||
<div class="btnContainer">
|
||||
<input type="submit" value="Submit" class="btn btnPrimary boxShadowIn boxShadowOut">
|
||||
<a href="#" class="loginBtn btn btnPrimary boxShadowIn boxShadowOut" id="resendEmail">Resend Email</a>
|
||||
<button type="button" class="loginBtn btn btnPrimary boxShadowIn boxShadowOut" id="resendEmail">Resend
|
||||
Email
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -102,7 +106,7 @@
|
||||
|
||||
<div class="btnContainer">
|
||||
<input type="submit" value="Submit" class="btn btnPrimary boxShadowIn boxShadowOut">
|
||||
<a href="#" class="loginBtn btn btnPrimary boxShadowIn boxShadowOut">Login</a>
|
||||
<button type="button" class="loginBtn btn btnPrimary boxShadowIn boxShadowOut">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+77
-12
@@ -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 =>
|
||||
@@ -818,6 +858,31 @@ function createEditors(...ids)
|
||||
{language: 'zephir', label: 'Zephir'},
|
||||
],
|
||||
},
|
||||
mediaEmbed: {
|
||||
previewsInData: true,
|
||||
providers: [
|
||||
{
|
||||
name: 'youtube',
|
||||
url: [
|
||||
/^(?:m\.)?youtube\.com\/watch\?v=([\w-]+)(?:&t=(\d+))?/,
|
||||
/^(?:m\.)?youtube\.com\/v\/([\w-]+)(?:\?t=(\d+))?/,
|
||||
/^youtube\.com\/embed\/([\w-]+)(?:\?start=(\d+))?/,
|
||||
/^youtu\.be\/([\w-]+)(?:\?t=(\d+))?/,
|
||||
],
|
||||
html: match =>
|
||||
{
|
||||
const id = match[1];
|
||||
const time = match[2];
|
||||
|
||||
return (
|
||||
`<iframe width="560" height="315" style="text-align:center;" src="https://www.youtube.com/embed/${id}${time ? `?start=${time}` : ''}" ` +
|
||||
'allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen>' +
|
||||
'</iframe>'
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}).then(CKEditor =>
|
||||
{
|
||||
editors[id] = CKEditor;
|
||||
@@ -1157,14 +1222,14 @@ function updateProjectItem(id, e)
|
||||
{
|
||||
e.preventDefault();
|
||||
let data = {}
|
||||
data["title"] = document.querySelector(`#title${id}`).value;
|
||||
data["isMainProject"] = document.querySelector(`#isMainProject${id}`).checked ? "true" : "false";
|
||||
data["information"] = document.querySelector(`#info${id}`).value;
|
||||
data["projectLink"] = document.querySelector(`#viewProj${id}`).value;
|
||||
data["gitLink"] = document.querySelector(`#git${id}`).value;
|
||||
data['title'] = document.querySelector(`#title${id}proj`).value;
|
||||
data['isMainProject'] = document.querySelector(`#isMainProject${id}proj`).checked ? 'true' : 'false';
|
||||
data['information'] = document.querySelector(`#info${id}proj`).value;
|
||||
data['projectLink'] = document.querySelector(`#viewProj${id}proj`).value;
|
||||
data['gitLink'] = document.querySelector(`#git${id}proj`).value;
|
||||
|
||||
let imgData = new FormData();
|
||||
imgData.append("img", document.querySelector(`#img${id}`).files[0]);
|
||||
imgData.append('img', document.querySelector(`#img${id}proj`).files[0]);
|
||||
|
||||
fetch("/api/projectData/" + id, {
|
||||
method: "PATCH",
|
||||
@@ -1189,8 +1254,8 @@ function updateProjectItem(id, e)
|
||||
}
|
||||
|
||||
document.querySelector(`#projectItem${id}`).classList.toggle("editing");
|
||||
document.querySelector(`#title${id}`).setAttribute("disabled", "");
|
||||
document.querySelector(`#info${id}`).setAttribute("disabled", "");
|
||||
document.querySelector(`#title${id}proj`).setAttribute('disabled', '');
|
||||
document.querySelector(`#info${id}proj`).setAttribute('disabled', '');
|
||||
return;
|
||||
}
|
||||
console.log("updating image")
|
||||
@@ -1228,8 +1293,8 @@ function updateProjectItem(id, e)
|
||||
}
|
||||
|
||||
document.querySelector(`#projectItem${id}`).classList.toggle("editing");
|
||||
document.querySelector(`#title${id}`).setAttribute("disabled", "");
|
||||
document.querySelector(`#info${id}`).setAttribute("disabled", "");
|
||||
document.querySelector(`#title${id}proj`).setAttribute('disabled', '');
|
||||
document.querySelector(`#info${id}proj`).setAttribute('disabled', '');
|
||||
document.querySelector(`#projectImage${id}`).src = updatedProjectImage.imgLocation;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+4
-2
@@ -32,6 +32,7 @@
|
||||
<li><a href="/blog" class="textShadow link">blog</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<header>
|
||||
<div>
|
||||
<h1>full stack developer</h1>
|
||||
@@ -108,7 +109,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#">
|
||||
<a href="https://rohitpai.co.uk/blog">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M15.5 14.625c0 .484-.387.875-.864.875h-5.273c-.477 0-.863-.392-.863-.875s.387-.875.863-.875h5.272c.478 0 .865.391.865.875zm-6.191-4.375h2.466c.448 0 .809-.392.809-.875s-.361-.875-.81-.875h-2.465c-.447 0-.809.392-.809.875s.362.875.809.875zm14.691 1.75c0 6.627-5.373 12-12 12s-12-5.373-12-12 5.373-12 12-12 12 5.373 12 12zm-5-1.039c0-.383-.311-.692-.691-.692h-1.138c-.583 0-.69-.446-.69-.996-.001-2.36-1.91-4.273-4.265-4.273h-2.952c-2.355 0-4.264 1.913-4.264 4.272v5.455c0 2.36 1.909 4.273 4.264 4.273h5.474c2.353 0 4.262-1.913 4.262-4.272v-3.767z"/>
|
||||
</svg>
|
||||
@@ -173,6 +174,7 @@
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="flexRow">
|
||||
<div class="spacer"></div>
|
||||
@@ -181,7 +183,7 @@
|
||||
<button id="goBackToTop"><i class="fa-solid fa-chevron-up"></i></button>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<script src="js/index.js"></script>
|
||||
<script src="js/typewriter.js"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user