Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47f71c733b | ||
|
|
326e4478c0 | ||
|
|
68a1a64fe3 | ||
|
|
cdd165ab03 | ||
|
|
a8f4d68f1e | ||
|
|
b57873091b | ||
|
|
7a1884d1cf | ||
|
|
5be6b1ecec | ||
|
|
ea89975b14 | ||
|
|
850114af2d | ||
|
|
39d8254b9b | ||
|
|
adf73a4623 | ||
|
|
1e022d6116 | ||
|
|
d832e66402 | ||
|
|
94d0a83f46 | ||
|
|
824a8dae23 |
@@ -1,4 +1,5 @@
|
||||
*.marko.js
|
||||
*.prebuild.json
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
/.cache
|
||||
|
||||
+2
-2
@@ -26,8 +26,8 @@ RUN rm -rf .static
|
||||
RUN rm -rf .cache
|
||||
RUN rm -rf .git
|
||||
|
||||
# Build templates (when that works)
|
||||
# RUN node build.js
|
||||
# Build templates
|
||||
RUN node build.js
|
||||
|
||||
## 2. Regroup
|
||||
FROM node:10-alpine
|
||||
|
||||
@@ -1,53 +1,27 @@
|
||||
const build = require("./builder")
|
||||
const prebuild = require("@marko/prebuild");
|
||||
|
||||
// Setup global environment
|
||||
require("marko/node-require").install()
|
||||
require("es6-promise").polyfill()
|
||||
const config = require("./marko-config")
|
||||
config.loadPrebuild = false
|
||||
|
||||
// Configure lasso
|
||||
require("lasso").configure({
|
||||
plugins: [
|
||||
"lasso-marko",
|
||||
"lasso-less",
|
||||
],
|
||||
outputDir: "./.static",
|
||||
bundlingEnabled: true,
|
||||
minify: true,
|
||||
fingerprintsEnabled: true,
|
||||
prebuild.run({
|
||||
config: config,
|
||||
pages: [ // List of pages to prebuild.
|
||||
"./marko/page/story/list.marko",
|
||||
"./marko/page/story/tag-list.marko",
|
||||
"./marko/page/logs/list.marko",
|
||||
"./marko/page/data/channels.marko",
|
||||
"./marko/page/data/characters.marko",
|
||||
"./marko/page/story-content/view.marko",
|
||||
"./marko/page/logs-content/view.marko",
|
||||
]
|
||||
}).then(lassoPrebuildResult => {
|
||||
const builds = lassoPrebuildResult._buildsByPath
|
||||
|
||||
require: {
|
||||
builtins: {
|
||||
fs: require.resolve("empty-module"),
|
||||
},
|
||||
for (const key in builds) {
|
||||
if (!builds.hasOwnProperty(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
transforms: [
|
||||
{
|
||||
transform: "lasso-babel-transform",
|
||||
config: {
|
||||
extensions: [".js", ".es6"], // Enabled file extensions. Default: [".js", ".es6"]
|
||||
babelOptions: {
|
||||
presets: [ "es2015" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
console.log("Built", key)
|
||||
}
|
||||
})
|
||||
|
||||
// Render templates
|
||||
async function buildAll() {
|
||||
await build("./marko/page/story/list.marko")
|
||||
await build("./marko/page/story/tag-list.marko", {tags: []})
|
||||
await build("./marko/page/logs/list.marko")
|
||||
await build("./marko/page/data/channels.marko", {channels: [], user: {}})
|
||||
await build("./marko/page/data/characters.marko", {characters: [], user: {}})
|
||||
await build("./marko/page/story-content/view.marko", {story: {chapters: []}})
|
||||
await build("./marko/page/logs-content/view.marko", {log: {posts: [], channel: {}}})
|
||||
}
|
||||
|
||||
buildAll().then(() => {
|
||||
console.log("Done!")
|
||||
}).catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
module.exports = function build(path, data) {
|
||||
console.log("Building", path)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const render = require(path).render(data)
|
||||
render.once("error", err => reject)
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (render._remaining === 0) {
|
||||
clearInterval(interval)
|
||||
resolve()
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
const isProduction = process.env.NODE_ENV === "production"
|
||||
|
||||
module.exports = {
|
||||
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
|
||||
loadPrebuild: isProduction, // Only load prebuilt stuff in prod.
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,10 @@ module.exports = class {
|
||||
|
||||
this.state.text = dateStr
|
||||
|
||||
if (updated != null && Date.parse(updated) > Date.parse(value)) {
|
||||
if (updated != null && Date.parse(updated) > (Date.parse(value) + 7200000)) {
|
||||
const m2 = moment(updated)
|
||||
this.state.tooltip = "Originally posted: " + dateStr
|
||||
this.state.text = m2.format("MMM D, YYYY") + " *"
|
||||
this.state.text = m2.format("MMM D, YYYY")
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
@@ -7,6 +7,11 @@ tr.content-list-item {
|
||||
text-align: center;
|
||||
width: 2.5ch;
|
||||
outline: 0.05px solid;
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
width: 1.25ch;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
> td.content {
|
||||
|
||||
@@ -7,7 +7,7 @@ module.exports = class {
|
||||
|
||||
onInput(input) {
|
||||
if (!input.user || !input.user.loggedIn) {
|
||||
this.state.permitted = false
|
||||
this.state.permitted = input.not
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,5 +18,10 @@ module.exports = class {
|
||||
} else {
|
||||
this.state.permitted = input.user.permissions.includes(input.permission)
|
||||
}
|
||||
|
||||
// Flip the above permission
|
||||
if (input.not) {
|
||||
this.state.permitted = !this.state.permitted
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<div class="menu-gap"></div>
|
||||
<div class=["menu-gap", input.class]></div>
|
||||
@@ -6,7 +6,7 @@ module.exports = class {
|
||||
}
|
||||
|
||||
onInput(input) {
|
||||
this.state.classes = ["menu-link", "color-menu"]
|
||||
this.state.classes = ["menu-link", "color-menu"].concat(input.class)
|
||||
|
||||
if (input.dark) {
|
||||
this.state.classes.push(input)
|
||||
|
||||
@@ -3,9 +3,14 @@ module.exports = class {
|
||||
this.state = {
|
||||
loggingIn: false,
|
||||
loggingOut: false,
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
toggleEnabled() {
|
||||
this.state.enabled = !this.state.enabled
|
||||
}
|
||||
|
||||
loginClicked() {
|
||||
this.state.loggingIn = true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<div class="menu">
|
||||
<div class=["menu", state.enabled ? "enabled" : "disabled"]>
|
||||
<div class="menu-body">
|
||||
<menu-link icon=(state.enabled ? "<" : ">") on-click("toggleEnabled") class="showhide">${state.enabled ? "Hide Menu" : "Show Menu"}</menu-link>
|
||||
<menu-gap class="showhide" />
|
||||
<include(input.renderBody) />
|
||||
</div>
|
||||
<if(input.user != null)>
|
||||
|
||||
@@ -12,4 +12,28 @@
|
||||
user-select: none;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
.showhide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.menu.disabled {
|
||||
.menu-link, .menu-header, .menu-blurb, .menu-gap {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.showhide {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: static;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-left: -0.25ch;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
module.exports = class {
|
||||
onCreate() {
|
||||
this.mounted = false
|
||||
this.opened = false
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.emit("close")
|
||||
this.opened = false
|
||||
|
||||
if (this.mounted) {
|
||||
window.removeEventListener("keydown", this.onPress)
|
||||
@@ -17,8 +19,9 @@ module.exports = class {
|
||||
}
|
||||
|
||||
if (input) {
|
||||
if (input.enabled && (!this.input || !this.input.enabled)) {
|
||||
if (input.enabled && !this.opened) {
|
||||
this.emit("open")
|
||||
this.opened = true
|
||||
}
|
||||
|
||||
if (input.closable) {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class=["right-menu", input.class]>
|
||||
<div class="menu-body">
|
||||
<include(input.renderBody) />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
.right-menu {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 28ch;
|
||||
|
||||
padding: 0.333em;
|
||||
padding-bottom: 1em;
|
||||
|
||||
text-align: left;
|
||||
|
||||
user-select: none;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
.showhide {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.right-menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="tag-options">
|
||||
<div class="field">
|
||||
<label>Tag Name</label>
|
||||
<input key="name" placeholder="Name" on-change("change", "name") value=state.values.name />
|
||||
<input key="name" autofocus placeholder="Name" on-change("change", "name") value=state.values.name />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Tag Kind</label>
|
||||
|
||||
+16
-2
@@ -7,19 +7,33 @@ body {
|
||||
|
||||
main {
|
||||
margin-left: 30ch; // See side-menu
|
||||
width: calc(100% - 30ch);
|
||||
width: calc(100% - 32ch);
|
||||
margin-right: 0;
|
||||
}
|
||||
@media screen and (min-width: 700px) {
|
||||
main.with-right {
|
||||
width: calc(100% - 60ch)
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
body {
|
||||
main {
|
||||
margin-left: 0.25ch;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Less glaring chrome/ium scrollbars.
|
||||
*/
|
||||
::-webkit-scrollbar {
|
||||
::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
/* for vertical scrollbars */
|
||||
height: 2px;
|
||||
|
||||
@@ -19,7 +19,7 @@ body.theme-story {
|
||||
color: #4BF;
|
||||
}
|
||||
.color-highlight-dark {
|
||||
background-color: rgba(0, 9, 18, 0.50);
|
||||
background-color: rgba(0, 4, 9, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ body.theme-logs {
|
||||
color: #BBB;
|
||||
}
|
||||
.color-highlight-dark {
|
||||
background-color: rgba(18, 9, 0, 0.50);
|
||||
background-color: rgba(9, 4, 0, 0.75);
|
||||
}
|
||||
.color-highlight-primary {
|
||||
background-color: rgba(255, 187, 17, 0.25);
|
||||
@@ -83,7 +83,7 @@ body.theme-data {
|
||||
color: #BBB;
|
||||
}
|
||||
.color-highlight-dark {
|
||||
background-color: rgba(18, 8, 0, 0.50);
|
||||
background-color: rgba(8, 4, 0, 0.75);
|
||||
}
|
||||
.color-highlight-primary {
|
||||
background-color: rgba(204, 204, 34, 0.25);
|
||||
|
||||
@@ -30,4 +30,14 @@ nav.layout-navbar {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
nav.layout-navbar {
|
||||
position: static;
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
|
||||
font-size: 1.25em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<p class="color-error">${state.error}</p>
|
||||
|
||||
<label>Name</label>
|
||||
<input key="name" placeholder="(Required)" class="big" on-change("change", "name") value=state.values.name />
|
||||
<input key="name" autofocus placeholder="(Required)" class="big" on-change("change", "name") value=state.values.name />
|
||||
|
||||
<label>Event Name</label>
|
||||
<input key="eventName" placeholder="(None)" on-change("change", "eventName") value=state.values.eventName />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<p class="color-error">${state.error}</p>
|
||||
|
||||
<label>Full Name</label>
|
||||
<input key="name" placeholder="" class="big" on-change("change", "name") value=state.values.name />
|
||||
<input key="name" autofocus placeholder="" class="big" on-change("change", "name") value=state.values.name />
|
||||
|
||||
<label>IRC Nick</label>
|
||||
<input key="nick" placeholder="" on-change("change", "nick") value=state.values.nick />
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<menu-header>List</menu-header>
|
||||
<menu-link key="characters" selected=input.selected.characters icon="C" href="/data/characters/">Characters</menu-link>
|
||||
<menu-link key="channels" selected=input.selected.channels icon="#" href="/data/channels/">Channels</menu-link>
|
||||
<menu-gap />
|
||||
<if-permitted user=input.user permission=["member", "character.add", "channel.add"]>
|
||||
<menu-gap />
|
||||
<menu-header>Add</menu-header>
|
||||
<if-permitted user=input.user permission=["character.add", "member"]>
|
||||
<menu-link dark key="characters_add" icon="+" on-click("emit", "open", "character.add")>Character</menu-link>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<p class="color-error">${state.error}</p>
|
||||
|
||||
<label>Event Name</label>
|
||||
<input key="eventName" placeholder="(None)" on-change("change", "eventName") value=state.values.eventName />
|
||||
<input key="eventName" autofocus placeholder="(None)" on-change("change", "eventName") value=state.values.eventName />
|
||||
|
||||
<label>Location Name</label>
|
||||
<input key="locationName" placeholder="(None)" on-change("change", "locationName") value=state.values.locationName />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<p class="color-error">${state.error}</p>
|
||||
|
||||
<label>Full Name</label>
|
||||
<input key="name" placeholder="" class="big" on-change("change", "name") value=state.values.name />
|
||||
<input key="name" autofocus placeholder="" class="big" on-change("change", "name") value=state.values.name />
|
||||
|
||||
<label>Short Name</label>
|
||||
<input key="shortName" placeholder="E.g. first/given name" on-change("change", "shortName") value=state.values.shortName />
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<head>
|
||||
<meta content="AiteStory is a timeline of aiterp IC content that doesn't fit the wiki format" name="description" />
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes" name="viewport" />
|
||||
<meta content="#111111" name="theme-color" />
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
|
||||
|
||||
<link rel="stylesheet" href="/hax/global.css">
|
||||
</head>
|
||||
<title>${input.title ? `${input.title} - Aite RP` : "Aite RP"}</title>
|
||||
<lasso-head/>
|
||||
</head>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<p key="error" class="color-error">${state.error}</p>
|
||||
|
||||
<label>Timestamp</label>
|
||||
<input key="time" placeholder="Title" class="big" on-change("change", "time") value=state.values.time />
|
||||
<input key="time" autofocus placeholder="Title" class="big" on-change("change", "time") value=state.values.time />
|
||||
|
||||
<label>Kind</label>
|
||||
<select key="kind" class="big" placeholder="Kind" on-change("change", "kind") value=state.values.kind>
|
||||
|
||||
@@ -6,7 +6,7 @@ import moment from "moment"
|
||||
<p key="error" class="color-error">${state.error}</p>
|
||||
|
||||
<label>Title</label>
|
||||
<input key="title" placeholder=(input.log.channel.name + " – " + moment(input.log.date).format("MMMM D, YYYY")) class="big" on-change("change", "title") value=state.values.title />
|
||||
<input key="title" autofocus placeholder=(input.log.channel.name + " – " + moment(input.log.date).format("MMMM D, YYYY")) class="big" on-change("change", "title") value=state.values.title />
|
||||
|
||||
<label>Event Name</label>
|
||||
<input key="event" placeholder="(Optional)" on-change("change", "event") value=state.values.event />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<p key="error" class="color-error">${state.error}</p>
|
||||
|
||||
<label>Timestamp</label>
|
||||
<input key="time" placeholder="Title" class="big" on-change("change", "time") value=state.values.time />
|
||||
<input key="time" autofocus placeholder="Title" class="big" on-change("change", "time") value=state.values.time />
|
||||
|
||||
<label>Kind</label>
|
||||
<select key="kind" class="big" placeholder="Kind" on-change("change", "kind") value=state.values.kind>
|
||||
|
||||
@@ -7,6 +7,7 @@ module.exports = class {
|
||||
nameSuffix: "",
|
||||
modal: null,
|
||||
removed: false,
|
||||
multipart: false,
|
||||
}
|
||||
|
||||
this.updatePost(input)
|
||||
@@ -38,6 +39,9 @@ module.exports = class {
|
||||
return
|
||||
}
|
||||
|
||||
const timediff = Date.parse(input.post.time) - Date.parse(input.prev.time)
|
||||
this.state.multipart = (input.post.nick == input.prev.nick && input.prev.multipart !== false) && (Math.abs(timediff) < 60000);
|
||||
|
||||
this.updatePost(input)
|
||||
}
|
||||
|
||||
@@ -46,7 +50,7 @@ module.exports = class {
|
||||
this.state.name = input.post.nick
|
||||
this.state.nameSuffix = ""
|
||||
|
||||
this.state.text = input.post.text.replace(/\x02/g, "**")
|
||||
this.state.text = input.post.text.replace(/\x02/g, "**").replace(/\x1D/g, "_")
|
||||
|
||||
if (input.post.kind === "text" && !this.state.text.includes("\"")) {
|
||||
this.state.text = '"' + this.state.text + '"'
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
$ const multipart = (input.post.nick == input.prev.nick && input.prev.multipart !== false);
|
||||
|
||||
<div if(!state.removed) class=["post", component.kindClass("post-type"), multipart ? "multipart" : null]>
|
||||
<div if(!state.removed) class=["post", component.kindClass("post-type"), state.multipart ? "multipart" : null]>
|
||||
<div class="post-meta-row">
|
||||
<div class="options color-menu">
|
||||
<if-permitted user=input.user permission="post.move">
|
||||
@@ -26,7 +24,7 @@ $ const multipart = (input.post.nick == input.prev.nick && input.prev.multipart
|
||||
<markdown class="post-content post-scene" source=state.text />
|
||||
</div>
|
||||
<div if(input.post.kind === "action") class="post-body color-text">
|
||||
$ const prefix = (!multipart ? state.shortName + state.nameSuffix + " " : "");
|
||||
$ const prefix = (!state.multipart ? state.shortName + state.nameSuffix + " " : "");
|
||||
<markdown class="post-content post-action" source=(prefix + state.text) />
|
||||
</div>
|
||||
<div if(input.post.kind === "text") class="post-body color-text">
|
||||
|
||||
@@ -9,7 +9,7 @@ div.post {
|
||||
div.options {
|
||||
position: absolute;
|
||||
text-align: right;
|
||||
width: calc(100% - 35.5ch);
|
||||
width: calc(100% - 38.75ch);
|
||||
max-width: 95ch;
|
||||
|
||||
a {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<label>Search</label>
|
||||
<input key="search" placeholder="" class="big" value=state.search />
|
||||
<input key="search" autofocus placeholder="" class="big" value=state.search />
|
||||
|
||||
<label>Filters</label>
|
||||
<div if(state.search.length > 0) class="filter-row" >
|
||||
|
||||
@@ -6,7 +6,7 @@ import moment from "moment"
|
||||
<p key="error" class="color-error">${state.error}</p>
|
||||
|
||||
<label>Title</label>
|
||||
<input key="title" placeholder="(Optional)" class="big" on-change("change", "title") value=state.values.title />
|
||||
<input key="title" autofocus placeholder="(Optional)" class="big" on-change("change", "title") value=state.values.title />
|
||||
|
||||
<label>Date</label>
|
||||
<input key="date" placeholder="(Required)" on-change("change", "date") value=state.values.date />
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
this.state = {
|
||||
characters: [],
|
||||
channels: [],
|
||||
eventNames: [],
|
||||
filters: [],
|
||||
search: "",
|
||||
activeMap: {
|
||||
events: {},
|
||||
characters: {},
|
||||
channels: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
onInput(input) {
|
||||
this.update(input)
|
||||
}
|
||||
|
||||
onMount(input) {
|
||||
this.getEl("search").addEventListener("keydown", ev => {
|
||||
if (this.waiting) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.timeout != null) {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
|
||||
if (ev.key === "Enter") {
|
||||
this.emit("add", "search", ev.target.value)
|
||||
this.getEl("search").value = ""
|
||||
}
|
||||
|
||||
this.timeout = setTimeout(() => {
|
||||
this.timeout = null
|
||||
this.state.search = ev.target.value
|
||||
|
||||
this.update(this.input)
|
||||
}, 200)
|
||||
})
|
||||
}
|
||||
|
||||
update(input) {
|
||||
if (input.filter) {
|
||||
this.updateActive(input)
|
||||
}
|
||||
|
||||
const search = this.state.search.toLocaleLowerCase()
|
||||
|
||||
this.state.characters = input.characters.filter(c => !this.state.activeMap.characters[c.id]).filter(c => this.matches(c.name, search)).sort((a,b) => a.name.localeCompare(b.name))
|
||||
this.state.channels = input.channels.filter(c => !this.state.activeMap.channels[c.name]).filter(c => this.matches(c.name, search)).sort((a,b) => a.name.localeCompare(b.name))
|
||||
this.state.eventNames = input.eventNames.filter(e => !this.state.activeMap.events[e]).filter(e => this.matches(e, search)).sort((a,b) => a.localeCompare(b))
|
||||
}
|
||||
|
||||
updateActive({filter, characters, channels, eventNames}) {
|
||||
const filters = []
|
||||
const activeMap = {
|
||||
events: {},
|
||||
characters: {},
|
||||
channels: {},
|
||||
}
|
||||
|
||||
if (filter.search != null) {
|
||||
filters.push({
|
||||
type: "search",
|
||||
key: "search",
|
||||
icon: "T",
|
||||
color: "color-text",
|
||||
text: filter.search,
|
||||
})
|
||||
}
|
||||
|
||||
for (const character of (filter.characters || [])) {
|
||||
activeMap.characters[character] = true
|
||||
|
||||
filters.push({
|
||||
type: "characters",
|
||||
key: character,
|
||||
icon: "C",
|
||||
color: "color-tag-character",
|
||||
id: character,
|
||||
text: (characters.find(c => c.id === character) || {name: "Unknown ("+character+")"}).name,
|
||||
})
|
||||
}
|
||||
|
||||
for (const channel of (filter.channels || [])) {
|
||||
activeMap.channels[channel] = true
|
||||
|
||||
filters.push({
|
||||
type: "channels",
|
||||
key: channel.replace(/'/g, "__"),
|
||||
icon: "#",
|
||||
color: "color-tag-location",
|
||||
id: channel,
|
||||
text: channel,
|
||||
})
|
||||
}
|
||||
|
||||
for (const event of (filter.events || [])) {
|
||||
activeMap.events[event] = true
|
||||
|
||||
filters.push({
|
||||
type: "events",
|
||||
key: event.replace(/['\s\.]/g, "__"),
|
||||
icon: "E",
|
||||
color: "color-tag-event",
|
||||
id: event,
|
||||
text: event,
|
||||
})
|
||||
}
|
||||
|
||||
this.state.filters = filters
|
||||
this.state.activeMap = activeMap
|
||||
}
|
||||
|
||||
matches(str, search) {
|
||||
if (search == "") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (search.length > str.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return str.toLowerCase().indexOf(search) !== -1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<right-menu class="filter-menu">
|
||||
<input key="search" />
|
||||
<menu-gap />
|
||||
<menu-header if(state.filters.length > 0)>Active Filters</menu-header>
|
||||
<div class="filter-section">
|
||||
<menu-link for(filter in state.filters)
|
||||
on-click("emit", "remove", filter.type, filter.id)
|
||||
textClass=filter.color
|
||||
unselectable
|
||||
icon=filter.icon >${filter.text}</menu-link>
|
||||
</div>
|
||||
<menu-gap if(state.filters.length > 0) />
|
||||
<menu-header>Characters</menu-header>
|
||||
<div class="filter-section">
|
||||
<menu-link for(character in state.characters)
|
||||
on-click("emit", "add", "characters", character.id)
|
||||
icon=character.name.charAt(0).toUpperCase()
|
||||
textClass="color-tag-character">${character.name}</menu-link>
|
||||
</div>
|
||||
<menu-gap />
|
||||
<menu-header>Channels</menu-header>
|
||||
<div class="filter-section">
|
||||
<menu-link for(channel in state.channels)
|
||||
on-click("emit", "add", "channels", channel.name)
|
||||
icon=channel.name.charAt(1).toUpperCase()
|
||||
textClass="color-tag-location">${channel.name}</menu-link>
|
||||
</div>
|
||||
<menu-gap />
|
||||
<menu-header>Events</menu-header>
|
||||
<div class="filter-section">
|
||||
<menu-link for(eventName in state.eventNames)
|
||||
on-click("emit", "add", "events", eventName)
|
||||
icon=eventName.charAt(0).toUpperCase()
|
||||
textClass="color-tag-event">${eventName}</menu-link>
|
||||
</div>
|
||||
</right-menu>
|
||||
@@ -0,0 +1,24 @@
|
||||
div.right-menu.filter-menu {
|
||||
input {
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
width: 95%;
|
||||
margin-top: 1em;
|
||||
margin-left: 2.5%;
|
||||
border: 1px solid rgba(119, 119, 119, 0.25);
|
||||
font-size: 1.1em;
|
||||
box-sizing: border-box;
|
||||
|
||||
color: #999;
|
||||
}
|
||||
input:focus {
|
||||
color: #FC1;
|
||||
}
|
||||
|
||||
div.filter-section {
|
||||
max-height: calc(25vh - 3em);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
@@ -13,54 +13,7 @@ module.exports = class {
|
||||
return
|
||||
}
|
||||
|
||||
const filters = []
|
||||
const {characters, channels, events, search} = input.filter
|
||||
|
||||
if (search != null) {
|
||||
filters.push({
|
||||
type: "search",
|
||||
key: "search",
|
||||
icon: "T",
|
||||
color: "color-text",
|
||||
text: search,
|
||||
})
|
||||
}
|
||||
|
||||
for (const character of (characters || [])) {
|
||||
filters.push({
|
||||
type: "characters",
|
||||
key: character,
|
||||
icon: "C",
|
||||
color: "color-tag-character",
|
||||
id: character,
|
||||
text: (input.characters.find(c => c.id === character) || {name: "Unknown ("+character+")"}).name,
|
||||
})
|
||||
}
|
||||
|
||||
for (const channel of (channels || [])) {
|
||||
filters.push({
|
||||
type: "channels",
|
||||
key: channel.replace(/'/g, "__"),
|
||||
icon: "#",
|
||||
color: "color-tag-location",
|
||||
id: channel,
|
||||
text: channel,
|
||||
})
|
||||
}
|
||||
|
||||
for (const event of (events || [])) {
|
||||
filters.push({
|
||||
type: "events",
|
||||
key: event.replace(/['\s\.]/g, "__"),
|
||||
icon: "E",
|
||||
color: "color-tag-event",
|
||||
id: event,
|
||||
text: event,
|
||||
})
|
||||
}
|
||||
|
||||
this.state.filters = filters
|
||||
this.state.filtered = (filters.length > 0)
|
||||
this.state.filtered = input.filter.search || input.filter.channels || input.filter.characters || input.filter.events
|
||||
}
|
||||
|
||||
select(value) {
|
||||
|
||||
@@ -4,9 +4,4 @@
|
||||
<if-permitted key="if-permitted" user=input.user permission="log.add">
|
||||
<menu-link key="add_log" dark on-click("select", "log.add") icon="+">Add Log</menu-link>
|
||||
</if-permitted>
|
||||
<menu-gap />
|
||||
<menu-header key="filters">Filters</menu-header>
|
||||
<menu-link for(filter in state.filters) textClass=filter.color unselectable icon=filter.icon on-click("emit", "removefilter", filter.type, filter.id)>${filter.text}</menu-link>
|
||||
<menu-link key="add_filter" dark on-click("select", "filter.add") icon="+">Add Filter</menu-link>
|
||||
<menu-gap />
|
||||
</menu>
|
||||
@@ -0,0 +1,10 @@
|
||||
const moment = require("moment")
|
||||
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
this.state = {
|
||||
expand: false,
|
||||
dateStr: moment(input.log.date).format("MMM D, YYYY"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
$ const fullNames = (input.log.characters.length < 4);
|
||||
|
||||
<tr key=input.log.shortId class="logs-table-row color-text">
|
||||
<td class="date color-primary">
|
||||
<a href=("/logs/"+input.log.id)>${state.dateStr}</a>
|
||||
</td>
|
||||
<td class="channel color-menu">
|
||||
<a on-click("emit", "addfilter", "channels", input.log.channelName)>${input.log.channelName}</a>
|
||||
</td>
|
||||
<td class="event color-tag-event">
|
||||
<a on-click("emit", "addfilter", "events", input.log.eventName)>${(input.log.eventName || "")}</a>
|
||||
</td>
|
||||
<td class="characters color-menu">
|
||||
<span for(character in input.log.characters | status-var=loop)>
|
||||
<a on-click("emit", "addfilter", "characters", character.id) class="name">${fullNames ? character.name : character.shortName}</a>
|
||||
<span class="comma" if(loop.getIndex() < (loop.getLength() - 1))>, </span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr if(state.expand) key=(input.log.shortId + "_expand") class="logs-table-row-expanded">
|
||||
|
||||
</tr>
|
||||
@@ -0,0 +1,31 @@
|
||||
tr.logs-table-row {
|
||||
td {
|
||||
padding: 0.25em 1ch;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(51, 51, 51, 0.5);
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
td.expand {
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
td.date {
|
||||
width: 16%;
|
||||
}
|
||||
td.channel {
|
||||
width: 14%;
|
||||
}
|
||||
td.event {
|
||||
width: 18%;
|
||||
}
|
||||
|
||||
td.character {
|
||||
a.name {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import moment from "moment"
|
||||
|
||||
<table class="logs-table color-highlight-dark color-primary">
|
||||
<tr key="_header">
|
||||
<th>Date</th>
|
||||
<th>Channel</th>
|
||||
<th class="event">Event</th>
|
||||
<th>Characters</th>
|
||||
</tr>
|
||||
<logs-table-row for(log in input.logs) key=log.shortId log=log on-addfilter("emit", "addfilter") />
|
||||
</table>
|
||||
@@ -0,0 +1,25 @@
|
||||
table.logs-table {
|
||||
outline: 1px solid;
|
||||
margin: 1em auto;
|
||||
width: 90%;
|
||||
max-width: 140ch;
|
||||
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
td.event, th.event {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
td.date {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
font-size: 0.66em;
|
||||
}
|
||||
}
|
||||
@@ -19,16 +19,22 @@ module.exports = class {
|
||||
|
||||
addFilter(type, filter) {
|
||||
if (type === "search") {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {search: filter})
|
||||
if (filter.trim().length > 0) {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {search: filter})
|
||||
} else {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {search: null})
|
||||
}
|
||||
|
||||
} else {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {[type]: (this.state.filter[type] || []).concat(filter)})
|
||||
this.state.filter = Object.assign({}, this.state.filter, {[type]: (this.state.filter[type] || []).filter(f => f !== filter).concat(filter)})
|
||||
}
|
||||
|
||||
this.updateQuery(this.state.filter)
|
||||
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
removeFilter(type, filter) {
|
||||
|
||||
if (type === "search") {
|
||||
this.state.filter = Object.assign({}, this.state.filter, {search: null})
|
||||
this.refresh()
|
||||
@@ -42,6 +48,31 @@ module.exports = class {
|
||||
this.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
this.updateQuery(this.state.filter)
|
||||
}
|
||||
|
||||
updateQuery(filter) {
|
||||
const queries = []
|
||||
|
||||
if (filter.characters) {
|
||||
queries.push("characters=" + filter.characters.join(","))
|
||||
}
|
||||
if (filter.channels) {
|
||||
queries.push("channels=" + filter.channels.map(c => encodeURIComponent(c)).join(","))
|
||||
}
|
||||
if (filter.events) {
|
||||
queries.push("events=" + filter.events.map(e => encodeURIComponent(e)).join(","))
|
||||
}
|
||||
if (filter.search) {
|
||||
queries.push("search=" + encodeURIComponent(filter.search))
|
||||
}
|
||||
|
||||
if (queries.length > 0) {
|
||||
history.replaceState("", "", `/logs/?${queries.join("&")}`)
|
||||
} else {
|
||||
history.replaceState("", "", "/logs/")
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<background src="/assets/images/bg.png" opacity=0.25 />
|
||||
<logs-menu key="menu" on-select("open") on-removefilter("removeFilter") selected=(input.selected || {}) user=input.user filter=state.filter characters=input.characters />
|
||||
<main>
|
||||
<logs-list logs=state.logs />
|
||||
<logs-menu key="menu" on-select("open") selected=(input.selected || {}) user=input.user filter=state.filter characters=input.characters />
|
||||
<filter-menu on-add("addFilter") on-remove("removeFilter") filter=state.filter characters=input.characters channels=input.channels eventNames=input.eventNames />
|
||||
<main class="with-right">
|
||||
<logs-table logs=state.logs on-addfilter("addFilter") />
|
||||
</main>
|
||||
<add-log-modal enabled=(state.modal === "log.add") on-close("close") />
|
||||
<add-filter-modal enabled=(state.modal === "filter.add") characters=input.characters filter=state.filter on-add("addFilter") on-close("close") />
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<include("../layout", {title: "Logs", site: "logs"})>
|
||||
<@body>
|
||||
<!-- Page needed to get component.js functionality -->
|
||||
<page logs=input.logs filter=input.filter characters=input.characters user=input.user selected=input.selected />
|
||||
<page logs=input.logs
|
||||
filter=input.filter
|
||||
characters=input.characters
|
||||
channels=input.channels
|
||||
eventNames=input.eventNames
|
||||
user=input.user
|
||||
selected=input.selected />
|
||||
</@body>
|
||||
</include>
|
||||
@@ -10,6 +10,10 @@ module.exports = class {
|
||||
this.emit("edit", data)
|
||||
}
|
||||
|
||||
moveChapter(data) {
|
||||
this.emit("move", data)
|
||||
}
|
||||
|
||||
removeChapter() {
|
||||
this.state.removed = true
|
||||
this.emit("remove")
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import moment from "moment"
|
||||
|
||||
<div class="story-chapter">
|
||||
<a class="anchor" id=input.chapter.id />
|
||||
<h1 if(input.chapter.title) class="title color-primary">${input.chapter.title}</h1>
|
||||
<div class="metadata">
|
||||
<div class="options color-menu">
|
||||
<if-permitted user=input.user author=input.chapter.author permission="chapter.edit">
|
||||
<a on-click("open", "edit") >Edit</a>
|
||||
</if-permitted>
|
||||
<if-permitted user=input.user author=input.chapter.author permission="chapter.move">
|
||||
<a on-click("open", "move") >Move</a>
|
||||
</if-permitted>
|
||||
<if-permitted user=input.user author=input.chapter.author permission="chapter.remove">
|
||||
<a on-click("open", "remove") >Remove</a>
|
||||
</if-permitted>
|
||||
</div>
|
||||
<chapter-meta kind="title" value=input.chapter.title />
|
||||
<chapter-meta kind="date" utc=true value=input.chapter.fictionalDate />
|
||||
<chapter-meta weak kind="date" value=input.chapter.createdDate />
|
||||
<chapter-meta weak kind="author" value=input.chapter.author />
|
||||
@@ -19,5 +20,6 @@ import moment from "moment"
|
||||
<markdown class="chapter-content color-text" source=input.chapter.source />
|
||||
|
||||
<edit-chapter-modal enabled=(state.modal === "edit") chapter=input.chapter on-close("close") on-edit("updateChapter") />
|
||||
<move-chapter-modal enabled=(state.modal === "move") chapter=input.chapter user=input.user on-close("close") on-move("moveChapter") />
|
||||
<remove-chapter-modal enabled=(state.modal === "remove") chapter=input.chapter on-close("close") on-remove("removeChapter") />
|
||||
</div>
|
||||
@@ -1,5 +1,12 @@
|
||||
.story-chapter {
|
||||
padding-bottom: 1em;
|
||||
padding-bottom: 2em;
|
||||
|
||||
h1.title {
|
||||
border-bottom: 1px solid rgba(34, 187, 255, 0.25);
|
||||
font-size: 1.75em;
|
||||
margin: 0;
|
||||
padding-bottom: 0.125em;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
padding: 0;
|
||||
@@ -15,7 +22,7 @@
|
||||
.options {
|
||||
position: absolute;
|
||||
text-align: right;
|
||||
width: calc(100% - 35.5ch);
|
||||
width: calc(100% - 38.75ch);
|
||||
max-width: 95ch;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<p key="error" class="color-error">${state.error}</p>
|
||||
|
||||
<label>Title</label>
|
||||
<input key="title" placeholder="(Optional)" class="big" on-change("change", "title") value=state.values.title />
|
||||
<input key="title" autofocus placeholder="(Optional)" class="big" on-change("change", "title") value=state.values.title />
|
||||
|
||||
<label>IC Date</label>
|
||||
<input key="icdate" placeholder="(Optional)" on-change("change", "fictionalDate") value=state.values.fictionalDate />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<p key="error" class="color-error">${state.error}</p>
|
||||
|
||||
<label>Title</label>
|
||||
<input key="title" placeholder="Title" class="big" on-change("change", "title") value=state.values.title />
|
||||
<input key="title" autofocus placeholder="Title" class="big" on-change("change", "title") value=state.values.title />
|
||||
|
||||
<label>IC Date</label>
|
||||
<input key="icdate" placeholder="IC Date" on-change("change", "fictionalDate") value=state.values.fictionalDate />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<p key="error" class="color-error">${state.error}</p>
|
||||
|
||||
<label>Title</label>
|
||||
<input key="name" placeholder="Name" class="big" on-change("change", "name") value=state.values.name />
|
||||
<input key="name" autofocus placeholder="Name" class="big" on-change("change", "name") value=state.values.name />
|
||||
|
||||
<label>IC Date</label>
|
||||
<input key="icdate" placeholder="IC Date" on-change("change", "fictionalDate") value=state.values.fictionalDate />
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
const {storyApi} = require("../../../../../rpdata/api/Story")
|
||||
const {chapterApi} = require("../../../../../rpdata/api/Chapter")
|
||||
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
this.state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
loadingStories: true,
|
||||
stories: [],
|
||||
selected: null,
|
||||
}
|
||||
|
||||
this.first = false
|
||||
}
|
||||
|
||||
change(key, ev) {
|
||||
const value = ev.target.value
|
||||
|
||||
switch (key) {
|
||||
case "selected": {
|
||||
this.state.selected = Object.assign({}, this.state.stories.find(s => s.id === value))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
const promises = []
|
||||
if ((this.input.user.permissions || []).includes("chapter.move")) {
|
||||
promises.push(storyApi.list())
|
||||
} else {
|
||||
promises.push(storyApi.list({author: this.input.user.name}))
|
||||
promises.push(storyApi.list({open: true}))
|
||||
}
|
||||
|
||||
this.state.loadingStories = true
|
||||
|
||||
Promise.all(promises).then(lists => {
|
||||
this.state.stories = unique(Array.prototype.concat.call(...lists))
|
||||
this.state.selected = this.state.stories[0]
|
||||
}).catch(err => {
|
||||
console.warn(err)
|
||||
}).then(() => {
|
||||
this.state.loadingStories = false
|
||||
})
|
||||
}
|
||||
|
||||
close() {
|
||||
this.first = false
|
||||
this.emit("close")
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this.state.loading) {
|
||||
return
|
||||
}
|
||||
|
||||
const input = {id: this.input.chapter.id, storyId: this.state.selected.id}
|
||||
|
||||
this.state.loading = true
|
||||
chapterApi.moveChapter(input).then(data => {
|
||||
this.emit("move", input)
|
||||
this.emit("close")
|
||||
}).catch(errs => {
|
||||
console.warn("Failed to edit:", errs)
|
||||
|
||||
this.state.error = "Failed to edit: " + errs[0].message
|
||||
}).then(() => {
|
||||
this.state.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function unique(stories) {
|
||||
const ids = {}
|
||||
const results = []
|
||||
|
||||
for (const story of stories) {
|
||||
if (ids[story.id]) {
|
||||
continue
|
||||
}
|
||||
|
||||
ids[story.id] = true
|
||||
results.push(story)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
$ const selected = (state.selected||{id:null});
|
||||
|
||||
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
|
||||
<h1>Move Chapter</h1>
|
||||
|
||||
<p key="error" class="color-error">${state.error}</p>
|
||||
|
||||
<label>Target story</label>
|
||||
<select key="story" on-change("change", "selected") autofocus value=selected.id>
|
||||
<option for(story in state.stories) value=story.id selected=(selected.id === story.id)>${story.name}</option>
|
||||
</select>
|
||||
|
||||
<if-permitted not permission="story.move">
|
||||
<p>Only stories you are allowed to move the chapter to appear in the above list.</p>
|
||||
</if-permitted>
|
||||
|
||||
<button disabled=state.loading on-click("save")>Move</button>
|
||||
</modal>
|
||||
@@ -24,6 +24,12 @@ module.exports = class {
|
||||
this.refreshStory()
|
||||
}
|
||||
|
||||
moveChapter(chapter, data) {
|
||||
if (data.storyId !== this.state.story.id) {
|
||||
this.state.story = Object.assign({}, this.state.story, {chapters: this.state.story.chapters.filter(ch => ch.id !== chapter.id)})
|
||||
}
|
||||
}
|
||||
|
||||
updateChapter(id, data) {
|
||||
const index = this.state.story.chapters.findIndex(ch => ch.id === id)
|
||||
if (index === -1) {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
so you can still read it (or back it up) before leaving the page.
|
||||
</p>
|
||||
</annotation>
|
||||
<chapter for(chapter in state.story.chapters) key=chapter.id chapter=chapter user=(!state.removed ? input.user : null) on-edit("updateChapter", chapter.id) on-remove("removeChapter", chapter.id)/>
|
||||
<chapter for(chapter in state.story.chapters) key=chapter.id chapter=chapter user=(!state.removed ? input.user : null) on-edit("updateChapter", chapter.id) on-remove("removeChapter", chapter.id) on-move("moveChapter", chapter) />
|
||||
</div>
|
||||
|
||||
<create-chapter-modal storyId=state.story.id enabled=(state.modal === "chapter.add") chapter=input.chapter on-close("close") on-add("addChapter") />
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<p key="error" class="color-error">${state.error}</p>
|
||||
|
||||
<label>Title</label>
|
||||
<input key="name" placeholder="Name" class="big" on-change("change", "name") value=state.values.name />
|
||||
<input key="name" autofocus placeholder="Name" class="big" on-change("change", "name") value=state.values.name />
|
||||
|
||||
<label>IC Date</label>
|
||||
<input key="icdate" placeholder="IC Date" on-change("change", "fictionalDate") value=state.values.fictionalDate />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<menu user=input.user>
|
||||
<menu-header>Story</menu-header>
|
||||
<menu-link key="index" selected=input.selected.index icon="S" href="/story/">Stories</menu-link>
|
||||
<menu-link if(input.user.loggedIn) key="unlisted" selected=input.selected.unlisted icon="S" href="/story/unlisted/">Unlisted</menu-link>
|
||||
<menu-link selected=input.selected.tags icon="T" href="/story/tag-list/">Tags</menu-link>
|
||||
<menu-gap if(input.menuAuthor || input.menuMonth || input.menuTags) />
|
||||
<if(input.menuMonth != null)>
|
||||
@@ -17,5 +18,4 @@
|
||||
<menu-gap />
|
||||
<menu-header>Categories</menu-header>
|
||||
<menu-link for(category in (input.categories||[])) key=(category.name) selected=(input.selected.category == category.name) href=("/story/by-category/"+category.name) icon=category.name.charAt(0)>${category.name}</menu-link>
|
||||
<menu-gap />
|
||||
</menu>
|
||||
@@ -3,7 +3,7 @@ const config = require("../config")
|
||||
module.exports = (req, res, next) => {
|
||||
if (res.marko) {
|
||||
res.markoAsync = async(template, input) => {
|
||||
const locals = Object.assign((res.locals || {}), input)
|
||||
const locals = Object.assign({}, (res.locals || {}), input)
|
||||
|
||||
try {
|
||||
for (const key in locals) {
|
||||
@@ -12,7 +12,7 @@ module.exports = (req, res, next) => {
|
||||
locals[key] = await value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (locals.user.permissions != null) {
|
||||
locals.user.permissions = await locals.user.permissions
|
||||
}
|
||||
|
||||
Generated
+10
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@gisle/rpdata-frontend",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.7",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -16,6 +16,15 @@
|
||||
"raptor-util": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"@marko/prebuild": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@marko/prebuild/-/prebuild-4.0.5.tgz",
|
||||
"integrity": "sha512-wuE73v78YTH5NQ8T8mr3Yr7j2hpivTnNQdpNMPBfy7w9AF8CtD5Jb0OciP5MuAukLXHhXv3ZVzJxYJbJYJXwIg==",
|
||||
"requires": {
|
||||
"babel-runtime": "^6.26.0",
|
||||
"lasso": "3.2.6"
|
||||
}
|
||||
},
|
||||
"JSONStream": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.4.tgz",
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@gisle/rpdata-frontend",
|
||||
"version": "1.0.7",
|
||||
"version": "1.1.0",
|
||||
"description": "RPData (www.aiterp.net) frontend/proxy",
|
||||
"main": "./index.js",
|
||||
"scripts": {
|
||||
@@ -12,6 +12,7 @@
|
||||
"homepage": "https://github.com/marko-js-samples/marko-express",
|
||||
"dependencies": {
|
||||
"@lasso/marko-taglib": "^1.0.12",
|
||||
"@marko/prebuild": "^4.0.5",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"body-parser": "^1.18.3",
|
||||
"browser-refresh": "^1.7.3",
|
||||
@@ -30,6 +31,7 @@
|
||||
"lasso-babel-transform": "^1.0.2",
|
||||
"lasso-less": "^3.0.1",
|
||||
"lasso-marko": "^2.4.4",
|
||||
"less": "^3.8.1",
|
||||
"markdown-it": "^8.4.2",
|
||||
"markdown-it-container": "^2.0.0",
|
||||
"markdown-it-regexp": "^0.4.0",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const fs = require("fs")
|
||||
const express = require("express")
|
||||
const less = require("less")
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
let lessData = ""
|
||||
let data = ""
|
||||
for (const name of fs.readdirSync("./marko/global/", "utf8")) {
|
||||
data += fs.readFileSync("./marko/global/"+name, {encoding:"utf8"}) + "\n\n"
|
||||
}
|
||||
data = less.render(data, (err, output) => {
|
||||
if (err == null) {
|
||||
lessData = output.css
|
||||
}
|
||||
})
|
||||
|
||||
router.get("/", (req, res) => {
|
||||
res.contentType("text/css").end(lessData)
|
||||
})
|
||||
|
||||
|
||||
module.exports = router
|
||||
@@ -3,6 +3,7 @@ const router = express.Router()
|
||||
|
||||
const {logHeaderApi, eventNames} = require("../../rpdata/api/LogHeader")
|
||||
const {charactersApi} = require("../../rpdata/api/Character")
|
||||
const {channelApi} = require("../../rpdata/api/Channel")
|
||||
|
||||
const listTemplate = require("../../marko/page/logs/list.marko")
|
||||
|
||||
@@ -27,6 +28,8 @@ router.get("/", async(req, res) => {
|
||||
filter: filter,
|
||||
logs: logHeaderApi.list(filter),
|
||||
characters: charactersApi.listHeaders(),
|
||||
channels: channelApi.list(),
|
||||
eventNames: logHeaderApi.eventNames(),
|
||||
selected: {index: true},
|
||||
})
|
||||
} catch(err) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const jwt = require("jsonwebtoken")
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
|
||||
const common = require("./common")
|
||||
const config = require("../../config")
|
||||
const {storyApi} = require("../../rpdata/api/Story")
|
||||
|
||||
const listTemplate = require("../../marko/page/story/list.marko")
|
||||
|
||||
router.get("/", common, async(req, res) => {
|
||||
if (!res.locals.user.loggedIn) {
|
||||
return res.status(401).end("You are not logged in")
|
||||
}
|
||||
|
||||
try {
|
||||
await res.markoAsync(listTemplate, {
|
||||
stories: storyApi.list({unlisted: true, author: res.locals.user.name}, {token: generateToken(res.locals.user.name)}),
|
||||
selected: {unlisted: true},
|
||||
})
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
*/
|
||||
function generateToken(user) {
|
||||
return jwt.sign({user, permissions: ["member"], exp: Math.floor((Date.now() / 1000) + 1200)}, config.backend.secret, {header: {kid: config.backend.kid}})
|
||||
}
|
||||
|
||||
module.exports = router
|
||||
+23
-5
@@ -21,7 +21,7 @@ class Chapter {
|
||||
}
|
||||
}
|
||||
|
||||
const chapterApi = {
|
||||
class ChapterAPI {
|
||||
/**
|
||||
* Call `addChapter(input)` mutation, returns the entire object.
|
||||
*
|
||||
@@ -46,7 +46,7 @@ const chapterApi = {
|
||||
|
||||
return new Chapter(ac.id, ac.title, ac.author, ac.source, ac.createdDate, ac.fictionalDate, ac.editedDate)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `editChapter(input)` mutation, returns editable fields
|
||||
@@ -70,7 +70,25 @@ const chapterApi = {
|
||||
|
||||
return editChapter
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `moveChapter(input)` mutation, returns the ID.
|
||||
*
|
||||
* @param {{id:string, storyId:string}} input
|
||||
* @returns {Promise<{id:string}>}
|
||||
*/
|
||||
moveChapter(input) {
|
||||
return query(`
|
||||
mutation MoveChapter($input: ChapterMoveInput!) {
|
||||
moveChapter(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`, {input}, {permissions: ["member", "story.move"]}).then(({moveChapter}) => {
|
||||
return moveChapter
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `removeChapter(input)` mutation, returns only the id
|
||||
@@ -88,7 +106,7 @@ const chapterApi = {
|
||||
`, {input}, {permissions: ["member", "story.edit"]}).then(({removeChapter}) => {
|
||||
return removeChapter
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {Chapter, chapterApi}
|
||||
module.exports = {Chapter, chapterApi: new ChapterAPI}
|
||||
+2
-2
@@ -54,7 +54,7 @@ const storyApi = {
|
||||
* @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 = {}) {
|
||||
list(filter = {}, options = {}) {
|
||||
if (filter.earliestFictionalDate != null && typeof(filter.earliestFictionalDate) !== "string") {
|
||||
filter.earliestFictionalDate = filter.earliestFictionalDate.toISOString()
|
||||
}
|
||||
@@ -80,7 +80,7 @@ const storyApi = {
|
||||
updatedDate
|
||||
}
|
||||
}
|
||||
`, {filter}).then(({stories}) => {
|
||||
`, {filter}, options).then(({stories}) => {
|
||||
return stories.map(d => new Story(d.id, d.name, d.author, d.category, d.listed, d.open, d.createdDate, d.updatedDate, d.fictionalDate, d.tags))
|
||||
})
|
||||
},
|
||||
|
||||
@@ -5,9 +5,6 @@ require("es6-promise").polyfill()
|
||||
// Load server config
|
||||
const config = require("./config")
|
||||
|
||||
// Load utilities
|
||||
const build = require("./builder")
|
||||
|
||||
// Express depedencies
|
||||
const express = require("express")
|
||||
const proxy = require("express-http-proxy")
|
||||
@@ -24,35 +21,7 @@ const session = require("./middleware/session")
|
||||
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
|
||||
}
|
||||
})
|
||||
lasso.configure(require("./marko-config"))
|
||||
|
||||
// Set trusted proxy
|
||||
if (config.trustedProxy != null) {
|
||||
@@ -79,8 +48,18 @@ app.use("/auth", require("./routes/auth"))
|
||||
app.use("/graphql", require("./routes/graphql"))
|
||||
app.use("/playground", proxy(config.playgroundEndpoint))
|
||||
|
||||
// Global css doesn't work with prebuild.
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
app.use("/hax/global.css", require("./routes/globalcss"))
|
||||
} else {
|
||||
app.get("/hax/global.css", (req, res) => {
|
||||
res.type(".css").end("")
|
||||
})
|
||||
}
|
||||
|
||||
// Page routes
|
||||
app.use("/story/", require("./routes/story"))
|
||||
app.use("/story/unlisted/", require("./routes/story/unlisted"))
|
||||
app.use("/story/by-category/", require("./routes/story/by-category"))
|
||||
app.use("/story/by-tag/", require("./routes/story/by-tag"))
|
||||
app.use("/story/by-month/", require("./routes/story/by-month"))
|
||||
@@ -99,33 +78,15 @@ app.get("/", function(req, res) {
|
||||
res.redirect("/story/")
|
||||
})
|
||||
|
||||
async function main() {
|
||||
// Build templates
|
||||
if (isProduction) {
|
||||
await build("./marko/page/story/list.marko")
|
||||
await build("./marko/page/story/tag-list.marko", {tags: []})
|
||||
await build("./marko/page/logs/list.marko")
|
||||
await build("./marko/page/data/channels.marko", {channels: [], user: {}})
|
||||
await build("./marko/page/data/characters.marko", {characters: [], user: {}})
|
||||
await build("./marko/page/story-content/view.marko", {story: {chapters: []}})
|
||||
await build("./marko/page/logs-content/view.marko", {log: {posts: [], channel: {}}})
|
||||
// Start server
|
||||
app.listen(config.port, function() {
|
||||
console.log("Server started: http://localhost:" + config.port + "/")
|
||||
|
||||
if (process.send) {
|
||||
setTimeout(() => process.send("online"), 100)
|
||||
}
|
||||
|
||||
// Start server
|
||||
app.listen(config.port, function() {
|
||||
console.log("Server started: http://localhost:" + config.port + "/")
|
||||
|
||||
if (process.send) {
|
||||
setTimeout(() => process.send("online"), 100)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Handle shutdown signals (Docker needs this to shutdown quickly)
|
||||
process.on('SIGINT', () => process.exit(0))
|
||||
process.on('SIGTERM', () => process.exit(0))
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
process.on('SIGTERM', () => process.exit(0))
|
||||
Reference in New Issue
Block a user