Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2486c98da7 | ||
|
|
5955e6a71e | ||
|
|
59a711f150 | ||
|
|
b75c3461f7 | ||
|
|
a10b29a0ea | ||
|
|
79c3a76300 | ||
|
|
6aef0412e8 | ||
|
|
5fe56eaae8 | ||
|
|
bb2ccf6f4b | ||
|
|
1583eb412e | ||
|
|
5c094a2471 | ||
|
|
99aec4d2cb | ||
|
|
3f944ff029 | ||
|
|
3cbe42e1ba | ||
|
|
01b501f9e5 | ||
|
|
9b7d182955 | ||
|
|
7c55fc03aa | ||
|
|
6fca626ea2 | ||
|
|
d92e6bfeb0 | ||
|
|
2d89096430 | ||
|
|
3988028256 | ||
|
|
c51733ef3b | ||
|
|
f81f05d274 | ||
|
|
25d4aa73d1 | ||
|
|
e1a5968341 | ||
|
|
5c4446f161 | ||
|
|
3a057f6f24 | ||
|
|
853076fd5e | ||
|
|
06bf2b67a2 | ||
|
|
40259b8839 | ||
|
|
3fc9d5de7f | ||
|
|
c69e528105 | ||
|
|
6ea9632623 | ||
|
|
490725ecd2 | ||
|
|
494d0404ed | ||
|
|
8bfc0dad14 | ||
|
|
3ed3e50486 | ||
|
|
5260d96f37 |
+28
-20
@@ -1,25 +1,33 @@
|
|||||||
pipeline:
|
name: rpdata-frontend
|
||||||
docker-trunk:
|
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: docker-release-tag
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
repo: vmaple/rpdata-frontend
|
settings:
|
||||||
tags: trunk
|
|
||||||
secrets: [ docker_username, docker_password ]
|
|
||||||
when:
|
|
||||||
event: push
|
|
||||||
branch: master
|
|
||||||
docker-release:
|
|
||||||
image: plugins/docker
|
|
||||||
repo: vmaple/rpdata-frontend
|
|
||||||
auto_tag: true
|
auto_tag: true
|
||||||
secrets: [ docker_username, docker_password ]
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
repo: r.vmaple.dev/rpdata/rpdata-frontend
|
||||||
|
registry: r.vmaple.dev
|
||||||
when:
|
when:
|
||||||
event: tag
|
event:
|
||||||
ref: refs/tags/*
|
- tag
|
||||||
docker-release-latest:
|
|
||||||
|
- name: docker-release-latest
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
repo: vmaple/rpdata-frontend
|
settings:
|
||||||
tags: latest
|
auto_tag: false
|
||||||
secrets: [ docker_username, docker_password ]
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
repo: r.vmaple.dev/rpdata/rpdata-frontend
|
||||||
|
registry: r.vmaple.dev
|
||||||
when:
|
when:
|
||||||
event: tag
|
event:
|
||||||
ref: refs/tags/*
|
- tag
|
||||||
@@ -12,6 +12,7 @@ prebuild.run({
|
|||||||
"./marko/page/data/channels.marko",
|
"./marko/page/data/channels.marko",
|
||||||
"./marko/page/data/characters.marko",
|
"./marko/page/data/characters.marko",
|
||||||
"./marko/page/data/changes.marko",
|
"./marko/page/data/changes.marko",
|
||||||
|
"./marko/page/data/files.marko",
|
||||||
"./marko/page/story-content/view.marko",
|
"./marko/page/story-content/view.marko",
|
||||||
"./marko/page/logs-content/view.marko",
|
"./marko/page/logs-content/view.marko",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<tr class="compact-list-item color-highlight-dark">
|
<tr class=["compact-list-item", input.highlight ? "color-highlight-primary" : "color-highlight-dark"]>
|
||||||
<include(input.renderBody)/>
|
<include(input.renderBody)/>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -3,3 +3,21 @@ tr.compact-list-item {
|
|||||||
outline: 0.05px dotted;
|
outline: 0.05px dotted;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tr.compact-list-item.color-highlight-primary {
|
||||||
|
a {
|
||||||
|
color: #FFF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
color: #CCC !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.label {
|
||||||
|
color: #AA8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.value.color-text {
|
||||||
|
color: #FFF !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
table.compact-list {
|
table.compact-list {
|
||||||
padding-top: 0.5em;
|
padding-top: 0.5em;
|
||||||
padding-bottom: 3em;
|
|
||||||
|
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 100ch;
|
max-width: 100ch;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<td class="icon color-highlight-primary">${input.icon}</td>
|
<td class="icon color-highlight-primary">${input.icon}</td>
|
||||||
<td class="content color-primary color-highlight-dark">
|
<td class="content color-primary color-highlight-dark">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<a class="color-primary" href=input.link>${input.name}</a>
|
<a class="color-primary" href=input.link>${input.name || "(Untitled)"}</a>
|
||||||
</div>
|
</div>
|
||||||
<div if(input.description) class="description color-text">
|
<div if(input.description) class="description color-text">
|
||||||
<div class="content">${input.description}</div>
|
<div class="content">${input.description}</div>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
<if(input.value != null)>
|
||||||
|
<span class=["date", input.class]>${(input.utc ? moment.utc(input.value) : moment(input.value)).format(input.format || "MMM D, YYYY")}</span>
|
||||||
|
</if>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const {parse, BOLD} = require("irc-caret-notation")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate() {
|
||||||
|
this.state = {
|
||||||
|
paragraphs: [],
|
||||||
|
prev: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(input) {
|
||||||
|
if (this.state == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.source != this.state.prev) {
|
||||||
|
this.state.prev = input.source
|
||||||
|
this.state.paragraphs = input.source.split("\n").filter(l => l.length > 0).map(l => parse(l, {
|
||||||
|
"*": (!(input.source.startsWith("(")) ? BOLD : null),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<div class=["irc-caret-notation", input.class]>
|
||||||
|
<p for(p in state.paragraphs)>
|
||||||
|
<span for(chunk in p) class=chunk.cssClasses("icn-")>${chunk.text}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
<div class=["markdown-content", input.class]>
|
<div class=["markdown-content", input.class]>
|
||||||
|
<include(input.renderBody) />
|
||||||
<include(state.render) />
|
<include(state.render) />
|
||||||
</div>
|
</div>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<a on-click("onClick") href=input.href>
|
<a on-click("onClick") href=input.href>
|
||||||
<div class=state.classes>
|
<div class=[state.classes, input.weak ? "dark" : null]>
|
||||||
<div class="icon color-primary">${input.icon}</div>
|
<div class=["icon", "color-primary"]>${input.icon}</div>
|
||||||
<div class=["text", input.selected ? "color-primary" : input.textClass]>
|
<div class=["text", input.selected ? "color-primary" : input.textClass]>
|
||||||
<include(input.renderBody) />
|
<include(input.renderBody) />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ module.exports = class {
|
|||||||
if (this.input.enabled) {
|
if (this.input.enabled) {
|
||||||
window.removeEventListener("keydown", this.onPress)
|
window.removeEventListener("keydown", this.onPress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.mounted = false
|
||||||
|
this.opened = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -51,3 +51,71 @@ body {
|
|||||||
background-color: #555;
|
background-color: #555;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Tooltip magic.
|
||||||
|
*/
|
||||||
|
.tooltip {
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 0.5px dotted; /* If you want dots under the hoverable text */
|
||||||
|
|
||||||
|
span {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .tooltip-content {
|
||||||
|
visibility: hidden;
|
||||||
|
width: 400px;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.25em 1ch;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
z-index: 11000;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 105%;
|
||||||
|
|
||||||
|
border: 1px solid;
|
||||||
|
|
||||||
|
> h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> h2 {
|
||||||
|
font-size: 1em;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 0.15em;
|
||||||
|
margin-top: -0.25em;
|
||||||
|
font-weight: 200;
|
||||||
|
opacity: 0.5;
|
||||||
|
text-align: center;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-of-type {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tooltip {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:hover {
|
||||||
|
> .tooltip-content {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
.tooltip {
|
||||||
|
> .tooltip-content {
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,7 +92,7 @@ body.theme-data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 {
|
div.markdown-content .orange, body.theme-logs .color-blockquote, 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);
|
background-color: rgba(221, 170, 17, 0.125);
|
||||||
color: #DA1;
|
color: #DA1;
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ div.markdown-content .orange, body.theme-logs div.markdown-content blockquote, b
|
|||||||
color: #DA1;
|
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 {
|
div.markdown-content .blue, body.theme-story .color-blockquote, 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);
|
background-color: rgba(34, 187, 255, 0.125);
|
||||||
color: #4BF;
|
color: #4BF;
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ div.markdown-content .blue, body.theme-story div.markdown-content blockquote, bo
|
|||||||
color: #4BF; //
|
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 {
|
div.markdown-content .green, body.theme-mapp .color-blockquote, 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);
|
background-color: rgba(17,255,136, 0.125);
|
||||||
color: #1F8;
|
color: #1F8;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
const moment = require("moment")
|
||||||
|
|
||||||
|
const {fileApi} = require("../../../../../rpdata/api/File")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
values: {
|
||||||
|
file: null,
|
||||||
|
name: "",
|
||||||
|
public: false,
|
||||||
|
|
||||||
|
realFile: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
change(key, ev) {
|
||||||
|
if (key === "file") {
|
||||||
|
this.state.values = Object.assign({}, this.state.values, {
|
||||||
|
[key]: ev.target.value,
|
||||||
|
realFile: ev.target.files[0],
|
||||||
|
name: ev.target.files[0].name,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.emit("close")
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
if (this.state.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = Object.assign({}, this.state.values)
|
||||||
|
input.file = input.realFile
|
||||||
|
delete input.realFile
|
||||||
|
|
||||||
|
this.state.loading = true
|
||||||
|
fileApi.uploadFile(input).then((res) => {
|
||||||
|
this.emit("added", res)
|
||||||
|
this.emit("close")
|
||||||
|
}).catch(errs => {
|
||||||
|
console.warn("Failed to add:", errs)
|
||||||
|
|
||||||
|
this.state.error = "Failed to add: " + JSON.stringify(errs)
|
||||||
|
}).then(() => {
|
||||||
|
this.state.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
|
||||||
|
<h1>Upload File</h1>
|
||||||
|
|
||||||
|
<p class="color-error">${state.error}</p>
|
||||||
|
|
||||||
|
<label>File</label>
|
||||||
|
<input key="eventName" type="file" placeholder="(None)" on-change("change", "file") value=state.values.file />
|
||||||
|
|
||||||
|
<label>Name</label>
|
||||||
|
<input key="name" autofocus placeholder="(Required)" class="big" on-change("change", "name") value=state.values.name />
|
||||||
|
|
||||||
|
<label>Public</label>
|
||||||
|
<toggle value=state.values.public on="On" off="Off"
|
||||||
|
onDesc="Other members may see this file on this page."
|
||||||
|
offDesc="Only you may see this file on this page."
|
||||||
|
on-change("change", "public") />
|
||||||
|
|
||||||
|
<button disabled=state.loading on-click("save")>Save</button>
|
||||||
|
</modal>
|
||||||
@@ -65,7 +65,22 @@ module.exports = class {
|
|||||||
|
|
||||||
this.state.operation += " chapter"
|
this.state.operation += " chapter"
|
||||||
this.state.links = [{
|
this.state.links = [{
|
||||||
text: chapter.title || `Untitled chapter (${chapterKey.id})`,
|
text: (chapter||{}).title || `Untitled chapter (${(chapterKey||chapter).id})`,
|
||||||
|
href: `/story/${storyKey.id}#${(chapterKey||chapter).id}`,
|
||||||
|
}]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Comment": {
|
||||||
|
const storyKey = data.keys.find(k => k.model === "Story") || {}
|
||||||
|
const chapterKey = data.keys.find(k => k.model === "Chapter")
|
||||||
|
const chapter = data.objects.find(o => o.type === "Chapter") || {}
|
||||||
|
const comment = data.objects.find(o => o.type === "Comment") || {}
|
||||||
|
|
||||||
|
this.state.operation += ' ' + comment.characterName + " comment in chapter"
|
||||||
|
this.state.links = [{
|
||||||
|
text: chapter.title || `ID (${chapterKey.id})`,
|
||||||
href: `/story/${storyKey.id}#${chapterKey.id}`,
|
href: `/story/${storyKey.id}#${chapterKey.id}`,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@@ -84,6 +99,19 @@ module.exports = class {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "File": {
|
||||||
|
const key = data.keys.find(k => k.id !== "*")
|
||||||
|
const fileObj = data.objects.find(o => o.type === "File") || {name: key.id}
|
||||||
|
|
||||||
|
this.state.operation += " file"
|
||||||
|
this.state.links = [{
|
||||||
|
text: fileObj.name,
|
||||||
|
href: `/data/files#${key.id}`,
|
||||||
|
}]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case "Post": {
|
case "Post": {
|
||||||
const logKey = data.keys.find(k => k.model === "Log")
|
const logKey = data.keys.find(k => k.model === "Log")
|
||||||
const postKeys = data.keys.filter(k => k.model === "Post")
|
const postKeys = data.keys.filter(k => k.model === "Post")
|
||||||
@@ -109,6 +137,7 @@ module.exports = class {
|
|||||||
const opText = {
|
const opText = {
|
||||||
add: "added",
|
add: "added",
|
||||||
remove: "removed",
|
remove: "removed",
|
||||||
|
upload: "uploaded",
|
||||||
move: "moved",
|
move: "moved",
|
||||||
edit: "edited",
|
edit: "edited",
|
||||||
import: "imported",
|
import: "imported",
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
|
const { changesApi } = require("../../../../../rpdata/api/Change")
|
||||||
|
|
||||||
|
|
||||||
module.exports = class {
|
module.exports = class {
|
||||||
onCreate(input) {
|
onCreate(input) {
|
||||||
this.state = {
|
this.state = {
|
||||||
characters: input.characters,
|
changes: input.changes,
|
||||||
|
shownAll: false,
|
||||||
modal: null,
|
modal: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearLimit(e) {
|
||||||
|
// Disable mobile URL fallback.
|
||||||
|
if (e && e.preventDefault) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
changesApi.list({limit: 0}).then(changes => {
|
||||||
|
this.state.changes = changes
|
||||||
|
this.state.shownAll = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
open(modal) {
|
open(modal) {
|
||||||
this.state.modal = modal
|
this.state.modal = modal
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
<div class="changes-page">
|
<div class="changes-page">
|
||||||
<h1 class="color-primary">Changes</h1>
|
<h1 class="color-primary">Changes</h1>
|
||||||
<p>All changes to <em>listed</em> resources is stored for 90 days for transparency reasons. The current state of an object after each change exists, but is only available through the GraphQL API at this time.</p>
|
<p>All changes to <em>listed</em> resources is stored for 90 days for transparency reasons. The current state of an object after each change exists, but is only available through the GraphQL API at this time.</p>
|
||||||
<for (change in input.changes)>
|
<for (change in state.changes)>
|
||||||
<change data=change />
|
<change data=change />
|
||||||
</for>
|
</for>
|
||||||
|
<a if(!state.shownAll) href="/data/changes?limit=0" class="show-more-button" on-click("clearLimit")>Show All</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-added("characterAdded") on-close("close") />
|
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-added("characterAdded") on-close("close") />
|
||||||
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-close("close") />
|
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-close("close") />
|
||||||
|
<add-file-modal enabled=(state.modal === "file.add") user=input.user on-close("close") />
|
||||||
|
|||||||
@@ -33,4 +33,21 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.show-more-button {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0.5em 2em;
|
||||||
|
font-size: 2em;
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
color: #DA1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more-button:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,3 +4,4 @@
|
|||||||
</main>
|
</main>
|
||||||
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-added("channelAdded") on-close("close") />
|
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-added("channelAdded") on-close("close") />
|
||||||
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-close("close") />
|
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-close("close") />
|
||||||
|
<add-file-modal enabled=(state.modal === "file.add") user=input.user on-close("close") />
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ module.exports = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
characterAdded(character) {
|
characterAdded(character) {
|
||||||
|
const index = this.state.characters.findIndex(c => c.id === character.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
const characters = this.state.characters.slice()
|
||||||
|
characters[index] = character
|
||||||
|
this.state.characters = characters
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.state.characters = this.state.characters.concat(character)
|
this.state.characters = this.state.characters.concat(character)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,4 @@
|
|||||||
</main>
|
</main>
|
||||||
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-added("characterAdded") on-close("close") />
|
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-added("characterAdded") on-close("close") />
|
||||||
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-close("close") />
|
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-close("close") />
|
||||||
|
<add-file-modal enabled=(state.modal === "file.add") user=input.user on-close("close") />
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
<menu-link key="characters" selected=input.selected.characters icon="C" href="/data/characters/">Characters</menu-link>
|
<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-link key="channels" selected=input.selected.channels icon="#" href="/data/channels/">Channels</menu-link>
|
||||||
<menu-link key="history" selected=input.selected.changes icon="H" href="/data/changes/">History</menu-link>
|
<menu-link key="history" selected=input.selected.changes icon="H" href="/data/changes/">History</menu-link>
|
||||||
<if-permitted user=input.user permission=["member", "character.add", "channel.add"]>
|
<if-permitted user=input.user permission=["member", "character.add", "channel.add", "file.add"]>
|
||||||
<menu-gap />
|
<menu-gap />
|
||||||
<menu-header>Add</menu-header>
|
<menu-header>Add</menu-header>
|
||||||
<if-permitted user=input.user permission=["character.add", "member"]>
|
<if-permitted key="characters_add" user=input.user permission=["character.add", "member"]>
|
||||||
<menu-link dark key="characters_add" icon="+" on-click("emit", "open", "character.add")>Character</menu-link>
|
<menu-link dark icon="+" on-click("emit", "open", "character.add")>Character</menu-link>
|
||||||
</if-permitted>
|
</if-permitted>
|
||||||
<if-permitted user=input.user permission="channel.add">
|
<if-permitted key="channels_add" user=input.user permission="channel.add">
|
||||||
<menu-link dark key="channels_add" icon="+" on-click("emit", "open", "channel.add")>Channel</menu-link>
|
<menu-link dark icon="+" on-click("emit", "open", "channel.add")>Channel</menu-link>
|
||||||
</if-permitted>
|
</if-permitted>
|
||||||
</if-permitted>
|
</if-permitted>
|
||||||
</menu>
|
</menu>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
const moment = require("moment")
|
||||||
|
|
||||||
|
const {fileApi} = require("../../../../../rpdata/api/File")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
values: {
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
public: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(input) {
|
||||||
|
this.state.values = {
|
||||||
|
id: input.file.id,
|
||||||
|
name: input.file.name,
|
||||||
|
public: input.file.public,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
change(key, ev) {
|
||||||
|
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.emit("close")
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
if (this.state.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = Object.assign({}, this.state.values)
|
||||||
|
input.file = input.realFile
|
||||||
|
delete input.realFile
|
||||||
|
|
||||||
|
this.state.loading = true
|
||||||
|
fileApi.editFile(input).then((res) => {
|
||||||
|
this.emit("edited", res)
|
||||||
|
this.emit("close")
|
||||||
|
}).catch(errs => {
|
||||||
|
console.warn("Failed to add:", errs)
|
||||||
|
|
||||||
|
this.state.error = "Failed to add: " + JSON.stringify(errs)
|
||||||
|
}).then(() => {
|
||||||
|
this.state.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
|
||||||
|
<h1>Upload File</h1>
|
||||||
|
|
||||||
|
<p class="color-error">${state.error}</p>
|
||||||
|
|
||||||
|
<label>Name</label>
|
||||||
|
<input key="name" autofocus placeholder="(Required)" class="big" on-change("change", "name") value=state.values.name />
|
||||||
|
|
||||||
|
<label>Public</label>
|
||||||
|
<toggle value=state.values.public on="On" off="Off"
|
||||||
|
onDesc="Other members may see this file on this page."
|
||||||
|
offDesc="Only you may see this file on this page."
|
||||||
|
on-change("change", "public") />
|
||||||
|
|
||||||
|
<button disabled=state.loading on-click("save")>Save</button>
|
||||||
|
</modal>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = class {
|
||||||
|
isHighlighted(id) {
|
||||||
|
if (typeof(window) !== "undefined") {
|
||||||
|
return window.location.hash.endsWith(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<compact-list>
|
||||||
|
<compact-list-item for(file in input.files) key=file.id id=file.id highlight=component.isHighlighted(file.id)>
|
||||||
|
<file data=file
|
||||||
|
user=input.user
|
||||||
|
on-removed("emit", "removed", file)
|
||||||
|
on-edited("emit", "edited", file)
|
||||||
|
/>
|
||||||
|
</compact-list-item>
|
||||||
|
</compact-list>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
const moment = require("moment")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
modal: null,
|
||||||
|
deleted: false,
|
||||||
|
date: moment(input.data.time).format("MMM D, YYYY"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(input) {
|
||||||
|
if (input && input.data) {
|
||||||
|
this.state.date = moment(input.data.time).format("MMM D, YYYY")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open(modal) {
|
||||||
|
this.state.modal = modal
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.state.modal = null
|
||||||
|
}
|
||||||
|
|
||||||
|
removed() {
|
||||||
|
this.state.deleted = true
|
||||||
|
this.emit("removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeString(bytes) {
|
||||||
|
const order = Math.floor(Math.log2(bytes) / 10)
|
||||||
|
const unit = ["B", "KB", "MB", "GB", "TB", "PB"][order]
|
||||||
|
const number = bytes / Math.pow(2, (order)*10)
|
||||||
|
|
||||||
|
return `${number.toFixed(2)} ${unit}`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<if (!state.deleted)>
|
||||||
|
<a class="anchor" id=input.data.id />
|
||||||
|
<compact-list-property primary long label="Name" value=input.data.name />
|
||||||
|
<compact-list-property short label="Date" value=state.date />
|
||||||
|
<compact-list-property short label="Type" value=input.data.mimeType />
|
||||||
|
<compact-list-property short label="Author" value=input.data.author />
|
||||||
|
<compact-list-property short label="Size" value=component.sizeString(input.data.size) />
|
||||||
|
<compact-list-options long>
|
||||||
|
<a key="link" class="color-menu" on-click("open", "link")>Link</a>
|
||||||
|
<if(input.data.kind === "upload")>
|
||||||
|
<if-permitted user=input.user author=input.data.author permission="file.edit">
|
||||||
|
<a key="edit" on-click("open", "edit", input.data)>Edit</a>
|
||||||
|
</if-permitted>
|
||||||
|
<if-permitted user=input.user author=input.data.author permission="file.remove">
|
||||||
|
<a key="remove" on-click("open", "remove", input.data)>Remove</a>
|
||||||
|
</if-permitted>
|
||||||
|
</if>
|
||||||
|
</compact-list-options>
|
||||||
|
<link-file-modal enabled=(state.modal === "link") file=input.data on-close("close") />
|
||||||
|
<edit-file-modal enabled=(state.modal === "edit") file=input.data on-edited("emit", "edited") on-close("close") />
|
||||||
|
<remove-file-modal enabled=(state.modal === "remove") file=input.data on-removed("removed") on-close("close") />
|
||||||
|
</if>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
publics: input.publics.sort(byDate),
|
||||||
|
privates: input.privates.sort(byDate),
|
||||||
|
modal: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount() {
|
||||||
|
}
|
||||||
|
|
||||||
|
open(modal) {
|
||||||
|
this.state.modal = modal
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.state.modal = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fileRemoved({id}) {
|
||||||
|
this.state.publics = this.state.publics.filter(f => f.id !== id)
|
||||||
|
this.state.privates = this.state.privates.filter(f => f.id !== id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileEdited(file, patch) {
|
||||||
|
this.fileRemoved({id: file.id})
|
||||||
|
this.fileAdded(Object.assign({}, file, patch))
|
||||||
|
|
||||||
|
console.log(Object.assign({}, file, patch))
|
||||||
|
}
|
||||||
|
|
||||||
|
fileAdded(file) {
|
||||||
|
if (file.public) {
|
||||||
|
this.state.publics = [].concat(...this.state.publics).concat(file).sort(byDate)
|
||||||
|
} else {
|
||||||
|
this.state.privates = [].concat(...this.state.privates).concat(file).sort(byDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function byDate(a, b) {
|
||||||
|
const ad = new Date(a.time)
|
||||||
|
const bd = new Date(b.time)
|
||||||
|
|
||||||
|
return ad.getTime() - bd.getTime()
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<data-menu categories=input.categories selected=(input.selected || {}) user=input.user on-open("open") />
|
||||||
|
<main class="data-file-page">
|
||||||
|
<h1 if(state.privates && state.privates.length > 0) class="file-list-headers color-primary">Your Private Files</h1>
|
||||||
|
<file-list user=input.user files=state.privates on-removed("fileRemoved") on-edited("fileEdited") on-nicks("fileNicksChanged") />
|
||||||
|
|
||||||
|
<h1 if(state.publics && state.publics.length > 0) class="file-list-headers color-primary">Public Files</h1>
|
||||||
|
<file-list user=input.user files=state.publics on-removed("fileRemoved") on-edited("fileEdited") on-nicks("fileNicksChanged") />
|
||||||
|
</main>
|
||||||
|
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-close("close") />
|
||||||
|
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-close("close") />
|
||||||
|
<add-file-modal enabled=(state.modal === "file.add") user=input.user on-added("fileAdded") on-close("close") />
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
h1.file-list-headers {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.data-file-page {
|
||||||
|
margin-bottom: 4em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
isImage: (input.file && input.file.mimeType.startsWith("image/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(input) {
|
||||||
|
this.state.isImage = (input.file && input.file.mimeType.startsWith("image/"))
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.emit("close")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<modal class="modal color-text nolabel link-file-modal" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
|
||||||
|
<h1>Links for ${input.file.name}</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Below are links and copy-pastable syntax for sharing the file. They can be used for linking to it
|
||||||
|
in your stories and wiki pages.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="color-danger">
|
||||||
|
This is the link to the actual file on the CDN, not a share of it. You must delete the file if you want
|
||||||
|
to un-share it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<if(state.isImage)>
|
||||||
|
<p class="color-danger">
|
||||||
|
Images uploaded here cannot be inlined on the wiki. For that you have to upload it there.
|
||||||
|
</p>
|
||||||
|
</if>
|
||||||
|
|
||||||
|
<label>URL</label>
|
||||||
|
<input key="url" disabled placeholder="(None)" on-change("change", "eventName") value=input.file.url />
|
||||||
|
|
||||||
|
<label>Markdown link</label>
|
||||||
|
<input key="mdLink" disabled placeholder="(None)" on-change("change", "locationName") value=`[Link text](${input.file.url})` />
|
||||||
|
|
||||||
|
<label>Wiki link</label>
|
||||||
|
<input key="wikiLink" disabled placeholder="(None)" on-change("change", "locationName") value=`[${input.file.url} Link text]` />
|
||||||
|
|
||||||
|
<if(state.isImage)>
|
||||||
|
<label>Markdown image</label>
|
||||||
|
<input key="mdImage" disabled placeholder="(None)" on-change("change", "locationName") value=`` />
|
||||||
|
|
||||||
|
<label>Image Preview</label>
|
||||||
|
<img src=input.file.url alt="File preview"/>
|
||||||
|
</if>
|
||||||
|
</modal>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
div.link-file-modal {
|
||||||
|
user-select: text;
|
||||||
|
|
||||||
|
p {
|
||||||
|
user-select: text;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
user-select: all !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
const {fileApi} = require("../../../../../rpdata/api/File")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
values: {
|
||||||
|
id: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(input) {
|
||||||
|
this.state.values = {
|
||||||
|
id: input.file.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
change(key, ev) {
|
||||||
|
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.emit("close")
|
||||||
|
}
|
||||||
|
|
||||||
|
doIt() {
|
||||||
|
if (this.state.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = Object.assign({}, this.state.values)
|
||||||
|
|
||||||
|
this.state.loading = true
|
||||||
|
fileApi.removeFile(input).then((res) => {
|
||||||
|
this.emit("removed", res)
|
||||||
|
this.emit("close")
|
||||||
|
}).catch(errs => {
|
||||||
|
console.warn("Failed to add:", errs)
|
||||||
|
|
||||||
|
this.state.error = "Failed to add: " + JSON.stringify(errs)
|
||||||
|
}).then(() => {
|
||||||
|
this.state.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
|
||||||
|
<h1>Delete File ${input.file.name}</h1>
|
||||||
|
|
||||||
|
<p class="color-error">${state.error}</p>
|
||||||
|
|
||||||
|
<p class="color-danger">
|
||||||
|
It might take up to an hour for it to be removed from CDN caches if it's been recently loaded.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button disabled=state.loading on-click("doIt")>Remove</button>
|
||||||
|
</modal>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<include("../layout", {title: "Data", site: "data"})>
|
||||||
|
<@body>
|
||||||
|
<background src="/assets/images/bg.png" opacity=0.25 />
|
||||||
|
<files-page user=input.user privates=input.privates publics=input.publics selected=input.selected />
|
||||||
|
</@body>
|
||||||
|
</include>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
id: input.log.id,
|
||||||
|
channelName: input.log.channelName,
|
||||||
|
eventName: input.log.eventName,
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import moment from "moment"
|
||||||
|
|
||||||
|
<div class="log-suggestions">
|
||||||
|
<if (input.log.nextLogs.length > 0 || input.log.prevLogs.length > 0)>
|
||||||
|
<if (input.log.nextLogs.length > 0)>
|
||||||
|
<h2 class="color-primary">Next Logs</h2>
|
||||||
|
<div for(s in input.log.nextLogs) class="suggestion color-primary">
|
||||||
|
<div class="suggestion-header color-primary"><a href=`/logs/${s.log.id}`>${s.log.title || `${s.log.channelName} - ${moment(s.log.date).format("MMMM D, YYYY")}`}</a></div>
|
||||||
|
<div for(paragraph in (s.log.description||"").split("\n").filter(p => p)) class="suggestion-description color-text">${paragraph}</div>
|
||||||
|
<div class="suggestion-tags">
|
||||||
|
<div if(s.hasEvent) class="suggestion-tag color-tag-event">${input.log.eventName}</div>
|
||||||
|
<div if(s.hasChannel) class="suggestion-tag color-tag-location">${s.log.channelName}</div>
|
||||||
|
<div for(c in s.characters) class="suggestion-tag color-tag-character">${c.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</if>
|
||||||
|
<if (input.log.prevLogs.length > 0)>
|
||||||
|
<h2 class="color-primary">Previous Logs</h2>
|
||||||
|
<div for(s in input.log.prevLogs) class="suggestion color-primary">
|
||||||
|
<div class="suggestion-header color-primary"><a href=`/logs/${s.log.id}`>${s.log.title || `${s.log.channelName} - ${moment(s.log.date).format("MMMM D, YYYY")}`}</a></div>
|
||||||
|
<div for(paragraph in (s.log.description||"").split("\n").filter(p => p)) class="suggestion-description color-text">${paragraph}</div>
|
||||||
|
<div class="suggestion-tags">
|
||||||
|
<div if(s.hasEvent) class="suggestion-tag color-tag-event">${input.log.eventName}</div>
|
||||||
|
<div if(s.hasChannel) class="suggestion-tag color-tag-location">${s.log.channelName}</div>
|
||||||
|
<div for(c in s.characters) class="suggestion-tag color-tag-character">${c.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<div class="empty color-menu">(No suggestions)</div>
|
||||||
|
</else>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
div.log-suggestions {
|
||||||
|
padding: 0 0.5ch;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.empty, div.loading {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.suggestion {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
div.suggestion-header {
|
||||||
|
margin: 0.125em 0.5ch;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.suggestion-description {
|
||||||
|
margin: 0.125em 0.5ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.suggestion-tags {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
|
||||||
|
div.suggestion-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 0.5ch;
|
||||||
|
margin: 0.125em 0.5ch;
|
||||||
|
border: 0.25px solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
module.exports = class {
|
module.exports = class {
|
||||||
|
onCreate() {
|
||||||
|
this.state = {
|
||||||
|
highlights: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
select(value) {
|
select(value) {
|
||||||
this.emit("select", value)
|
this.emit("select", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleHighlight(id) {
|
||||||
|
if (this.state.highlights.includes(id)) {
|
||||||
|
this.state.highlights = this.state.highlights.filter(id2 => id2 !== id)
|
||||||
|
} else {
|
||||||
|
this.state.highlights = this.state.highlights.concat(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit("hightlights", this.state.highlights);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,10 +4,23 @@
|
|||||||
<menu-link dark key="_create" on-click("select", "post.add", null) icon="+">Add Post</menu-link>
|
<menu-link dark key="_create" on-click("select", "post.add", null) icon="+">Add Post</menu-link>
|
||||||
<menu-gap />
|
<menu-gap />
|
||||||
</if-permitted>
|
</if-permitted>
|
||||||
<menu-header>Links</menu-header>
|
<menu-header>Channel</menu-header>
|
||||||
<menu-link href=("/logs/?channels="+encodeURIComponent(input.log.channel.name)) icon="#">${input.log.channel.name}</menu-link>
|
<menu-link href=("/logs/?channels="+encodeURIComponent(input.log.channel.name)) icon="#">${input.log.channel.name}</menu-link>
|
||||||
<menu-link if(input.log.channel.eventName) href=("/logs/?channels="+input.log.channel.eventName) icon="E">${input.log.channel.eventName}</menu-link>
|
<menu-gap />
|
||||||
|
<if(input.log.eventName)>
|
||||||
|
<menu-header>Event</menu-header>
|
||||||
|
<menu-link href=("/logs/?events="+input.log.eventName) icon="E">${input.log.eventName}</menu-link>
|
||||||
|
<menu-gap />
|
||||||
|
</if>
|
||||||
|
<if(input.log.characters.length > 0)>
|
||||||
|
<menu-header>Characters</menu-header>
|
||||||
<for(character in input.log.characters)>
|
<for(character in input.log.characters)>
|
||||||
<menu-link href=("/logs/?characters="+character.id) icon="C">${character.name} <span class="weak">(${character.author})</span></menu-link>
|
<menu-link weak=(state.highlights.length > 0 && !state.highlights.includes(character.id))
|
||||||
|
icon="C"
|
||||||
|
on-click("toggleHighlight", character.id)>
|
||||||
|
${character.name} <span class="weak">(${character.author})</span>
|
||||||
|
</menu-link>
|
||||||
</for>
|
</for>
|
||||||
|
<menu-gap />
|
||||||
|
</if>
|
||||||
</menu>
|
</menu>
|
||||||
@@ -9,6 +9,7 @@ module.exports = class {
|
|||||||
log: input.log,
|
log: input.log,
|
||||||
modal: null,
|
modal: null,
|
||||||
removed: false,
|
removed: false,
|
||||||
|
highlights: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +112,11 @@ module.exports = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
postAdded(post) {
|
postAdded(post) {
|
||||||
|
// Stop duplicates in case of race condition.
|
||||||
|
if (this.state.log.posts.find(p => p.id === post.id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.state.log.posts = this.state.log.posts.concat([post])
|
this.state.log.posts = this.state.log.posts.concat([post])
|
||||||
this.state.log = Object.assign({}, this.state.log)
|
this.state.log = Object.assign({}, this.state.log)
|
||||||
}
|
}
|
||||||
@@ -161,6 +167,10 @@ module.exports = class {
|
|||||||
this.state.removed = true
|
this.state.removed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
highlightsChanged(highlights) {
|
||||||
|
this.state.highlights = highlights.slice();
|
||||||
|
}
|
||||||
|
|
||||||
get title() {
|
get title() {
|
||||||
if (this.state.log.title) {
|
if (this.state.log.title) {
|
||||||
return this.state.log.title
|
return this.state.log.title
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<logs-content-menu user=input.user log=state.log on-select("open") />
|
<logs-content-menu on-hightlights("highlightsChanged") user=input.user log=state.log on-select("open") />
|
||||||
<main>
|
<main>
|
||||||
<div class="logs-content">
|
<div class="logs-content">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1 class="color-primary">${component.title}</h1>
|
<h1 class="color-primary">${component.title}</h1>
|
||||||
|
<p for(paragraph in state.log.description.split("\n").filter(p => p)) class="color-text">${paragraph}</p>
|
||||||
<if-permitted user=input.user permission="log.edit">
|
<if-permitted user=input.user permission="log.edit">
|
||||||
<a on-click("open", "log.edit") class="color-menu">Edit</a>
|
<a on-click("open", "log.edit") class="color-menu">Edit</a>
|
||||||
</if-permitted>
|
</if-permitted>
|
||||||
@@ -25,8 +26,10 @@
|
|||||||
prev=(state.log.posts[loop.getIndex() - 1] || {})
|
prev=(state.log.posts[loop.getIndex() - 1] || {})
|
||||||
post=post
|
post=post
|
||||||
characters=state.log.characters
|
characters=state.log.characters
|
||||||
|
highlights=state.highlights
|
||||||
last=(post.position === state.log.posts.length)
|
last=(post.position === state.log.posts.length)
|
||||||
/>
|
/>
|
||||||
|
<log-suggestions log=state.log />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<add-post-modal enabled=(state.modal === "post.add") logId=state.log.id on-close("close") on-added("postAdded") />
|
<add-post-modal enabled=(state.modal === "post.add") logId=state.log.id on-close("close") on-added("postAdded") />
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ div.logs-content {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.25em 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ module.exports = class {
|
|||||||
multipart: false,
|
multipart: false,
|
||||||
anchored: false,
|
anchored: false,
|
||||||
prevWasText: false,
|
prevWasText: false,
|
||||||
|
dark: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
@@ -48,7 +49,7 @@ module.exports = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timediff = Date.parse(input.post.time) - Date.parse(input.prev.time)
|
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.state.multipart = (!input.post.kind.startsWith("annotation.") && input.post.nick == input.prev.nick && input.prev.multipart !== false) && (Math.abs(timediff) < 60000);
|
||||||
this.state.prevWasText = (input.prev.kind === "text");
|
this.state.prevWasText = (input.prev.kind === "text");
|
||||||
|
|
||||||
this.updatePost(input)
|
this.updatePost(input)
|
||||||
@@ -81,24 +82,12 @@ module.exports = class {
|
|||||||
this.state.name = input.post.nick
|
this.state.name = input.post.nick
|
||||||
this.state.nameSuffix = ""
|
this.state.nameSuffix = ""
|
||||||
|
|
||||||
this.state.text = input.post.text.replace(/\x02/g, "**").replace(/\x1D/g, "*").replace(/\`/g, "\\`").trim();
|
this.state.text = input.post.text
|
||||||
|
|
||||||
if (this.state.text.startsWith("|")) {
|
if (input.highlights.length > 0 && (input.post.kind === "action" || input.post.kind === "text")) {
|
||||||
this.state.name = ""
|
this.state.dark = true
|
||||||
this.state.text = this.state.text.slice(1).trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.post.kind === "text" && !this.state.text.includes("\"") && !this.state.text.includes("\''") && !this.state.text.includes("|")) {
|
|
||||||
const colonIndex = this.state.text.indexOf(": ");
|
|
||||||
if (colonIndex != -1 && colonIndex < this.state.text.indexOf(" ")) {
|
|
||||||
this.state.text = this.state.text.replace(": ", ": \"") + "\"";
|
|
||||||
} else {
|
} else {
|
||||||
this.state.text = '"' + this.state.text + '"'
|
this.state.dark = false
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.text.charAt(0) == this.state.text.charAt(0).toUpperCase()) {
|
|
||||||
this.state.name = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!input.post.nick.startsWith("=")) {
|
if (!input.post.nick.startsWith("=")) {
|
||||||
@@ -106,13 +95,19 @@ module.exports = class {
|
|||||||
for (const character of input.characters) {
|
for (const character of input.characters) {
|
||||||
for (const nick of character.nicks) {
|
for (const nick of character.nicks) {
|
||||||
if (nick === postNick) {
|
if (nick === postNick) {
|
||||||
this.state.name = character.name
|
this.state.name = character.name + character.id
|
||||||
this.state.shortName = character.shortName
|
this.state.shortName = character.shortName
|
||||||
|
|
||||||
if (input.post.nick.endsWith("'s") || input.post.nick.endsWith("'")) {
|
if (input.post.nick.endsWith("'s") || input.post.nick.endsWith("'")) {
|
||||||
this.state.nameSuffix = (character.shortName.endsWith("s") || character.shortName.endsWith("z")) ? "'" : "'s"
|
this.state.nameSuffix = (character.shortName.endsWith("s") || character.shortName.endsWith("z")) ? "'" : "'s"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.highlights.length > 0) {
|
||||||
|
this.state.dark = !(input.highlights.includes(character.id))
|
||||||
|
} else {
|
||||||
|
this.state.dark = false
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div if(!state.removed) class=["post", component.kindClass("pk-"), state.multipart ? "multipart" : null, state.anchored ? "anchored" : null]>
|
<div if(!state.removed) class=["post", component.kindClass("pk-"), state.multipart ? "multipart" : null, state.anchored ? "anchored" : null, state.dark ? "dark" : null]>
|
||||||
<a class="anchor" id=input.post.id />
|
<a class="anchor" id=input.post.id />
|
||||||
<div class="post-meta-row">
|
<div class="post-meta-row">
|
||||||
<div class="options color-menu">
|
<div class="options color-menu">
|
||||||
@@ -19,18 +19,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div if(input.post.kind.startsWith("annotation.")) class="post-body" >
|
<div if(input.post.kind.startsWith("annotation.")) class="post-body" >
|
||||||
<annotation level=(input.post.kind.substring(11))>
|
<annotation level=(input.post.kind.substring(11))>
|
||||||
<markdown class="post-content" source=state.text />
|
<irc-caret-notation class="post-content" source=state.text />
|
||||||
</annotation>
|
</annotation>
|
||||||
</div>
|
</div>
|
||||||
<div if(input.post.kind === "scene") class="post-body color-text">
|
<div if(input.post.kind === "scene") class="post-body color-text">
|
||||||
<markdown class="post-content post-scene" source=state.text />
|
<irc-caret-notation class="post-content post-scene" source=state.text />
|
||||||
</div>
|
</div>
|
||||||
<div if(input.post.kind === "action") class="post-body color-text">
|
<div if(input.post.kind === "action") class="post-body color-text">
|
||||||
$ const prefix = ((!state.multipart || state.prevWasText) ? state.shortName + state.nameSuffix + " " : "");
|
$ const prefix = ((!state.multipart || state.prevWasText) ? state.shortName + state.nameSuffix + " " : "");
|
||||||
<markdown class="post-content post-action" source=(prefix + state.text) />
|
<irc-caret-notation class="post-content post-action" source=(prefix + state.text) />
|
||||||
</div>
|
</div>
|
||||||
<div if(input.post.kind === "text") class="post-body color-text">
|
<div if(input.post.kind === "text") class="post-body color-text">
|
||||||
<markdown class="post-content post-text" source=state.text />
|
<irc-caret-notation class="post-content post-text" source=state.text />
|
||||||
</div>
|
</div>
|
||||||
<remove-post-modal enabled=(state.modal === "remove") post=input.post on-close("close") on-remove("remove") />
|
<remove-post-modal enabled=(state.modal === "remove") post=input.post on-close("close") on-remove("remove") />
|
||||||
<edit-post-modal enabled=(state.modal === "edit") post=input.post on-close("close") on-edited("edited") />
|
<edit-post-modal enabled=(state.modal === "edit") post=input.post on-close("close") on-edited("edited") />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
div.post {
|
div.post {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
margin-bottom: 1ch;
|
margin-bottom: 1.25em;
|
||||||
margin-left: -12px;
|
margin-left: -12px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
padding-right: 0.5ch;
|
padding-right: 0.5ch;
|
||||||
@@ -65,12 +65,54 @@ div.post {
|
|||||||
margin-top: 0.33em;
|
margin-top: 0.33em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.33em;
|
||||||
|
margin-bottom: -0.25em;
|
||||||
|
|
||||||
|
span.icn-bold { font-weight: 800; }
|
||||||
|
span.icn-underline { text-decoration: underline; }
|
||||||
|
span.icn-italic { font-style: italic; }
|
||||||
|
|
||||||
|
span.icn-fg-0 { color: #fff; }
|
||||||
|
span.icn-fg-1 { color: #111; }
|
||||||
|
span.icn-fg-2 { color: #13d; }
|
||||||
|
span.icn-fg-3 { color: #173; }
|
||||||
|
span.icn-fg-4 { color: #d21; }
|
||||||
|
span.icn-fg-5 { color: #721; }
|
||||||
|
span.icn-fg-6 { color: #727; }
|
||||||
|
span.icn-fg-7 { color: #d71; }
|
||||||
|
span.icn-fg-8 { color: #aa1; }
|
||||||
|
span.icn-fg-9 { color: #1d3; }
|
||||||
|
span.icn-fg-10 { color: #177; }
|
||||||
|
span.icn-fg-11 { color: #1DD; }
|
||||||
|
span.icn-fg-12 { color: #127; }
|
||||||
|
span.icn-fg-13 { color: #c1a; }
|
||||||
|
span.icn-fg-14 { color: #444; }
|
||||||
|
span.icn-fg-15 { color: #aaa; }
|
||||||
|
span.icn-bg-0 { background-color: #fff; }
|
||||||
|
span.icn-bg-1 { background-color: #111; }
|
||||||
|
span.icn-bg-2 { background-color: #13d; }
|
||||||
|
span.icn-bg-3 { background-color: #173; }
|
||||||
|
span.icn-bg-4 { background-color: #d21; }
|
||||||
|
span.icn-bg-5 { background-color: #721; }
|
||||||
|
span.icn-bg-6 { background-color: #727; }
|
||||||
|
span.icn-bg-7 { background-color: #d71; }
|
||||||
|
span.icn-bg-8 { background-color: #aa1; }
|
||||||
|
span.icn-bg-9 { background-color: #1d3; }
|
||||||
|
span.icn-bg-10 { background-color: #177; }
|
||||||
|
span.icn-bg-11 { background-color: #1DD; }
|
||||||
|
span.icn-bg-12 { background-color: #127; }
|
||||||
|
span.icn-bg-13 { background-color: #c1a; }
|
||||||
|
span.icn-bg-14 { background-color: #444; }
|
||||||
|
span.icn-bg-15 { background-color: #aaa; }
|
||||||
|
}
|
||||||
p:first-of-type {
|
p:first-of-type {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
p:last-of-type {
|
p:last-of-type {
|
||||||
padding-bottom: 0.1em;
|
padding-bottom: 0.1em;
|
||||||
|
margin-bottom: 0em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +133,7 @@ div.post.pk-text.anchored {
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.post.multipart {
|
div.post.multipart {
|
||||||
margin-top: -0.9em;
|
margin-top: -1.5em;
|
||||||
|
|
||||||
div.post-meta {
|
div.post-meta {
|
||||||
span {
|
span {
|
||||||
@@ -99,3 +141,7 @@ div.post.multipart {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.post.dark {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ module.exports = class {
|
|||||||
channelName: "",
|
channelName: "",
|
||||||
date: "",
|
date: "",
|
||||||
data: "",
|
data: "",
|
||||||
|
sessionThresholdHours: 12,
|
||||||
},
|
},
|
||||||
timezones: tz.names(),
|
timezones: tz.names(),
|
||||||
}
|
}
|
||||||
@@ -58,12 +59,24 @@ module.exports = class {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (input.importer === "MircLike") {
|
} else if (input.importer === "MircLike") {
|
||||||
this.state.error = "Date is required for mIRC-like logs"
|
this.state.error = "Date is required for mIRC-like logs."
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
input.date = null;
|
input.date = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.importer === "IRCCloudLog") {
|
||||||
|
const hours = parseFloat(input.sessionThresholdHours)
|
||||||
|
if (Number.isNaN(hours)) {
|
||||||
|
this.state.error = "The session change threshold must be a number."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
input.sessionThresholdMs = Math.floor(hours * 3600000)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete input.sessionThresholdHours
|
||||||
|
|
||||||
if (input.channelName.length < 2 || input.channelName.includes(" ") || input.channelName.includes(" ") || input.channelName.charAt(0) !== "#") {
|
if (input.channelName.length < 2 || input.channelName.includes(" ") || input.channelName.includes(" ") || input.channelName.charAt(0) !== "#") {
|
||||||
this.state.error = "A valid channelName name is required"
|
this.state.error = "A valid channelName name is required"
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import moment from "moment"
|
|||||||
<select key="importer" autofocus class="big" placeholder="Kind" on-change("change", "importer") value=state.values.importer>
|
<select key="importer" autofocus class="big" placeholder="Kind" on-change("change", "importer") value=state.values.importer>
|
||||||
<option value="MircLike" selected=(state.values.importer === "MircLike")>mIRC-Like log</option>
|
<option value="MircLike" selected=(state.values.importer === "MircLike")>mIRC-Like log</option>
|
||||||
<option value="ForumLog" selected=(state.values.importer === "ForumLog")>Forum Log</option>
|
<option value="ForumLog" selected=(state.values.importer === "ForumLog")>Forum Log</option>
|
||||||
|
<option value="IrcCloud" selected=(state.values.importer === "IrcCloud")>IRCCloud log export</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label>Timezone</label>
|
<label>Timezone</label>
|
||||||
@@ -25,6 +26,11 @@ import moment from "moment"
|
|||||||
<input key="date" class="big" placeholder="(Required)" on-change("change", "date") value=state.values.date />
|
<input key="date" class="big" placeholder="(Required)" on-change("change", "date") value=state.values.date />
|
||||||
</if>
|
</if>
|
||||||
|
|
||||||
|
<if(state.values.importer === "IrcCloud")>
|
||||||
|
<label>Session Change Threshold (Hours)</label>
|
||||||
|
<input key="sessionThreshold" class="big" placeholder="(Required)" on-change("change", "sessionThresholdHours") value=state.values.sessionThresholdHours />
|
||||||
|
</if>
|
||||||
|
|
||||||
<label>Content</label>
|
<label>Content</label>
|
||||||
<textarea key="data" placeholder="(Required)" class="tall" on-change("change", "data") value=state.values.data />
|
<textarea key="data" placeholder="(Required)" class="tall" on-change("change", "data") value=state.values.data />
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ module.exports = class {
|
|||||||
if (filter.search) {
|
if (filter.search) {
|
||||||
queries.push("search=" + encodeURIComponent(filter.search))
|
queries.push("search=" + encodeURIComponent(filter.search))
|
||||||
}
|
}
|
||||||
if (filter.limit) {
|
if (filter.limit != null) {
|
||||||
queries.push("limit=" + encodeURIComponent(filter.limit.toString()))
|
queries.push("limit=" + encodeURIComponent(filter.limit.toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,9 +79,14 @@ module.exports = class {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearLimit() {
|
clearLimit(e) {
|
||||||
this.state.filter = Object.assign({}, this.state.filter, {limit: 0})
|
this.state.filter = Object.assign({}, this.state.filter, {limit: 0})
|
||||||
|
|
||||||
|
// Disable mobile URL fallback.
|
||||||
|
if (e && e.preventDefault) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
this.refresh()
|
this.refresh()
|
||||||
this.updateQuery(this.state.filter)
|
this.updateQuery(this.state.filter)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
<main class="logs-page with-right">
|
<main class="logs-page with-right">
|
||||||
<logs-table logs=state.logs on-addfilter("addFilter") />
|
<logs-table logs=state.logs on-addfilter("addFilter") />
|
||||||
<if(state.filter.limit > 0)>
|
<if(state.filter.limit > 0)>
|
||||||
<div class="show-more-button" on-click("clearLimit")>
|
<a href="/logs?limit=0" class="show-more-button" on-click("clearLimit")>
|
||||||
<span>Show All</span>
|
<span>Show All</span>
|
||||||
</div>
|
</a>
|
||||||
</if>
|
</if>
|
||||||
</main>
|
</main>
|
||||||
<add-log-modal enabled=(state.modal === "log.add") on-close("close") />
|
<add-log-modal enabled=(state.modal === "log.add") on-close("close") />
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
main.logs-page {
|
main.logs-page {
|
||||||
div.show-more-button {
|
.show-more-button {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
padding: 0.5em 2em;
|
padding: 0.5em 2em;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
display: block;
|
||||||
|
|
||||||
color: #DA1;
|
color: #DA1;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.show-more-button:hover {
|
.show-more-button:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ module.exports = class {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "title": {
|
||||||
|
this.state.color = "color-primary"
|
||||||
|
this.state.text = value
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
this.state.color = "color-menu"
|
this.state.color = "color-menu"
|
||||||
this.state.text = value
|
this.state.text = value
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
<if(state.text != null && state.text != "")>
|
<if(state.text != null && state.text != "")>
|
||||||
<div class=["chapter-meta", input.kind, state.color, input.weak ? "weak" : null] title=state.tooltip>
|
<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>
|
<a if(state.href != null) href=state.href>${state.text}</a>
|
||||||
<span else>${state.text}</span>
|
<span else class="tooltip">
|
||||||
|
<span>${state.text}</span>
|
||||||
|
|
||||||
|
<div if(input.character != null) class="tooltip-content color-highlight-dark color-primary">
|
||||||
|
<h1>${input.character.name}</h1>
|
||||||
|
<h2>${input.character.author}</h2>
|
||||||
|
<markdown source=input.character.description />
|
||||||
|
</div>
|
||||||
|
<div else-if(input.ukAuthor != null) class="tooltip-content color-highlight-dark color-primary">
|
||||||
|
<h1>(Unknown Character)</h1>
|
||||||
|
<h2>${input.ukAuthor}</h2>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</if>
|
</if>
|
||||||
@@ -13,6 +13,10 @@ div.chapter-meta {
|
|||||||
a:hover {
|
a:hover {
|
||||||
border-bottom: 0.5px solid;
|
border-bottom: 0.5px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
border-bottom: 0.25px dotted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.chapter-meta.weak {
|
div.chapter-meta.weak {
|
||||||
|
|||||||
@@ -3,9 +3,42 @@ module.exports = class {
|
|||||||
this.state = {
|
this.state = {
|
||||||
modal: null,
|
modal: null,
|
||||||
removed: false,
|
removed: false,
|
||||||
|
commentLabels: {
|
||||||
|
Article: "Comment",
|
||||||
|
Message: "Message",
|
||||||
|
Chat: "Chat Message",
|
||||||
|
},
|
||||||
|
selectedComment: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOpenComment(comment, modal) {
|
||||||
|
this.state.selectedComment = comment
|
||||||
|
this.state.modal = modal
|
||||||
|
}
|
||||||
|
|
||||||
|
addComment(comment) {
|
||||||
|
this.updateChapter({
|
||||||
|
comments: this.input.chapter.comments.concat(comment),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
editComment(comment) {
|
||||||
|
const comments = this.input.chapter.comments.slice()
|
||||||
|
const index = comments.findIndex(c => c.id === comment.id)
|
||||||
|
if (index != -1) {
|
||||||
|
comments[index] = comment
|
||||||
|
} else {
|
||||||
|
comments.push(comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateChapter({comments})
|
||||||
|
}
|
||||||
|
|
||||||
|
removeComment(comment) {
|
||||||
|
this.updateChapter({comments: this.input.chapter.comments.filter(c => c.id !== comment.id)})
|
||||||
|
}
|
||||||
|
|
||||||
updateChapter(data) {
|
updateChapter(data) {
|
||||||
this.emit("edit", data)
|
this.emit("edit", data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,26 @@
|
|||||||
<chapter-meta kind="date" utc=true value=input.chapter.fictionalDate />
|
<chapter-meta kind="date" utc=true value=input.chapter.fictionalDate />
|
||||||
<chapter-meta weak kind="date" value=input.chapter.createdDate />
|
<chapter-meta weak kind="date" value=input.chapter.createdDate />
|
||||||
<chapter-meta weak kind="author" value=input.chapter.author />
|
<chapter-meta weak kind="author" value=input.chapter.author />
|
||||||
|
<chapter-meta weak kind="message" value=input.chapter.commentMode />
|
||||||
</div>
|
</div>
|
||||||
<markdown class="chapter-content color-text" source=input.chapter.source />
|
<markdown class="chapter-content color-text" source=input.chapter.source />
|
||||||
|
<div class=["comment-section", "type-" + (input.chapter.commentMode.toLowerCase() || "disabled")]>
|
||||||
|
<comment for(comment in input.chapter.comments) key=comment.id
|
||||||
|
comment=comment
|
||||||
|
locked=input.chapter.commentsLocked
|
||||||
|
mode=input.chapter.commentMode
|
||||||
|
user=input.user
|
||||||
|
on-open("onOpenComment", comment)
|
||||||
|
/>
|
||||||
|
|
||||||
|
<if-permitted if(input.chapter.canComment) user=input.user permission=["member", "chapter.edit"]>
|
||||||
|
<div class="add-comment color-menu" on-click("open", "addComment") >Add ${state.commentLabels[input.chapter.commentMode] || "Comment"}</div>
|
||||||
|
</if-permitted>
|
||||||
|
</div>
|
||||||
<edit-chapter-modal enabled=(state.modal === "edit") chapter=input.chapter on-close("close") on-edit("updateChapter") />
|
<edit-chapter-modal enabled=(state.modal === "edit") chapter=input.chapter on-close("close") on-edit("updateChapter") />
|
||||||
|
<create-comment-modal enabled=(state.modal === "addComment") chapter=input.chapter user=input.user on-close("close") on-add("addComment") />
|
||||||
|
<edit-comment-modal enabled=(state.modal === "editComment") chapter=input.chapter comment=state.selectedComment user=input.user on-close("close") on-edit("editComment") />
|
||||||
|
<remove-comment-modal enabled=(state.modal === "removeComment") comment=state.selectedComment user=input.user on-close("close") on-remove("removeComment", state.selectedComment) />
|
||||||
<move-chapter-modal enabled=(state.modal === "move") chapter=input.chapter user=input.user on-close("close") on-move("moveChapter") />
|
<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") />
|
<remove-chapter-modal enabled=(state.modal === "remove") chapter=input.chapter on-close("close") on-remove("removeChapter") />
|
||||||
</div>
|
</div>
|
||||||
@@ -49,4 +49,30 @@
|
|||||||
div.chapter-content {
|
div.chapter-content {
|
||||||
font-size: 1.25em;
|
font-size: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.comment-section {
|
||||||
|
margin: 1em 4ch 2em 4ch;
|
||||||
|
|
||||||
|
div.add-comment {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1em;
|
||||||
|
padding: 1em 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
border: 1px solid;
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
div.add-comment:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
div.add-comment:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
div.comment-section {
|
||||||
|
margin: 1em 2ch 2em 2ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
showCharacter: false,
|
||||||
|
paragraphs: input.comment.source.split('\n').filter(l => l.length > 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(input) {
|
||||||
|
if (this.state) {
|
||||||
|
this.state.paragraphs = input.comment.source.split('\n').filter(l => l.length > 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(key) {
|
||||||
|
this.state[key] = !this.state[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import moment from "moment"
|
||||||
|
|
||||||
|
<div class=["comment", "mode-" + input.mode.toLowerCase()]>
|
||||||
|
<if (input.mode === "Message")>
|
||||||
|
<div class="metadata">
|
||||||
|
<div class="options color-menu">
|
||||||
|
<if-permitted user=input.user author=input.comment.author permission="story.edit">
|
||||||
|
<a on-click("emit", "open", "editComment", input.comment) >Edit</a>
|
||||||
|
</if-permitted>
|
||||||
|
<if-permitted user=input.user author=input.comment.author permission="story.edit">
|
||||||
|
<a on-click("emit", "open", "removeComment", input.comment) >Remove</a>
|
||||||
|
</if-permitted>
|
||||||
|
</div>
|
||||||
|
<chapter-meta weak kind="date" value=input.comment.createdDate />
|
||||||
|
<chapter-meta weak kind="author" value=input.comment.author />
|
||||||
|
</div>
|
||||||
|
<div class="content color-blockquote">
|
||||||
|
<table class="header">
|
||||||
|
<tbody>
|
||||||
|
<tr if(input.comment.subject != "")>
|
||||||
|
<th>Subject:</th>
|
||||||
|
<td>${input.comment.subject}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>From:</th>
|
||||||
|
<td>
|
||||||
|
<div if(input.comment.character != null) class="tooltip" style="display: inline-block">
|
||||||
|
<span>${input.comment.characterName}</span>
|
||||||
|
|
||||||
|
<div class="tooltip-content color-highlight-dark">
|
||||||
|
<h1>${input.comment.character.name}</h1>
|
||||||
|
<h2>${input.comment.character.author}</h2>
|
||||||
|
<markdown source=input.comment.character.description />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div else>
|
||||||
|
<span class="tooltip color-primary">
|
||||||
|
<span>${input.comment.characterName}</span>
|
||||||
|
|
||||||
|
<div class="tooltip-content color-highlight-dark color-primary">
|
||||||
|
<h1>(Unknown Character)</h1>
|
||||||
|
<h2>${input.comment.author}</h2>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr if(input.comment.fictionalDate != null)>
|
||||||
|
<th>Date:</th>
|
||||||
|
<td>
|
||||||
|
<date utc value=input.comment.fictionalDate></date>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<markdown class="body" source=input.comment.source />
|
||||||
|
</div>
|
||||||
|
</if>
|
||||||
|
<if(input.mode === "Chat")>
|
||||||
|
<div class="content">
|
||||||
|
<div class="options color-menu">
|
||||||
|
<if-permitted user=input.user author=input.comment.author permission="story.edit">
|
||||||
|
<a on-click("emit", "open", "editComment", input.comment) >Edit</a>
|
||||||
|
</if-permitted>
|
||||||
|
<if-permitted user=input.user author=input.comment.author permission="story.edit">
|
||||||
|
<a on-click("emit", "open", "removeComment", input.comment) >Remove</a>
|
||||||
|
</if-permitted>
|
||||||
|
</div>
|
||||||
|
<for (paragraph in state.paragraphs)>
|
||||||
|
<div class="message-text color-text">
|
||||||
|
<markdown class="body" source=paragraph>
|
||||||
|
<if (input.comment.fictionalDate)>
|
||||||
|
<span class="decoration color-menu">[</span>
|
||||||
|
<date utc class="color-menu" value=input.comment.fictionalDate></date>
|
||||||
|
<span class="decoration color-menu">] </span>
|
||||||
|
</if>
|
||||||
|
<span class="decoration color-menu"><</span>
|
||||||
|
<span if(input.comment.character != null) class="tooltip color-primary">
|
||||||
|
<span>${input.comment.characterName}</span>
|
||||||
|
|
||||||
|
<div class="tooltip-content color-highlight-dark color-primary">
|
||||||
|
<h1>${input.comment.character.name}</h1>
|
||||||
|
<h2>${input.comment.character.author}</h2>
|
||||||
|
<markdown source=input.comment.character.description />
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span class="tooltip color-primary" else>
|
||||||
|
<span>${input.comment.characterName}</span>
|
||||||
|
|
||||||
|
<div class="tooltip-content color-highlight-dark color-primary">
|
||||||
|
<h1>(Unknown Character)</h1>
|
||||||
|
<h2>${input.comment.author}</h2>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span class="decoration color-menu">> </span>
|
||||||
|
</markdown>
|
||||||
|
</div>
|
||||||
|
</for>
|
||||||
|
</div>
|
||||||
|
</if>
|
||||||
|
<if(input.mode === "Article")>
|
||||||
|
<div class="metadata">
|
||||||
|
<div class="options color-menu">
|
||||||
|
<if-permitted user=input.user author=input.comment.author permission="story.edit">
|
||||||
|
<a on-click("emit", "open", "editComment", input.comment) >Edit</a>
|
||||||
|
</if-permitted>
|
||||||
|
<if-permitted user=input.user author=input.comment.author permission="story.edit">
|
||||||
|
<a on-click("emit", "open", "removeComment", input.comment) >Remove</a>
|
||||||
|
</if-permitted>
|
||||||
|
</div>
|
||||||
|
<chapter-meta kind="title" value=input.comment.subject />
|
||||||
|
<chapter-meta kind="name" value=input.comment.characterName character=(input.comment.character) ukAuthor=input.comment.author />
|
||||||
|
<chapter-meta utc kind="date" value=input.comment.fictionalDate />
|
||||||
|
<chapter-meta weak kind="date" value=input.comment.createdDate />
|
||||||
|
<chapter-meta weak kind="author" value=input.comment.author />
|
||||||
|
</div>
|
||||||
|
<div class="content color-text">
|
||||||
|
<markdown class="body" source=input.comment.source />
|
||||||
|
</div>
|
||||||
|
</if>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
div.comment {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
div.options {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 95ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.comment.mode-message {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
font-size: 1em;
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
outline: 1px solid;
|
||||||
|
padding: 0.25em 1ch;
|
||||||
|
|
||||||
|
font-size: 1.25em;
|
||||||
|
|
||||||
|
table.header {
|
||||||
|
border-bottom: 0.5px solid;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
th {
|
||||||
|
width: 12ch;
|
||||||
|
padding-right: 1ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body {
|
||||||
|
padding-top: 0.5em;
|
||||||
|
|
||||||
|
p:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.comment.mode-chat {
|
||||||
|
div.options {
|
||||||
|
display: none;
|
||||||
|
bottom: -0.25em;
|
||||||
|
|
||||||
|
a {
|
||||||
|
background: black;
|
||||||
|
opacity: 0.666;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.content {
|
||||||
|
line-height: 2em;
|
||||||
|
font-size: 1.125em;
|
||||||
|
|
||||||
|
div.tooltip-content {
|
||||||
|
text-indent: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.message-text {
|
||||||
|
vertical-align: top;
|
||||||
|
padding-left: 2ch;
|
||||||
|
text-indent: -2ch;
|
||||||
|
|
||||||
|
div.body {
|
||||||
|
p:first-of-type {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.comment.mode-article {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.comment.mode-chat:hover {
|
||||||
|
div.options {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ module.exports = class {
|
|||||||
title: "",
|
title: "",
|
||||||
source: "",
|
source: "",
|
||||||
fictionalDate: "",
|
fictionalDate: "",
|
||||||
|
commentMode: "Disabled",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,18 +20,18 @@ module.exports = class {
|
|||||||
|
|
||||||
onInput(input) {
|
onInput(input) {
|
||||||
if (input.chapter && !this.first) {
|
if (input.chapter && !this.first) {
|
||||||
let {fictionalDate, title, source} = input.chapter
|
let {fictionalDate, title, source, commentMode, commentsLocked} = input.chapter
|
||||||
|
|
||||||
if (fictionalDate != null) {
|
if (fictionalDate != null) {
|
||||||
fictionalDate = moment.utc(fictionalDate).format("MMM D, YYYY")
|
fictionalDate = moment.utc(fictionalDate).format("MMM D, YYYY")
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.values = {fictionalDate, title, source}
|
this.state.values = {fictionalDate, title, source, commentMode, commentsLocked}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
change(key, ev) {
|
change(key, ev) {
|
||||||
this.state.values[key] = ev.target.value
|
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
@@ -52,15 +53,19 @@ module.exports = class {
|
|||||||
|
|
||||||
let fictionalDate = new Date(values.fictionalDate + " UTC")
|
let fictionalDate = new Date(values.fictionalDate + " UTC")
|
||||||
if (values.fictionalDate != "") {
|
if (values.fictionalDate != "") {
|
||||||
if (Number.isNaN(fictionalDate)) {
|
if (Number.isNaN(fictionalDate.getTime())) {
|
||||||
this.state.error = `Could not parse ${values.fictionalDate} as date!`
|
this.state.error = `Could not parse ${values.fictionalDate} as date.`
|
||||||
|
if (values.fictionalDate.includes("th")) {
|
||||||
|
this.state.error += " Try to remove the 'th'."
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fictionalDate = null
|
fictionalDate = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = {storyId: this.input.storyId, title: values.title, source: values.source, fictionalDate}
|
const input = {storyId: this.input.storyId, title: values.title, source: values.source, fictionalDate, commentMode: values.commentMode}
|
||||||
|
|
||||||
chapterApi.addChapter(input).then(chapter => {
|
chapterApi.addChapter(input).then(chapter => {
|
||||||
this.emit("add", chapter)
|
this.emit("add", chapter)
|
||||||
@@ -70,11 +75,12 @@ module.exports = class {
|
|||||||
title: "",
|
title: "",
|
||||||
source: "",
|
source: "",
|
||||||
fictionalDate: "",
|
fictionalDate: "",
|
||||||
|
commentMode: "Disabled",
|
||||||
}
|
}
|
||||||
}).catch(errs => {
|
}).catch(errs => {
|
||||||
console.warn("Failed to edit:", errs)
|
console.warn("Failed to edit:", errs)
|
||||||
|
|
||||||
this.state.error = "Failed to edit: " + errs[0].message
|
this.state.error = "Failed to edit: " + (errs[0]||errs||{}).message || errs
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.state.loading = false
|
this.state.loading = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,5 +12,13 @@
|
|||||||
<label>Content</label>
|
<label>Content</label>
|
||||||
<textarea key="source" placeholder="" class="tall" on-change("change", "source") value=state.values.source />
|
<textarea key="source" placeholder="" class="tall" on-change("change", "source") value=state.values.source />
|
||||||
|
|
||||||
|
<label>Comments</label>
|
||||||
|
<select key="kind" class="big" placeholder="Kind" on-change("change", "commentMode") value=state.values.commentMode>
|
||||||
|
<option value="Disabled" selected=(state.values.commentMode === "Disabled")>Disabled – Hide all comments</option>
|
||||||
|
<option value="Article" selected=(state.values.commentMode === "Article")>Article – Looks like story chapters</option>
|
||||||
|
<option value="Chat" selected=(state.values.commentMode === "Chat")>Chat – One-line chat messages</option>
|
||||||
|
<option value="Message" selected=(state.values.commentMode === "Message")>Message – Extranet Mail</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<button disabled=state.loading on-click("save")>Save</button>
|
<button disabled=state.loading on-click("save")>Save</button>
|
||||||
</modal>
|
</modal>
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
const moment = require("moment")
|
||||||
|
|
||||||
|
const {commentApi} = require("../../../../../rpdata/api/Comment")
|
||||||
|
const {charactersApi} = require("../../../../../rpdata/api/Character")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
values: {
|
||||||
|
subject: "",
|
||||||
|
source: "",
|
||||||
|
characterName: "",
|
||||||
|
characterId: "",
|
||||||
|
fictionalDate: "",
|
||||||
|
},
|
||||||
|
characters: [],
|
||||||
|
opened: false,
|
||||||
|
placeholder: "(Required)",
|
||||||
|
}
|
||||||
|
|
||||||
|
this.first = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount() {
|
||||||
|
charactersApi.list({author: (this.input.user||{}).name}).then((characters) => {
|
||||||
|
this.state.characters = characters.sort((a,b) => a.name.localeCompare(b.name))
|
||||||
|
})
|
||||||
|
|
||||||
|
const comments = this.input.chapter.comments || []
|
||||||
|
const lastComment = comments[comments.length - 1]
|
||||||
|
if (lastComment != null) {
|
||||||
|
let date = lastComment.fictionalDate || this.input.chapter.fictionalDate
|
||||||
|
if (date != null) {
|
||||||
|
this.change("fictionalDate", {target: {value: moment(date).format("MMM D, YYYY")}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
change(key, ev) {
|
||||||
|
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
|
||||||
|
|
||||||
|
if (key === "characterId") {
|
||||||
|
if (ev.target.value !== "") {
|
||||||
|
const character = this.state.characters.find(c => c.id === this.state.values.characterId)
|
||||||
|
if (character != null) {
|
||||||
|
this.state.placeholder = character.name
|
||||||
|
} else {
|
||||||
|
this.state.placeholder = "(Required)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.state.placeholder = "(Required)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
if (!this.state.opened) {
|
||||||
|
charactersApi.list({author: (this.input.user||{}).name}).then((characters) => {
|
||||||
|
this.state.characters = characters.sort((a,b) => a.name.localeCompare(b.name))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.first = false
|
||||||
|
this.state.opened = false
|
||||||
|
this.emit("close")
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
const values = Object.assign({}, this.state.values)
|
||||||
|
|
||||||
|
if (values.source.length == 0) {
|
||||||
|
this.state.error = "The comment body is empty."
|
||||||
|
console.warn(values)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.input.chapter.commentMode !== "Chat" && values.subject.trim().length == 0) {
|
||||||
|
this.state.error = "The subject is empty."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.characterName.trim().length < 1) {
|
||||||
|
const character = this.state.characters.find(c => c.id === this.state.values.characterId)
|
||||||
|
if (character == null) {
|
||||||
|
this.state.error = "You need to give a display name for an anonymous character."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
values.characterName = character.name
|
||||||
|
}
|
||||||
|
|
||||||
|
let fictionalDate = new Date(values.fictionalDate + " UTC")
|
||||||
|
if (values.fictionalDate != "") {
|
||||||
|
if (Number.isNaN(fictionalDate)) {
|
||||||
|
this.state.error = `Could not parse ${values.fictionalDate} as date!`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fictionalDate = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
chapterId: this.input.chapter.id,
|
||||||
|
subject: values.subject,
|
||||||
|
source: values.source,
|
||||||
|
characterName: values.characterName,
|
||||||
|
characterId: values.characterId,
|
||||||
|
fictionalDate: fictionalDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.characterId == "") {
|
||||||
|
input.characterId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
commentApi.addComment(input).then(comment => {
|
||||||
|
this.emit("add", comment)
|
||||||
|
this.emit("close")
|
||||||
|
|
||||||
|
this.state.values = {
|
||||||
|
subject: "",
|
||||||
|
source: "",
|
||||||
|
characterName: "",
|
||||||
|
characterId: "",
|
||||||
|
fictionalDate: "",
|
||||||
|
}
|
||||||
|
this.state.opened = false
|
||||||
|
}).catch(errs => {
|
||||||
|
console.warn("Failed to add comemnt:", errs)
|
||||||
|
|
||||||
|
this.state.error = "Failed to add comemnt: " + (errs[0]||errs||{}).message || errs
|
||||||
|
}).then(() => {
|
||||||
|
this.state.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
|
||||||
|
<h1>Add Comment</h1>
|
||||||
|
|
||||||
|
<p key="error" class="color-error">${state.error}</p>
|
||||||
|
|
||||||
|
<if(input.chapter.commentMode !== "Chat")>
|
||||||
|
<label>Subject</label>
|
||||||
|
<input key="subject" autofocus placeholder="" class="big" on-change("change", "subject") value=state.values.subject />
|
||||||
|
</if>
|
||||||
|
|
||||||
|
<label>Character ${state.characterId}</label>
|
||||||
|
<select on-change("change", "characterId")>
|
||||||
|
<option value="" selected=state.values.characterId=="">Anonymous</option>
|
||||||
|
<option for(character in state.characters) value=character.id selected=(state.values.characterId==character.id)>${character.name}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Display Name</label>
|
||||||
|
<input key="characterName" autofocus placeholder=state.placeholder on-change("change", "characterName") value=state.values.characterName />
|
||||||
|
|
||||||
|
<label>IC Date</label>
|
||||||
|
<input key="icdate" placeholder="(Optional)" on-change("change", "fictionalDate") value=state.values.fictionalDate />
|
||||||
|
|
||||||
|
<label>Content</label>
|
||||||
|
<textarea key="source" placeholder="" class="tall" on-change("change", "source") value=state.values.source />
|
||||||
|
|
||||||
|
<button disabled=state.loading on-click("save")>Save</button>
|
||||||
|
</modal>
|
||||||
@@ -11,6 +11,8 @@ module.exports = class {
|
|||||||
title: "",
|
title: "",
|
||||||
source: "",
|
source: "",
|
||||||
fictionalDate: "",
|
fictionalDate: "",
|
||||||
|
commentMode: "Article",
|
||||||
|
commentsLocked: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,18 +21,20 @@ module.exports = class {
|
|||||||
|
|
||||||
onInput(input) {
|
onInput(input) {
|
||||||
if (input.chapter && !this.first) {
|
if (input.chapter && !this.first) {
|
||||||
let {fictionalDate, title, source} = input.chapter
|
let {fictionalDate, title, source, commentMode, commentsLocked} = input.chapter
|
||||||
|
|
||||||
if (fictionalDate != null) {
|
if (fictionalDate != null) {
|
||||||
fictionalDate = moment.utc(fictionalDate).format("MMM D, YYYY")
|
fictionalDate = moment.utc(fictionalDate).format("MMM D, YYYY")
|
||||||
|
} else {
|
||||||
|
fictionalDate = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.values = {fictionalDate, title, source}
|
this.state.values = {fictionalDate, title, source, commentMode, commentsLocked}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
change(key, ev) {
|
change(key, ev) {
|
||||||
this.state.values[key] = ev.target.value
|
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
@@ -47,15 +51,19 @@ module.exports = class {
|
|||||||
|
|
||||||
let fictionalDate = new Date(values.fictionalDate + " UTC")
|
let fictionalDate = new Date(values.fictionalDate + " UTC")
|
||||||
if (values.fictionalDate != "") {
|
if (values.fictionalDate != "") {
|
||||||
if (Number.isNaN(fictionalDate)) {
|
if (Number.isNaN(fictionalDate.getTime())) {
|
||||||
this.state.error = `Could not parse ${values.fictionalDate} as date`
|
this.state.error = `Could not parse ${values.fictionalDate} as date.`
|
||||||
|
if (values.fictionalDate.includes("th")) {
|
||||||
|
this.state.error += " Try to remove the 'th'."
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fictionalDate = null
|
fictionalDate = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = {id: this.input.chapter.id, title: values.title, source: values.source}
|
const input = {id: this.input.chapter.id, title: values.title, source: values.source, commentsLocked: values.commentsLocked, commentMode: values.commentMode}
|
||||||
|
|
||||||
if (fictionalDate != null) {
|
if (fictionalDate != null) {
|
||||||
input.fictionalDate = fictionalDate
|
input.fictionalDate = fictionalDate
|
||||||
|
|||||||
@@ -12,5 +12,17 @@
|
|||||||
<label>Content</label>
|
<label>Content</label>
|
||||||
<textarea key="source" placeholder="Content" class="tall" on-change("change", "source") value=state.values.source />
|
<textarea key="source" placeholder="Content" class="tall" on-change("change", "source") value=state.values.source />
|
||||||
|
|
||||||
|
<label>Comments</label>
|
||||||
|
<select key="kind" class="big" placeholder="Kind" on-change("change", "commentMode") value=state.values.commentMode>
|
||||||
|
<option value="Disabled" selected=(state.values.commentMode === "Disabled")>Disabled – Hide all comments</option>
|
||||||
|
<option value="Article" selected=(state.values.commentMode === "Article")>Article – Looks like story chapters</option>
|
||||||
|
<option value="Chat" selected=(state.values.commentMode === "Chat")>Chat – One-line chat messages</option>
|
||||||
|
<option value="Message" selected=(state.values.commentMode === "Message")>Message – Extranet Mail</option>
|
||||||
|
</select>
|
||||||
|
<toggle value=state.values.commentsLocked on="Locked" off="Unlocked"
|
||||||
|
onDesc="Logged-in users can post comments."
|
||||||
|
offDesc="Nobody can post comments to this chapter."
|
||||||
|
on-change("change", "commentsLocked") />
|
||||||
|
|
||||||
<button disabled=state.loading on-click("save")>Save</button>
|
<button disabled=state.loading on-click("save")>Save</button>
|
||||||
</modal>
|
</modal>
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
const moment = require("moment")
|
||||||
|
|
||||||
|
const {commentApi} = require("../../../../../rpdata/api/Comment")
|
||||||
|
const {charactersApi} = require("../../../../../rpdata/api/Character")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
values: {
|
||||||
|
subject: "",
|
||||||
|
source: "",
|
||||||
|
characterName: "",
|
||||||
|
characterId: "",
|
||||||
|
fictionalDate: "",
|
||||||
|
},
|
||||||
|
characters: [],
|
||||||
|
opened: false,
|
||||||
|
placeholder: "(Required)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(input) {
|
||||||
|
if (input.comment) {
|
||||||
|
charactersApi.list({author: (input.user||{}).name}).then((characters) => {
|
||||||
|
this.state.characters = characters.sort((a,b) => a.name.localeCompare(b.name))
|
||||||
|
})
|
||||||
|
|
||||||
|
const comment = input.comment
|
||||||
|
|
||||||
|
let fictionalDate = new Date(comment.fictionalDate)
|
||||||
|
if (comment.fictionalDate && !Number.isNaN(fictionalDate.getTime()) && fictionalDate.getTime() !== 0) {
|
||||||
|
fictionalDate = moment(fictionalDate).format("MMM D, YYYY")
|
||||||
|
} else {
|
||||||
|
fictionalDate = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.values = {
|
||||||
|
subject: comment.subject,
|
||||||
|
characterId: comment.characterId,
|
||||||
|
characterName: comment.characterName,
|
||||||
|
fictionalDate: fictionalDate,
|
||||||
|
source: comment.source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
change(key, ev) {
|
||||||
|
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
|
||||||
|
|
||||||
|
if (key === "characterId") {
|
||||||
|
if (ev.target.value !== "") {
|
||||||
|
const character = this.state.characters.find(c => c.id === this.state.values.characterId)
|
||||||
|
if (character != null) {
|
||||||
|
this.state.placeholder = character.name
|
||||||
|
} else {
|
||||||
|
this.state.placeholder = "(Required)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.state.placeholder = "(Required)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.emit("close")
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
const values = Object.assign({}, this.state.values)
|
||||||
|
let clearFictionalDate = false
|
||||||
|
|
||||||
|
if (values.source.length == 0) {
|
||||||
|
this.state.error = "The comment body is empty."
|
||||||
|
console.warn(values)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.input.chapter.commentMode !== "Chat" && values.subject.trim().length == 0) {
|
||||||
|
this.state.error = "The subject is empty."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.characterName.trim().length < 1) {
|
||||||
|
const character = this.state.characters.find(c => c.id === this.state.values.characterId)
|
||||||
|
if (character == null) {
|
||||||
|
this.state.error = "You need to give a display name for an anonymous character."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
values.characterName = character.name
|
||||||
|
}
|
||||||
|
|
||||||
|
let fictionalDate = new Date(values.fictionalDate + " UTC")
|
||||||
|
if (values.fictionalDate != "") {
|
||||||
|
if (Number.isNaN(fictionalDate)) {
|
||||||
|
this.state.error = `Could not parse ${values.fictionalDate} as date!`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fictionalDate = null
|
||||||
|
clearFictionalDate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
commentId: this.input.comment.id,
|
||||||
|
subject: values.subject,
|
||||||
|
source: values.source,
|
||||||
|
characterName: values.characterName,
|
||||||
|
characterId: values.characterId,
|
||||||
|
fictionalDate: fictionalDate,
|
||||||
|
clearFictionalDate: clearFictionalDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.characterId == "") {
|
||||||
|
input.characterId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
commentApi.editComment(input).then(comment => {
|
||||||
|
this.emit("edit", comment)
|
||||||
|
this.close()
|
||||||
|
|
||||||
|
this.state.values = {
|
||||||
|
subject: "",
|
||||||
|
source: "",
|
||||||
|
characterName: "",
|
||||||
|
characterId: "",
|
||||||
|
fictionalDate: "",
|
||||||
|
}
|
||||||
|
}).catch(errs => {
|
||||||
|
console.warn("Failed to add comemnt:", errs)
|
||||||
|
|
||||||
|
this.state.error = "Failed to add comemnt: " + (errs[0]||errs||{}).message || errs
|
||||||
|
}).then(() => {
|
||||||
|
this.state.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") >
|
||||||
|
<h1>Edit Comment</h1>
|
||||||
|
|
||||||
|
<p key="error" class="color-error">${state.error}</p>
|
||||||
|
|
||||||
|
<if(input.chapter.commentMode !== "Chat")>
|
||||||
|
<label>Subject</label>
|
||||||
|
<input key="subject" autofocus placeholder="" class="big" on-change("change", "subject") value=state.values.subject />
|
||||||
|
</if>
|
||||||
|
|
||||||
|
<label>Character ${state.characterId}</label>
|
||||||
|
<select on-change("change", "characterId")>
|
||||||
|
<option value="" selected=state.values.characterId=="">Anonymous</option>
|
||||||
|
<option for(character in state.characters) value=character.id selected=(state.values.characterId==character.id)>${character.name}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Display Name</label>
|
||||||
|
<input key="characterName" autofocus placeholder=state.placeholder on-change("change", "characterName") value=state.values.characterName />
|
||||||
|
|
||||||
|
<label>IC Date</label>
|
||||||
|
<input key="icdate" placeholder="(Optional)" on-change("change", "fictionalDate") value=state.values.fictionalDate />
|
||||||
|
|
||||||
|
<label>Content</label>
|
||||||
|
<textarea key="source" placeholder="" class="tall" on-change("change", "source") value=state.values.source />
|
||||||
|
|
||||||
|
<button disabled=state.loading on-click("save")>Save</button>
|
||||||
|
</modal>
|
||||||
@@ -2,6 +2,8 @@ const moment = require("moment")
|
|||||||
|
|
||||||
const {storyApi} = require("../../../../../rpdata/api/Story")
|
const {storyApi} = require("../../../../../rpdata/api/Story")
|
||||||
|
|
||||||
|
const ORDINAL_REGEX = /[0-9]{1,2}(st|nd|rd|th)/g
|
||||||
|
|
||||||
module.exports = class {
|
module.exports = class {
|
||||||
onCreate() {
|
onCreate() {
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -45,9 +47,15 @@ module.exports = class {
|
|||||||
const values = this.state.values
|
const values = this.state.values
|
||||||
|
|
||||||
let fictionalDate = new Date(values.fictionalDate + " UTC")
|
let fictionalDate = new Date(values.fictionalDate + " UTC")
|
||||||
if (values.fictionalDate != "") {
|
if (values.fictionalDate) {
|
||||||
if (Number.isNaN(fictionalDate)) {
|
if (Number.isNaN(fictionalDate.getTime())) {
|
||||||
this.state.error = `Could not parse ${values.fictionalDate} as date`
|
this.state.error = `Could not parse ${values.fictionalDate} as date.`
|
||||||
|
|
||||||
|
const ordinal = values.fictionalDate.match(ORDINAL_REGEX)[0]
|
||||||
|
if (ordinal != null) {
|
||||||
|
this.state.error += " Try to remove the '"+ordinal.slice(-2)+"'."
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -36,13 +36,7 @@ module.exports = class {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in data) {
|
this.state.story.chapters[index] = Object.assign({}, this.state.story.chapters[index], data);
|
||||||
if (!data.hasOwnProperty(key)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.story.chapters[index][key] = data[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshStory()
|
this.refreshStory()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
const {commentApi} = require("../../../../../rpdata/api/Comment")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
values: {
|
||||||
|
title: "",
|
||||||
|
source: "",
|
||||||
|
fictionalDate: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
change(key, ev) {
|
||||||
|
this.state.values[key] = ev.target.value
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.emit("close")
|
||||||
|
}
|
||||||
|
|
||||||
|
doIt() {
|
||||||
|
commentApi.removeComment({commentId: this.input.comment.id}).then(() => {
|
||||||
|
this.emit("remove")
|
||||||
|
this.emit("close")
|
||||||
|
}).catch(errs => {
|
||||||
|
console.warn("Failed to delete:", errs)
|
||||||
|
|
||||||
|
this.state.error = "Failed to delete: " + errs.message || errs[0].message
|
||||||
|
}).then(() => {
|
||||||
|
this.state.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) notification closable on-close("close") >
|
||||||
|
<h1>Delete Comment</h1>
|
||||||
|
|
||||||
|
<p class="color-error">${state.error}</p>
|
||||||
|
|
||||||
|
<p class="color-danger">This is irreversible!</p>
|
||||||
|
|
||||||
|
<button disabled=state.loading on-click("doIt")>${Math.random() > 0.99 ? "Deleet" : "Delete"}</button>
|
||||||
|
</modal>
|
||||||
@@ -42,8 +42,12 @@ module.exports = class {
|
|||||||
|
|
||||||
let fictionalDate = new Date(values.fictionalDate + " UTC")
|
let fictionalDate = new Date(values.fictionalDate + " UTC")
|
||||||
if (values.fictionalDate != "") {
|
if (values.fictionalDate != "") {
|
||||||
if (Number.isNaN(fictionalDate)) {
|
if (Number.isNaN(fictionalDate.getTime())) {
|
||||||
this.state.error = `Could not parse ${values.fictionalDate} as date`
|
this.state.error = `Could not parse ${values.fictionalDate} as date.`
|
||||||
|
if (values.fictionalDate.includes("th")) {
|
||||||
|
this.state.error += " Try to remove the 'th'."
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ module.exports = (req, res, next) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (["/static/", "/hax/", "/assets/"].find(p => req.path.startsWith(p)) == null) {
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
res.locals.user = {
|
res.locals.user = {
|
||||||
loggedIn: true,
|
loggedIn: true,
|
||||||
@@ -39,6 +40,7 @@ module.exports = (req, res, next) => {
|
|||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ const strategy = new Auth0Strategy({
|
|||||||
domain: config.auth0.domain,
|
domain: config.auth0.domain,
|
||||||
clientID: config.auth0.clientId,
|
clientID: config.auth0.clientId,
|
||||||
clientSecret: config.auth0.secret, // Replace this with the client secret for your app
|
clientSecret: config.auth0.secret, // Replace this with the client secret for your app
|
||||||
callbackURL: config.root + "/auth/callback"
|
callbackURL: config.root + "/auth/callback",
|
||||||
|
state: true,
|
||||||
}, (accessToken, refreshToken, extraParams, profile, done) => {
|
}, (accessToken, refreshToken, extraParams, profile, done) => {
|
||||||
// accessToken is the token to call Auth0 API (not needed in the most cases)
|
// accessToken is the token to call Auth0 API (not needed in the most cases)
|
||||||
// extraParams.id_token has the JSON Web Token
|
// extraParams.id_token has the JSON Web Token
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module.exports = session({
|
|||||||
secret: config.session.secret,
|
secret: config.session.secret,
|
||||||
store: new RedisStore(config.redis),
|
store: new RedisStore(config.redis),
|
||||||
cookie: {
|
cookie: {
|
||||||
secure: config.session.secure
|
secure: config.session.secure,
|
||||||
},
|
},
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: true,
|
saveUninitialized: true,
|
||||||
|
|||||||
Generated
+7766
-118
File diff suppressed because it is too large
Load Diff
+4
-3
@@ -27,17 +27,18 @@
|
|||||||
"graphql-query-compress": "^1.1.0",
|
"graphql-query-compress": "^1.1.0",
|
||||||
"http-proxy-middleware": "^0.19.1",
|
"http-proxy-middleware": "^0.19.1",
|
||||||
"ip": "^1.1.5",
|
"ip": "^1.1.5",
|
||||||
|
"irc-caret-notation": "^1.0.0",
|
||||||
"isomorphic-fetch": "^2.2.1",
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"jsonwebtoken": "^8.3.0",
|
"jsonwebtoken": "^8.3.0",
|
||||||
"lasso": "^3.2.6",
|
"lasso": "^3.3.1",
|
||||||
"lasso-babel-transform": "^1.0.2",
|
"lasso-babel-transform": "^1.0.2",
|
||||||
"lasso-less": "^3.0.1",
|
"lasso-less": "^3.0.1",
|
||||||
"lasso-marko": "^2.4.4",
|
"lasso-marko": "^2.4.7",
|
||||||
"less": "^3.8.1",
|
"less": "^3.8.1",
|
||||||
"markdown-it": "^8.4.2",
|
"markdown-it": "^8.4.2",
|
||||||
"markdown-it-container": "^2.0.0",
|
"markdown-it-container": "^2.0.0",
|
||||||
"markdown-it-regexp": "^0.4.0",
|
"markdown-it-regexp": "^0.4.0",
|
||||||
"marko": "^4.13.8",
|
"marko": "^4.18.13",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"moment-timezone": "^0.5.23",
|
"moment-timezone": "^0.5.23",
|
||||||
"passport": "^0.4.0",
|
"passport": "^0.4.0",
|
||||||
|
|||||||
+8
-4
@@ -17,13 +17,17 @@ router.get("/login", saveReferer, passport.authenticate("auth0", {scope: "openid
|
|||||||
res.redirect("/")
|
res.redirect("/")
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get("/callback", passport.authenticate("auth0"), (req, res) => {
|
router.get("/callback", function (req, res, next) {
|
||||||
if (req.user == null) {
|
passport.authenticate("auth0", function (err, user, info) {
|
||||||
throw new Error("user null");
|
if (err) { return next(err) }
|
||||||
}
|
if (!user) { console.log(err, user, info); return res.redirect("/auth/login") }
|
||||||
|
req.logIn(user, function (err) {
|
||||||
|
if (err) { return next(err) }
|
||||||
|
|
||||||
res.redirect(req.session.loginReferrer || "/");
|
res.redirect(req.session.loginReferrer || "/");
|
||||||
})
|
})
|
||||||
|
})(req, res, next);
|
||||||
|
})
|
||||||
|
|
||||||
router.get("/user", (req, res) => {
|
router.get("/user", (req, res) => {
|
||||||
if (req.user == null) {
|
if (req.user == null) {
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ const {changesApi} = require("../../rpdata/api/Change")
|
|||||||
const changesTemplate = require("../../marko/page/data/changes.marko")
|
const changesTemplate = require("../../marko/page/data/changes.marko")
|
||||||
|
|
||||||
router.get("/", async(req, res) => {
|
router.get("/", async(req, res) => {
|
||||||
|
const limit = parseInt(req.query.limit) || 100
|
||||||
|
|
||||||
res.markoAsync(changesTemplate, {
|
res.markoAsync(changesTemplate, {
|
||||||
changes: changesApi.list({limit: 2000}),
|
changes: changesApi.list({limit}),
|
||||||
selected: {changes: true},
|
selected: {changes: true},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
const {generateToken} = require("../graphql")
|
||||||
|
const {fileApi} = require("../../rpdata/api/File")
|
||||||
|
|
||||||
|
const filesTemplate = require("../../marko/page/data/files.marko")
|
||||||
|
|
||||||
|
router.get("/", async(req, res) => {
|
||||||
|
let privates = []
|
||||||
|
if (res.locals.user && res.locals.user.loggedIn) {
|
||||||
|
privates = fileApi.list({public: false}, {token: generateToken(res.locals.user.name)})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.markoAsync(filesTemplate, {
|
||||||
|
privates: privates,
|
||||||
|
publics: fileApi.list({public: true}),
|
||||||
|
selected: {files: true},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
+6
-12
@@ -3,16 +3,10 @@ const express = require("express")
|
|||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
|
|
||||||
const config = require("../config")
|
const config = require("../config")
|
||||||
const { query } = require("../rpdata/client")
|
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.post("/", (req, res) => {
|
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 user = res.locals.user
|
||||||
const permissions = (req.header("X-Permissions") || "").split(",").filter(t => t != "" && t != "undefined" && t != "null")
|
const permissions = (req.header("X-Permissions") || "").split(",").filter(t => t != "" && t != "undefined" && t != "null")
|
||||||
let authorization = req.header("Authorization")
|
let authorization = req.header("Authorization")
|
||||||
@@ -20,17 +14,17 @@ router.post("/", (req, res) => {
|
|||||||
authorization = ""
|
authorization = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authorization && permissions.length > 0 && user.loggedIn) {
|
if (!authorization && user.loggedIn) {
|
||||||
authorization = `Bearer ${generateToken(user.name, permissions)}`
|
authorization = `Bearer ${generateToken(user.name, permissions)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(config.graphqlEndpoint, {
|
fetch(config.graphqlEndpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": req.header("Content-Type"),
|
||||||
"Authorization": authorization,
|
"Authorization": authorization,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(req.body),
|
body: req,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
}).then(fetchRes => {
|
}).then(fetchRes => {
|
||||||
res.setHeader("Content-Type", fetchRes.headers.get("Content-Type"))
|
res.setHeader("Content-Type", fetchRes.headers.get("Content-Type"))
|
||||||
@@ -51,8 +45,8 @@ router.use("/", proxy(config.graphqlEndpoint, {ws: true}))
|
|||||||
* @param {string} user
|
* @param {string} user
|
||||||
* @param {string[]} permissions
|
* @param {string[]} permissions
|
||||||
*/
|
*/
|
||||||
function generateToken(user, permissions) {
|
function generateToken(user) {
|
||||||
return jwt.sign({user, permissions, exp: Math.floor((Date.now() / 1000) + 1200)}, config.backend.secret, {header: {kid: config.backend.kid}})
|
return jwt.sign({user, exp: Math.floor((Date.now() / 1000) + 1200)}, config.backend.secret, {header: {kid: config.backend.kid}})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router
|
module.exports = {router, generateToken}
|
||||||
@@ -48,6 +48,11 @@ class ChangesAPI {
|
|||||||
objects {
|
objects {
|
||||||
type: __typename
|
type: __typename
|
||||||
|
|
||||||
|
...on File {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
...on Character {
|
...on Character {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -68,6 +73,11 @@ class ChangesAPI {
|
|||||||
time
|
time
|
||||||
nick
|
nick
|
||||||
}
|
}
|
||||||
|
|
||||||
|
...on Comment {
|
||||||
|
chapterId
|
||||||
|
characterName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-3
@@ -1,4 +1,5 @@
|
|||||||
const {query} = require("../client")
|
const {query} = require("../client")
|
||||||
|
const {Comment} = require("./Comment")
|
||||||
|
|
||||||
class Chapter {
|
class Chapter {
|
||||||
/**
|
/**
|
||||||
@@ -9,8 +10,12 @@ class Chapter {
|
|||||||
* @param {string | Date} createdDate
|
* @param {string | Date} createdDate
|
||||||
* @param {string | Date} fictionalDate
|
* @param {string | Date} fictionalDate
|
||||||
* @param {string | Date} editedDate
|
* @param {string | Date} editedDate
|
||||||
|
* @param {boolean} canComment
|
||||||
|
* @param {"Disabled"|"Article"|"Chat"|"Message"} commentMode
|
||||||
|
* @param {boolean} commentsLocked
|
||||||
|
* @param {any[]} comments
|
||||||
*/
|
*/
|
||||||
constructor(id, title, author, source, createdDate, fictionalDate, editedDate) {
|
constructor(id, title, author, source, createdDate, fictionalDate, editedDate, canComment, commentMode, commentsLocked, comments) {
|
||||||
this.id = id
|
this.id = id
|
||||||
this.title = title
|
this.title = title
|
||||||
this.author = author
|
this.author = author
|
||||||
@@ -18,6 +23,10 @@ class Chapter {
|
|||||||
this.createdDate = new Date(createdDate)
|
this.createdDate = new Date(createdDate)
|
||||||
this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null
|
this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null
|
||||||
this.editedDate = new Date(editedDate)
|
this.editedDate = new Date(editedDate)
|
||||||
|
this.canComment = canComment
|
||||||
|
this.commentMode = commentMode
|
||||||
|
this.commentsLocked = commentsLocked
|
||||||
|
this.comments = (comments||[]).map(c => Comment.fromData(c))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,12 +48,31 @@ class ChapterAPI {
|
|||||||
createdDate
|
createdDate
|
||||||
fictionalDate
|
fictionalDate
|
||||||
editedDate
|
editedDate
|
||||||
|
canComment
|
||||||
|
commentMode
|
||||||
|
commentsLocked
|
||||||
|
comments {
|
||||||
|
id
|
||||||
|
subject
|
||||||
|
author
|
||||||
|
characterName
|
||||||
|
character {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
author
|
||||||
|
description
|
||||||
|
}
|
||||||
|
fictionalDate
|
||||||
|
createdDate
|
||||||
|
editedDate
|
||||||
|
source
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, {input}, {permissions: ["member", "story.add"]}).then((data) => {
|
`, {input}, {permissions: ["member", "story.add"]}).then((data) => {
|
||||||
const ac = data.addChapter
|
const ac = data.addChapter
|
||||||
|
|
||||||
return new Chapter(ac.id, ac.title, ac.author, ac.source, ac.createdDate, ac.fictionalDate, ac.editedDate)
|
return new Chapter(ac.id, ac.title, ac.author, ac.source, ac.createdDate, ac.fictionalDate, ac.editedDate, ac.canComment, ac.commentMode, ac.commentsLocked, ac.comments)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +89,25 @@ class ChapterAPI {
|
|||||||
title
|
title
|
||||||
fictionalDate
|
fictionalDate
|
||||||
source
|
source
|
||||||
|
canComment
|
||||||
|
commentMode
|
||||||
|
commentsLocked
|
||||||
|
comments {
|
||||||
|
id
|
||||||
|
subject
|
||||||
|
author
|
||||||
|
characterName
|
||||||
|
character {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
author
|
||||||
|
description
|
||||||
|
}
|
||||||
|
fictionalDate
|
||||||
|
createdDate
|
||||||
|
editedDate
|
||||||
|
source
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, {input}, {permissions: ["member", "story.edit"]}).then(({editChapter}) => {
|
`, {input}, {permissions: ["member", "story.edit"]}).then(({editChapter}) => {
|
||||||
@@ -68,7 +115,7 @@ class ChapterAPI {
|
|||||||
editChapter.fictionalDate = new Date(editChapter.fictionalDate)
|
editChapter.fictionalDate = new Date(editChapter.fictionalDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
return editChapter
|
return Object.assign({comments:[]}, editChapter)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
const {query} = require("../client")
|
||||||
|
|
||||||
|
class Comment {
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {string} subject
|
||||||
|
* @param {string} author
|
||||||
|
* @param {string} characterName
|
||||||
|
* @param {CommentCharacter} character
|
||||||
|
* @param {string | number | Date} fictionalDate
|
||||||
|
* @param {string | number | Date} createdDate
|
||||||
|
* @param {string | number | Date} editedDate
|
||||||
|
* @param {string} source
|
||||||
|
*/
|
||||||
|
constructor(id, subject, author, characterName, character, fictionalDate, createdDate, editedDate, source) {
|
||||||
|
this.id = id
|
||||||
|
this.subject = subject
|
||||||
|
this.author = author
|
||||||
|
this.characterName = characterName
|
||||||
|
this.character = character != null ? CommentCharacter.fromData(character) : null
|
||||||
|
this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null
|
||||||
|
this.createdDate = new Date(createdDate)
|
||||||
|
this.editedDate = new Date(editedDate)
|
||||||
|
this.source = source
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromData(data) {
|
||||||
|
return new Comment(data.id, data.subject, data.author, data.characterName, data.character, data.fictionalDate, data.createdDate, data.editedDate, data.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommentCharacter {
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} author
|
||||||
|
* @param {string} description
|
||||||
|
*/
|
||||||
|
constructor(id, name, author, description) {
|
||||||
|
this.id = id
|
||||||
|
this.name = name
|
||||||
|
this.author = author
|
||||||
|
this.description = description
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromData(data) {
|
||||||
|
return new CommentCharacter(data.id, data.name, data.author, data.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommentAPI {
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {Promise<Comment>}
|
||||||
|
*/
|
||||||
|
find(id) {
|
||||||
|
return query(`
|
||||||
|
query FindComment($id: String!) {
|
||||||
|
comment(id: $id) {
|
||||||
|
id
|
||||||
|
subject
|
||||||
|
author
|
||||||
|
characterName
|
||||||
|
character {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
author
|
||||||
|
description
|
||||||
|
}
|
||||||
|
fictionalDate
|
||||||
|
createdDate
|
||||||
|
editedDate
|
||||||
|
source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {id}).then(({comment}) => {
|
||||||
|
return new Comment(
|
||||||
|
comment.id, comment.subject, comment.author,
|
||||||
|
comment.characterName, comment.character,
|
||||||
|
comment.fictionalDate, comment.createdDate,
|
||||||
|
comment.editedDate, comment.source,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} input
|
||||||
|
* @returns {Promise<Comment>}
|
||||||
|
*/
|
||||||
|
addComment(input) {
|
||||||
|
return query(`
|
||||||
|
mutation AddComment($input: CommentAddInput!) {
|
||||||
|
addComment(input: $input) {
|
||||||
|
id
|
||||||
|
subject
|
||||||
|
author
|
||||||
|
characterName
|
||||||
|
character {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
author
|
||||||
|
description
|
||||||
|
}
|
||||||
|
fictionalDate
|
||||||
|
createdDate
|
||||||
|
editedDate
|
||||||
|
source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {input}, {permissions:["member", "story.edit"]}).then(({addComment: comment}) => {
|
||||||
|
return new Comment(
|
||||||
|
comment.id, comment.subject, comment.author,
|
||||||
|
comment.characterName, comment.character,
|
||||||
|
comment.fictionalDate, comment.createdDate,
|
||||||
|
comment.editedDate, comment.source,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} input
|
||||||
|
* @returns {Promise<Comment>}
|
||||||
|
*/
|
||||||
|
editComment(input) {
|
||||||
|
return query(`
|
||||||
|
mutation EditComment($input: CommentEditInput!) {
|
||||||
|
editComment(input: $input) {
|
||||||
|
id
|
||||||
|
subject
|
||||||
|
author
|
||||||
|
characterName
|
||||||
|
character {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
author
|
||||||
|
description
|
||||||
|
}
|
||||||
|
fictionalDate
|
||||||
|
createdDate
|
||||||
|
editedDate
|
||||||
|
source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {input}, {permissions:["member", "story.edit"]}).then(({editComment: comment}) => {
|
||||||
|
return new Comment(
|
||||||
|
comment.id, comment.subject, comment.author,
|
||||||
|
comment.characterName, comment.character,
|
||||||
|
comment.fictionalDate, comment.createdDate,
|
||||||
|
comment.editedDate, comment.source,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{commentId:string}} input
|
||||||
|
* @returns {Promise<{commentId:string}>}
|
||||||
|
*/
|
||||||
|
removeComment(input) {
|
||||||
|
return query(`
|
||||||
|
mutation RemoveComment($input: CommentRemoveInput!) {
|
||||||
|
removeComment(input: $input) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {input}, {permissions:["member", "story.edit"]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {Comment, CommentCharacter, commentApi: new CommentAPI}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
const {query, queryForm} = require("../client")
|
||||||
|
|
||||||
|
class File {
|
||||||
|
/**
|
||||||
|
* @param {{id:string,kind:string,time:Date|string|number,public:boolean,name:string,mimeType:string,size:number,author:string,url:string}} data
|
||||||
|
*/
|
||||||
|
constructor(data) {
|
||||||
|
this.id = data.id
|
||||||
|
this.kind = data.kind
|
||||||
|
this.time = new Date(data.time)
|
||||||
|
this.public = data.public
|
||||||
|
this.name = data.name
|
||||||
|
this.mimeType = data.mimeType
|
||||||
|
this.size = data.size
|
||||||
|
this.author = data.author
|
||||||
|
this.url = data.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FileAPI {
|
||||||
|
/**
|
||||||
|
* Call `characters(filter)` query
|
||||||
|
*
|
||||||
|
* @param {{public:boolean,mimeTypes:string[]}} filter
|
||||||
|
* @returns {Promise<File[]>}
|
||||||
|
*/
|
||||||
|
list(filter = {public: true}, opts = {}) {
|
||||||
|
return query(`
|
||||||
|
query ListFiles($filter: FilesFilter!) {
|
||||||
|
files(filter:$filter) {
|
||||||
|
id
|
||||||
|
kind
|
||||||
|
time
|
||||||
|
public
|
||||||
|
name
|
||||||
|
mimeType
|
||||||
|
size
|
||||||
|
author
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {filter}, opts).then(({files}) => {
|
||||||
|
return files.map(d => new File(d))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload a file
|
||||||
|
*
|
||||||
|
* @param {{public:boolean, file:File, name:string}} input
|
||||||
|
*/
|
||||||
|
uploadFile(input) {
|
||||||
|
return queryForm(`
|
||||||
|
mutation UploadFile($input: FileUploadInput!) {
|
||||||
|
uploadFile(input:$input) {
|
||||||
|
id
|
||||||
|
kind
|
||||||
|
time
|
||||||
|
public
|
||||||
|
name
|
||||||
|
mimeType
|
||||||
|
size
|
||||||
|
author
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {input}).then(({uploadFile}) => {
|
||||||
|
return new File(uploadFile)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit a file
|
||||||
|
*
|
||||||
|
* @param {{id: string, public:boolean, name:string}} input
|
||||||
|
*/
|
||||||
|
editFile(input) {
|
||||||
|
return queryForm(`
|
||||||
|
mutation EditFile($input: FileEditInput!) {
|
||||||
|
editFile(input:$input) {
|
||||||
|
id
|
||||||
|
public
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {input}).then(({editFile}) => {
|
||||||
|
return editFile
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit a file
|
||||||
|
*
|
||||||
|
* @param {{id: string, public:boolean, name:string}} input
|
||||||
|
*/
|
||||||
|
removeFile(input) {
|
||||||
|
return queryForm(`
|
||||||
|
mutation RemoveFIle($input: FileRemoveInput!) {
|
||||||
|
removeFile(input:$input) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {input}).then(({removeFile}) => {
|
||||||
|
return removeFile
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { File, fileApi: new FileAPI() }
|
||||||
+19
-3
@@ -31,7 +31,11 @@ class Log {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromData(data) {
|
static fromData(data) {
|
||||||
return new Log(data.id, data.shortId, data.date, data.title, data.eventName, data.description, data.open, data.channel, data.characters, data.posts)
|
return new Log(
|
||||||
|
data.id, data.shortId, data.date,
|
||||||
|
data.title, data.eventName, data.description,
|
||||||
|
data.open, data.channel, data.characters, data.posts
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,10 +74,22 @@ class LogAPI {
|
|||||||
nick
|
nick
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
nextLogs {
|
||||||
|
log {id title date channelName}
|
||||||
|
hasEvent
|
||||||
|
hasChannel
|
||||||
|
characters {name}
|
||||||
|
}
|
||||||
|
prevLogs {
|
||||||
|
log {id title date channelName}
|
||||||
|
hasEvent
|
||||||
|
hasChannel
|
||||||
|
characters {name}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, {id}).then(({log}) => {
|
`, {id}).then(({log}) => {
|
||||||
return Log.fromData(log)
|
return log
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +235,7 @@ class LogAPI {
|
|||||||
subscribeChanges(logId) {
|
subscribeChanges(logId) {
|
||||||
return new Subscription(`
|
return new Subscription(`
|
||||||
subscription Changes($keys: [ChangeKeyInput!]!) {
|
subscription Changes($keys: [ChangeKeyInput!]!) {
|
||||||
changes(keys: $keys) {
|
changes(filter: {keys: $keys}) {
|
||||||
id
|
id
|
||||||
model
|
model
|
||||||
op
|
op
|
||||||
|
|||||||
@@ -27,6 +27,15 @@ class LogHeader {
|
|||||||
this.open = open
|
this.open = open
|
||||||
this.characters = characters.map(ch => new LogHeaderCharacter(ch.id, ch.name, ch.shortName, ch.author))
|
this.characters = characters.map(ch => new LogHeaderCharacter(ch.id, ch.name, ch.shortName, ch.author))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all characters in that log.
|
||||||
|
*
|
||||||
|
* @param {...string} ids Character IDs
|
||||||
|
*/
|
||||||
|
findCharacters(...ids) {
|
||||||
|
return this.characters.filter(c => ids.includes(c.id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogHeaderCharacter {
|
class LogHeaderCharacter {
|
||||||
@@ -89,6 +98,11 @@ const logHeaderApi = {
|
|||||||
* @returns {Promise<LogHeader[]>}
|
* @returns {Promise<LogHeader[]>}
|
||||||
*/
|
*/
|
||||||
list(filter = {}) {
|
list(filter = {}) {
|
||||||
|
filter = Object.assign({}, filter)
|
||||||
|
if (filter.limit != null && filter.limit <= 0) {
|
||||||
|
delete filter.limit
|
||||||
|
}
|
||||||
|
|
||||||
return query(`
|
return query(`
|
||||||
query LogHeaders($filter: LogsFilter) {
|
query LogHeaders($filter: LogsFilter) {
|
||||||
headers: logs(filter:$filter) {
|
headers: logs(filter:$filter) {
|
||||||
|
|||||||
+20
-1
@@ -29,7 +29,7 @@ class Story {
|
|||||||
this.updatedDate = new Date(updatedDate)
|
this.updatedDate = new Date(updatedDate)
|
||||||
this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null
|
this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null
|
||||||
this.tags = tags.map(dt => new Tag(dt.kind, dt.name))
|
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
|
this.chapters = chapters != null ? chapters.map(c => new Chapter(c.id, c.title, c.author, c.source, c.createdDate, c.fictionalDate, c.editedDate, c.canComment, c.commentMode, c.commentsLocked, c.comments)) : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +114,25 @@ const storyApi = {
|
|||||||
createdDate
|
createdDate
|
||||||
fictionalDate
|
fictionalDate
|
||||||
editedDate
|
editedDate
|
||||||
|
canComment
|
||||||
|
commentMode
|
||||||
|
commentsLocked
|
||||||
|
comments {
|
||||||
|
id
|
||||||
|
subject
|
||||||
|
author
|
||||||
|
characterName
|
||||||
|
character {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
author
|
||||||
|
description
|
||||||
|
}
|
||||||
|
fictionalDate
|
||||||
|
createdDate
|
||||||
|
editedDate
|
||||||
|
source
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-2
@@ -16,7 +16,7 @@ function query(query, variables = {}, options = {}) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": options.token ? `Bearer ${options.token}` : null,
|
"Authorization": options.token ? `Bearer ${options.token}` : null,
|
||||||
"X-Permissions": options.permissions ? options.permissions.join(",") : null,
|
//"X-Permissions": options.permissions ? options.permissions.join(",") : null,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({query: query.length > 256 ? compressQuery(query || "") : query, variables, operationName: options.operationName}),
|
body: JSON.stringify({query: query.length > 256 ? compressQuery(query || "") : query, variables, operationName: options.operationName}),
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
@@ -31,6 +31,67 @@ function query(query, variables = {}, options = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {string} query GQL Query
|
||||||
|
* @param {Object} variables Variables
|
||||||
|
* @param {{token?:string,permissions?:string[]}} options
|
||||||
|
*/
|
||||||
|
function queryForm(query, variables = {}, options = {}) {
|
||||||
|
const map = {}
|
||||||
|
const files = []
|
||||||
|
|
||||||
|
function findFiles(prefix, data) {
|
||||||
|
for (const key in data) {
|
||||||
|
if (!data.hasOwnProperty(key)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const value = data[key]
|
||||||
|
|
||||||
|
if (value instanceof File) {
|
||||||
|
map[files.length] = [`${prefix}.${key}`]
|
||||||
|
files.push(value)
|
||||||
|
} else if (typeof(value) === 'object') {
|
||||||
|
data[key] = findFiles(`${prefix}.${key}`, Object.assign({}, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
const operations = {
|
||||||
|
query: query,
|
||||||
|
variables: findFiles("variables", Object.assign({}, variables)),
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = new FormData()
|
||||||
|
form.append("operations", JSON.stringify(operations))
|
||||||
|
form.append("map", JSON.stringify(map))
|
||||||
|
for (let i = 0; i < files.length; ++i) {
|
||||||
|
form.append(i.toString(), files[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(form)
|
||||||
|
|
||||||
|
return fetch(config.graphqlEndpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": options.token ? `Bearer ${options.token}` : null,
|
||||||
|
"X-Permissions": options.permissions ? options.permissions.join(",") : null,
|
||||||
|
},
|
||||||
|
body: form,
|
||||||
|
credentials: "include",
|
||||||
|
}).then(res => {
|
||||||
|
return res.json()
|
||||||
|
}).then(json => {
|
||||||
|
if (json.errors != null && json.errors.length > 0) {
|
||||||
|
return Promise.reject(json.errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
class Subscription extends EventEmitter {
|
class Subscription extends EventEmitter {
|
||||||
constructor(query, variables) {
|
constructor(query, variables) {
|
||||||
super()
|
super()
|
||||||
@@ -115,4 +176,4 @@ class Subscription extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { query, Subscription }
|
module.exports = { query, queryForm, Subscription }
|
||||||
@@ -28,7 +28,12 @@ lasso.configure(require("./marko-config"))
|
|||||||
|
|
||||||
// Set trusted proxy
|
// Set trusted proxy
|
||||||
if (config.trustedProxy != null) {
|
if (config.trustedProxy != null) {
|
||||||
if (ip.isV4Format(config.trustedProxy) || ip.isV6Format(config.trustedProxy)) {
|
if (config.trustedProxy === "GATEWAY") {
|
||||||
|
const ip = require("child_process").execSync("/sbin/ip route|awk '/default/ { print $3 }'").toString().trim()
|
||||||
|
app.set('trust proxy', ip)
|
||||||
|
|
||||||
|
console.log("Trusting proxy from default gateway", ip);
|
||||||
|
} else if (ip.isV4Format(config.trustedProxy) || ip.isV6Format(config.trustedProxy)) {
|
||||||
app.set('trust proxy', config.trustedProxy)
|
app.set('trust proxy', config.trustedProxy)
|
||||||
|
|
||||||
console.log("Trusting proxy", config.trustedProxy);
|
console.log("Trusting proxy", config.trustedProxy);
|
||||||
@@ -69,6 +74,12 @@ app.use(session)
|
|||||||
app.use(passport.initialize())
|
app.use(passport.initialize())
|
||||||
app.use(passport.session())
|
app.use(passport.session())
|
||||||
app.use(require("./middleware/locals"))
|
app.use(require("./middleware/locals"))
|
||||||
|
|
||||||
|
// Proxy (does not want all middleware)
|
||||||
|
app.use("/graphql", require("./routes/graphql").router)
|
||||||
|
app.use("/playground", proxy(config.playgroundEndpoint))
|
||||||
|
|
||||||
|
// The rest of the middleware
|
||||||
app.use(bodyParser.json({limit: "1mb"}))
|
app.use(bodyParser.json({limit: "1mb"}))
|
||||||
app.use(bodyParser.text({limit: "256kb"}))
|
app.use(bodyParser.text({limit: "256kb"}))
|
||||||
|
|
||||||
@@ -79,10 +90,6 @@ app.use(lassoMiddleware.serveStatic())
|
|||||||
// Authentication
|
// Authentication
|
||||||
app.use("/auth", require("./routes/auth"))
|
app.use("/auth", require("./routes/auth"))
|
||||||
|
|
||||||
// API Proxy
|
|
||||||
app.use("/graphql", require("./routes/graphql"))
|
|
||||||
app.use("/playground", proxy(config.playgroundEndpoint))
|
|
||||||
|
|
||||||
// Global css doesn't work with prebuild.
|
// Global css doesn't work with prebuild.
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
app.use("/hax/global.css", require("./routes/globalcss"))
|
app.use("/hax/global.css", require("./routes/globalcss"))
|
||||||
@@ -102,12 +109,13 @@ app.use("/story/by-author/", require("./routes/story/by-author"))
|
|||||||
app.use("/story/tag-list/", require("./routes/story/tag-list"))
|
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("/story/:id(S[0-9a-z]{15})/", require("./routes/story-content"))
|
||||||
app.use("/logs/", require("./routes/logs"))
|
app.use("/logs/", require("./routes/logs"))
|
||||||
app.use("/logs/:id([0-9]{4}-[0-1][0-9]-[0-3][0-9]_[0-9]{9}_[A-Za-z0-9\,\']{2,})/", require("./routes/logs-content"))
|
app.use("/logs/:id([0-9]{4}-[0-1][0-9]-[0-3][0-9]_[0-9]{9}_[A-Za-z0-9\,\'\-]{2,})/", require("./routes/logs-content"))
|
||||||
app.use("/logs/:id(L[0-9]{0,7})/", require("./routes/logs-content"))
|
app.use("/logs/:id(L[0-9]{0,7})/", require("./routes/logs-content"))
|
||||||
app.use("/data/", require("./routes/data"))
|
app.use("/data/", require("./routes/data"))
|
||||||
app.use("/data/characters/", require("./routes/data/characters"))
|
app.use("/data/characters/", require("./routes/data/characters"))
|
||||||
app.use("/data/channels/", require("./routes/data/channels"))
|
app.use("/data/channels/", require("./routes/data/channels"))
|
||||||
app.use("/data/changes/", require("./routes/data/changes"))
|
app.use("/data/changes/", require("./routes/data/changes"))
|
||||||
|
app.use("/data/files/", require("./routes/data/files"))
|
||||||
|
|
||||||
// Entry point
|
// Entry point
|
||||||
app.get("/", function(req, res) {
|
app.get("/", function(req, res) {
|
||||||
|
|||||||
Reference in New Issue
Block a user