Initial Commit
This commit is contained in:
@@ -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
|
||||
}
|
||||
Vendored
+12
@@ -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"
|
||||
}
|
||||
+39
@@ -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.
|
||||
Binary file not shown.
|
After Width: | Height: | 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)
|
||||
Generated
+4922
File diff suppressed because it is too large
Load Diff
@@ -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))
|
||||
Reference in New Issue
Block a user