Browse Source

Initial Commit

1.0
Gisle Aune 6 years ago
commit
09f406cba7
  1. 5
      .babelrc
  2. 6
      .gitignore
  3. 38
      .jshintrc
  4. 12
      .vscode/settings.json
  5. 39
      Dockerfile
  6. 14
      README.md
  7. BIN
      assets/images/bg.png
  8. 1
      assets/test.txt
  9. 47
      config.js
  10. 7
      marko/browser.json
  11. 51
      marko/components/background/component.js
  12. 1
      marko/components/background/index.marko
  13. 23
      marko/components/background/style.less
  14. 52
      marko/components/content-list-item/components/item-meta/component.js
  15. 2
      marko/components/content-list-item/components/item-meta/index.marko
  16. 6
      marko/components/content-list-item/components/item-tag-list/index.marko
  17. 15
      marko/components/content-list-item/index.marko
  18. 51
      marko/components/content-list-item/style.less
  19. 5
      marko/components/content-list/index.marko
  20. 12
      marko/components/content-list/style.less
  21. 10
      marko/components/layout-navbar/index.marko
  22. 39
      marko/components/markdown/component.js
  23. 3
      marko/components/markdown/index.marko
  24. 40
      marko/components/markdown/plugins/wikilink.js
  25. 117
      marko/components/markdown/style.less
  26. 1
      marko/components/menu-blurb/index.marko
  27. 7
      marko/components/menu-blurb/style.less
  28. 1
      marko/components/menu-gap/index.marko
  29. 3
      marko/components/menu-gap/style.less
  30. 1
      marko/components/menu-header/index.marko
  31. 5
      marko/components/menu-header/style.less
  32. 13
      marko/components/menu-link/component.js
  33. 8
      marko/components/menu-link/index.marko
  34. 28
      marko/components/menu-link/style.less
  35. 12
      marko/components/menu/index.marko
  36. 14
      marko/components/menu/style.less
  37. 39
      marko/global/base.less
  38. 121
      marko/global/colors.less
  39. 32
      marko/global/navbar.less
  40. 16
      marko/page/layout.marko
  41. 13
      marko/page/logs/components/logs-list/index.marko
  42. 10
      marko/page/logs/components/logs-menu/index.marko
  43. 15
      marko/page/logs/components/page/component.js
  44. 5
      marko/page/logs/components/page/index.marko
  45. 6
      marko/page/logs/list.marko
  46. 52
      marko/page/story-content/components/chapter-meta/component.js
  47. 6
      marko/page/story-content/components/chapter-meta/index.marko
  48. 20
      marko/page/story-content/components/chapter-meta/style.less
  49. 5
      marko/page/story-content/components/chapter/component.js
  50. 10
      marko/page/story-content/components/chapter/index.marko
  51. 14
      marko/page/story-content/components/chapter/style.less
  52. 8
      marko/page/story-content/components/page/index.marko
  53. 9
      marko/page/story-content/components/page/style.less
  54. 40
      marko/page/story-content/components/story-content-menu/component.js
  55. 9
      marko/page/story-content/components/story-content-menu/index.marko
  56. 3
      marko/page/story-content/components/story-tags/index.marko
  57. 20
      marko/page/story-content/components/story-tags/style.less
  58. 6
      marko/page/story-content/view.marko
  59. 11
      marko/page/story/components/story-list/index.marko
  60. 11
      marko/page/story/components/story-menu/index.marko
  61. 9
      marko/page/story/list.marko
  62. 8
      marko/page/story/tag-list.marko
  63. 29
      middleware/locals.js
  64. 29
      middleware/passport.js
  65. 15
      middleware/session.js
  66. 4922
      package-lock.json
  67. 41
      package.json
  68. 46
      routes/auth.js
  69. 56
      routes/graphql.js
  70. 19
      routes/logs/index.js
  71. 13
      routes/story-content/index.js
  72. 22
      routes/story/by-category.js
  73. 24
      routes/story/by-tag.js
  74. 7
      routes/story/common.js
  75. 20
      routes/story/index.js
  76. 20
      routes/story/tag-list.js
  77. 53
      rpdata/api/Chapter.js
  78. 5
      rpdata/api/Character.js
  79. 84
      rpdata/api/LogHeader.js
  80. 147
      rpdata/api/Story.js
  81. 34
      rpdata/api/Tag.js
  82. 6
      rpdata/api/index.js
  83. 40
      rpdata/client.js
  84. 101
      server.js

5
.babelrc

@ -0,0 +1,5 @@
{
"presets": [
"es2015"
]
}

6
.gitignore

@ -0,0 +1,6 @@
*.marko.js
/node_modules
npm-debug.log
/.cache
/node_modules
/.static

38
.jshintrc

@ -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
}

12
.vscode/settings.json

@ -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
Dockerfile

@ -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"]

14
README.md

@ -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.

BIN
assets/images/bg.png

After

Width: 301  |  Height: 256  |  Size: 62 KiB

1
assets/test.txt

@ -0,0 +1 @@
Hello

47
config.js

@ -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

7
marko/browser.json

@ -0,0 +1,7 @@
{
"dependencies": [
"./global/base.less",
"./global/colors.less",
"./global/navbar.less"
]
}

51
marko/components/background/component.js

@ -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
}
}
}

1
marko/components/background/index.marko

@ -0,0 +1 @@
<img key="image" src=input.src style=state.style class=["background", state.orientation] />

23
marko/components/background/style.less

@ -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;
}

52
marko/components/content-list-item/components/item-meta/component.js

@ -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
}
}
}
}

2
marko/components/content-list-item/components/item-meta/index.marko

@ -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">&nbsp;</div>

