Merge pull request #61 from PretendoNetwork/blog-cms-wip

Blog content managment system
This commit is contained in:
Jip Frijlink 2021-09-25 18:28:22 +02:00 committed by GitHub
commit 766726d1cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 2940 additions and 18 deletions

139
blogposts/_test.md Normal file
View File

@ -0,0 +1,139 @@
---
title: "Test"
author: "Monty"
author_image: "https://www.github.com/montylion.png"
date: "January 20, 2038"
caption: "A post to test the styling of the various elements we might use (rename to _test.md before deploying the blog section)"
cover_image: "https://media.discordapp.net/attachments/413884110667251722/886474243662037062/image1.jpg"
---
A post to test the styling of the various elements we might use (rename to _test.md before deploying the blog section)
**bold**
[**bold url**](https://www.youtube.com/watch?v=HGoCNOFpWpo)
_italic_
[_italic url_](https://www.youtube.com/watch?v=HGoCNOFpWpo)
~strikethrough~
[~strikethrough url~](https://www.youtube.com/watch?v=HGoCNOFpWpo)
# h1
## h2
### h3
#### h4
##### h5
###### h6
---
| Element | Description |
| :----------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| table | The table HTML element represents tabular data — that is, information presented in a two-dimensional table comprised of rows and columns of cells containing data. |
| tuble | The tuble HTML element represents tubular data — that is, information presented in a totally gnarly and radical way. |
| table | A table is an item of furniture with a flat top and one or more legs, used as a surface for working at, eating from or on which to place things. |
| Element | Description |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| table | The table HTML element represents tabular data — that is, information presented in a two-dimensional table comprised of rows and columns of cells containing data. |
| tuble | The tuble HTML element represents tubular data — that is, information presented in a totally gnarly and radical way. |
| table | A table is an item of furniture with a flat top and one or more legs, used as a surface for working at, eating from or on which to place things. |
| Element | Description |
| -----------: | -----------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| table | The table HTML element represents tabular data — that is, information presented in a two-dimensional table comprised of rows and columns of cells containing data. |
| tuble | The tuble HTML element represents tubular data — that is, information presented in a totally gnarly and radical way. |
| table | A table is an item of furniture with a flat top and one or more legs, used as a surface for working at, eating from or on which to place things. |
Yee haw 🤠
- list
- list
- list
- list
- list
69. list
1. list
1. list
1. list
1. list
- [ ] Unchecked checkbox
- [x] Checked checkbox
```js
class trueOrFalseObject {
constructor(trueOrFalse) {
this.trueOrFalse = trueOrFalse;
}
get trueOrFalse() {
return this.trueOrFalse();
}
trueOrFalse() {
return this.trueOrFalse;
}
}
let objectWhichWeKnowIsTrue = new trueOrFalseObject(true);
function checkIfTrueOrFalse(objectToCheckIfTrueOrFalse) {
if (objectToCheckIfTrueOrFalse === objectWhichWeKnowIsTrue.trueOrFalse) {
return objectWhichWeKnowIsTrue.trueOrFalse;
console.log(
"Successfully checked if the object is true or false. Result: the object is true."
);
// For whatever reason this doesn't log, can't figure out why /s
} else {
objectWhichWeKnowIsTrue = new trueOrFalseObject(false);
if (objectToCheckIfTrueOrFalse === objectWhichWeKnowIsTrue.trueOrFalse) {
return objectWhichWeKnowIsTrue.trueOrFalse;
console.log(
"Successfully checked if the object is true or false. Result: the object is false."
);
// For whatever reason this doesn't log either, will probably ask on StackOverflow or something /s
} else {
// something went horribly wrong
}
objectWhichWeKnowIsTrue = new trueOrFalseObject(true);
}
}
const isTrueTrueOrFalse = checkIfTrueOrFalse(true);
const isfalseTrueOrFalse = checkIfTrueOrFalse(false);
const trueOrFalseJSON = {
true: isTrueTrueOrFalse,
false: isfalseTrueOrFalse,
};
console.log(trueOrFalseJSON);
// Ok but seriously don't run this for the love of god I feel sorry for writing this even as a joke
```
> The blockquote HTML element indicates that the enclosed text is an extended quotation. Usually, this is rendered visually by indentation. A URL for the source of the quotation may be given using the cite attribute, while a text representation of the source can be given using the cite element.
> > The blockquote HTML element indicates that the enclosed text is an extended quotation. Usually, this is rendered visually by indentation. A URL for the source of the quotation may be given using the cite attribute, while a text representation of the source can be given using the cite element.
> > > The blockquote HTML element indicates that the enclosed text is an extended quotation. Usually, this is rendered visually by indentation. A URL for the source of the quotation may be given using the cite attribute, while a text representation of the source can be given using the cite element.
<cite>Adapted from [blockquote: The Block Quotation element, from MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote)</cite>
<div class="aspectratio-fallback">
<iframe src="https://www.youtube.com/embed/HGoCNOFpWpo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
![test](https://media.discordapp.net/attachments/413884110667251722/886474243662037062/image1.jpg)
<video controls>
<source src="https://cdn.discordapp.com/attachments/413884110667251722/878216238940160040/video0.mov">
</video>
Blogposts whose filename starts with a \_ will not show up on the /blogs page, but will still be accessible from the url (keep in mind that the file is still going to be publicly accessible on GitHub).

View File

@ -3,7 +3,8 @@
"about": "About",
"faq": "FAQ",
"credits": "Credits",
"progress": "Progress"
"progress": "Progress",
"blog": "Blog"
},
"hero": {
"subtitle": "Game servers",
@ -136,6 +137,10 @@
"title": "Our progress",
"description": "Check the project progress and goals! (Updated every hour or so, does not reflect ALL project goals or progress)"
},
"blogPage": {
"title": "Blog",
"description": "Here we have our lovely blog, which we all love so very much."
},
"localizationPage": {
"title": "Let's localize",
"description": "Paste a link to a publicly accessible JSON locale to test it on the website",

2195
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,9 +23,14 @@
"express-handlebars": "^4.0.4",
"express-locale": "^2.0.0",
"fs-extra": "^9.1.0",
"gray-matter": "^4.0.3",
"ioredis": "^4.26.0",
"marked": "^3.0.4",
"morgan": "^1.10.0",
"redis-json": "^5.0.0",
"trello": "^0.10.0"
},
"devDependencies": {
"eslint": "^7.32.0"
}
}

View File

@ -0,0 +1,79 @@
.blog-card {
display: flex;
flex-flow: row nowrap;
padding: 0;
margin: 0 auto;
max-width: 1000px;
margin-bottom: 30px;
text-decoration: none;
position: relative;
border-radius: 10px;
overflow: hidden;
}
.blog-card .post-info {
flex: 50%;
padding: 40px;
display: flex;
flex-flow: column;
color: var(--text-secondary);
}
.blog-card .post-info .title {
color: var(--text);
margin: 0;
}
.blog-card .post-info .caption {
margin: 4px 0 32px 0;
}
.blog-card .pub-info {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: left;
margin-top: auto;
}
.blog-card .pub-info .date {
font-weight: bold;
color: var(--text);
}
.blog-card .pub-info > * {
margin-right: 0.5em;
margin-top: 0.2em;
}
.blog-card .profile {
display: inline-grid;
grid-template-columns: 30px auto;
grid-gap: 10px;
font-weight: bold;
color: var(--text);
align-items: center;
height: 32px;
}
.blog-card .profile img {
border-radius: 4px;
border: 1px solid var(--border);
max-width: 100%;
}
.blog-card .cover {
flex: 50%;
}
@media screen and (max-width: 600px) {
.blog-card {
flex-flow: column;
}
.blog-card .post-info {
padding: 30px;
}
.blog-card .cover {
order: -1;
min-height: 200px;
}
}

View File

@ -0,0 +1,219 @@
.wrapper {
display: flex;
flex-direction: column;
width: 95%;
min-height: 100vh;
}
header {
width: 100%;
}
.card-wrap {
width: 100%;
}
.blog-card {
padding: 60px;
max-width: 1100px;
margin: auto;
margin-top: 50px;
color: var(--text-secondary);
}
.blog-card h1,
.blog-card h2,
.blog-card h3,
.blog-card h4,
.blog-card h5,
.blog-card h6,
.blog-card strong {
color: var(--text);
}
.blog-card a,
.blog-card a * {
color: #9d6ff3;
text-decoration: none;
font-weight: bold;
}
.blog-card a:hover,
.blog-card a:hover {
text-decoration: underline;
}
.blog-card del {
text-decoration: line-through;
}
.blog-card .title {
margin: 0;
margin-bottom: 8px;
}
.blog-card .pub-info {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: left;
margin-top: auto;
margin-bottom: 30px;
}
.blog-card .pub-info .date {
font-weight: bold;
color: var(--text);
}
.blog-card .pub-info > * {
margin-right: 0.5em;
margin-top: 0.2em;
}
.blog-card .profile {
display: inline-grid;
grid-template-columns: 30px auto;
grid-gap: 10px;
font-weight: bold;
color: var(--text);
align-items: center;
height: 32px;
}
.blog-card .profile img {
margin: 0;
border-radius: 4px;
border: 1px solid var(--border);
max-width: 100%;
}
.blog-card p,
.post-info {
color: var(--text-secondary);
}
.blog-card img {
max-width: 100%;
max-height: 800px;
margin: 10px auto;
display: block;
border-radius: 4px;
border: 1px solid var(--border);
}
.blog-card img.emoji {
display: inline;
margin: 0;
border: none;
}
.blog-card video {
width: 100%;
border-radius: 4px;
border: 1px solid var(--border);
}
.blog-card iframe {
width: 100%;
aspect-ratio: 16/9;
border-radius: 4px;
border: 1px solid var(--border);
}
/* Fallback for aspect-ratio since it's unsupported by some browsers (looking at you Safari) */
@supports not (aspect-ratio: 16/9) {
.blog-card .aspectratio-fallback {
position: relative;
height: 0;
padding-top: 56.25%;
}
.blog-card iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
}
.blog-card table {
border-radius: 4px;
border-collapse: collapse;
background: #31375e;
margin-bottom: 30px;
overflow: hidden;
}
.blog-card table th {
padding: 8px 12px;
background: #3f4778;
color: var(--text);
}
.blog-card table td {
padding: 8px 12px;
vertical-align: top;
border-radius: inherit;
}
.blog-card table tr:nth-child(even) {
background: #2a2f50;
}
.blog-card pre code {
border-radius: 4px;
margin-bottom: 30px;
}
.blog-card input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
display: inline-block;
background: var(--btn-secondary);
padding: 12px;
margin: 4px;
border-radius: 4px;
vertical-align: -60%;
}
.blog-card input[type="checkbox"]:checked {
content: "checkboxtest";
background: no-repeat center/contain url(../images/check.svg),
var(--btn-secondary);
}
.blog-card hr {
border: 1px solid var(--text-secondary);
margin: 30px 0;
}
.blog-card blockquote {
border-left: 2px solid var(--text-secondary);
padding: 8px 24px;
margin: 0;
margin-bottom: 30px;
}
footer {
padding-top: 40px;
margin-top: auto;
}
@media screen and (max-width: 800px) {
.blog-card {
padding: 40px;
}
}
@media screen and (max-width: 600px) {
.wrapper {
width: 100%;
}
header {
width: 90%;
margin: 35px auto;
}
.blog-card {
padding: 40px 5vw;
border-radius: 0;
margin-top: 0;
}
footer {
width: 95%;
margin: auto auto 40px;
}
}

View File

@ -1,6 +1,6 @@
.select-box {
display: flex;
width: 200px;
width: 188px;
flex-direction: column;
position: relative;
user-select: none;

View File

@ -0,0 +1,91 @@
/**
* Adapted from Shades of Purple Theme for Highlightjs.
*
* @author (c) Ahmad Awais <https://twitter.com/mrahmadawais/>
* @link GitHub Repo https://github.com/ahmadawais/Shades-of-Purple-HighlightJS
* @version 1.5.0
*/
.hljs {
background: var(--btn-secondary);
color: #e3dfff;
font-weight: normal;
}
.hljs-subst {
color: #e3dfff;
}
.hljs-title {
color: #fad000;
font-weight: normal;
}
.hljs-name {
color: #a1feff;
}
.hljs-tag {
color: #ffffff;
}
.hljs-attr {
color: #f8d000;
font-style: italic;
}
.hljs-built_in,
.hljs-selector-tag,
.hljs-section {
color: #fb9e00;
}
.hljs-keyword {
color: #fb9e00;
}
.hljs-string,
.hljs-attribute,
.hljs-symbol,
.hljs-bullet,
.hljs-addition,
.hljs-code,
.hljs-regexp,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-template-tag,
.hljs-quote,
.hljs-deletion {
color: #4cd213;
}
.hljs-meta,
.hljs-meta .hljs-string {
color: #fb9e00;
}
.hljs-comment {
color: #ac65ff;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-name,
.hljs-strong {
font-weight: normal;
}
.hljs-literal,
.hljs-number {
color: #fa658d;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View File

@ -8,6 +8,7 @@
--theme: var(--btn);
--theme-light: #A185D6;
--text-secondary-2: #8990C1;
--border: rgba(255, 255, 255, 0.1);
}
body {
@ -17,11 +18,12 @@ body {
body, .main-body {
width: 100%;
max-width: 100vw;
position: relative; /* This fixes overflow-x not hiding on Safari on mobile */
overflow-x: hidden;
margin: 0;
color: var(--text);
justify-content: center;
font-family: Poppins, Arial, Helvetica, sans-serif;
overflow-x: hidden;
}
.animateDot {
@ -324,7 +326,13 @@ section.faq {
color: var(--text-secondary-2);
}
.question-and-answer .text a {
color: white;
color: #9d6ff3;
text-decoration: none;
font-weight: bold;
}
.question-and-answer .text a:hover {
text-decoration: underline;
}
section.team {
@ -571,6 +579,10 @@ footer {
display: none;
}
header .logo-link {
margin-right: 20px;
}
.hero-meta {
margin-top: 100px;
}
@ -659,7 +671,7 @@ footer {
}
@media screen and (max-width: 500px) {
@media screen and (max-width: 580px) {
.selected-locale .locale-names {
display: none;
}
@ -670,17 +682,40 @@ footer {
margin-right: 12px;
}
.locale-dropdown {
width: fit-content;
}
.select-box .options-container {
width: 150px;
right: 12px;
}
}
@media screen and (max-width: 400px) {
.select-box .options-container {
width: 125px;
right: 12px;
@media screen and (max-width: 480px) {
header .logo-link svg text {
display: none;
}
header .logo-link svg {
width: 39.876px;
}
header .logo-link {
margin-right: 10px;
}
header nav a {
margin: 0 10px;
}
}
@media screen and (max-width: 330px) {
.locale-dropdown {
.locale-dropdown .selected-locale {
width: 50px;
}
.locale-dropdown .selected-locale::after {
display: none;
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>

After

Width:  |  Height:  |  Size: 254 B

79
src/routers/blog.js Normal file
View File

@ -0,0 +1,79 @@
const { Router } = require('express');
const util = require('../util');
const logger = require('../logger');
const router = new Router();
const fs = require('fs');
const path = require('path');
const marked = require('marked');
const matter = require('gray-matter');
router.get('/', async (request, response) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
const localeString = reqLocale.toString();
const fileList = fs.readdirSync('blogposts');
// We get the info for each blogpost, ignoring the ones starting with _
const postList = fileList
.filter(filename => !filename.startsWith('_'))
.filter(filename => filename.endsWith('.md')) // Ignores other files/folders
.map((filename) => {
const slug = filename.replace('.md', '');
const rawPost = fs.readFileSync(path.join('blogposts', `${filename}`), 'utf-8');
const { data: postInfo } = matter(rawPost);
return {
slug, postInfo
};
});
postList.sort((a, b) => {
return new Date(b.postInfo.date) - new Date(a.postInfo.date);
});
response.render('blog', {
layout: 'main',
locale,
localeString,
postList
});
});
router.get('/:slug', async (request, response) => {
const reqLocale = request.locale;
const locale = util.getLocale(reqLocale.region, reqLocale.language);
const localeString = reqLocale.toString();
// Get the name of the post from the URL
const postName = request.params.slug;
// Get the markdown file corresponding to the post
let rawPost;
try {
rawPost = fs.readFileSync(path.join('blogposts', `${postName}.md`), 'utf-8');
} catch(err) {
logger.error(err);
response.sendStatus(404);
logger.warn(`HTTP 404 at /blog/${postName}`);
return;
}
// Convert the post info into JSON and separate it and the content
const { data: postInfo, content } = matter(rawPost);
// Convert the content into HTML
const htmlPost = marked(content);
response.render('blogpost', {
layout: 'main',
locale,
localeString,
postInfo,
htmlPost
});
});
module.exports = router;

View File

@ -23,6 +23,7 @@ const routers = {
home: require('./routers/home'),
faq: require('./routers/faq'),
progress: require('./routers/progress'),
blog: require('./routers/blog'),
localization: require('./routers/localization')
};
@ -31,8 +32,7 @@ app.use(cookieParser());
// Locale express middleware setup
app.use(expressLocale({
'priority': ['cookie', 'accept-language', 'map', 'default'],
cookie: {name: 'preferredLocale'},
cookie: { name: 'preferredLocale' },
// Map unavailable regions to available locales from the same language
map: {
/* TODO: map more regions to the available locales */
@ -69,15 +69,15 @@ app.use('/', routers.home);
app.use('/faq', routers.faq);
app.use('/progress', routers.progress);
app.use('/localization', routers.localization);
app.use('/blog', routers.blog);
logger.info('Creating 404 status handler');
// This works because it is the last router created
// Meaning the request could not find a valid router
app.use((request, response) => {
app.use((request, response, next) => {
const fullUrl = util.fullUrl(request);
logger.warn(`HTTP 404 at ${fullUrl}`);
response.sendStatus(404); // TODO: 404 page
next();
});
logger.info('Setting up handlebars engine');
@ -86,11 +86,11 @@ app.engine('handlebars', handlebars({
doFaq(value, options) {
let htmlLeft = '';
let htmlRight = '';
for(const [i, v] of Object.entries(value)) {
for (const [i, v] of Object.entries(value)) {
const appendHtml = options.fn({
...v
}); // Tis is an HTML string
if(i % 2 === 0) {
if (i % 2 === 0) {
htmlLeft += appendHtml;
} else {
htmlRight += appendHtml;

38
views/blog.handlebars Normal file
View File

@ -0,0 +1,38 @@
<link rel="stylesheet" href="/assets/css/blog.css" />
<div class="wrapper blog">
{{> header}}
<div class="progress-hero">
<div class="hero-meta reduced-margin">
<svg xmlns="http://www.w3.org/2000/svg" width="48.87" height="71.093" viewBox="0 0 48.87 71.093"><g id="XMLID_6_" transform="translate(0)"><path id="XMLID_15_" d="M69.581,29.593c-2.029,1.068-.249,4.129,1.78,3.061,5.162-2.67,11.463-2.6,16.981-1.1,4.735,1.282,9.5,3.845,12.246,8.045,1.246,1.922,4.307.142,3.061-1.78C96.921,27.386,80.3,24.04,69.581,29.593Z" transform="translate(-60.112 -20.086)" fill="#9d6ff3"/><path id="XMLID_14_" d="M103.359,21.045c-3.951-6.159-10.751-10-17.657-11.89C77.763,6.948,68.721,7.019,61.281,10.9c-2.029,1.068-.249,4.129,1.78,3.061,6.586-3.453,14.667-3.311,21.644-1.388,5.981,1.638,12.1,4.913,15.521,10.252C101.507,24.783,104.569,23,103.359,21.045Z" transform="translate(-54.766 -7.693)" fill="#9d6ff3"/><path id="XMLID_9_" d="M65.995,47.8a20.7,20.7,0,0,0-12.958,4.45H47.27a2.579,2.579,0,0,0-2.67,2.456v47.239a2.763,2.763,0,0,0,2.67,2.67h5.838a2.639,2.639,0,0,0,2.528-2.67V87.564A21.228,21.228,0,1,0,65.995,47.8Zm0,33.178a12,12,0,1,1,12-12A12,12,0,0,1,65.995,80.978Z" transform="translate(-44.6 -33.522)" fill="#9d6ff3"/></g></svg>
<h1 class="title dot">{{ locale.blogPage.title }}</h1>
<p class="text">{{ locale.blogPage.description }}</p>
</div>
</div>
{{#each postList }}
<a href="/blog/{{this.slug}}" class="purple-card blog-card">
<div class="post-info">
<h2 class="title">{{{ this.postInfo.title }}}</h2>
<p class="caption">{{{ this.postInfo.caption }}}</p>
<div class="pub-info">
<span>Published by</span>
<div class="profile">
<img class="profile-picture" src="{{{ this.postInfo.author_image }}}" alt>
<span>{{{ this.postInfo.author }}}</span>
</div>
<span>on
<span class="date">{{{ this.postInfo.date }}}</span>
</span>
</div>
</div>
<div class="cover" style="background: no-repeat center/cover url({{this.postInfo.cover_image}}">
</div>
</a>
{{/each}}
{{> footer }}
</div>

35
views/blogpost.handlebars Normal file
View File

@ -0,0 +1,35 @@
<link rel="stylesheet" href="/assets/css/blogpost.css" />
<div class="wrapper blog">
{{> header}}
<div class="card-wrap">
<div class="purple-card blog-card">
<h1 class="title">{{{ postInfo.title }}}</h1>
<div class="pub-info">
<span>Published by</span>
<div class="profile">
<img class="profile-picture" src="{{ postInfo.author_image }}" alt="{{ postInfo.author }}">
<span>{{{ postInfo.author }}}</span>
</div>
<span>on
<span class="date">{{{ postInfo.date }}}</span>
</span>
</div>
{{{ htmlPost }}}
</div>
</div>
{{> footer }}
</div>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/styles/default.min.css">
<link rel="stylesheet" href="/assets/css/highlightjs.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>

View File

@ -1,6 +1,6 @@
<header>
<a href="/" class="logo-link">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="39.876" viewBox="0 0 120 39.876">
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="39.876">
<g id="logo_type" data-name="logo type" transform="translate(-553 -467)">
<g id="logo" transform="translate(553 467)">
<rect id="XMLID_158_" width="39.876" height="39.876" fill="#9d6ff3" opacity="0" />
@ -29,6 +29,7 @@
<a href="/#faq">{{ locale.nav.faq }}</a>
<a href="/#credits">{{ locale.nav.credits }}</a>
<a href="/progress" class="keep-on-mobile">{{ locale.nav.progress }}</a>
<a href="/blog" class="keep-on-mobile">{{ locale.nav.blog }}</a>
</nav>
<!-- Ordered the locales in the same way YouTube's language selector orders them -->