mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-04-24 15:37:12 -05:00
Merge pull request #61 from PretendoNetwork/blog-cms-wip
Blog content managment system
This commit is contained in:
commit
766726d1cb
139
blogposts/_test.md
Normal file
139
blogposts/_test.md
Normal 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>
|
||||
|
||||

|
||||
|
||||
<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).
|
||||
|
|
@ -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
2195
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
79
public/assets/css/blog.css
Normal file
79
public/assets/css/blog.css
Normal 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;
|
||||
}
|
||||
}
|
||||
219
public/assets/css/blogpost.css
Normal file
219
public/assets/css/blogpost.css
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
.select-box {
|
||||
display: flex;
|
||||
width: 200px;
|
||||
width: 188px;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
|
|
|||
91
public/assets/css/highlightjs.css
Normal file
91
public/assets/css/highlightjs.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
1
public/assets/images/check.svg
Normal file
1
public/assets/images/check.svg
Normal 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
79
src/routers/blog.js
Normal 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;
|
||||
|
|
@ -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
38
views/blog.handlebars
Normal 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
35
views/blogpost.handlebars
Normal 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>
|
||||
|
|
@ -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 -->
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user