6
marko/components/content-list-item/components/item-tag-list/index.marko

@ -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>

15
marko/components/content-list-item/index.marko

@ -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>

51
marko/components/content-list-item/style.less

@ -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;
}
}
}
}

5
marko/components/content-list/index.marko

@ -0,0 +1,5 @@
<table class="content-list" cellSpacing="0px">
<tbody>
<include(input.renderBody)/>
</tbody>
</table>

12
marko/components/content-list/style.less

@ -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;
}

10
marko/components/layout-navbar/index.marko

@ -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>

39
marko/components/markdown/component.js

@ -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

3
marko/components/markdown/index.marko

@ -0,0 +1,3 @@
<div class=["markdown-content", "color-text", input.class]>
<include(state.render) />
</div>

40
marko/components/markdown/plugins/wikilink.js

@ -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>`
}
)

117
marko/components/markdown/style.less

@ -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;
}
}

1
marko/components/menu-blurb/index.marko

@ -0,0 +1 @@
<div class=["menu-blurb", input.class]><include(input.renderBody)/></div>

7
marko/components/menu-blurb/style.less

@ -0,0 +1,7 @@
.menu-blurb {
opacity: 0.5;
padding: 0.25em 1.5ch;
}
.menu-blurb.error {
color: red;
}

1
marko/components/menu-gap/index.marko

@ -0,0 +1 @@
<div class="menu-gap"></div>

3
marko/components/menu-gap/style.less

@ -0,0 +1,3 @@
.menu-gap {
padding: 0.25em;
}

1
marko/components/menu-header/index.marko

@ -0,0 +1 @@
<div class="menu-header color-menu"><include(input.renderBody)/></div>

5
marko/components/menu-header/style.less

@ -0,0 +1,5 @@
.menu-header {
padding: 0.125em 1.5ch;
opacity: 0.5;
user-select: none;
}

13
marko/components/menu-link/component.js

@ -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")
}
}

8
marko/components/menu-link/index.marko

@ -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>

28
marko/components/menu-link/style.less

@ -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;
}
}
}

12
marko/components/menu/index.marko

@ -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>

14
marko/components/menu/style.less

@ -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;
}

39
marko/global/base.less

@ -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;
}

121
marko/global/colors.less

@ -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;
}

32
marko/global/navbar.less

@ -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;
}
}
}

16
marko/page/layout.marko

@ -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>

13
marko/page/logs/components/logs-list/index.marko

@ -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>

10
marko/page/logs/components/logs-menu/index.marko

@ -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>

15
marko/page/logs/components/page/component.js

@ -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
})
}
}

5
marko/page/logs/components/page/index.marko

@ -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>

6
marko/page/logs/list.marko

@ -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>

52
marko/page/story-content/components/chapter-meta/component.js

@ -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
}
}
}
}

6
marko/page/story-content/components/chapter-meta/index.marko

@ -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>

20
marko/page/story-content/components/chapter-meta/style.less

@ -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;
}

5
marko/page/story-content/components/chapter/component.js

@ -0,0 +1,5 @@
module.exports = class {
onCreate(input) {
this.state = Object.assign({}, input.chapter)
}
}

10
marko/page/story-content/components/chapter/index.marko

@ -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>

14
marko/page/story-content/components/chapter/style.less

@ -0,0 +1,14 @@
.story-chapter {
padding-bottom: 1em;
.metadata {
padding: 0;
margin: 0;
}
a.anchor {
display: block;
position: relative;
top: -8em;
}
}

8
marko/page/story-content/components/page/index.marko

@ -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>

9
marko/page/story-content/components/page/style.less

@ -0,0 +1,9 @@
.story-content {
width: 90%;
max-width: 75ch;
margin: auto;
h1 {
text-align: center;
}
}

40
marko/page/story-content/components/story-content-menu/component.js

@ -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
}
}

9
marko/page/story-content/components/story-content-menu/index.marko

@ -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>

3
marko/page/story-content/components/story-tags/index.marko

@ -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>

20
marko/page/story-content/components/story-tags/style.less

@ -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;
}
}

6
marko/page/story-content/view.marko

@ -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>

11
marko/page/story/components/story-list/index.marko

@ -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>

11
marko/page/story/components/story-menu/index.marko

@ -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>

9
marko/page/story/list.marko

@ -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>

8
marko/page/story/tag-list.marko

@ -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>

29
middleware/locals.js

@ -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()
}

29
middleware/passport.js

@ -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

15
middleware/session.js

@ -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

41
package.json

@ -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"
}
}

46
routes/auth.js

@ -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

56
routes/graphql.js

@ -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

19
routes/logs/index.js

@ -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

13
routes/story-content/index.js

@ -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},
})
}

22
routes/story/by-category.js

@ -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

24
routes/story/by-tag.js

@ -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

7
routes/story/common.js

@ -0,0 +1,7 @@
const {storyApi} = require("../../rpdata/api/Story")
module.exports = (req, res, next) => {
res.locals.categories = storyApi.categories()
next()
}

20
routes/story/index.js

@ -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

20
routes/story/tag-list.js

@ -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

53
rpdata/api/Chapter.js

@ -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}

5
rpdata/api/Character.js

@ -0,0 +1,5 @@
const {query} = require("../client")
class Character {
}

84
rpdata/api/LogHeader.js

@ -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}

147
rpdata/api/Story.js

@ -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}

34
rpdata/api/Tag.js

@ -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}

6
rpdata/api/index.js

@ -0,0 +1,6 @@
module.exports = {
...require("./LogHeader"),
...require("./Chapter"),
...require("./Story"),
...require("./Tag"),
}

40
rpdata/client.js

@ -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 }

101
server.js

@ -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))
Loading…
Cancel
Save