Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
268f906ea4 | ||
|
|
44a98c8823 | ||
|
|
fe44300c0a | ||
|
|
2c7ba6df1d | ||
|
|
283ffc64b2 | ||
|
|
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 | ||
|
|
624f7046af | ||
|
|
8bfc0dad14 | ||
|
|
3ed3e50486 | ||
|
|
5260d96f37 | ||
|
|
8b3d642104 | ||
|
|
46369f84bd | ||
|
|
68f3effa8d | ||
|
|
4657ae2aec | ||
|
|
f134d57b79 | ||
|
|
9b4da98ac4 | ||
|
|
599ca83f0f | ||
|
|
b952007f3b | ||
|
|
c0e74f4756 | ||
|
|
68a8e7ef97 | ||
|
|
5b81f7ff0d | ||
|
|
78a14ae86b | ||
|
|
c28f17abdd | ||
|
|
de3bae3353 | ||
|
|
fcf2361fbf | ||
|
|
7c1623493a | ||
|
|
013c50d4f4 | ||
|
|
f236d35974 | ||
|
|
55379fcfe3 | ||
|
|
c4636cf843 | ||
|
|
8fd9b19bd0 | ||
|
|
5a5dfea774 | ||
|
|
c157f5f592 | ||
|
|
47f71c733b | ||
|
|
326e4478c0 | ||
|
|
68a1a64fe3 | ||
|
|
cdd165ab03 | ||
|
|
a8f4d68f1e | ||
|
|
b57873091b | ||
|
|
7a1884d1cf | ||
|
|
5be6b1ecec | ||
|
|
ea89975b14 | ||
|
|
850114af2d | ||
|
|
39d8254b9b | ||
|
|
adf73a4623 | ||
|
|
1e022d6116 | ||
|
|
d832e66402 | ||
|
|
94d0a83f46 | ||
|
|
824a8dae23 | ||
|
|
d2194347fd | ||
|
|
f850e383b1 | ||
|
|
cb2f54cecf | ||
|
|
4fd512a8e8 | ||
|
|
a6cef51a37 | ||
|
|
94a15f7388 | ||
|
|
0380b98331 | ||
|
|
cdae732654 | ||
|
|
5935cbbf54 | ||
|
|
7e94b0047c |
@@ -0,0 +1,4 @@
|
|||||||
|
.git
|
||||||
|
**/*.marko.js
|
||||||
|
.cache
|
||||||
|
.static
|
||||||
+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
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
*.marko.js
|
*.marko.js
|
||||||
|
*.prebuild.json
|
||||||
/node_modules
|
/node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
/.cache
|
/.cache
|
||||||
|
|||||||
+2
-2
@@ -26,8 +26,8 @@ RUN rm -rf .static
|
|||||||
RUN rm -rf .cache
|
RUN rm -rf .cache
|
||||||
RUN rm -rf .git
|
RUN rm -rf .git
|
||||||
|
|
||||||
# Build templates (when that works)
|
# Build templates
|
||||||
# RUN node build.js
|
RUN node build.js
|
||||||
|
|
||||||
## 2. Regroup
|
## 2. Regroup
|
||||||
FROM node:10-alpine
|
FROM node:10-alpine
|
||||||
|
|||||||
@@ -1,53 +1,29 @@
|
|||||||
const build = require("./builder")
|
const prebuild = require("@marko/prebuild");
|
||||||
|
|
||||||
// Setup global environment
|
const config = require("./marko-config")
|
||||||
require("marko/node-require").install()
|
config.loadPrebuild = false
|
||||||
require("es6-promise").polyfill()
|
|
||||||
|
|
||||||
// Configure lasso
|
prebuild.run({
|
||||||
require("lasso").configure({
|
config: config,
|
||||||
plugins: [
|
pages: [ // List of pages to prebuild.
|
||||||
"lasso-marko",
|
"./marko/page/story/list.marko",
|
||||||
"lasso-less",
|
"./marko/page/story/tag-list.marko",
|
||||||
],
|
"./marko/page/logs/list.marko",
|
||||||
outputDir: "./.static",
|
"./marko/page/data/channels.marko",
|
||||||
bundlingEnabled: true,
|
"./marko/page/data/characters.marko",
|
||||||
minify: true,
|
"./marko/page/data/changes.marko",
|
||||||
fingerprintsEnabled: true,
|
"./marko/page/data/files.marko",
|
||||||
|
"./marko/page/story-content/view.marko",
|
||||||
require: {
|
"./marko/page/logs-content/view.marko",
|
||||||
builtins: {
|
|
||||||
fs: require.resolve("empty-module"),
|
|
||||||
},
|
|
||||||
|
|
||||||
transforms: [
|
|
||||||
{
|
|
||||||
transform: "lasso-babel-transform",
|
|
||||||
config: {
|
|
||||||
extensions: [".js", ".es6"], // Enabled file extensions. Default: [".js", ".es6"]
|
|
||||||
babelOptions: {
|
|
||||||
presets: [ "es2015" ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
|
}).then(lassoPrebuildResult => {
|
||||||
|
const builds = lassoPrebuildResult._buildsByPath
|
||||||
|
|
||||||
|
for (const key in builds) {
|
||||||
|
if (!builds.hasOwnProperty(key)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Built", key)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Render templates
|
|
||||||
async function buildAll() {
|
|
||||||
await build("./marko/page/story/list.marko")
|
|
||||||
await build("./marko/page/story/tag-list.marko", {tags: []})
|
|
||||||
await build("./marko/page/logs/list.marko")
|
|
||||||
await build("./marko/page/data/channels.marko", {channels: [], user: {}})
|
|
||||||
await build("./marko/page/data/characters.marko", {characters: [], user: {}})
|
|
||||||
await build("./marko/page/story-content/view.marko", {story: {chapters: []}})
|
|
||||||
await build("./marko/page/logs-content/view.marko", {log: {posts: [], channel: {}}})
|
|
||||||
}
|
|
||||||
|
|
||||||
buildAll().then(() => {
|
|
||||||
console.log("Done!")
|
|
||||||
}).catch(err => {
|
|
||||||
console.error(err)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
module.exports = function build(path, data) {
|
|
||||||
console.log("Building", path)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const render = require(path).render(data)
|
|
||||||
render.once("error", err => reject)
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (render._remaining === 0) {
|
|
||||||
clearInterval(interval)
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}, 500)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
|
|
||||||
const config = {
|
let config = {
|
||||||
graphqlEndpoint: "http://127.0.0.1:17000/graphql",
|
graphqlEndpoint: "http://127.0.0.1:17000/graphql",
|
||||||
playgroundEndpoint: "http://127.0.0.1:17000/playground",
|
playgroundEndpoint: "http://127.0.0.1:17000/playground",
|
||||||
port: 8080,
|
port: 8080,
|
||||||
root: "http://localhost:8080",
|
root: "http://localhost:8080",
|
||||||
|
trustedProxy: null,
|
||||||
auth0: {
|
auth0: {
|
||||||
secret: "INVALID_SECRET",
|
secret: "INVALID_SECRET",
|
||||||
clientId: "",
|
clientId: "",
|
||||||
@@ -34,6 +35,31 @@ if (typeof(window) !== "undefined") {
|
|||||||
delete config.redis
|
delete config.redis
|
||||||
delete config.backend
|
delete config.backend
|
||||||
} else {
|
} else {
|
||||||
|
config = {
|
||||||
|
graphqlEndpoint: process.env.RPDATA_API_GRAPHQL || "http://127.0.0.1:17000/graphql",
|
||||||
|
playgroundEndpoint: process.env.RPDATA_API_PLAYGROUND || "http://127.0.0.1:17000/playground",
|
||||||
|
port: Number(process.env.PORT || "8080"),
|
||||||
|
root: process.env.ROOT || "http://localhost:8080",
|
||||||
|
trustedProxy: process.env.TRUSTED_PROXY || null,
|
||||||
|
auth0: {
|
||||||
|
secret: process.env.AUTH0_SECRET || "INVALID_SECRET",
|
||||||
|
clientId: process.env.AUTH0_CLIENT_ID || "",
|
||||||
|
domain: process.env.AUTH0_DOMAIN || "aiterp.eu.auth0.com",
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
secret: process.env.SESSION_SECRET || "",
|
||||||
|
secure: process.env.SESSION_SECURE === "true",
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
host: process.env.REDIS_HOST || "127.0.0.1",
|
||||||
|
port: process.env.REDIS_PORT || 6379,
|
||||||
|
},
|
||||||
|
backend: {
|
||||||
|
kid: process.env.RPDATA_KID || "",
|
||||||
|
secret: process.env.RPDATA_SECRET || "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = fs.readFileSync("/etc/aiterp/rpdata-frontend.json", "utf8")
|
const data = fs.readFileSync("/etc/aiterp/rpdata-frontend.json", "utf8")
|
||||||
const parsed = JSON.parse(data)
|
const parsed = JSON.parse(data)
|
||||||
@@ -43,6 +69,11 @@ if (typeof(window) !== "undefined") {
|
|||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error("Failed to load configuration: " + err.message || err.stack || err)
|
console.error("Failed to load configuration: " + err.message || err.stack || err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Config/Auth0", config.auth0.clientId, config.auth0.domain);
|
||||||
|
console.log("Config/API", config.graphqlEndpoint, config.backend.kid);
|
||||||
|
console.log("Config/Redis", config.redis.host, config.redis.port);
|
||||||
|
console.log("Config/Root", config.root);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = config
|
module.exports = config
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const isProduction = process.env.NODE_ENV === "production"
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
"lasso-marko",
|
||||||
|
"lasso-less",
|
||||||
|
],
|
||||||
|
outputDir: "./.static", // Place all generated JS/CSS/etc. files into the "static" dir
|
||||||
|
bundlingEnabled: isProduction, // Only enable bundling in production
|
||||||
|
minify: isProduction, // Only minify JS and CSS code in production
|
||||||
|
fingerprintsEnabled: isProduction, // Only add fingerprints to URLs in production
|
||||||
|
loadPrebuild: isProduction, // Only load prebuilt stuff in prod.
|
||||||
|
|
||||||
|
require: {
|
||||||
|
builtins: {
|
||||||
|
fs: require.resolve("empty-module"),
|
||||||
|
},
|
||||||
|
|
||||||
|
transforms: isProduction ? [
|
||||||
|
{
|
||||||
|
transform: "lasso-babel-transform",
|
||||||
|
config: {
|
||||||
|
extensions: [".js", ".es6"], // Enabled file extensions. Default: [".js", ".es6"]
|
||||||
|
babelOptions: {
|
||||||
|
presets: [ "es2015" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
] : null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const moment = require("moment")
|
const moment = require("moment")
|
||||||
|
|
||||||
module.exports = class {
|
module.exports = class {
|
||||||
onCreate({kind, value, updated}) {
|
onCreate({kind, value, updated, utc}) {
|
||||||
this.state = {text: null, color: "color-menu", tooltip: null}
|
this.state = {text: null, color: "color-menu", tooltip: null}
|
||||||
|
|
||||||
if (value == null || value == "") {
|
if (value == null || value == "") {
|
||||||
@@ -10,7 +10,7 @@ module.exports = class {
|
|||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case "date": {
|
case "date": {
|
||||||
const m = moment(value)
|
const m = utc ? moment.utc(value) : moment(value)
|
||||||
if (m.year() < 2) {
|
if (m.year() < 2) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -19,10 +19,10 @@ module.exports = class {
|
|||||||
|
|
||||||
this.state.text = dateStr
|
this.state.text = dateStr
|
||||||
|
|
||||||
if (updated != null && Date.parse(updated) > Date.parse(value)) {
|
if (updated != null && Date.parse(updated) > (Date.parse(value) + 7200000)) {
|
||||||
const m2 = moment(updated)
|
const m2 = moment(updated)
|
||||||
this.state.tooltip = "Originally posted: " + dateStr
|
this.state.tooltip = "Originally posted: " + dateStr
|
||||||
this.state.text = m2.format("MMM D, YYYY") + " *"
|
this.state.text = m2.format("MMM D, YYYY")
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<item-meta kind="date" value=input.createdDate updated=input.updatedDate />
|
<item-meta kind="date" value=input.createdDate updated=input.updatedDate />
|
||||||
<item-meta kind="date" value=input.fictionalDate />
|
<item-meta kind="date" value=input.fictionalDate utc=true />
|
||||||
<item-tag-list tags=input.tags enableAllTags=input.enableAllTags />
|
<item-tag-list tags=input.tags enableAllTags=input.enableAllTags />
|
||||||
<item-meta kind="author" value=input.author />
|
<item-meta kind="author" value=input.author />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ tr.content-list-item {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
width: 2.5ch;
|
width: 2.5ch;
|
||||||
outline: 0.05px solid;
|
outline: 0.05px solid;
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
width: 1.25ch;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> td.content {
|
> td.content {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -7,7 +7,7 @@ module.exports = class {
|
|||||||
|
|
||||||
onInput(input) {
|
onInput(input) {
|
||||||
if (!input.user || !input.user.loggedIn) {
|
if (!input.user || !input.user.loggedIn) {
|
||||||
this.state.permitted = false
|
this.state.permitted = input.not
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,5 +18,10 @@ module.exports = class {
|
|||||||
} else {
|
} else {
|
||||||
this.state.permitted = input.user.permissions.includes(input.permission)
|
this.state.permitted = input.user.permissions.includes(input.permission)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flip the above permission
|
||||||
|
if (input.not) {
|
||||||
|
this.state.permitted = !this.state.permitted
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 +1 @@
|
|||||||
<div class="menu-gap"></div>
|
<div class=["menu-gap", input.class]></div>
|
||||||
@@ -6,7 +6,7 @@ module.exports = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onInput(input) {
|
onInput(input) {
|
||||||
this.state.classes = ["menu-link", "color-menu"]
|
this.state.classes = ["menu-link", "color-menu"].concat(input.class)
|
||||||
|
|
||||||
if (input.dark) {
|
if (input.dark) {
|
||||||
this.state.classes.push(input)
|
this.state.classes.push(input)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -3,9 +3,14 @@ module.exports = class {
|
|||||||
this.state = {
|
this.state = {
|
||||||
loggingIn: false,
|
loggingIn: false,
|
||||||
loggingOut: false,
|
loggingOut: false,
|
||||||
|
enabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleEnabled() {
|
||||||
|
this.state.enabled = !this.state.enabled
|
||||||
|
}
|
||||||
|
|
||||||
loginClicked() {
|
loginClicked() {
|
||||||
this.state.loggingIn = true
|
this.state.loggingIn = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<div class="menu">
|
<div class=["menu", state.enabled ? "enabled" : "disabled"]>
|
||||||
<div class="menu-body">
|
<div class="menu-body">
|
||||||
|
<menu-link icon=(state.enabled ? "<" : ">") on-click("toggleEnabled") class="showhide">${state.enabled ? "Hide Menu" : "Show Menu"}</menu-link>
|
||||||
|
<menu-gap class="showhide" />
|
||||||
<include(input.renderBody) />
|
<include(input.renderBody) />
|
||||||
</div>
|
</div>
|
||||||
<if(input.user != null)>
|
<if(input.user != null)>
|
||||||
|
|||||||
@@ -12,4 +12,28 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.showhide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
.menu.disabled {
|
||||||
|
.menu-link, .menu-header, .menu-blurb, .menu-gap {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.showhide {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: static;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-left: -0.25ch;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
module.exports = class {
|
module.exports = class {
|
||||||
onCreate() {
|
onCreate() {
|
||||||
this.mounted = false
|
this.mounted = false
|
||||||
|
this.opened = false
|
||||||
}
|
}
|
||||||
|
|
||||||
closeModal() {
|
closeModal() {
|
||||||
this.emit("close")
|
this.emit("close")
|
||||||
|
this.opened = false
|
||||||
|
|
||||||
if (this.mounted) {
|
if (this.mounted) {
|
||||||
window.removeEventListener("keydown", this.onPress)
|
window.removeEventListener("keydown", this.onPress)
|
||||||
@@ -17,8 +19,9 @@ module.exports = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (input) {
|
if (input) {
|
||||||
if (input.enabled && (!this.input || !this.input.enabled)) {
|
if (input.enabled && !this.opened) {
|
||||||
this.emit("open")
|
this.emit("open")
|
||||||
|
this.opened = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.closable) {
|
if (input.closable) {
|
||||||
@@ -44,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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ div.overlay {
|
|||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
|
||||||
background-color: rgba(0, 0, 0, 0.90);
|
background-color: rgba(0, 0, 0, 0.95);
|
||||||
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<div class=["right-menu", input.class]>
|
||||||
|
<div class="menu-body">
|
||||||
|
<include(input.renderBody) />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
.right-menu {
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 28ch;
|
||||||
|
|
||||||
|
padding: 0.333em;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.showhide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 800px) {
|
||||||
|
.right-menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="tag-options">
|
<div class="tag-options">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Tag Name</label>
|
<label>Tag Name</label>
|
||||||
<input key="name" placeholder="Name" on-change("change", "name") value=state.values.name />
|
<input key="name" autofocus placeholder="Name" on-change("change", "name") value=state.values.name />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Tag Kind</label>
|
<label>Tag Kind</label>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
const {charactersApi} = require("../../../rpdata/api/Character")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate() {
|
||||||
|
this.state = {
|
||||||
|
nicks: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount() {
|
||||||
|
charactersApi.unknownNicks(100).then(unknowns => {
|
||||||
|
this.state.nicks = unknowns.map(u => u.nick)
|
||||||
|
}).catch(err => {
|
||||||
|
console.warn("Failed to get unknown nicks:", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="unknown-nick-list">
|
||||||
|
<div for(nick in state.nicks) on-click("emit", "select", nick) key=nick class="nick">${nick}</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
div.unknown-nick-list {
|
||||||
|
padding: 0.5em 0;
|
||||||
|
|
||||||
|
div.nick {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25em 0.5ch;
|
||||||
|
margin: 2px;
|
||||||
|
background-color: #111;
|
||||||
|
color: #881;
|
||||||
|
}
|
||||||
|
div.nick:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #222;
|
||||||
|
color: #CC2;
|
||||||
|
}
|
||||||
|
}
|
||||||
+84
-2
@@ -7,19 +7,33 @@ body {
|
|||||||
|
|
||||||
main {
|
main {
|
||||||
margin-left: 30ch; // See side-menu
|
margin-left: 30ch; // See side-menu
|
||||||
width: calc(100% - 30ch);
|
width: calc(100% - 32ch);
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
@media screen and (min-width: 700px) {
|
||||||
|
main.with-right {
|
||||||
|
width: calc(100% - 60ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
body {
|
||||||
|
main {
|
||||||
|
margin-left: 0.25ch;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Less glaring chrome/ium scrollbars.
|
* Less glaring chrome/ium scrollbars.
|
||||||
*/
|
*/
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 2px;
|
width: 2px;
|
||||||
/* for vertical scrollbars */
|
/* for vertical scrollbars */
|
||||||
height: 2px;
|
height: 2px;
|
||||||
@@ -37,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ body.theme-story {
|
|||||||
color: #4BF;
|
color: #4BF;
|
||||||
}
|
}
|
||||||
.color-highlight-dark {
|
.color-highlight-dark {
|
||||||
background-color: rgba(0, 9, 18, 0.50);
|
background-color: rgba(0, 4, 9, 0.75);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ body.theme-logs {
|
|||||||
color: #BBB;
|
color: #BBB;
|
||||||
}
|
}
|
||||||
.color-highlight-dark {
|
.color-highlight-dark {
|
||||||
background-color: rgba(18, 9, 0, 0.50);
|
background-color: rgba(9, 4, 0, 0.75);
|
||||||
}
|
}
|
||||||
.color-highlight-primary {
|
.color-highlight-primary {
|
||||||
background-color: rgba(255, 187, 17, 0.25);
|
background-color: rgba(255, 187, 17, 0.25);
|
||||||
@@ -83,7 +83,7 @@ body.theme-data {
|
|||||||
color: #BBB;
|
color: #BBB;
|
||||||
}
|
}
|
||||||
.color-highlight-dark {
|
.color-highlight-dark {
|
||||||
background-color: rgba(18, 8, 0, 0.50);
|
background-color: rgba(8, 4, 0, 0.75);
|
||||||
}
|
}
|
||||||
.color-highlight-primary {
|
.color-highlight-primary {
|
||||||
background-color: rgba(204, 204, 34, 0.25);
|
background-color: rgba(204, 204, 34, 0.25);
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -31,3 +31,13 @@ nav.layout-navbar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
nav.layout-navbar {
|
||||||
|
position: static;
|
||||||
|
margin-left: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<include("../layout", {title: "Data", site: "data"})>
|
||||||
|
<@body>
|
||||||
|
<background src="/assets/images/bg.png" opacity=0.25 />
|
||||||
|
<changes-page user=input.user changes=input.changes selected=input.selected />
|
||||||
|
</@body>
|
||||||
|
</include>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<p class="color-error">${state.error}</p>
|
<p class="color-error">${state.error}</p>
|
||||||
|
|
||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
<input key="name" placeholder="(Required)" class="big" on-change("change", "name") value=state.values.name />
|
<input key="name" autofocus placeholder="(Required)" class="big" on-change("change", "name") value=state.values.name />
|
||||||
|
|
||||||
<label>Event Name</label>
|
<label>Event Name</label>
|
||||||
<input key="eventName" placeholder="(None)" on-change("change", "eventName") value=state.values.eventName />
|
<input key="eventName" placeholder="(None)" on-change("change", "eventName") value=state.values.eventName />
|
||||||
@@ -18,7 +18,5 @@
|
|||||||
offDesc="The logbot will not monitor this channel."
|
offDesc="The logbot will not monitor this channel."
|
||||||
on-change("change", "logged") />
|
on-change("change", "logged") />
|
||||||
|
|
||||||
<p class="color-danger" if(state.values.logged)>The logbot is not yet available, so this does nothing right now.</p>
|
|
||||||
|
|
||||||
<button disabled=state.loading on-click("save")>Save</button>
|
<button disabled=state.loading on-click("save")>Save</button>
|
||||||
</modal>
|
</modal>
|
||||||
@@ -33,6 +33,14 @@ module.exports = class {
|
|||||||
this.emit("close")
|
this.emit("close")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectUnknown(nick) {
|
||||||
|
this.state.values = Object.assign({}, this.state.values, {
|
||||||
|
nick: nick,
|
||||||
|
name: nick.replace(/_/g, " ").replace(/`/g, "'"),
|
||||||
|
shortName: (!nick.includes("_") && nick.includes("`")) ? nick.split("`")[0] : "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<p class="color-error">${state.error}</p>
|
<p class="color-error">${state.error}</p>
|
||||||
|
|
||||||
<label>Full Name</label>
|
<label>Full Name</label>
|
||||||
<input key="name" placeholder="" class="big" on-change("change", "name") value=state.values.name />
|
<input key="name" autofocus placeholder="" class="big" on-change("change", "name") value=state.values.name />
|
||||||
|
|
||||||
<label>IRC Nick</label>
|
<label>IRC Nick</label>
|
||||||
<input key="nick" placeholder="" on-change("change", "nick") value=state.values.nick />
|
<input key="nick" placeholder="" on-change("change", "nick") value=state.values.nick />
|
||||||
@@ -21,4 +21,12 @@
|
|||||||
<textarea key="description" placeholder="Description" on-change("change", "description") value=state.values.description />
|
<textarea key="description" placeholder="Description" on-change("change", "description") value=state.values.description />
|
||||||
|
|
||||||
<button disabled=state.loading on-click("save")>Save</button>
|
<button disabled=state.loading on-click("save")>Save</button>
|
||||||
|
|
||||||
|
<h2>Unknown Nicks</h2>
|
||||||
|
<p>After you're done adding the one you're here for, why not take a look at the unknown nicks discovered in the last log character updates?</p>
|
||||||
|
<p>Clicking one will change the nick in the form above, and add a guess for what the full name is.</p>
|
||||||
|
<if-permitted not user=input.user permission="character.add">
|
||||||
|
<p class="color-danger">You don't have permission to set a different author on characters, so only add your own.</p>
|
||||||
|
</if-permitted>
|
||||||
|
<unknown-nick-list on-select("selectUnknown") />
|
||||||
</modal>
|
</modal>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
const moment = require("moment")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate() {
|
||||||
|
this.state = {
|
||||||
|
operation: "",
|
||||||
|
links: [],
|
||||||
|
parentLink: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(input) {
|
||||||
|
if (input.data) {
|
||||||
|
this.refresh(input.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh(data) {
|
||||||
|
this.state.links = []
|
||||||
|
this.state.operation = opText[data.op]
|
||||||
|
this.state.parentLink = null
|
||||||
|
|
||||||
|
switch (data.model) {
|
||||||
|
case "Channel": {
|
||||||
|
const key = data.keys.find(k => k.id !== "*")
|
||||||
|
|
||||||
|
this.state.operation += " channel"
|
||||||
|
this.state.links = [{
|
||||||
|
text: key.id,
|
||||||
|
href: `/data/channels/`,
|
||||||
|
}]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Character": {
|
||||||
|
const character = data.objects.find(o => o.type === "Character")
|
||||||
|
|
||||||
|
this.state.operation += " character"
|
||||||
|
this.state.links = [{
|
||||||
|
text: `${character.name} (${character.id})`,
|
||||||
|
href: `/data/characters/`,
|
||||||
|
}]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Story": {
|
||||||
|
const key = data.keys.find(k => k.model === "Story" && k.id !== "*")
|
||||||
|
const story = data.objects.find(o => o.type === "Story")
|
||||||
|
|
||||||
|
this.state.operation += " story"
|
||||||
|
this.state.links = [{
|
||||||
|
text: story.name,
|
||||||
|
href: `/story/${key.id}`,
|
||||||
|
}]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Chapter": {
|
||||||
|
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")
|
||||||
|
|
||||||
|
this.state.operation += " chapter"
|
||||||
|
this.state.links = [{
|
||||||
|
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}`,
|
||||||
|
}]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Log": {
|
||||||
|
const key = data.keys.find(k => k.id !== "*")
|
||||||
|
|
||||||
|
this.state.operation += " log"
|
||||||
|
this.state.links = [{
|
||||||
|
text: key.id,
|
||||||
|
href: `/logs/${key.id}`,
|
||||||
|
}]
|
||||||
|
|
||||||
|
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": {
|
||||||
|
const logKey = data.keys.find(k => k.model === "Log")
|
||||||
|
const postKeys = data.keys.filter(k => k.model === "Post")
|
||||||
|
const postObjs = data.objects.filter(o => o.type === "Post")
|
||||||
|
|
||||||
|
this.state.operation += (postKeys.length > 1) ? " posts" : " post"
|
||||||
|
this.state.links = postObjs.map(po => ({
|
||||||
|
text: `[${moment(po.time).format("HH:mm")}] ${po.nick}`,
|
||||||
|
href: `/logs/${logKey.id}#${po.id}`,
|
||||||
|
}))
|
||||||
|
this.state.parentLink = {
|
||||||
|
prefix: "in log",
|
||||||
|
text: `${logKey.id}`,
|
||||||
|
href: `/logs/${logKey.id}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const opText = {
|
||||||
|
add: "added",
|
||||||
|
remove: "removed",
|
||||||
|
upload: "uploaded",
|
||||||
|
move: "moved",
|
||||||
|
edit: "edited",
|
||||||
|
import: "imported",
|
||||||
|
tag: "tagged",
|
||||||
|
untag: "untagged",
|
||||||
|
"move-in": "moved in",
|
||||||
|
"move-out": "moved out",
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
<div class="change">
|
||||||
|
<span class="change-id color-menu">[${moment(input.data.date).format("MMM D HH:mm:ss")}] * </span>
|
||||||
|
<span class="author color-text">${input.data.author} </span>
|
||||||
|
<span class="text color-text">${state.operation} </span>
|
||||||
|
<span for(link in state.links | status-var=loop) key=link.href class="link">
|
||||||
|
<a class="color-primary" href=link.href>${link.text}</a>
|
||||||
|
<span if(loop.getIndex() < state.links.length - 1) class="comma">, </span>
|
||||||
|
</span>
|
||||||
|
<if (state.parentLink != null)>
|
||||||
|
<span class="text"> ${state.parentLink.prefix} </span>
|
||||||
|
<a class="color-primary" href=state.parentLink.href>${state.parentLink.text}</a>
|
||||||
|
</if>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
div.change {
|
||||||
|
text-indent: -2ch;
|
||||||
|
margin-left: 2ch;
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
const { changesApi } = require("../../../../../rpdata/api/Change")
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
changes: input.changes,
|
||||||
|
shownAll: false,
|
||||||
|
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) {
|
||||||
|
this.state.modal = modal
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.state.modal = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<data-menu categories=input.categories selected=(input.selected || {}) user=input.user on-open("open") />
|
||||||
|
<main>
|
||||||
|
<div class="changes-page">
|
||||||
|
<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>
|
||||||
|
<for (change in state.changes)>
|
||||||
|
<change data=change />
|
||||||
|
</for>
|
||||||
|
<a if(!state.shownAll) href="/data/changes?limit=0" class="show-more-button" on-click("clearLimit")>Show All</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<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-file-modal enabled=(state.modal === "file.add") user=input.user on-close("close") />
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
.changes-page {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 95ch;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 200;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5em 0.5ch 0.5em 0.5ch;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
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") />
|
||||||
|
|||||||
@@ -39,7 +39,24 @@ module.exports = class {
|
|||||||
}).catch(errs => {
|
}).catch(errs => {
|
||||||
console.warn("Failed to add nick:", errs)
|
console.warn("Failed to add nick:", errs)
|
||||||
|
|
||||||
this.state.error = "Failed to add nick: " + errs[0].message
|
this.state.error = errs[0].message
|
||||||
|
}).then(() => {
|
||||||
|
this.state.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
selectUnknown(nick) {
|
||||||
|
if (this.state.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = {id: this.input.character.id, nick}
|
||||||
|
|
||||||
|
this.state.loading = true
|
||||||
|
charactersApi.addNick(input).then((res) => {
|
||||||
|
this.emit("nicks", res.nicks)
|
||||||
|
}).catch(errs => {
|
||||||
|
this.state.error = errs[0].message
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.state.loading = false
|
this.state.loading = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,9 +10,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label>Current Nicks</label>
|
<label>Current Nicks</label>
|
||||||
<div for(nick in input.character.nicks) class="existing-nick-row" >
|
<div for(nick in input.character.nicks) class="existing-nick-row" key=nick >
|
||||||
<div class="content color-text">${nick}</div>
|
<div class="content color-text">${nick}</div>
|
||||||
<button disabled=state.loading on-click("removeNick", nick)>Remove</button>
|
<button disabled=state.loading on-click("removeNick", nick)>Remove</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2>Unknown Nicks</h2>
|
||||||
|
<p>These are the 100 most common nicks in the logs, maybe one of these belongs to <strong>${input.character.name}</strong>?</p>
|
||||||
|
<unknown-nick-list key="un-list" on-select("selectUnknown") />
|
||||||
</modal>
|
</modal>
|
||||||
@@ -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") />
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
<menu-header>List</menu-header>
|
<menu-header>List</menu-header>
|
||||||
<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>
|
||||||
|
<if-permitted user=input.user permission=["member", "character.add", "channel.add", "file.add"]>
|
||||||
<menu-gap />
|
<menu-gap />
|
||||||
<if-permitted user=input.user permission=["member", "character.add", "channel.add"]>
|
|
||||||
<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>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<p class="color-error">${state.error}</p>
|
<p class="color-error">${state.error}</p>
|
||||||
|
|
||||||
<label>Event Name</label>
|
<label>Event Name</label>
|
||||||
<input key="eventName" placeholder="(None)" on-change("change", "eventName") value=state.values.eventName />
|
<input key="eventName" autofocus placeholder="(None)" on-change("change", "eventName") value=state.values.eventName />
|
||||||
|
|
||||||
<label>Location Name</label>
|
<label>Location Name</label>
|
||||||
<input key="locationName" placeholder="(None)" on-change("change", "locationName") value=state.values.locationName />
|
<input key="locationName" placeholder="(None)" on-change("change", "locationName") value=state.values.locationName />
|
||||||
@@ -15,7 +15,5 @@
|
|||||||
offDesc="The logbot will not monitor this channel."
|
offDesc="The logbot will not monitor this channel."
|
||||||
on-change("change", "logged") />
|
on-change("change", "logged") />
|
||||||
|
|
||||||
<p class="color-danger" if(state.values.logged)>The logbot is not yet available, so this does nothing right now.</p>
|
|
||||||
|
|
||||||
<button disabled=state.loading on-click("save")>Save</button>
|
<button disabled=state.loading on-click("save")>Save</button>
|
||||||
</modal>
|
</modal>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<p class="color-error">${state.error}</p>
|
<p class="color-error">${state.error}</p>
|
||||||
|
|
||||||
<label>Full Name</label>
|
<label>Full Name</label>
|
||||||
<input key="name" placeholder="" class="big" on-change("change", "name") value=state.values.name />
|
<input key="name" autofocus placeholder="" class="big" on-change("change", "name") value=state.values.name />
|
||||||
|
|
||||||
<label>Short Name</label>
|
<label>Short Name</label>
|
||||||
<input key="shortName" placeholder="E.g. first/given name" on-change("change", "shortName") value=state.values.shortName />
|
<input key="shortName" placeholder="E.g. first/given name" on-change("change", "shortName") value=state.values.shortName />
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -4,6 +4,15 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
|
<head>
|
||||||
|
<meta content="AiteStory is a timeline of aiterp IC content that doesn't fit the wiki format" name="description" />
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes" name="viewport" />
|
||||||
|
<meta content="#111111" name="theme-color" />
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/hax/global.css">
|
||||||
|
</head>
|
||||||
<title>${input.title ? `${input.title} - Aite RP` : "Aite RP"}</title>
|
<title>${input.title ? `${input.title} - Aite RP` : "Aite RP"}</title>
|
||||||
<lasso-head/>
|
<lasso-head/>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<p key="error" class="color-error">${state.error}</p>
|
<p key="error" class="color-error">${state.error}</p>
|
||||||
|
|
||||||
<label>Timestamp</label>
|
<label>Timestamp</label>
|
||||||
<input key="time" placeholder="Title" class="big" on-change("change", "time") value=state.values.time />
|
<input key="time" autofocus placeholder="Title" class="big" on-change("change", "time") value=state.values.time />
|
||||||
|
|
||||||
<label>Kind</label>
|
<label>Kind</label>
|
||||||
<select key="kind" class="big" placeholder="Kind" on-change("change", "kind") value=state.values.kind>
|
<select key="kind" class="big" placeholder="Kind" on-change("change", "kind") value=state.values.kind>
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
<option value="annotation.info" selected=(state.values.kind === "annotation.info")>Annotation - Info</option>
|
<option value="annotation.info" selected=(state.values.kind === "annotation.info")>Annotation - Info</option>
|
||||||
<option value="annotation.warning" selected=(state.values.kind === "annotation.warning")>Annotation - Warning</option>
|
<option value="annotation.warning" selected=(state.values.kind === "annotation.warning")>Annotation - Warning</option>
|
||||||
<option value="annotation.error" selected=(state.values.kind === "annotation.error")>Annotation - Error</option>
|
<option value="annotation.error" selected=(state.values.kind === "annotation.error")>Annotation - Error</option>
|
||||||
|
<option value="chars" selected=(state.values.kind === "chars")>Character Hints</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label>Nick</label>
|
<label>Nick</label>
|
||||||
|
|||||||
@@ -19,16 +19,12 @@ module.exports = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onInput(input) {
|
onInput(input) {
|
||||||
if (input.log && !this.first) {
|
|
||||||
this.state.values = {
|
this.state.values = {
|
||||||
title: input.log.title,
|
title: input.log.title,
|
||||||
event: input.log.eventName,
|
event: input.log.eventName,
|
||||||
description: input.log.description,
|
description: input.log.description,
|
||||||
open: input.log.open,
|
open: input.log.open,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.first = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
change(key, ev) {
|
change(key, ev) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import moment from "moment"
|
|||||||
<p key="error" class="color-error">${state.error}</p>
|
<p key="error" class="color-error">${state.error}</p>
|
||||||
|
|
||||||
<label>Title</label>
|
<label>Title</label>
|
||||||
<input key="title" placeholder=(input.log.channel.name + " – " + moment(input.log.date).format("MMMM D, YYYY")) class="big" on-change("change", "title") value=state.values.title />
|
<input key="title" autofocus placeholder=(input.log.channel.name + " – " + moment(input.log.date).format("MMMM D, YYYY")) class="big" on-change("change", "title") value=state.values.title />
|
||||||
|
|
||||||
<label>Event Name</label>
|
<label>Event Name</label>
|
||||||
<input key="event" placeholder="(Optional)" on-change("change", "event") value=state.values.event />
|
<input key="event" placeholder="(Optional)" on-change("change", "event") value=state.values.event />
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<p key="error" class="color-error">${state.error}</p>
|
<p key="error" class="color-error">${state.error}</p>
|
||||||
|
|
||||||
<label>Timestamp</label>
|
<label>Timestamp</label>
|
||||||
<input key="time" placeholder="Title" class="big" on-change("change", "time") value=state.values.time />
|
<input key="time" autofocus placeholder="Title" class="big" on-change("change", "time") value=state.values.time />
|
||||||
|
|
||||||
<label>Kind</label>
|
<label>Kind</label>
|
||||||
<select key="kind" class="big" placeholder="Kind" on-change("change", "kind") value=state.values.kind>
|
<select key="kind" class="big" placeholder="Kind" on-change("change", "kind") value=state.values.kind>
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
<option value="annotation.info" selected=(state.values.kind === "annotation.info")>Annotation - Info</option>
|
<option value="annotation.info" selected=(state.values.kind === "annotation.info")>Annotation - Info</option>
|
||||||
<option value="annotation.warning" selected=(state.values.kind === "annotation.warning")>Annotation - Warning</option>
|
<option value="annotation.warning" selected=(state.values.kind === "annotation.warning")>Annotation - Warning</option>
|
||||||
<option value="annotation.error" selected=(state.values.kind === "annotation.error")>Annotation - Error</option>
|
<option value="annotation.error" selected=(state.values.kind === "annotation.error")>Annotation - Error</option>
|
||||||
|
<option value="chars" selected=(state.values.kind === "chars")>Character Hints</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<label>Nick</label>
|
<label>Nick</label>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
const moment = require("moment")
|
const moment = require("moment")
|
||||||
|
|
||||||
const {postApi} = require("../../../../../rpdata/api/Post")
|
const {postApi} = require("../../../../../rpdata/api/Post")
|
||||||
|
const {logsApi} = require("../../../../../rpdata/api/Log")
|
||||||
|
|
||||||
module.exports = class {
|
module.exports = class {
|
||||||
onCreate(input) {
|
onCreate(input) {
|
||||||
@@ -8,6 +9,23 @@ module.exports = class {
|
|||||||
log: input.log,
|
log: input.log,
|
||||||
modal: null,
|
modal: null,
|
||||||
removed: false,
|
removed: false,
|
||||||
|
highlights: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount() {
|
||||||
|
this.sub = logsApi.subscribeChanges(this.state.log.id)
|
||||||
|
|
||||||
|
this.sub.on("data", data => {
|
||||||
|
this.handleChange(data.changes)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.sub.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmount() {
|
||||||
|
if (this.sub != null) {
|
||||||
|
this.sub.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,8 +48,6 @@ module.exports = class {
|
|||||||
for (const patch of patches) {
|
for (const patch of patches) {
|
||||||
const index = posts.findIndex(p => p.id === patch.id)
|
const index = posts.findIndex(p => p.id === patch.id)
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
console.warn("Post not found for patch:", patch)
|
|
||||||
// TODO: Force full refresh
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +58,7 @@ module.exports = class {
|
|||||||
posts.sort((a, b) => a.position - b.position)
|
posts.sort((a, b) => a.position - b.position)
|
||||||
for (let i = 1; i < posts.length; ++i) {
|
for (let i = 1; i < posts.length; ++i) {
|
||||||
if (posts[i].position !== posts[i - 1].position + 1) {
|
if (posts[i].position !== posts[i - 1].position + 1) {
|
||||||
console.warn("Discontinuity detected!")
|
console.warn("Discontinuity detected:", i, posts)
|
||||||
// TODO: Force full refresh
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,15 +80,7 @@ module.exports = class {
|
|||||||
|
|
||||||
removePost(post) {
|
removePost(post) {
|
||||||
postApi.remove({id: post.id}).then(() => {
|
postApi.remove({id: post.id}).then(() => {
|
||||||
this.state.log.posts = this.state.log.posts.filter(p => p.id !== post.id)
|
this.postRemoved(post)
|
||||||
|
|
||||||
for (const p of this.state.log.posts) {
|
|
||||||
if (p.position > post.position) {
|
|
||||||
p.position--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.log = Object.assign({}, this.state.log)
|
|
||||||
}).catch(errs => {
|
}).catch(errs => {
|
||||||
console.warn("Failed to delete:", errs)
|
console.warn("Failed to delete:", errs)
|
||||||
this.state.error = "Failed to delete: " + (errs.message || errs[0].message)
|
this.state.error = "Failed to delete: " + (errs.message || errs[0].message)
|
||||||
@@ -88,15 +95,82 @@ module.exports = class {
|
|||||||
this.state.log = Object.assign(Object.assign({}, this.state.log), patch)
|
this.state.log = Object.assign(Object.assign({}, this.state.log), patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postRemoved(post) {
|
||||||
|
if (this.state.log.posts.find(p => p.id === post.id) == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts = this.state.log.posts.filter(p => p.id !== post.id).map(p => {
|
||||||
|
if (p.position > post.position) {
|
||||||
|
return Object.assign({}, p, {position: p.position - 1})
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
|
||||||
|
this.state.log = Object.assign({}, this.state.log, {posts: posts})
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleChange(change) {
|
||||||
|
switch (`${change.model}.${change.op}`) {
|
||||||
|
case "Post.add": {
|
||||||
|
for (const post of change.objects.filter(o => o.type === "Post")) {
|
||||||
|
this.postAdded(post)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Post.move":
|
||||||
|
case "Post.edit": {
|
||||||
|
this.patch(change.objects.filter(o => o.type === "Post"))
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Post.remove": {
|
||||||
|
for (const post of change.objects.filter(o => o.type === "Post")) {
|
||||||
|
this.postRemoved(post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Log.edit": {
|
||||||
|
const patch = change.objects.find(o => o.type === "Log")
|
||||||
|
if (patch == null) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logEdited(patch)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Log.remove": {
|
||||||
|
this.logRemoved()
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logRemoved() {
|
logRemoved() {
|
||||||
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>
|
||||||
@@ -16,16 +17,19 @@
|
|||||||
so you can still read it (or back it up) before leaving the page.
|
so you can still read it (or back it up) before leaving the page.
|
||||||
</p>
|
</p>
|
||||||
</annotation>
|
</annotation>
|
||||||
<post for(post in state.log.posts)
|
<post for(post in state.log.posts | status-var=loop)
|
||||||
key=post.id
|
key=post.id
|
||||||
on-move("movePost", post)
|
on-move("movePost", post)
|
||||||
on-remove("removePost", post)
|
on-remove("removePost", post)
|
||||||
on-edited("postEdited")
|
on-edited("postEdited")
|
||||||
user=input.user
|
user=input.user
|
||||||
|
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") />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
div.logs-content {
|
div.logs-content {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 75ch;
|
max-width: 95ch;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
||||||
div.header {
|
div.header {
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<if(state.text != null && state.text != "")>
|
<if(state.text != null && state.text != "")>
|
||||||
<div class=["post-meta", input.kind, state.color, input.weak ? "weak" : null] title=state.tooltip>
|
<div class=["post-meta", input.kind, state.color, input.weak ? "weak" : null, input.class] 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>${state.text}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,3 +18,13 @@ div.post-meta {
|
|||||||
div.post-meta.weak {
|
div.post-meta.weak {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.post-meta.nick.pk-text {
|
||||||
|
color: #AAA;
|
||||||
|
}
|
||||||
|
div.post-meta.nick.pk-action {
|
||||||
|
color: #A64;
|
||||||
|
}
|
||||||
|
div.post-meta.nick.pk-scene {
|
||||||
|
color: #68A;
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
const EventEmitter = require("events");
|
||||||
|
|
||||||
|
let linkSignal = null;
|
||||||
|
|
||||||
module.exports = class {
|
module.exports = class {
|
||||||
onCreate(input) {
|
onCreate(input) {
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -7,8 +11,14 @@ module.exports = class {
|
|||||||
nameSuffix: "",
|
nameSuffix: "",
|
||||||
modal: null,
|
modal: null,
|
||||||
removed: false,
|
removed: false,
|
||||||
|
multipart: false,
|
||||||
|
anchored: false,
|
||||||
|
prevWasText: false,
|
||||||
|
dark: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.mounted = false;
|
||||||
|
|
||||||
this.updatePost(input)
|
this.updatePost(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,18 +48,46 @@ module.exports = class {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timediff = Date.parse(input.post.time) - Date.parse(input.prev.time)
|
||||||
|
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.updatePost(input)
|
this.updatePost(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount() {
|
||||||
|
this.linkSignalCallback = () => {
|
||||||
|
this.state.anchored = false
|
||||||
|
}
|
||||||
|
if (linkSignal == null) {
|
||||||
|
linkSignal = new EventEmitter()
|
||||||
|
linkSignal.setMaxListeners(10000)
|
||||||
|
}
|
||||||
|
linkSignal.on("signal", this.linkSignalCallback)
|
||||||
|
|
||||||
|
this.state.anchored = window.location.hash.slice(1) === this.input.post.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmount() {
|
||||||
|
linkSignal.removeListener("signal", this.linkSignalCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
link() {
|
||||||
|
linkSignal.emit("signal")
|
||||||
|
this.state.anchored = true
|
||||||
|
}
|
||||||
|
|
||||||
updatePost(input) {
|
updatePost(input) {
|
||||||
this.state.shortName = input.post.nick.split("_").shift()
|
this.state.shortName = input.post.nick.split("_").shift()
|
||||||
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, "**")
|
this.state.text = input.post.text
|
||||||
|
|
||||||
if (input.post.kind === "text" && !this.state.text.includes("\"")) {
|
if (input.highlights.length > 0 && (input.post.kind === "action" || input.post.kind === "text")) {
|
||||||
this.state.text = '"' + this.state.text + '"'
|
this.state.dark = true
|
||||||
|
} else {
|
||||||
|
this.state.dark = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!input.post.nick.startsWith("=")) {
|
if (!input.post.nick.startsWith("=")) {
|
||||||
@@ -57,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,6 +1,8 @@
|
|||||||
<div if(!state.removed) class=["post", component.kindClass("post-type")]>
|
<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 />
|
||||||
<div class="post-meta-row">
|
<div class="post-meta-row">
|
||||||
<div class="options color-menu">
|
<div class="options color-menu">
|
||||||
|
<a on-click("link") href=("#" + input.post.id) class="color-menu">Link</a>
|
||||||
<if-permitted user=input.user permission="post.move">
|
<if-permitted user=input.user permission="post.move">
|
||||||
<a on-click("move", -1, true)>Up</a>
|
<a on-click("move", -1, true)>Up</a>
|
||||||
<a on-click("move", +1, true)>Down</a>
|
<a on-click("move", +1, true)>Down</a>
|
||||||
@@ -13,21 +15,25 @@
|
|||||||
</if-permitted>
|
</if-permitted>
|
||||||
</div>
|
</div>
|
||||||
<post-meta kind="timestamp" value=input.post.time />
|
<post-meta kind="timestamp" value=input.post.time />
|
||||||
<post-meta kind="nick" value=input.post.nick />
|
<post-meta kind="nick" class=("pk-"+input.post.kind) value=input.post.nick />
|
||||||
</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 === "chars") class="post-body color-menu" >
|
||||||
|
<p><em>Character hint: ${input.post.text}</em></p>
|
||||||
|
</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">
|
||||||
<markdown class="post-content post-action" source=(state.shortName + state.nameSuffix + " " + state.text) />
|
$ const prefix = ((!state.multipart || state.prevWasText) ? state.shortName + state.nameSuffix + " " : "");
|
||||||
|
<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,16 +1,28 @@
|
|||||||
div.post {
|
div.post {
|
||||||
margin-bottom: 1ch;
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1.25em;
|
||||||
|
margin-left: -12px;
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 0.5ch;
|
||||||
|
|
||||||
|
transition: background-color 0.333s linear;
|
||||||
|
|
||||||
div.post-meta-row {
|
div.post-meta-row {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.anchor {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
top: -4em;
|
||||||
|
}
|
||||||
|
|
||||||
div.options {
|
div.options {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
width: calc(100% - 35.5ch);
|
width: calc(100% - 38.75ch);
|
||||||
max-width: 75ch;
|
max-width: 95ch;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -24,6 +36,10 @@ div.post {
|
|||||||
a:hover {
|
a:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
width: calc(100% - 6.5ch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.post-body {
|
div.post-body {
|
||||||
@@ -48,9 +64,84 @@ div.post {
|
|||||||
div.annotation {
|
div.annotation {
|
||||||
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; }
|
||||||
}
|
}
|
||||||
div.post-body p:first-of-type {
|
p:first-of-type {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
p:last-of-type {
|
||||||
|
padding-bottom: 0.1em;
|
||||||
|
margin-bottom: 0em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post.pk-scene {
|
||||||
|
margin-left: -14px;
|
||||||
|
border-left: 2px solid #68A;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post.pk-scene.anchored {
|
||||||
|
background-color: rgba(102, 136, 170, 0.25);
|
||||||
|
}
|
||||||
|
div.post.pk-action.anchored {
|
||||||
|
background-color: rgba(170, 102, 68, 0.25);
|
||||||
|
}
|
||||||
|
div.post.pk-text.anchored {
|
||||||
|
background-color: rgba(170, 170, 170, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post.multipart {
|
||||||
|
margin-top: -1.5em;
|
||||||
|
|
||||||
|
div.post-meta {
|
||||||
|
span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post.dark {
|
||||||
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<include("../layout", {title: "Logs", site: "logs"})>
|
<include("../layout", {title: "Logs", site: "logs"})>
|
||||||
<@body>
|
<@body>
|
||||||
<background src="/assets/images/bg.png" opacity=0.25 />
|
<background src="/assets/images/bg.png" opacity=0.125 />
|
||||||
<page log=input.log user=input.user selected=input.selected />
|
<page log=input.log user=input.user selected=input.selected />
|
||||||
</@body>
|
</@body>
|
||||||
</include>
|
</include>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<label>Search</label>
|
<label>Search</label>
|
||||||
<input key="search" placeholder="" class="big" value=state.search />
|
<input key="search" autofocus placeholder="" class="big" value=state.search />
|
||||||
|
|
||||||
<label>Filters</label>
|
<label>Filters</label>
|
||||||
<div if(state.search.length > 0) class="filter-row" >
|
<div if(state.search.length > 0) class="filter-row" >
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import moment from "moment"
|
|||||||
<p key="error" class="color-error">${state.error}</p>
|
<p key="error" class="color-error">${state.error}</p>
|
||||||
|
|
||||||
<label>Title</label>
|
<label>Title</label>
|
||||||
<input key="title" placeholder="(Optional)" class="big" on-change("change", "title") value=state.values.title />
|
<input key="title" autofocus placeholder="(Optional)" class="big" on-change("change", "title") value=state.values.title />
|
||||||
|
|
||||||
<label>Date</label>
|
<label>Date</label>
|
||||||
<input key="date" placeholder="(Required)" on-change("change", "date") value=state.values.date />
|
<input key="date" placeholder="(Required)" on-change("change", "date") value=state.values.date />
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
characters: [],
|
||||||
|
channels: [],
|
||||||
|
eventNames: [],
|
||||||
|
filters: [],
|
||||||
|
search: "",
|
||||||
|
activeMap: {
|
||||||
|
events: {},
|
||||||
|
characters: {},
|
||||||
|
channels: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(input) {
|
||||||
|
this.update(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(input) {
|
||||||
|
this.getEl("search").addEventListener("keydown", ev => {
|
||||||
|
if (this.waiting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.timeout != null) {
|
||||||
|
clearTimeout(this.timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.key === "Enter") {
|
||||||
|
this.emit("add", "search", ev.target.value)
|
||||||
|
this.getEl("search").value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.timeout = null
|
||||||
|
this.state.search = ev.target.value
|
||||||
|
|
||||||
|
this.update(this.input)
|
||||||
|
}, 200)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
update(input) {
|
||||||
|
if (input.filter) {
|
||||||
|
this.updateActive(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
const search = this.state.search.toLocaleLowerCase()
|
||||||
|
|
||||||
|
this.state.characters = input.characters.filter(c => !this.state.activeMap.characters[c.id]).filter(c => this.matches(c.name, search)).sort((a,b) => compareFilters(a.name, b.name, search))
|
||||||
|
this.state.channels = input.channels.filter(c => !this.state.activeMap.channels[c.name]).filter(c => this.matches(c.name, search)).sort((a,b) => compareFilters(a.name, b.name, search))
|
||||||
|
this.state.eventNames = input.eventNames.filter(e => !this.state.activeMap.events[e]).filter(e => this.matches(e, search)).sort((a,b) => compareFilters(a, b, search))
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActive({filter, characters, channels, eventNames}) {
|
||||||
|
const filters = []
|
||||||
|
const activeMap = {
|
||||||
|
events: {},
|
||||||
|
characters: {},
|
||||||
|
channels: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.search != null) {
|
||||||
|
filters.push({
|
||||||
|
type: "search",
|
||||||
|
key: "search",
|
||||||
|
icon: "T",
|
||||||
|
color: "color-text",
|
||||||
|
text: filter.search,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const character of (filter.characters || [])) {
|
||||||
|
activeMap.characters[character] = true
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
type: "characters",
|
||||||
|
key: character,
|
||||||
|
icon: "C",
|
||||||
|
color: "color-tag-character",
|
||||||
|
id: character,
|
||||||
|
text: (characters.find(c => c.id === character) || {name: "Unknown ("+character+")"}).name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const channel of (filter.channels || [])) {
|
||||||
|
activeMap.channels[channel] = true
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
type: "channels",
|
||||||
|
key: channel.replace(/'/g, "__"),
|
||||||
|
icon: "#",
|
||||||
|
color: "color-tag-location",
|
||||||
|
id: channel,
|
||||||
|
text: channel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of (filter.events || [])) {
|
||||||
|
activeMap.events[event] = true
|
||||||
|
|
||||||
|
filters.push({
|
||||||
|
type: "events",
|
||||||
|
key: event.replace(/['\s\.]/g, "__"),
|
||||||
|
icon: "E",
|
||||||
|
color: "color-tag-event",
|
||||||
|
id: event,
|
||||||
|
text: event,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.filters = filters
|
||||||
|
this.state.activeMap = activeMap
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(str, search) {
|
||||||
|
if (search == "") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search.length > str.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.toLowerCase().indexOf(search) !== -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} a
|
||||||
|
* @param {string} b
|
||||||
|
* @param {string} search
|
||||||
|
*/
|
||||||
|
function compareFilters(a, b, search) {
|
||||||
|
if (search != "") {
|
||||||
|
if (search.charAt(0) != "#" && a.charAt(0) == "#" && b.charAt(0) == "#") {
|
||||||
|
search = '#' + search
|
||||||
|
}
|
||||||
|
|
||||||
|
const aStarts = a.toLocaleLowerCase().startsWith(search)
|
||||||
|
const bStarts = b.toLocaleLowerCase().startsWith(search)
|
||||||
|
|
||||||
|
if (aStarts && !bStarts) {
|
||||||
|
return -1
|
||||||
|
} else if (bStarts && !aStarts) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.localeCompare(b)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<right-menu class="filter-menu">
|
||||||
|
<input key="search" />
|
||||||
|
<menu-gap />
|
||||||
|
<menu-header if(state.filters.length > 0)>Active Filters</menu-header>
|
||||||
|
<div class="filter-section">
|
||||||
|
<menu-link for(filter in state.filters)
|
||||||
|
on-click("emit", "remove", filter.type, filter.id)
|
||||||
|
textClass=filter.color
|
||||||
|
unselectable
|
||||||
|
icon=filter.icon >${filter.text}</menu-link>
|
||||||
|
</div>
|
||||||
|
<menu-gap if(state.filters.length > 0) />
|
||||||
|
<menu-header>Characters</menu-header>
|
||||||
|
<div class="filter-section">
|
||||||
|
<menu-link for(character in state.characters)
|
||||||
|
on-click("emit", "add", "characters", character.id)
|
||||||
|
icon=character.name.charAt(0).toUpperCase()
|
||||||
|
textClass="color-tag-character">${character.name}</menu-link>
|
||||||
|
</div>
|
||||||
|
<menu-gap />
|
||||||
|
<menu-header>Channels</menu-header>
|
||||||
|
<div class="filter-section">
|
||||||
|
<menu-link for(channel in state.channels)
|
||||||
|
on-click("emit", "add", "channels", channel.name)
|
||||||
|
icon=channel.name.charAt(1).toUpperCase()
|
||||||
|
textClass="color-tag-location">${channel.name}</menu-link>
|
||||||
|
</div>
|
||||||
|
<menu-gap />
|
||||||
|
<menu-header>Events</menu-header>
|
||||||
|
<div class="filter-section">
|
||||||
|
<menu-link for(eventName in state.eventNames)
|
||||||
|
on-click("emit", "add", "events", eventName)
|
||||||
|
icon=eventName.charAt(0).toUpperCase()
|
||||||
|
textClass="color-tag-event">${eventName}</menu-link>
|
||||||
|
</div>
|
||||||
|
</right-menu>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
div.right-menu.filter-menu {
|
||||||
|
input {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
width: 95%;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-left: 2.5%;
|
||||||
|
border: 1px solid rgba(119, 119, 119, 0.25);
|
||||||
|
font-size: 1.1em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
input:focus {
|
||||||
|
color: #FC1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.filter-section {
|
||||||
|
max-height: calc(25vh - 3em);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
const {tz} = require("moment-timezone")
|
||||||
|
|
||||||
|
const {logsApi} = require("../../../../../rpdata/api/Log")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
imported: null,
|
||||||
|
values: {
|
||||||
|
importer: "MircLike",
|
||||||
|
timezone: tz.guess() || "UTC",
|
||||||
|
channelName: "",
|
||||||
|
date: "",
|
||||||
|
data: "",
|
||||||
|
sessionThresholdHours: 12,
|
||||||
|
},
|
||||||
|
timezones: tz.names(),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.first = false
|
||||||
|
}
|
||||||
|
|
||||||
|
change(key, ev) {
|
||||||
|
this.state.values[key] = ev.target.value
|
||||||
|
this.state.values = Object.assign({}, this.state.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this.state.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.emit("close")
|
||||||
|
|
||||||
|
this.state.values = {
|
||||||
|
importer: "MircLike",
|
||||||
|
timezone: tz.guess() || "UTC",
|
||||||
|
channelName: "",
|
||||||
|
date: "",
|
||||||
|
data: "",
|
||||||
|
}
|
||||||
|
this.state.imported = null
|
||||||
|
this.state.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
if (this.state.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.state.error = null;
|
||||||
|
|
||||||
|
const input = Object.assign({}, this.state.values)
|
||||||
|
if (input.date != "" && input.importer !== "ForumLog") {
|
||||||
|
input.date = new Date(input.date)
|
||||||
|
if (Number.isNaN(input.date)) {
|
||||||
|
this.state.error = "Invalid date"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (input.importer === "MircLike") {
|
||||||
|
this.state.error = "Date is required for mIRC-like logs."
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
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) !== "#") {
|
||||||
|
this.state.error = "A valid channelName name is required"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.loading = true
|
||||||
|
logsApi.import(input).then(logs => {
|
||||||
|
this.state.imported = logs
|
||||||
|
}).catch(errs => {
|
||||||
|
console.warn("Import failed:", errs)
|
||||||
|
|
||||||
|
this.state.error = "Import failed: " + errs[0].message
|
||||||
|
}).then(() => {
|
||||||
|
this.state.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import moment from "moment"
|
||||||
|
|
||||||
|
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
|
||||||
|
<if(state.imported == null)>
|
||||||
|
<h1>Import Log</h1>
|
||||||
|
|
||||||
|
<p key="error" class="color-error">${state.error}</p>
|
||||||
|
|
||||||
|
<label>Type</label>
|
||||||
|
<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="ForumLog" selected=(state.values.importer === "ForumLog")>Forum Log</option>
|
||||||
|
<option value="IrcCloud" selected=(state.values.importer === "IrcCloud")>IRCCloud log export</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Timezone</label>
|
||||||
|
<select key="timezone" class="big" placeholder="Kind" on-change("change", "timezone") value=state.values.timezone>
|
||||||
|
<option for(tz in state.timezones) value=tz selected=(state.values.timezone === tz)>${tz}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Channel</label>
|
||||||
|
<input key="channelName" placeholder="(Required)" class="big" on-change("change", "channelName") value=state.values.channelName />
|
||||||
|
|
||||||
|
<if(state.values.importer === "MircLike")>
|
||||||
|
<label>Date</label>
|
||||||
|
<input key="date" class="big" placeholder="(Required)" on-change("change", "date") value=state.values.date />
|
||||||
|
</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>
|
||||||
|
<textarea key="data" placeholder="(Required)" class="tall" on-change("change", "data") value=state.values.data />
|
||||||
|
|
||||||
|
<button disabled=state.loading on-click("save")>Import</button>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<h1>Import Successful</h1>
|
||||||
|
|
||||||
|
<p>The following logs were imported.</p>
|
||||||
|
|
||||||
|
<ol class="imported-log-list">
|
||||||
|
<for (log in state.imported)>
|
||||||
|
<li><a class="color-primary" href=("/logs/" + log.id)>${log.id} (${moment(log.date).format("MMMM D, YYYY")})</a></li>
|
||||||
|
</for>
|
||||||
|
</ol>
|
||||||
|
</else>
|
||||||
|
</modal>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ol.imported-log-list {
|
||||||
|
color: #777;
|
||||||
|
|
||||||
|
li {
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,54 +13,7 @@ module.exports = class {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = []
|
this.state.filtered = input.filter.search || input.filter.channels || input.filter.characters || input.filter.events
|
||||||
const {characters, channels, events, search} = input.filter
|
|
||||||
|
|
||||||
if (search != null) {
|
|
||||||
filters.push({
|
|
||||||
type: "search",
|
|
||||||
key: "search",
|
|
||||||
icon: "T",
|
|
||||||
color: "color-text",
|
|
||||||
text: search,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const character of (characters || [])) {
|
|
||||||
filters.push({
|
|
||||||
type: "characters",
|
|
||||||
key: character,
|
|
||||||
icon: "C",
|
|
||||||
color: "color-tag-character",
|
|
||||||
id: character,
|
|
||||||
text: (input.characters.find(c => c.id === character) || {name: "Unknown ("+character+")"}).name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const channel of (channels || [])) {
|
|
||||||
filters.push({
|
|
||||||
type: "channels",
|
|
||||||
key: channel.replace(/'/g, "__"),
|
|
||||||
icon: "#",
|
|
||||||
color: "color-tag-location",
|
|
||||||
id: channel,
|
|
||||||
text: channel,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const event of (events || [])) {
|
|
||||||
filters.push({
|
|
||||||
type: "events",
|
|
||||||
key: event.replace(/['\s\.]/g, "__"),
|
|
||||||
icon: "E",
|
|
||||||
color: "color-tag-event",
|
|
||||||
id: event,
|
|
||||||
text: event,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.filters = filters
|
|
||||||
this.state.filtered = (filters.length > 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
select(value) {
|
select(value) {
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
<menu-link key="logs" selected=(input.selected.index && !state.filtered) icon="L" href="/logs/">All</menu-link>
|
<menu-link key="logs" selected=(input.selected.index && !state.filtered) icon="L" href="/logs/">All</menu-link>
|
||||||
<if-permitted key="if-permitted" user=input.user permission="log.add">
|
<if-permitted key="if-permitted" user=input.user permission="log.add">
|
||||||
<menu-link key="add_log" dark on-click("select", "log.add") icon="+">Add Log</menu-link>
|
<menu-link key="add_log" dark on-click("select", "log.add") icon="+">Add Log</menu-link>
|
||||||
|
<menu-link key="import_log" dark on-click("select", "log.import") icon="+">Import Log</menu-link>
|
||||||
</if-permitted>
|
</if-permitted>
|
||||||
<menu-gap />
|
|
||||||
<menu-header key="filters">Filters</menu-header>
|
|
||||||
<menu-link for(filter in state.filters) textClass=filter.color unselectable icon=filter.icon on-click("emit", "removefilter", filter.type, filter.id)>${filter.text}</menu-link>
|
|
||||||
<menu-link key="add_filter" dark on-click("select", "filter.add") icon="+">Add Filter</menu-link>
|
|
||||||
<menu-gap />
|
|
||||||
</menu>
|
</menu>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user