Gisle Aune
6 years ago
commit
09f406cba7
84 changed files with 6920 additions and 0 deletions
-
5.babelrc
-
6.gitignore
-
38.jshintrc
-
12.vscode/settings.json
-
39Dockerfile
-
14README.md
-
BINassets/images/bg.png
-
1assets/test.txt
-
47config.js
-
7marko/browser.json
-
51marko/components/background/component.js
-
1marko/components/background/index.marko
-
23marko/components/background/style.less
-
52marko/components/content-list-item/components/item-meta/component.js
-
2marko/components/content-list-item/components/item-meta/index.marko
-
6marko/components/content-list-item/components/item-tag-list/index.marko
-
15marko/components/content-list-item/index.marko
-
51marko/components/content-list-item/style.less
-
5marko/components/content-list/index.marko
-
12marko/components/content-list/style.less
-
10marko/components/layout-navbar/index.marko
-
39marko/components/markdown/component.js
-
3marko/components/markdown/index.marko
-
40marko/components/markdown/plugins/wikilink.js
-
117marko/components/markdown/style.less
-
1marko/components/menu-blurb/index.marko
-
7marko/components/menu-blurb/style.less
-
1marko/components/menu-gap/index.marko
-
3marko/components/menu-gap/style.less
-
1marko/components/menu-header/index.marko
-
5marko/components/menu-header/style.less
-
13marko/components/menu-link/component.js
-
8marko/components/menu-link/index.marko
-
28marko/components/menu-link/style.less
-
12marko/components/menu/index.marko
-
14marko/components/menu/style.less
-
39marko/global/base.less
-
121marko/global/colors.less
-
32marko/global/navbar.less
-
16marko/page/layout.marko
-
13marko/page/logs/components/logs-list/index.marko
-
10marko/page/logs/components/logs-menu/index.marko
-
15marko/page/logs/components/page/component.js
-
5marko/page/logs/components/page/index.marko
-
6marko/page/logs/list.marko
-
52marko/page/story-content/components/chapter-meta/component.js
-
6marko/page/story-content/components/chapter-meta/index.marko
-
20marko/page/story-content/components/chapter-meta/style.less
-
5marko/page/story-content/components/chapter/component.js
-
10marko/page/story-content/components/chapter/index.marko
-
14marko/page/story-content/components/chapter/style.less
-
8marko/page/story-content/components/page/index.marko
-
9marko/page/story-content/components/page/style.less
-
40marko/page/story-content/components/story-content-menu/component.js
-
9marko/page/story-content/components/story-content-menu/index.marko
-
3marko/page/story-content/components/story-tags/index.marko
-
20marko/page/story-content/components/story-tags/style.less
-
6marko/page/story-content/view.marko
-
11marko/page/story/components/story-list/index.marko
-
11marko/page/story/components/story-menu/index.marko
-
9marko/page/story/list.marko
-
8marko/page/story/tag-list.marko
-
29middleware/locals.js
-
29middleware/passport.js
-
15middleware/session.js
-
4922package-lock.json
-
41package.json
-
46routes/auth.js
-
56routes/graphql.js
-
19routes/logs/index.js
-
13routes/story-content/index.js
-
22routes/story/by-category.js
-
24routes/story/by-tag.js
-
7routes/story/common.js
-
20routes/story/index.js
-
20routes/story/tag-list.js
-
53rpdata/api/Chapter.js
-
5rpdata/api/Character.js
-
84rpdata/api/LogHeader.js
-
147rpdata/api/Story.js
-
34rpdata/api/Tag.js
-
6rpdata/api/index.js
-
40rpdata/client.js
-
101server.js
@ -0,0 +1,5 @@ |
|||||
|
{ |
||||
|
"presets": [ |
||||
|
"es2015" |
||||
|
] |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
*.marko.js |
||||
|
/node_modules |
||||
|
npm-debug.log |
||||
|
/.cache |
||||
|
/node_modules |
||||
|
/.static |
@ -0,0 +1,38 @@ |
|||||
|
{ |
||||
|
"predef": [ |
||||
|
|
||||
|
], |
||||
|
|
||||
|
"globals": { |
||||
|
"define": true, |
||||
|
"require": true |
||||
|
}, |
||||
|
|
||||
|
"node" : true, |
||||
|
"es5" : false, |
||||
|
"browser" : true, |
||||
|
"boss" : false, |
||||
|
"curly": false, |
||||
|
"debug": false, |
||||
|
"devel": false, |
||||
|
"evil": true, |
||||
|
"forin": false, |
||||
|
"immed": true, |
||||
|
"laxbreak": false, |
||||
|
"newcap": true, |
||||
|
"noarg": true, |
||||
|
"noempty": false, |
||||
|
"nonew": true, |
||||
|
"nomen": false, |
||||
|
"onevar": false, |
||||
|
"plusplus": false, |
||||
|
"regexp": false, |
||||
|
"undef": true, |
||||
|
"sub": false, |
||||
|
"white": false, |
||||
|
"eqeqeq": true, |
||||
|
"latedef": true, |
||||
|
"unused": "vars", |
||||
|
"strict": false, |
||||
|
"eqnull": true |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
{ |
||||
|
"files.exclude": { |
||||
|
"**/**/**/**/*.marko.js": true, |
||||
|
"**/**/**/*.marko.js": true, |
||||
|
"**/**/*.marko.js": true, |
||||
|
"**/*.marko.js": true, |
||||
|
"node_modules": true, |
||||
|
".cache": true, |
||||
|
".static": true, |
||||
|
}, |
||||
|
"material-icon-theme.folders.theme": "classic" |
||||
|
} |
@ -0,0 +1,39 @@ |
|||||
|
## 1. Builder |
||||
|
# Use nodejs 10 |
||||
|
FROM node:10-alpine AS builder |
||||
|
|
||||
|
# Load repository into container |
||||
|
WORKDIR /rpdata-frontend |
||||
|
COPY . . |
||||
|
|
||||
|
# Setup development environment |
||||
|
RUN apk add --no-cache git |
||||
|
|
||||
|
# Erease all traces of browser-refresh |
||||
|
RUN sed -i '/browser-refresh/d' ./marko/page/layout.marko |
||||
|
RUN sed -i '/browser-refresh/d' ./package.json |
||||
|
|
||||
|
# Install and compress node_modules |
||||
|
RUN rm -rf node_modules |
||||
|
RUN mkdir node_modules |
||||
|
RUN npm install -g modclean |
||||
|
RUN npm install |
||||
|
RUN npm dedupe |
||||
|
RUN modclean -r |
||||
|
|
||||
|
# Remove dev stuff |
||||
|
RUN rm -rf .static |
||||
|
RUN rm -rf .cache |
||||
|
RUN rm -rf .git |
||||
|
|
||||
|
## 2. Regroup |
||||
|
FROM node:10-alpine |
||||
|
RUN apk add --no-cache ca-certificates |
||||
|
|
||||
|
# Bring data over from builder |
||||
|
WORKDIR /rpdata-frontend |
||||
|
COPY --from=builder /rpdata-frontend /rpdata-frontend |
||||
|
ENV NODE_ENV=production |
||||
|
|
||||
|
# Entry point |
||||
|
CMD ["/usr/local/bin/node", "server"] |
@ -0,0 +1,14 @@ |
|||||
|
# Sample App: Marko + Express |
||||
|
|
||||
|
This is the [official sample](https://github.com/marko-js-samples/marko-express) changed to get it working with the latest versions of lasso, as well as some personal preferences. |
||||
|
|
||||
|
A lot of compiled/cached files are hidden in Visual Studio Code, see the config file in the `.vscode` folder to change that. |
||||
|
|
||||
|
## Install |
||||
|
|
||||
|
```sh |
||||
|
npm install |
||||
|
npm run dev |
||||
|
``` |
||||
|
|
||||
|
Then open the URL printed in the console. |
After Width: 301 | Height: 256 | Size: 62 KiB |
@ -0,0 +1 @@ |
|||||
|
Hello |
@ -0,0 +1,47 @@ |
|||||
|
const fs = require("fs") |
||||
|
|
||||
|
const config = { |
||||
|
graphqlEndpoint: "http://127.0.0.1:17000/graphql", |
||||
|
port: 8080, |
||||
|
root: "http://localhost:8080", |
||||
|
auth0: { |
||||
|
secret: "INVALID_SECRET", |
||||
|
clientId: "", |
||||
|
domain: "aiterp.eu.auth0.com", |
||||
|
}, |
||||
|
session: { |
||||
|
secret: "", |
||||
|
secure: false, |
||||
|
}, |
||||
|
redis: { |
||||
|
host: "127.0.0.1", |
||||
|
port: 6379, |
||||
|
}, |
||||
|
backend: { |
||||
|
kid: "", |
||||
|
secret: "", |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (typeof(window) !== "undefined") { |
||||
|
config.graphqlEndpoint = "/graphql" |
||||
|
|
||||
|
delete config.port |
||||
|
delete config.root |
||||
|
delete config.auth0 |
||||
|
delete config.session |
||||
|
delete config.redis |
||||
|
delete config.backend |
||||
|
} else { |
||||
|
try { |
||||
|
const data = fs.readFileSync("/etc/aiterp/rpdata-frontend.json", "utf8") |
||||
|
const parsed = JSON.parse(data) |
||||
|
if (typeof(parsed) == "object" && !Array.isArray(parsed)) { |
||||
|
Object.assign(config, parsed) |
||||
|
} |
||||
|
} catch(err) { |
||||
|
console.error("Failed to load configuration: " + err.message || err.stack || err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = config |
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"dependencies": [ |
||||
|
"./global/base.less", |
||||
|
"./global/colors.less", |
||||
|
"./global/navbar.less" |
||||
|
] |
||||
|
} |
@ -0,0 +1,51 @@ |
|||||
|
module.exports = class { |
||||
|
onCreate(input) { |
||||
|
this.state = { |
||||
|
src: input.src, |
||||
|
orientation: "portrait", |
||||
|
style: { |
||||
|
filter: `opacity(${input.opacity || 0.20})` |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onInput(input) { |
||||
|
if (input.src != this.state.src) { |
||||
|
this.updateOrientation() |
||||
|
} |
||||
|
|
||||
|
if (input.opacity != this.state.opacity) { |
||||
|
this.state.style.filter = `opacity(${input.opacity || 0.20})` |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMount() { |
||||
|
this.updateOrientation() |
||||
|
} |
||||
|
|
||||
|
updateOrientation() { |
||||
|
const image = this.getEl("image", 0) |
||||
|
|
||||
|
if (image.complete && typeof(image.naturalHeight) !== 'undefined' && image.naturalHeight !== 0) { |
||||
|
const width = image.naturalWidth |
||||
|
const height = image.naturalHeight |
||||
|
|
||||
|
this.setLandscape(width > (height * 1.50)) |
||||
|
} else { |
||||
|
image.onload = () => { |
||||
|
const width = image.naturalWidth |
||||
|
const height = image.naturalHeight |
||||
|
|
||||
|
this.setLandscape(width > (height * 1.50)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setLandscape(value) { |
||||
|
const orientation = value ? "landscape" : "portrait" |
||||
|
|
||||
|
if (orientation !== this.state.orientation) { |
||||
|
this.state.orientation = orientation |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
<img key="image" src=input.src style=state.style class=["background", state.orientation] /> |
@ -0,0 +1,23 @@ |
|||||
|
.background { |
||||
|
/* Set rules to fill background */ |
||||
|
width: 100%; |
||||
|
/* Set up proportionate scaling */ |
||||
|
min-height: 100%; |
||||
|
height: auto; |
||||
|
/* Set up positioning */ |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
user-select: none; |
||||
|
-webkit-user-drag: none; |
||||
|
|
||||
|
z-index: -1; |
||||
|
} |
||||
|
|
||||
|
.background.landscape { |
||||
|
/* Set rules to fill background */ |
||||
|
height: 100%; |
||||
|
/* Set up proportionate scaling */ |
||||
|
min-width: 100%; |
||||
|
width: auto; |
||||
|
} |
@ -0,0 +1,52 @@ |
|||||
|
const moment = require("moment") |
||||
|
|
||||
|
module.exports = class { |
||||
|
onCreate({kind, value, updated}) { |
||||
|
this.state = {text: null, color: "color-menu", tooltip: null} |
||||
|
|
||||
|
if (value == null || value == "") { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
switch (kind) { |
||||
|
case "date": { |
||||
|
const m = moment(value) |
||||
|
if (m.year() < 2) { |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
const dateStr = m.format("MMM D, YYYY") |
||||
|
|
||||
|
this.state.text = dateStr |
||||
|
|
||||
|
if (updated != null && Date.parse(updated) > Date.parse(value)) { |
||||
|
const m2 = moment(updated) |
||||
|
this.state.tooltip = "Originally posted: " + dateStr |
||||
|
this.state.text = m2.format("MMM D, YYYY") + " *" |
||||
|
} |
||||
|
|
||||
|
break |
||||
|
} |
||||
|
|
||||
|
case "tag": { |
||||
|
this.state.color = `color-tag-${value.kind.toLowerCase()}` |
||||
|
this.state.text = value.name |
||||
|
|
||||
|
break |
||||
|
} |
||||
|
|
||||
|
case "author": { |
||||
|
this.state.text = value |
||||
|
|
||||
|
break |
||||
|
} |
||||
|
|
||||
|
default: { |
||||
|
this.state.color = "color-menu" |
||||
|
this.state.text = value |
||||
|
|
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,2 @@ |
|||||
|
<div if(state.text != null && state.text != "") class=["item-meta", input.kind, state.color] title=state.tooltip>${state.text}</div> |
||||
|
<div class="spc"> </div> |
@ -0,0 +1,6 @@ |
|||||
|
<if(input.enableAllTags)> |
||||
|
<item-meta for(tag in input.tags) kind="tag" value=tag /> |
||||
|
</if> |
||||
|
<else> |
||||
|
<item-meta kind="tag" value=(input.tags[0] || null) /> |
||||
|
</else> |
@ -0,0 +1,15 @@ |
|||||
|
<tr class="content-list-item color-primary"> |
||||
|
<td class="icon color-highlight-primary">${input.icon}</td> |
||||
|
<td class="content color-primary color-highlight-dark"> |
||||
|
<div class="title"> |
||||
|
<a class="color-primary" href=input.link>${input.name}</a> |
||||
|
</div> |
||||
|
<div class="description color-text">${input.description || ""}</div> |
||||
|
<div class="meta"> |
||||
|
<item-meta kind="date" value=input.createdDate updated=input.updatedDate /> |
||||
|
<item-meta kind="date" value=input.fictionalDate /> |
||||
|
<item-tag-list tags=input.tags enableAllTags=input.enableAllTags /> |
||||
|
<item-meta kind="author" value=input.author /> |
||||
|
</div> |
||||
|
</td> |
||||
|
</tr> |
@ -0,0 +1,51 @@ |
|||||
|
tr.content-list-item { |
||||
|
user-select: none; |
||||
|
outline: 0.05px solid; |
||||
|
|
||||
|
> td.icon { |
||||
|
font-size: 2.75em; |
||||
|
text-align: center; |
||||
|
width: 2.5ch; |
||||
|
outline: 0.05px solid; |
||||
|
} |
||||
|
|
||||
|
> td.content { |
||||
|
padding: 0.25em; |
||||
|
padding-bottom: 0.33em; |
||||
|
margin: 0; |
||||
|
|
||||
|
> .title { |
||||
|
padding: 0.25em 1ch; |
||||
|
user-select: text; |
||||
|
|
||||
|
a { |
||||
|
font-size: 1.333em; |
||||
|
} |
||||
|
a:hover { |
||||
|
color: white; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
> .description { |
||||
|
margin: 0 1ch; |
||||
|
|
||||
|
p { |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
> .meta { |
||||
|
> div { |
||||
|
display: inline-block; |
||||
|
padding: 0.25em 1ch; |
||||
|
|
||||
|
user-select: text; |
||||
|
} |
||||
|
|
||||
|
> div.spc { |
||||
|
padding: 0; |
||||
|
user-select: none; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
<table class="content-list" cellSpacing="0px"> |
||||
|
<tbody> |
||||
|
<include(input.renderBody)/> |
||||
|
</tbody> |
||||
|
</table> |
@ -0,0 +1,12 @@ |
|||||
|
table.content-list { |
||||
|
padding-bottom: 3em; |
||||
|
|
||||
|
width: 99%; |
||||
|
max-width: 100ch; |
||||
|
padding-right: 1em; |
||||
|
|
||||
|
margin: auto; |
||||
|
|
||||
|
border-collapse: separate; |
||||
|
border-spacing: 0 1em; |
||||
|
} |
@ -0,0 +1,10 @@ |
|||||
|
<!-- style in marko/global/navbar.less --> |
||||
|
|
||||
|
<nav class="layout-navbar color-primary"> |
||||
|
<h1 class="color-primary">Aite RP</h1> |
||||
|
<div class="vr color-menu" /> |
||||
|
<div class="options"> |
||||
|
<a class=(input.site === "story" ? "color-primary" : "color-menu") href="/story/">Story</a> |
||||
|
<a class=(input.site === "logs" ? "color-primary" : "color-menu") href="/logs/">Logs</a> |
||||
|
</div> |
||||
|
</nav> |
@ -0,0 +1,39 @@ |
|||||
|
const MarkdownIt = require("markdown-it") |
||||
|
const md = new MarkdownIt({ |
||||
|
html: true, |
||||
|
linkify: true, |
||||
|
typographer: false, |
||||
|
langPrefix: "language-", |
||||
|
}) |
||||
|
md.use(require("markdown-it-container"), "green") |
||||
|
md.use(require("markdown-it-container"), "orange") |
||||
|
md.use(require("markdown-it-container"), "blue") |
||||
|
md.use(require("./plugins/wikilink")) |
||||
|
|
||||
|
const renderer = md.renderer |
||||
|
|
||||
|
module.exports = class { |
||||
|
onCreate(input) { |
||||
|
this.state = { |
||||
|
render: () => {}, |
||||
|
} |
||||
|
|
||||
|
this.render(input.source || "") |
||||
|
} |
||||
|
|
||||
|
onInput(input) { |
||||
|
if (input.source) { |
||||
|
this.render(input.source) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
render(source) { |
||||
|
this.state.render = (out) => { |
||||
|
if (out.w) { |
||||
|
out.w(md.render(source)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
renderer.rules |
@ -0,0 +1,3 @@ |
|||||
|
<div class=["markdown-content", "color-text", input.class]> |
||||
|
<include(state.render) /> |
||||
|
</div> |
@ -0,0 +1,40 @@ |
|||||
|
/* |
||||
|
|
||||
|
Based on the markdown-it-wikilinks, which is distributed under the MIT license. |
||||
|
|
||||
|
https://github.com/jsepia/markdown-it-wikilinks @ 79fe609a03917f7df11128231118a66a559076d0
|
||||
|
|
||||
|
Copyright (c) 2016 Julio Sepia |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
SOFTWARE. |
||||
|
|
||||
|
*/ |
||||
|
|
||||
|
const Plugin = require('markdown-it-regexp') |
||||
|
|
||||
|
module.exports = new Plugin(/\[\[([\w'\s/]+)(\|([\w'\s/]+))?\]\]/, match => { |
||||
|
let label = "" |
||||
|
let page = "" |
||||
|
|
||||
|
if (match[3] != null) { |
||||
|
label = match[3] |
||||
|
page = encodeURIComponent(match[1]) |
||||
|
} else { |
||||
|
label = match[1] |
||||
|
page = encodeURIComponent(label) |
||||
|
} |
||||
|
|
||||
|
if (label == "" || page == "") { |
||||
|
return match.input |
||||
|
} |
||||
|
|
||||
|
return `<a class="wiki-link" href="https://wiki.aiterp.net?title=${page}">${label}</a>` |
||||
|
} |
||||
|
) |
||||
|
|
@ -0,0 +1,117 @@ |
|||||
|
div.markdown-content { |
||||
|
line-height: 1.333em; |
||||
|
font-size: 1.25em; |
||||
|
|
||||
|
h1 { |
||||
|
font-size: 2em; |
||||
|
margin: 0; |
||||
|
margin-top: 0.75em; |
||||
|
line-height: 1.2em; |
||||
|
text-align: left; |
||||
|
|
||||
|
border-bottom: 1px solid; |
||||
|
} |
||||
|
h1:first-child { |
||||
|
margin-top: 0; |
||||
|
} |
||||
|
|
||||
|
h2 { |
||||
|
font-size: 1.5em; |
||||
|
margin: 0; |
||||
|
margin-top: 0.75em; |
||||
|
padding-bottom: 0.25em; |
||||
|
|
||||
|
border-bottom: 1px solid; |
||||
|
} |
||||
|
|
||||
|
h3 { |
||||
|
font-size: 1.25em; |
||||
|
margin: 0; |
||||
|
margin-top: 1em; |
||||
|
padding: 0; |
||||
|
margin-bottom: -0.25em; |
||||
|
} |
||||
|
|
||||
|
h4 { |
||||
|
font-size: 1em; |
||||
|
margin-top: 1em; |
||||
|
margin-bottom: -0.5em; |
||||
|
padding: 0; |
||||
|
} |
||||
|
|
||||
|
h5 { |
||||
|
font-size: 1em; |
||||
|
margin: 0; |
||||
|
margin-bottom: -0.5em; |
||||
|
} |
||||
|
|
||||
|
h6 { |
||||
|
font-size: 1em; |
||||
|
opacity: 0.5; |
||||
|
margin: 0; |
||||
|
margin-bottom: -0.5em; |
||||
|
} |
||||
|
|
||||
|
hr { |
||||
|
border: none; |
||||
|
margin: 2em 2ch; |
||||
|
border-bottom: 4px dotted; |
||||
|
opacity: 0.75; |
||||
|
} |
||||
|
|
||||
|
ul, ol { |
||||
|
padding: 0.5em 3ch; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
p { |
||||
|
padding: 0.5em 0; |
||||
|
margin: 0; |
||||
|
} |
||||
|
p:first-child { |
||||
|
padding-top: 0.25em; |
||||
|
} |
||||
|
|
||||
|
.orange, .blue, .green, blockquote { |
||||
|
h1, h2, h3, h4, h5, h6 { |
||||
|
margin: 0; |
||||
|
padding: 0.5em 0; |
||||
|
} |
||||
|
|
||||
|
margin: 0.5em 0; |
||||
|
padding: 0.125em 0.5em 0 0.5em; |
||||
|
outline: 1px solid; |
||||
|
|
||||
|
p { |
||||
|
padding: 0.5em 0; |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
pre { |
||||
|
border: 1px solid; |
||||
|
padding: 0.5em; |
||||
|
} |
||||
|
|
||||
|
table { |
||||
|
width: 100%; |
||||
|
border: 1px solid; |
||||
|
padding: 0em 0.5em; |
||||
|
|
||||
|
th { |
||||
|
font-size: 0.75em; |
||||
|
padding: 0 1ch; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
img { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
a { |
||||
|
opacity: 0.75; |
||||
|
} |
||||
|
a:hover { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
<div class=["menu-blurb", input.class]><include(input.renderBody)/></div> |
@ -0,0 +1,7 @@ |
|||||
|
.menu-blurb { |
||||
|
opacity: 0.5; |
||||
|
padding: 0.25em 1.5ch; |
||||
|
} |
||||
|
.menu-blurb.error { |
||||
|
color: red; |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
<div class="menu-gap"></div> |
@ -0,0 +1,3 @@ |
|||||
|
.menu-gap { |
||||
|
padding: 0.25em; |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
<div class="menu-header color-menu"><include(input.renderBody)/></div> |
@ -0,0 +1,5 @@ |
|||||
|
.menu-header { |
||||
|
padding: 0.125em 1.5ch; |
||||
|
opacity: 0.5; |
||||
|
user-select: none; |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
module.exports = class { |
||||
|
onCreate(input) { |
||||
|
this.state = { |
||||
|
classes: ["menu-link", "color-menu", input.class], |
||||
|
} |
||||
|
|
||||
|
this.state.classes.push(input.selected ? "selected" : "not-selected") |
||||
|
} |
||||
|
|
||||
|
onClick() { |
||||
|
this.emit("click") |
||||
|
} |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
<a on-click("onClick") href=input.href> |
||||
|
<div class=state.classes> |
||||
|
<div class="icon color-primary">${input.icon}</div> |
||||
|
<div class=["text", input.selected ? "color-primary" : input.textClass]> |
||||
|
<include(input.renderBody) /> |
||||
|
</div> |
||||
|
</div> |
||||
|
</a> |
@ -0,0 +1,28 @@ |
|||||
|
div.menu-link { |
||||
|
width: 100%; |
||||
|
padding-left: 0.5ch; |
||||
|
font-size: 1.1em; |
||||
|
|
||||
|
cursor: pointer; |
||||
|
|
||||
|
div.icon { |
||||
|
display: inline-block; |
||||
|
width: 3ch; |
||||
|
padding: 0.15em 0.125em; |
||||
|
text-align: center; |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
|
||||
|
div.text { |
||||
|
display: inline-block; |
||||
|
max-width: 22ch; |
||||
|
white-space: nowrap; |
||||
|
overflow-x: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
vertical-align: middle; |
||||
|
|
||||
|
span.weak { |
||||
|
opacity: 0.3333; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
<div class="menu"> |
||||
|
<include(input.renderBody) /> |
||||
|
<if(input.user != null)> |
||||
|
<menu-gap /> |
||||
|
<menu-header>User</menu-header> |
||||
|
<menu-link if(!input.user.loggedIn) icon="L" href="/auth/login">Login</menu-link> |
||||
|
<menu-link if(input.user.loggedIn) icon="L" href="/auth/logout">Logout <span class="weak">(${input.user.name})</span></menu-link> |
||||
|
</if> |
||||
|
<else-if(!input.allowNoUser)> |
||||
|
<menu-blurb class="error">input.user is missing!</menu-blurb> |
||||
|
</else-if> |
||||
|
</div> |
@ -0,0 +1,14 @@ |
|||||
|
.menu { |
||||
|
position: fixed; |
||||
|
top: 5.5em; |
||||
|
bottom: 0; |
||||
|
width: 28ch; |
||||
|
|
||||
|
padding: 0.333em; |
||||
|
|
||||
|
text-align: left; |
||||
|
|
||||
|
user-select: none; |
||||
|
overflow-x: hidden; |
||||
|
overflow-y: auto; |
||||
|
} |
@ -0,0 +1,39 @@ |
|||||
|
body { |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
font-family: sans-serif; |
||||
|
background-color: black; |
||||
|
color: white; |
||||
|
|
||||
|
main { |
||||
|
margin-left: 30ch; // See side-menu |
||||
|
width: calc(100% - 30ch); |
||||
|
margin-right: 0; |
||||
|
} |
||||
|
|
||||
|
a { |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* Less glaring chrome/ium scrollbars. |
||||
|
*/ |
||||
|
::-webkit-scrollbar { |
||||
|
width: 2px; |
||||
|
/* for vertical scrollbars */ |
||||
|
height: 2px; |
||||
|
/* for horizontal scrollbars */ |
||||
|
} |
||||
|
::-webkit-scrollbar-button { |
||||
|
background-color: #555; |
||||
|
color: 000; |
||||
|
display: none; |
||||
|
} |
||||
|
::-webkit-scrollbar-track { |
||||
|
background-color: #222; |
||||
|
} |
||||
|
::-webkit-scrollbar-thumb { |
||||
|
background-color: #555; |
||||
|
color: #000; |
||||
|
} |
@ -0,0 +1,121 @@ |
|||||
|
body.theme-story { |
||||
|
.color-menu { |
||||
|
color: #789; |
||||
|
} |
||||
|
.menu-link.selected { |
||||
|
background-color: rgba(17, 187, 255, 0.125); |
||||
|
} |
||||
|
.menu-link:hover, .menu > .head-menu > a:hover { |
||||
|
background-color: rgba(17, 187, 255, 0.25); |
||||
|
} |
||||
|
.color-primary, .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content hr, .markdown-content a { |
||||
|
color: #4BF; |
||||
|
} |
||||
|
.color-text { |
||||
|
color: #ABC; |
||||
|
} |
||||
|
.color-highlight-primary { |
||||
|
background-color: rgba(34, 187, 255, 0.25); |
||||
|
color: #4BF; |
||||
|
} |
||||
|
.color-highlight-dark { |
||||
|
background-color: rgba(0, 9, 18, 0.50); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
body.theme-logs { |
||||
|
.color-menu { |
||||
|
color: #776; |
||||
|
} |
||||
|
.menu-link.selected { |
||||
|
background-color: rgba(255, 187, 17, 0.125); |
||||
|
} |
||||
|
.menu-link:hover, .Menu > .head-menu > a:hover { |
||||
|
background-color: rgba(255, 187, 17, 0.25); |
||||
|
} |
||||
|
.color-primary, .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content hr, .markdown-content a { |
||||
|
color: #DA1; |
||||
|
} |
||||
|
.color-text { |
||||
|
color: #AAA; |
||||
|
} |
||||
|
.color-highlight-dark { |
||||
|
background-color: rgba(18, 9, 0, 0.50); |
||||
|
} |
||||
|
.color-highlight-primary { |
||||
|
background-color: rgba(255, 187, 15, 0.25); |
||||
|
color: #DA1; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.theme-mapp { |
||||
|
.color-menu { |
||||
|
color: #898; |
||||
|
} |
||||
|
.menu-link.selected { |
||||
|
background-color: rgba(35, 255, 128, 0.125); |
||||
|
} |
||||
|
.menu-link:hover, .menu > .head-menu > a:hover { |
||||
|
background-color: rgba(35, 255, 128, 0.25); |
||||
|
} |
||||
|
.color-primary, .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content hr, .markdown-content a { |
||||
|
color: #1F8; |
||||
|
} |
||||
|
.color-text { |
||||
|
color: #ACB; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
div.markdown-content .orange, body.theme-logs div.markdown-content blockquote, body.theme-logs div.markdown-content table, body.theme-logs div.markdown-content pre { |
||||
|
background-color: rgba(221, 170, 17, 0.125); |
||||
|
color: #DA1; |
||||
|
|
||||
|
h1, h2, h3 { |
||||
|
color: #DA1; |
||||
|
} |
||||
|
} |
||||
|
div.markdown-content .blue, body.theme-story div.markdown-content blockquote, body.theme-story div.markdown-content table, body.theme-story div.markdown-content pre { |
||||
|
background-color: rgba(34, 187, 255, 0.125); |
||||
|
color: #4BF; |
||||
|
|
||||
|
h1, h2, h3 { |
||||
|
color: #4BF; // |
||||
|
} |
||||
|
} |
||||
|
div.markdown-content .green, body.theme-mapp div.markdown-content blockquote, body.theme-mapp div.markdown-content table, body.theme-mapp div.markdown-content pre { |
||||
|
background-color: rgba(17,255,136, 0.125); |
||||
|
color: #1F8; |
||||
|
|
||||
|
h1, h2, h3 { |
||||
|
color: #1F8; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* Tag colors */ |
||||
|
.color-tag-event { |
||||
|
color: #B77; |
||||
|
} |
||||
|
.color-tag-location { |
||||
|
color: #7B7; |
||||
|
} |
||||
|
.color-tag-organization { |
||||
|
color: #B7B; |
||||
|
} |
||||
|
.color-tag-series { |
||||
|
color: #BB7; |
||||
|
} |
||||
|
.color-tag-character { |
||||
|
color: #37B; |
||||
|
} |
||||
|
|
||||
|
/* Danger */ |
||||
|
.color-danger { |
||||
|
color: #FA3; |
||||
|
} |
||||
|
.color-error { |
||||
|
color: #D20; |
||||
|
} |
||||
|
|
||||
|
.menu-link:hover, .menu-link:hover .text, .menu > .head-menu > a:hover { |
||||
|
color: #EEE !important; |
||||
|
} |
@ -0,0 +1,32 @@ |
|||||
|
nav.layout-navbar { |
||||
|
position: fixed; |
||||
|
top: 0; |
||||
|
bottom: 0; |
||||
|
width: 30ch; |
||||
|
text-align: center; |
||||
|
|
||||
|
user-select: none; |
||||
|
|
||||
|
h1 { |
||||
|
display: inline-block; |
||||
|
font-size: 2em; |
||||
|
padding: 0.5em 0 0 0; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
.options { |
||||
|
display: inline-block; |
||||
|
vertical-align: middle; |
||||
|
|
||||
|
a { |
||||
|
display: inline-block; |
||||
|
padding: 0.25em; |
||||
|
margin: 0 0.25ch; |
||||
|
opacity: 0.666; |
||||
|
} |
||||
|
a:hover { |
||||
|
opacity: 1; |
||||
|
color: white; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,16 @@ |
|||||
|
<lasso-page package-path="../browser.json"/> |
||||
|
|
||||
|
<!doctype html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"/> |
||||
|
<title>${input.title ? `${input.title} - Aite RP` : "Aite RP"}</title> |
||||
|
<lasso-head/> |
||||
|
</head> |
||||
|
<body style={backgroundColor: "black"} class=("theme-" + input.site)> |
||||
|
<layout-navbar site=input.site /> |
||||
|
<include(input.body) /> |
||||
|
<lasso-body/> |
||||
|
<browser-refresh/> |
||||
|
</body> |
||||
|
</html> |
@ -0,0 +1,13 @@ |
|||||
|
import moment from "moment" |
||||
|
|
||||
|
<content-list> |
||||
|
<content-list-item for(log in input.logs) |
||||
|
key=log.shortId |
||||
|
icon="L" |
||||
|
link=("/logs/" + log.id) |
||||
|
description=log.description |
||||
|
name=(log.title || (log.channelName + " - " + moment(log.date).format("MMMM D, YYYY"))) |
||||
|
tags=[{kind: "Location", name: log.channelName}, {kind: "Event", name: log.event}] |
||||
|
enableAllTags=true |
||||
|
createdDate=log.date /> |
||||
|
</content-list> |
@ -0,0 +1,10 @@ |
|||||
|
<menu user=input.user> |
||||
|
<menu-header>Logs</menu-header> |
||||
|
<menu-link selected=input.selected.index icon="L" href="/logs/">All</menu-link> |
||||
|
<menu-gap /> |
||||
|
<menu-header>Filters</menu-header> |
||||
|
<menu-gap /> |
||||
|
<menu-header>Data</menu-header> |
||||
|
<menu-link selected=input.selected.channels icon="C" href="/logs/characters/">Characters</menu-link> |
||||
|
<menu-link selected=input.selected.channels icon="#" href="/logs/channels">Channels</menu-link> |
||||
|
</menu> |
@ -0,0 +1,15 @@ |
|||||
|
const {logHeaderApi} = require("../../../../../rpdata/api/LogHeader") |
||||
|
|
||||
|
module.exports = class { |
||||
|
onCreate(input) { |
||||
|
this.state = { |
||||
|
logs: input.logs |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMount() { |
||||
|
logHeaderApi.list({limit: 0}).then(logs => { |
||||
|
this.state.logs = logs |
||||
|
}) |
||||
|
} |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
<background src="/assets/images/bg.png" opacity=0.25 /> |
||||
|
<logs-menu key="menu" selected=(input.selected || {}) user=input.user /> |
||||
|
<main> |
||||
|
<logs-list logs=state.logs /> |
||||
|
</main> |
@ -0,0 +1,6 @@ |
|||||
|
<include("../layout", {title: "Logs", site: "logs"})> |
||||
|
<@body> |
||||
|
<!-- Page needed to get component.js functionality --> |
||||
|
<page logs=input.logs user=input.user selected=input.selected /> |
||||
|
</@body> |
||||
|
</include> |
@ -0,0 +1,52 @@ |
|||||
|
const moment = require("moment") |
||||
|
|
||||
|
module.exports = class { |
||||
|
onCreate({kind, weak, value, updated}) { |
||||
|
this.state = {text: null, color: "color-menu", href: null, tooltip: null} |
||||
|
|
||||
|
if (value == null || value == "") { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
switch (kind) { |
||||
|
case "date": { |
||||
|
const m = moment(value) |
||||
|
if (m.year() < 2) { |
||||
|
break |
||||
|
} |
||||
|
|
||||
|
if (!weak) { |
||||
|
this.state.tooltip = `See all stories set during ${m.format("MMMM YYYY")}` |
||||
|
this.state.href = `/story/by-month/${m.format("YYYY-MM")}/` |
||||
|
} |
||||
|
|
||||
|
const dateStr = m.format("MMM D, YYYY") |
||||
|
|
||||
|
this.state.text = dateStr |
||||
|
|
||||
|
if (updated != null && Date.parse(updated) > Date.parse(value)) { |
||||
|
const m2 = moment(updated) |
||||
|
this.state.tooltip = "Originally posted: " + dateStr |
||||
|
this.state.text = m2.format("MMM D, YYYY") + " *" |
||||
|
} |
||||
|
|
||||
|
break |
||||
|
} |
||||
|
|
||||
|
case "author": { |
||||
|
this.state.text = value |
||||
|
this.state.tooltip = "See all stories by " + value |
||||
|
this.state.href = `/story/by-author/${value}/` |
||||
|
|
||||
|
break |
||||
|
} |
||||
|
|
||||
|
default: { |
||||
|
this.state.color = "color-menu" |
||||
|
this.state.text = value |
||||
|
|
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
<if(state.text != null && state.text != "")> |
||||
|
<div class=["chapter-meta", input.kind, state.color, input.weak ? "weak" : null] title=state.tooltip> |
||||
|
<a if(state.href != null) href=state.href>${state.text}</a> |
||||
|
<span else>${state.text}</span> |
||||
|
</div> |
||||
|
</if> |
@ -0,0 +1,20 @@ |
|||||
|
div.chapter-meta { |
||||
|
display: inline-block; |
||||
|
padding: 0; |
||||
|
padding-right: 1em; |
||||
|
margin: 0; |
||||
|
|
||||
|
user-select: none; |
||||
|
|
||||
|
a { |
||||
|
color: inherit; |
||||
|
border-bottom: 0.25px dotted; |
||||
|
} |
||||
|
a:hover { |
||||
|
border-bottom: 0.5px solid; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
div.chapter-meta.weak { |
||||
|
opacity: 0.5; |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
module.exports = class { |
||||
|
onCreate(input) { |
||||
|
this.state = Object.assign({}, input.chapter) |
||||
|
} |
||||
|
} |
@ -0,0 +1,10 @@ |
|||||
|
<div class="story-chapter"> |
||||
|
<a class="anchor" id=state.id /> |
||||
|
<div class="metadata"> |
||||
|
<chapter-meta kind="title" value=state.title /> |
||||
|
<chapter-meta kind="date" value=state.fictionalDate /> |
||||
|
<chapter-meta weak kind="date" value=state.createdDate /> |
||||
|
<chapter-meta weak kind="author" value=state.author /> |
||||
|
</div> |
||||
|
<markdown class="chapter-content" source=state.source /> |
||||
|
</div> |
@ -0,0 +1,14 @@ |
|||||
|
.story-chapter { |
||||
|
padding-bottom: 1em; |
||||
|
|
||||
|
.metadata { |
||||
|
padding: 0; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
a.anchor { |
||||
|
display: block; |
||||
|
position: relative; |
||||
|
top: -8em; |
||||
|
} |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
<story-content-menu story=input.story selected=(input.selected || {}) user=input.user /> |
||||
|
<main> |
||||
|
<div class="story-content"> |
||||
|
<h1 class="color-primary">${input.story.name}</h1> |
||||
|
<story-tags tags=input.story.tags /> |
||||
|
<chapter for(chapter in input.story.chapters) key=chapter.id chapter=chapter /> |
||||
|
</div> |
||||
|
</main> |
@ -0,0 +1,9 @@ |
|||||
|
.story-content { |
||||
|
width: 90%; |
||||
|
max-width: 75ch; |
||||
|
margin: auto; |
||||
|
|
||||
|
h1 { |
||||
|
text-align: center; |
||||
|
} |
||||
|
} |
@ -0,0 +1,40 @@ |
|||||
|
const moment = require("moment") |
||||
|
|
||||
|
module.exports = class { |
||||
|
onCreate(input) { |
||||
|
this.state = { |
||||
|
chapterTitles: {}, |
||||
|
fdateLink: "", |
||||
|
fdate: "", |
||||
|
selectedChapter: "" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMount() { |
||||
|
this.state.selectedChapter = (window.location.hash||"#").slice(1) |
||||
|
} |
||||
|
|
||||
|
onInput(input) { |
||||
|
if (input.story.fictionalDate && input.story.fictionalDate.getUTCFullYear() > 10) { |
||||
|
const fdate = input.story.fictionalDate |
||||
|
|
||||
|
this.state.fdateLink = `/story/by-month/${moment(fdate).format("YYYY-MM")}` |
||||
|
this.state.fdate = moment(fdate).format("MMM D, YYYY") |
||||
|
} |
||||
|
|
||||
|
for (const chapter of input.story.chapters) { |
||||
|
let title = "Chapter " + input.story.chapters.indexOf(chapter) + 1 |
||||
|
if (chapter.title) { |
||||
|
title = chapter.title |
||||
|
} else if (chapter.fictionalDate && chapter.fictionalDate.getUTCFullYear() > 1) { |
||||
|
title = moment(chapter.fictionalDate).format("MMM D, YYYY") |
||||
|
} |
||||
|
|
||||
|
this.state.chapterTitles[chapter.id] = title |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
selectChapter(id) { |
||||
|
this.state.selectedChapter = id |
||||
|
} |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
<menu user=input.user> |
||||
|
<if(input.story.chapters.length > 1)> |
||||
|
<menu-header>${input.story.name}</menu-header> |
||||
|
<for(chapter in input.story.chapters | status-var=loop)> |
||||
|
<menu-link on-click("selectChapter", chapter.id) href=("#" + chapter.id) selected=(state.selectedChapter === chapter.id) icon=(loop.getIndex()+1)>${state.chapterTitles[chapter.id]}</menu-link> |
||||
|
</for> |
||||
|
<menu-gap /> |
||||
|
</if> |
||||
|
</menu> |
@ -0,0 +1,3 @@ |
|||||
|
<div class="story-tags"> |
||||
|
<a for(tag in input.tags) href=("/story/by-tag/" + tag.kind + "/" + encodeURIComponent(tag.name)) class=["tag", "color-tag-" + tag.kind.toLowerCase()]>${tag.name}</a> |
||||
|
</div> |
@ -0,0 +1,20 @@ |
|||||
|
div.story-tags { |
||||
|
text-align: center; |
||||
|
margin-bottom: 1em; |
||||
|
margin-top: -1em; |
||||
|
|
||||
|
a.tag { |
||||
|
display: inline-block; |
||||
|
margin: 0.5em 0.5ch; |
||||
|
padding: 0.0625em 0.5ch; |
||||
|
|
||||
|
outline: 1px solid; |
||||
|
opacity: 0.75; |
||||
|
|
||||
|
user-select: none; |
||||
|
} |
||||
|
a.tag:hover { |
||||
|
opacity: 1; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
<include("../layout", {title: "Stories", site: "story"})> |
||||
|
<@body> |
||||
|
<background src="/assets/images/bg.png" opacity=0.25 /> |
||||
|
<page story=input.story user=input.user selected=input.selected /> |
||||
|
</@body> |
||||
|
</include> |
@ -0,0 +1,11 @@ |
|||||
|
<content-list> |
||||
|
<content-list-item for(story in input.stories) |
||||
|
icon=story.category.charAt(0) |
||||
|
link=("/story/" + story.id) |
||||
|
name=story.name |
||||
|
createdDate=story.createdDate |
||||
|
fictionalDate=story.fictionalDate |
||||
|
updatedDate=story.updatedDate |
||||
|
tags=story.tags |
||||
|
author=story.author /> |
||||
|
</content-list> |
@ -0,0 +1,11 @@ |
|||||
|
<menu user=input.user> |
||||
|
<menu-header>Story</menu-header> |
||||
|
<menu-link selected=input.selected.index icon="S" href="/story/">Stories</menu-link> |
||||
|
<menu-gap /> |
||||
|
<menu-header>Categories</menu-header> |
||||
|
<menu-link for(category in (input.categories||[])) selected=(input.selected.category == category.name) href=`/story/by-category/${category.name}` icon=category.name.charAt(0)>${category.name}</menu-link> |
||||
|
<menu-gap /> |
||||
|
<menu-header>Tags</menu-header> |
||||
|
<menu-link for(tag in (input.menuTags||[])) selected=(input.selected.tag === `${tag.kind}:${tag.name}`) href=`/story/by-tag/${tag.kind}/${tag.name}` textClass=`color-tag-${tag.kind.toLowerCase()}` icon=tag.kind.charAt(0)>${tag.name}</menu-link> |
||||
|
<menu-link selected=input.selected.tags icon="T" href="/story/tag-list/">Tags</menu-link> |
||||
|
</menu> |
@ -0,0 +1,9 @@ |
|||||
|
<include("../layout", {title: "Stories", site: "story"})> |
||||
|
<@body> |
||||
|
<background src="/assets/images/bg.png" opacity=0.25 /> |
||||
|
<story-menu categories=input.categories selected=(input.selected || {}) menuTags=input.menuTags user=input.user /> |
||||
|
<main> |
||||
|
<story-list stories=input.stories /> |
||||
|
</main> |
||||
|
</@body> |
||||
|
</include> |
@ -0,0 +1,8 @@ |
|||||
|
<include("../layout", {title: "Stories", site: "story"})> |
||||
|
<@body> |
||||
|
<background src="/assets/images/bg.png" opacity=0.25 /> |
||||
|
<story-menu categories=input.categories selected=(input.selected || {}) menuTags=input.menuTags user=input.user /> |
||||
|
<main> |
||||
|
</main> |
||||
|
</@body> |
||||
|
</include> |
@ -0,0 +1,29 @@ |
|||||
|
module.exports = (req, res, next) => { |
||||
|
if (res.marko) { |
||||
|
res.markoAsync = async(template, input) => { |
||||
|
const locals = Object.assign((res.locals || {}), input) |
||||
|
|
||||
|
for (const key in locals) { |
||||
|
const value = locals[key] |
||||
|
if (value instanceof Promise) { |
||||
|
locals[key] = await value |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return res.marko(template, locals) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (req.user) { |
||||
|
res.locals.user = { |
||||
|
loggedIn: true, |
||||
|
name: req.user._json.name, |
||||
|
} |
||||
|
} else { |
||||
|
res.locals.user = { |
||||
|
loggedIn: false, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
next() |
||||
|
} |
@ -0,0 +1,29 @@ |
|||||
|
const Auth0Strategy = require("passport-auth0") |
||||
|
const passport = require("passport") |
||||
|
const config = require("../config") |
||||
|
|
||||
|
//passport-auth0
|
||||
|
const strategy = new Auth0Strategy({ |
||||
|
domain: config.auth0.domain, |
||||
|
clientID: config.auth0.clientId, |
||||
|
clientSecret: config.auth0.secret, // Replace this with the client secret for your app
|
||||
|
callbackURL: config.root + "/auth/callback" |
||||
|
}, (accessToken, refreshToken, extraParams, profile, done) => { |
||||
|
// accessToken is the token to call Auth0 API (not needed in the most cases)
|
||||
|
// extraParams.id_token has the JSON Web Token
|
||||
|
// profile has all the information from the user
|
||||
|
return done(null, profile); |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
passport.use(strategy) |
||||
|
|
||||
|
passport.serializeUser(function(user, done) { |
||||
|
done(null, user); |
||||
|
}); |
||||
|
|
||||
|
passport.deserializeUser(function(user, done) { |
||||
|
done(null, user); |
||||
|
}); |
||||
|
|
||||
|
module.exports = passport |
@ -0,0 +1,15 @@ |
|||||
|
const session = require("express-session") |
||||
|
const config = require("../config") |
||||
|
var RedisStore = require('connect-redis')(session); |
||||
|
|
||||
|
var sess = { |
||||
|
secret: config.session.secret, |
||||
|
store: new RedisStore(config.redis), |
||||
|
cookie: {}, |
||||
|
resave: false, |
||||
|
saveUninitialized: true, |
||||
|
} |
||||
|
|
||||
|
sess.cookie.secure = config.session.secure |
||||
|
|
||||
|
module.exports = session(sess) |
4922
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,41 @@ |
|||||
|
{ |
||||
|
"name": "@gisle/rpdata-frontend", |
||||
|
"version": "0.1.0", |
||||
|
"description": "RPData (www.aiterp.net) frontend/proxy", |
||||
|
"main": "./index.js", |
||||
|
"scripts": { |
||||
|
"dev": "browser-refresh server.js", |
||||
|
"prod": "NODE_ENV=production node server.js" |
||||
|
}, |
||||
|
"author": "Gisle Aune <dev@gisle.me>", |
||||
|
"license": "MIT", |
||||
|
"homepage": "https://github.com/marko-js-samples/marko-express", |
||||
|
"dependencies": { |
||||
|
"@lasso/marko-taglib": "^1.0.12", |
||||
|
"babel-preset-es2015": "^6.24.1", |
||||
|
"body-parser": "^1.18.3", |
||||
|
"browser-refresh": "^1.7.3", |
||||
|
"browser-refresh-taglib": "^1.1.0", |
||||
|
"connect-ensure-login": "^0.1.1", |
||||
|
"connect-redis": "^3.3.3", |
||||
|
"empty-module": "0.0.2", |
||||
|
"es6-promise": "^4.2.4", |
||||
|
"express": "^4.16.3", |
||||
|
"express-http-proxy": "^1.2.0", |
||||
|
"express-session": "^1.15.6", |
||||
|
"graphql-query-compress": "^1.1.0", |
||||
|
"isomorphic-fetch": "^2.2.1", |
||||
|
"jsonwebtoken": "^8.3.0", |
||||
|
"lasso": "^3.2.6", |
||||
|
"lasso-babel-transform": "^1.0.2", |
||||
|
"lasso-less": "^3.0.1", |
||||
|
"lasso-marko": "^2.4.4", |
||||
|
"markdown-it": "^8.4.2", |
||||
|
"markdown-it-container": "^2.0.0", |
||||
|
"markdown-it-regexp": "^0.4.0", |
||||
|
"marko": "^4.13.4", |
||||
|
"moment": "^2.22.2", |
||||
|
"passport": "^0.4.0", |
||||
|
"passport-auth0": "^1.0.0" |
||||
|
} |
||||
|
} |
@ -0,0 +1,46 @@ |
|||||
|
const express = require("express") |
||||
|
const passport = require("../middleware/passport") |
||||
|
|
||||
|
const router = express.Router() |
||||
|
|
||||
|
function saveReferer(req, res, next) { |
||||
|
req.session.loginReferrer = req.get("Referer") |
||||
|
req.session.save() |
||||
|
|
||||
|
next() |
||||
|
} |
||||
|
|
||||
|
// Perform the login, after login Auth0 will redirect to callback
|
||||
|
router.get("/login", saveReferer, passport.authenticate("auth0", {scope: "openid username profile"}), (req, res) => { |
||||
|
res.redirect("/") |
||||
|
}) |
||||
|
|
||||
|
router.get("/callback", passport.authenticate("auth0", { failureRedirect: "/auth/login" }), (req, res) => { |
||||
|
if (req.user == null) { |
||||
|
throw new Error("user null"); |
||||
|
} |
||||
|
|
||||
|
res.redirect(req.session.loginReferrer || "/"); |
||||
|
}) |
||||
|
|
||||
|
router.get("/user", (req, res) => { |
||||
|
if (req.user == null) { |
||||
|
return res.status(401).json({error: {message: "Unauthorized"}}) |
||||
|
} |
||||
|
|
||||
|
const name = req.user._json.name |
||||
|
|
||||
|
// TODO: Grab data from rpdata-api user database
|
||||
|
|
||||
|
res.json({data: { |
||||
|
name |
||||
|
}}) |
||||
|
}) |
||||
|
|
||||
|
router.get("/logout", saveReferer, (req, res) => { |
||||
|
req.logout() |
||||
|
|
||||
|
res.redirect(req.session.loginReferrer || "/"); |
||||
|
}) |
||||
|
|
||||
|
module.exports = router |
@ -0,0 +1,56 @@ |
|||||
|
const express = require("express") |
||||
|
const jwt = require("jsonwebtoken") |
||||
|
|
||||
|
const config = require("../config") |
||||
|
const { query } = require("../rpdata/client") |
||||
|
|
||||
|
const router = express.Router() |
||||
|
|
||||
|
router.post("/", (req, res) => { |
||||
|
if (!req.header("Content-Type").startsWith("application/json")) { |
||||
|
res.status(400).json({errors: [{message: "Incorrect input type, expected application/json"}]}) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const user = res.locals.user |
||||
|
const permissions = (req.header("X-Permissions") || "").split(",").filter(t => t != "" && t != "undefined" && t != "null") |
||||
|
let authorization = req.header("Authorization") |
||||
|
if (authorization == "null") { |
||||
|
authorization = "" |
||||
|
} |
||||
|
|
||||
|
if (!authorization && permissions.length > 0 && user.loggedIn) { |
||||
|
authorization = `Bearer ${generateToken(user.name, permissions)}` |
||||
|
} |
||||
|
|
||||
|
fetch(config.graphqlEndpoint, { |
||||
|
method: "POST", |
||||
|
headers: { |
||||
|
"Content-Type": "application/json", |
||||
|
"Authorization": authorization, |
||||
|
}, |
||||
|
body: JSON.stringify(req.body), |
||||
|
credentials: "include", |
||||
|
}).then(fetchRes => { |
||||
|
res.setHeader("Content-Type", fetchRes.headers.get("Content-Type")) |
||||
|
res.status(fetchRes.status) |
||||
|
|
||||
|
return fetchRes.json() |
||||
|
}).then(json => { |
||||
|
res.json(json) |
||||
|
}).catch(err => { |
||||
|
res.status(500).text(err) |
||||
|
return null |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* @param {string} user |
||||
|
* @param {string[]} permissions |
||||
|
*/ |
||||
|
function generateToken(user, permissions) { |
||||
|
return jwt.sign({user, permissions, exp: Math.floor((Date.now() / 1000) + 1200)}, config.backend.secret, {header: {kid: config.backend.kid}}) |
||||
|
} |
||||
|
|
||||
|
module.exports = router |
@ -0,0 +1,19 @@ |
|||||
|
const express = require("express") |
||||
|
const router = express.Router() |
||||
|
|
||||
|
const {logHeaderApi} = require("../../rpdata/api/LogHeader") |
||||
|
|
||||
|
const listTemplate = require("../../marko/page/logs/list.marko") |
||||
|
|
||||
|
router.get("/", async(req, res) => { |
||||
|
try { |
||||
|
res.markoAsync(listTemplate, { |
||||
|
logs: logHeaderApi.list({limit: 0}), |
||||
|
selected: {index: true}, |
||||
|
}) |
||||
|
} catch(err) { |
||||
|
console.error(err) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
module.exports = router |
@ -0,0 +1,13 @@ |
|||||
|
const express = require("express") |
||||
|
const router = express.Router() |
||||
|
|
||||
|
const {storyApi} = require("../../rpdata/api/Story") |
||||
|
|
||||
|
const viewTemplate = require("../../marko/page/story-content/view.marko") |
||||
|
|
||||
|
module.exports = async(req, res, next) => { |
||||
|
res.markoAsync(viewTemplate, { |
||||
|
story: storyApi.find(req.params.id), |
||||
|
selected: {index: true}, |
||||
|
}) |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
const express = require("express") |
||||
|
const router = express.Router() |
||||
|
|
||||
|
const common = require("./common") |
||||
|
const {storyApi} = require("../../rpdata/api/Story") |
||||
|
const {Tag} = require("../../rpdata/api/Tag") |
||||
|
|
||||
|
const listTemplate = require("../../marko/page/story/list.marko") |
||||
|
|
||||
|
router.get("/:category", common, async(req, res) => { |
||||
|
try { |
||||
|
res.markoAsync(listTemplate, { |
||||
|
stories: storyApi.list({category: req.params.category}), |
||||
|
selected: {category: req.params.category}, |
||||
|
}) |
||||
|
} catch(err) { |
||||
|
console.error(err) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
module.exports = router |
||||
|
|
@ -0,0 +1,24 @@ |
|||||
|
const express = require("express") |
||||
|
const router = express.Router() |
||||
|
|
||||
|
const common = require("./common") |
||||
|
const {storyApi} = require("../../rpdata/api/Story") |
||||
|
const {Tag} = require("../../rpdata/api/Tag") |
||||
|
|
||||
|
const listTemplate = require("../../marko/page/story/list.marko") |
||||
|
|
||||
|
router.get("/:tagkind/:tagname", common, async(req, res) => { |
||||
|
try { |
||||
|
const tag = new Tag(req.params.tagkind, req.params.tagname) |
||||
|
|
||||
|
res.markoAsync(listTemplate, { |
||||
|
stories: storyApi.list({tags: tag}), |
||||
|
menuTags: [tag], |
||||
|
selected: {tag: `${tag.kind}:${tag.name}`}, |
||||
|
}) |
||||
|
} catch(err) { |
||||
|
console.error(err) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
module.exports = router |
@ -0,0 +1,7 @@ |
|||||
|
const {storyApi} = require("../../rpdata/api/Story") |
||||
|
|
||||
|
module.exports = (req, res, next) => { |
||||
|
res.locals.categories = storyApi.categories() |
||||
|
|
||||
|
next() |
||||
|
} |
@ -0,0 +1,20 @@ |
|||||
|
const express = require("express") |
||||
|
const router = express.Router() |
||||
|
|
||||
|
const common = require("./common") |
||||
|
const {storyApi} = require("../../rpdata/api/Story") |
||||
|
|
||||
|
const listTemplate = require("../../marko/page/story/list.marko") |
||||
|
|
||||
|
router.get("/", common, (req, res) => { |
||||
|
try { |
||||
|
res.markoAsync(listTemplate, { |
||||
|
stories: storyApi.list(), |
||||
|
selected: {index: true}, |
||||
|
}) |
||||
|
} catch(err) { |
||||
|
console.error(err) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
module.exports = router |
@ -0,0 +1,20 @@ |
|||||
|
const express = require("express") |
||||
|
const router = express.Router() |
||||
|
|
||||
|
const common = require("./common") |
||||
|
const {tagApi} = require("../../rpdata/api/Tag") |
||||
|
|
||||
|
const tagListTemplate = require("../../marko/page/story/tag-list.marko") |
||||
|
|
||||
|
router.get("/", common, (req, res) => { |
||||
|
try { |
||||
|
res.markoAsync(tagListTemplate, { |
||||
|
tags: tagApi.list(), |
||||
|
selected: {tags: true}, |
||||
|
}) |
||||
|
} catch(err) { |
||||
|
console.error(err) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
module.exports = router |
@ -0,0 +1,53 @@ |
|||||
|
const {query} = require("../client") |
||||
|
|
||||
|
class Chapter { |
||||
|
/** |
||||
|
* @param {string} id |
||||
|
* @param {string} title |
||||
|
* @param {string} author |
||||
|
* @param {string} source |
||||
|
* @param {string | Date} createdDate |
||||
|
* @param {string | Date} fictionalDate |
||||
|
* @param {string | Date} editedDate |
||||
|
*/ |
||||
|
constructor(id, title, author, source, createdDate, fictionalDate, editedDate) { |
||||
|
this.id = id |
||||
|
this.title = title |
||||
|
this.author = author |
||||
|
this.source = source |
||||
|
this.createdDate = new Date(createdDate) |
||||
|
this.fictionalDate = new Date(fictionalDate) |
||||
|
this.editedDate = new Date(editedDate) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Query the chapter's editable fields and update this object. This promise returns a boolean |
||||
|
* whether there has been a change. |
||||
|
* |
||||
|
* @returns {Promise<boolean>} `true` if the story was edited in the meantime |
||||
|
*/ |
||||
|
update() { |
||||
|
return query(`
|
||||
|
query UpdateChapter($id: String!) { |
||||
|
update: chapter(id: $id) { |
||||
|
title |
||||
|
source |
||||
|
fictionalDate |
||||
|
editedDate |
||||
|
} |
||||
|
} |
||||
|
`, {id: this.id}).then(({update}) => {
|
||||
|
const editedDate = new Date(udpate.editedDate) |
||||
|
const wasEdited = editedDate.getTime() != this.editedDate.getTime() |
||||
|
|
||||
|
this.title = update.title |
||||
|
this.source = update.source |
||||
|
this.fictionalDate = new Date(update.fictionalDate) |
||||
|
this.editedDate = editedDate |
||||
|
|
||||
|
return wasEdited |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = {Chapter} |
@ -0,0 +1,5 @@ |
|||||
|
const {query} = require("../client") |
||||
|
|
||||
|
class Character { |
||||
|
|
||||
|
} |
@ -0,0 +1,84 @@ |
|||||
|
const {query} = require("../client") |
||||
|
|
||||
|
class LogHeader { |
||||
|
/** |
||||
|
* Construct a log header. You should probably use the logHeaderApi instead of doing |
||||
|
* this manually, even for mutations. |
||||
|
* |
||||
|
* @param {string} id |
||||
|
* @param {Date|string} date |
||||
|
* @param {string} channelName |
||||
|
* @param {string} title |
||||
|
* @param {string} description |
||||
|
* @param {string} event |
||||
|
* @param {boolean} open |
||||
|
* @param {{id:string,name:string,shortName:string,author:string}[]} characters |
||||
|
*/ |
||||
|
constructor(id, shortId, date, channelName, title, description, event, open, characters) { |
||||
|
this.id = id |
||||
|
this.shortId = shortId |
||||
|
this.date = new Date(date) |
||||
|
this.channelName = channelName |
||||
|
this.title = title || null |
||||
|
this.description = description || null |
||||
|
this.event = event || null |
||||
|
this.open = open |
||||
|
this.characters = characters.map(ch => new LogHeaderCharacter(ch.id, ch.name, ch.shortName, ch.author)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class LogHeaderCharacter { |
||||
|
/** |
||||
|
* Construct a log header character list entry. |
||||
|
* |
||||
|
* @param {string} id |
||||
|
* @param {string} name |
||||
|
* @param {string} shortName |
||||
|
* @param {string} author |
||||
|
*/ |
||||
|
constructor(id, name, shortName, author) { |
||||
|
this.id = id |
||||
|
this.name = name |
||||
|
this.shortName = shortName |
||||
|
this.author = author |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* logHeaderApi contains the API queries for the LogHeader frontend model, which is a subset of the |
||||
|
* logs model. |
||||
|
*/ |
||||
|
const logHeaderApi = { |
||||
|
/** |
||||
|
* Call `stories(filter)` query |
||||
|
* |
||||
|
* @param {{search:string, channels:string|string[], events:string|string[], open:boolean, characters:string|string[], limit:number}} filter |
||||
|
* @returns {Promise<LogHeader[]>} |
||||
|
*/ |
||||
|
list(filter = {}) { |
||||
|
return query(`
|
||||
|
query LogHeaders($filter: LogsFilter) { |
||||
|
headers: logs(filter:$filter) { |
||||
|
id |
||||
|
shortId |
||||
|
date |
||||
|
channelName |
||||
|
title |
||||
|
description |
||||
|
event |
||||
|
open |
||||
|
characters { |
||||
|
id |
||||
|
name |
||||
|
shortName |
||||
|
author |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
`, {filter}).then(({headers}) => {
|
||||
|
return headers.map(h => new LogHeader(h.id, h.shortId, h.date, h.channelName, h.title, h.description, h.event, h.open, h.characters)) |
||||
|
}) |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
module.exports = {LogHeader, LogHeaderCharacter, logHeaderApi} |
@ -0,0 +1,147 @@ |
|||||
|
const {query} = require("../client") |
||||
|
const {Tag} = require("./Tag") |
||||
|
const {Chapter} = require("./Chapter") |
||||
|
|
||||
|
class Story { |
||||
|
/** |
||||
|
* Construct a story object. You should use the API instead of calling this. |
||||
|
* |
||||
|
* @param {string} id |
||||
|
* @param {string} name |
||||
|
* @param {string} author |
||||
|
* @param {"Info"|"News"|"Document"|"Background"|"Story"} category Story's category |
||||
|
* @param {string} createdDate |
||||
|
* @param {string} updatedDate |
||||
|
* @param {string} fictionalDate |
||||
|
* @param {{kind:string, name:string}[]} tags |
||||
|
* @param {Chapter[]} chapters |
||||
|
*/ |
||||
|
constructor(id, name, author, category, createdDate, updatedDate, fictionalDate, tags, chapters) { |
||||
|
this.id = id |
||||
|
this.name = name |
||||
|
this.author = author |
||||
|
this.category = category |
||||
|
this.createdDate = new Date(createdDate) |
||||
|
this.updatedDate = new Date(updatedDate) |
||||
|
this.fictionalDate = new Date(fictionalDate) |
||||
|
this.tags = tags.map(dt => new Tag(dt.kind, dt.name)) |
||||
|
this.chapters = chapters != null ? chapters.map(c => new Chapter(c.id, c.title, c.author, c.source, c.createdDate, c.fictionalDate, c.editedDate)) : null |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class StoryCategory { |
||||
|
constructor(name, description) { |
||||
|
this.name = name |
||||
|
this.description = description |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const data = { |
||||
|
categories: null |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* storyApi contains the API queries |
||||
|
*/ |
||||
|
const storyApi = { |
||||
|
/** |
||||
|
* Call `stories(filter)` query |
||||
|
* |
||||
|
* @param {{author:string, category:string, tags:Tag[]|Tag, unlisted: boolean, open: boolean, earliestFictionalDate:Date|string, latestFictionalDate:Date|string, limit:number}} filter |
||||
|
* @returns {Promise<Story[]>} |
||||
|
*/ |
||||
|
list(filter = {}) { |
||||
|
if (filter.earliestFictionalDate != null && typeof(filter.earliestFictionalDate) !== "string") { |
||||
|
filter.earliestFictionalDate = filter.earliestFictionalDate.toISOString() |
||||
|
} |
||||
|
if (filter.latestFictionalDate != null && typeof(filter.latestFictionalDate) !== "string") { |
||||
|
filter.latestFictionalDate = filter.latestFictionalDate.toISOString() |
||||
|
} |
||||
|
|
||||
|
return query(`
|
||||
|
query ListStories($filter: StoriesFilter) { |
||||
|
stories(filter: $filter) { |
||||
|
id |
||||
|
name |
||||
|
author |
||||
|
category |
||||
|
tags { |
||||
|
kind |
||||
|
name |
||||
|
} |
||||
|
createdDate |
||||
|
fictionalDate |
||||
|
updatedDate |
||||
|
} |
||||
|
} |
||||
|
`, {filter}).then(({stories}) => {
|
||||
|
return stories.map(d => new Story(d.id, d.name, d.author, d.category, d.createdDate, d.updatedDate, d.fictionalDate, d.tags)) |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* @param {string} id |
||||
|
* @returns {Promise<Story>} |
||||
|
*/ |
||||
|
find(id) { |
||||
|
return query(`
|
||||
|
query FindStory($id: String!) { |
||||
|
story(id: $id) { |
||||
|
id |
||||
|
name |
||||
|
author |
||||
|
category |
||||
|
tags { |
||||
|
kind |
||||
|
name |
||||
|
} |
||||
|
createdDate |
||||
|
fictionalDate |
||||
|
updatedDate |
||||
|
chapters { |
||||
|
id |
||||
|
title |
||||
|
author |
||||
|
source |
||||
|
createdDate |
||||
|
fictionalDate |
||||
|
editedDate |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
`, {id}).then(({story}) => {
|
||||
|
return new Story( |
||||
|
story.id, story.name, story.author, story.category, |
||||
|
story.createdDate, story.updatedDate, story.fictionalDate, |
||||
|
story.tags, story.chapters |
||||
|
) |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Call `__type(name: "StoryCategory")` query and extracts the catogires from it |
||||
|
* |
||||
|
* @returns {Promise<StoryCategory[]>} |
||||
|
*/ |
||||
|
categories() { |
||||
|
if (data.categories !== null) { |
||||
|
return data.categories |
||||
|
} |
||||
|
|
||||
|
return query(`
|
||||
|
query ListStoryCategories { |
||||
|
categoryType: __type(name: "StoryCategory") { |
||||
|
enumValues { |
||||
|
name |
||||
|
description |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
`, {}).then(({categoryType}) => {
|
||||
|
data.categories = categoryType.enumValues.map(d => new StoryCategory(d.name, d.description)) |
||||
|
return data.categories |
||||
|
}) |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
module.exports = {storyApi, Story} |
@ -0,0 +1,34 @@ |
|||||
|
const {query} = require("../client") |
||||
|
|
||||
|
class Tag { |
||||
|
/** |
||||
|
* @param {string} kind |
||||
|
* @param {string} name |
||||
|
*/ |
||||
|
constructor(kind, name) { |
||||
|
this.kind = kind |
||||
|
this.name = name |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const tagApi = { |
||||
|
/** |
||||
|
* Call `tags` query |
||||
|
* |
||||
|
* @returns {Promise<Tag[]>} |
||||
|
*/ |
||||
|
list() { |
||||
|
return query(`
|
||||
|
query Tags { |
||||
|
tags { |
||||
|
kind |
||||
|
name |
||||
|
} |
||||
|
} |
||||
|
`, {}).then(({tags}) => {
|
||||
|
return tags.map(d => new Tag(d)) |
||||
|
}) |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
module.exports = {Tag, tagApi} |
@ -0,0 +1,6 @@ |
|||||
|
module.exports = { |
||||
|
...require("./LogHeader"), |
||||
|
...require("./Chapter"), |
||||
|
...require("./Story"), |
||||
|
...require("./Tag"), |
||||
|
} |
@ -0,0 +1,40 @@ |
|||||
|
const fetch = require("isomorphic-fetch") |
||||
|
const config = require("../config") |
||||
|
const compressQuery = require("graphql-query-compress") |
||||
|
|
||||
|
/** |
||||
|
* Run a GraphQL query against the rpdata backend |
||||
|
* |
||||
|
* @param {string} query The query to run |
||||
|
* @param {{[x:string]: any}} variables |
||||
|
* @param {{operationName:string, token: string, permissions: string[]}} options |
||||
|
*/ |
||||
|
function query(query, variables = {}, options = {}) { |
||||
|
return fetch(config.graphqlEndpoint, { |
||||
|
method: "POST", |
||||
|
headers: { |
||||
|
"Content-Type": "application/json", |
||||
|
"Authorization": options.token ? `Bearer ${options.token}` : null, |
||||
|
"X-Permissions": options.permissions ? options.permissions.join(",") : null, |
||||
|
}, |
||||
|
body: JSON.stringify({query: query.length > 256 ? compressQuery(query || "") : query, variables, operationName: options.operationName}), |
||||
|
credentials: "include", |
||||
|
}).then(res => { |
||||
|
if (!res.ok) { |
||||
|
return Promise.reject([{ |
||||
|
message: `HTTP: ${res.statusText}`, |
||||
|
location: null, |
||||
|
}]) |
||||
|
} |
||||
|
|
||||
|
return res.json() |
||||
|
}).then(json => { |
||||
|
if (json.errors != null && json.errors.length > 0) { |
||||
|
return Promise.reject(json.errors) |
||||
|
} |
||||
|
|
||||
|
return json.data |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
module.exports = { query } |
@ -0,0 +1,101 @@ |
|||||
|
// Setup global environment
|
||||
|
require("marko/node-require").install() |
||||
|
require("marko/express") |
||||
|
require("es6-promise").polyfill() |
||||
|
|
||||
|
// Load server config
|
||||
|
const config = require("./config") |
||||
|
|
||||
|
// Express depedencies
|
||||
|
const express = require("express") |
||||
|
const proxy = require("express-http-proxy") |
||||
|
const lasso = require("lasso") |
||||
|
const lassoMiddleware = require("lasso/middleware") |
||||
|
const bodyParser = require("body-parser") |
||||
|
|
||||
|
// Load middleware
|
||||
|
const passport = require("./middleware/passport") |
||||
|
const session = require("./middleware/session") |
||||
|
|
||||
|
// Setup express
|
||||
|
const app = express() |
||||
|
|
||||
|
// Configure lasso
|
||||
|
const isProduction = process.env.NODE_ENV === "production" |
||||
|
lasso.configure({ |
||||
|
plugins: [ |
||||
|
"lasso-marko", |
||||
|
"lasso-less", |
||||
|
], |
||||
|
outputDir: "./.static", // Place all generated JS/CSS/etc. files into the "static" dir
|
||||
|
bundlingEnabled: isProduction, // Only enable bundling in production
|
||||
|
minify: isProduction, // Only minify JS and CSS code in production
|
||||
|
fingerprintsEnabled: isProduction, // Only add fingerprints to URLs in production
|
||||
|
|
||||
|
require: { |
||||
|
builtins: { |
||||
|
fs: require.resolve("empty-module"), |
||||
|
}, |
||||
|
|
||||
|
transforms: isProduction ? [ |
||||
|
{ |
||||
|
transform: "lasso-babel-transform", |
||||
|
config: { |
||||
|
extensions: [".js", ".es6"], // Enabled file extensions. Default: [".js", ".es6"]
|
||||
|
babelOptions: { |
||||
|
presets: [ "es2015" ] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
] : null |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// Apply middleware
|
||||
|
app.use(session) |
||||
|
app.use(passport.initialize()) |
||||
|
app.use(passport.session()) |
||||
|
app.use(require("./middleware/locals")) |
||||
|
app.use(bodyParser.json({limit: "1mb"})) |
||||
|
app.use(bodyParser.text({limit: "256kb"})) |
||||
|
|
||||
|
// Static assets
|
||||
|
app.use("/assets", express.static(__dirname + "/assets")) |
||||
|
app.use(lassoMiddleware.serveStatic()) |
||||
|
|
||||
|
// Authentication
|
||||
|
app.use("/auth", require("./routes/auth")) |
||||
|
|
||||
|
// API Proxy
|
||||
|
app.use("/graphql", require("./routes/graphql")) |
||||
|
|
||||
|
// Page routes
|
||||
|
app.use("/story/", require("./routes/story")) |
||||
|
app.use("/story/by-category/", require("./routes/story/by-category")) |
||||
|
app.use("/story/by-tag/", require("./routes/story/by-tag")) |
||||
|
app.use("/story/tag-list/", require("./routes/story/tag-list")) |
||||
|
app.use("/story/:id(S[0-9a-z]{15})/", require("./routes/story-content")) |
||||
|
app.use("/logs/", require("./routes/logs")) |
||||
|
|
||||
|
// Entry point
|
||||
|
app.get("/", function(req, res) { |
||||
|
res.redirect("/story/") |
||||
|
}) |
||||
|
|
||||
|
// Render common templates
|
||||
|
require("./marko/page/story/list.marko").render() |
||||
|
require("./marko/page/logs/list.marko").render() |
||||
|
require("./marko/page/story-content/view.marko").render({story: {chapters: []}}) |
||||
|
|
||||
|
// Start server
|
||||
|
app.listen(config.port, function() { |
||||
|
console.log("Server started: http://localhost:" + config.port + "/") |
||||
|
|
||||
|
if (process.send) { |
||||
|
setTimeout(() => process.send("online"), 500) |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
// Handle shutdown signals (Docker needs this to shutdown quickly)
|
||||
|
process.on('SIGINT', () => process.exit(0)) |
||||
|
process.on('SIGTERM', () => process.exit(0)) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue