Compare commits

...
51 Commits
Author SHA1 Message Date
gisle 624f7046af server: Added resilience to trust proxy resolution to handle IP changes and bad start order. 2019-01-19 11:18:44 +01:00
gisle 8b3d642104 logs: Added default limit that's cleared by adding a filter or clicking show more at the bottom.
the build was successful
2019-01-12 15:35:21 +01:00
gisle 46369f84bd data: Fixed incosnsitensies and undefines in history.
the build was successful
2019-01-12 14:34:59 +01:00
gisle 68f3effa8d story-content: Added new checks for story tag management so any logged-in user can view the form if the story is open.
the build was successful
2019-01-12 12:18:55 +01:00
gisle 4657ae2aec logs: Added prioritization of filter names starting with the search query, for example bringing 'Li' higher up when the search query is 'li'.
the build was successful
2019-01-05 11:46:46 +01:00
gisle f134d57b79 data: Fixed centiseconds being used instead of seconds in change history entry timestamps.
the build was successful
2019-01-05 11:31:25 +01:00
gisle 9b4da98ac4 logs-content: Added escaping of back-ticks (`) to avoid monospace in log posts where a nick containing a back-tick has been copied into the text.
the build was successful
2019-01-05 11:28:22 +01:00
gisle 599ca83f0f logs-content: Fixed quote detection in text post not detecting double-apostrpohes (KVIrc has anti-RP sentiments among their dev team)
the build was successful
2019-01-05 11:19:45 +01:00
gisle b952007f3b logs-content: Fixed character names being omitted on multipart posts where an action post follows a text post.
the build was successful
2019-01-05 11:16:29 +01:00
gisle c0e74f4756 server: Added DNS resolution when setting trust proxy since IPs are temporary in Docker.
the build failed
2019-01-05 11:02:38 +01:00
gisle 68a8e7ef97 story-content: Added hiding of first chapter's title if it matches story name to conserve vertical space.
the build was successful
2019-01-05 10:46:16 +01:00
gisle 5b81f7ff0d data: Fixed unknown nick list sometimes disappearing in character nicks modal.
the build was successful
2018-12-09 12:21:47 +01:00
gisle 78a14ae86b data: Increased limit on change list to 2000. 2018-12-09 12:21:15 +01:00
gisle c28f17abdd data: Added op text for move-in, move-out and tag, added fallback to ID for untitled chapters.
the build was successful
2018-12-08 16:30:54 +01:00
gisle de3bae3353 data: Added change history.
the build was successful
2018-12-08 15:36:54 +01:00
gisle fcf2361fbf logs-content: Added anchoring on posts, link posts and background on linked-to post. 2018-12-08 15:36:23 +01:00
gisle 7c1623493a data: Added unknown nick list to character add and nicks modal.
the build was successful
2018-12-04 22:12:54 +01:00
gisle 013c50d4f4 client, logs-content: Added change subscription setup and hooked it up to the logs-content page.
the build was successful
2018-12-03 20:12:15 +01:00
gisle f236d35974 logs-content, story-content: Fixed options being too far left in chapter/post on mobile.
the build was successful
2018-12-02 14:39:23 +01:00
gisle 55379fcfe3 logs-content: Added styles for posts to differentiate post types more easily and highlight scenes. 2018-12-02 14:35:46 +01:00
gisle c4636cf843 logs: Added log import form.
the build was successful
2018-12-02 14:18:56 +01:00
gisle 8fd9b19bd0 package: Fixed browser-refresh infinite loop on site changed. 2018-12-02 14:18:42 +01:00
gisle 5a5dfea774 package: Fixed browser-refresh restarting server on git commands. 2018-12-02 12:25:35 +01:00
gisle c157f5f592 package: npm audit fix 2018-12-02 12:21:56 +01:00
gisle 47f71c733b package: Updated version number
the build was successful
2018-11-03 15:05:31 +01:00
gisle 326e4478c0 logs: Added filter menu.
the build was successful
2018-11-03 15:05:02 +01:00
gisle 68a1a64fe3 server: Integrated @marko/prebuild, and added workaround for global css issue.
the build was successful
2018-11-02 20:18:15 +01:00
gisle cdd165ab03 logs-content: Added time difference to multipart post detection.
the build was successful
2018-11-01 21:04:07 +01:00
gisle a8f4d68f1e *: Added media queries to make it less awful on mobile.
the build was successful
2018-11-01 20:56:24 +01:00
gisle b57873091b story-content: Improved visibility of chapter title.
the build was successful
2018-11-01 19:51:14 +01:00
gisle 7a1884d1cf *: Added autofocus on all forms' first input or select.
the build was successful
2018-10-31 23:31:40 +01:00
gisle 5be6b1ecec logs-content: Added italics to irc post rendering.
the build was successful
2018-10-31 23:25:35 +01:00
gisle ea89975b14 logs: Added logs-table that looks like old logs site.
the build was successful
2018-10-28 14:21:17 +01:00
gisle 850114af2d global: Made dark overlay darker. 2018-10-28 14:21:04 +01:00
gisle 39d8254b9b global: Fixed <main> being a little too wide. 2018-10-28 14:20:37 +01:00
gisle adf73a4623 story-content: Added chapter move option.
the build was successful
2018-10-27 14:57:25 +02:00
gisle 1e022d6116 components/if-permitted: Added not option. 2018-10-27 14:56:59 +02:00
gisle d832e66402 components/modal: Fixed open event firing repeatedly. 2018-10-27 14:56:37 +02:00
gisle 94d0a83f46 story: Added a 2-hour grace period for updated tooltip, removed *
the build was successful
2018-10-27 09:51:42 +02:00
gisle 824a8dae23 story: Added view of own unlisted stories.
the build was successful
2018-10-22 19:00:27 +02:00
gisle d2194347fd story-content: Fixed fictional dates not being UTC in chapter title fallback.
the build was successful
2018-10-22 17:48:20 +02:00
gisle f850e383b1 story-content: Fixed fictional dates not being UTC.
the build was successful
2018-10-21 16:11:11 +02:00
gisle cb2f54cecf story: Changed fictionalDate in story list to be rendered as UTC.
the build was successful
2018-10-21 09:32:29 +02:00
gisle 4fd512a8e8 server: Fixed 404 on logs where the channel name in the log ID contains a number.
the build was successful
2018-10-18 18:53:16 +02:00
gisle a6cef51a37 story: Changed tag list columns to have less empty space between.
the build was successful
2018-10-17 23:02:51 +02:00
gisle 94a15f7388 package: Updated version number
the build was successful
2018-10-17 21:23:16 +02:00
gisle 0380b98331 story-content: Fixed misaligned edit/remove on chapter. 2018-10-17 21:22:52 +02:00
gisle cdae732654 components/modal: Changed opacity to 0.95 to work better on some monitors. 2018-10-17 21:22:24 +02:00
gisle 5935cbbf54 story-content, logs-content: Changed text width from 75 to 90 characters, added grouping of multipart log posts, and darkened background.
the build was successful
2018-10-17 19:44:11 +02:00
gisle 7e94b0047c server: Added trustedProxy config.
the build was successful
2018-10-15 21:51:00 +02:00
gisle 6dd1939c5e server: Removed failure callback.
the build was successful
2018-10-15 21:26:28 +02:00
109 changed files with 3051 additions and 349 deletions
+4
View File
@@ -0,0 +1,4 @@
.git
**/*.marko.js
.cache
.static
+1
View File
@@ -1,4 +1,5 @@
*.marko.js *.marko.js
*.prebuild.json
/node_modules /node_modules
npm-debug.log npm-debug.log
/.cache /.cache
+2 -2
View File
@@ -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
+22 -47
View File
@@ -1,53 +1,28 @@
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/story-content/view.marko",
"./marko/page/logs-content/view.marko",
]
}).then(lassoPrebuildResult => {
const builds = lassoPrebuildResult._buildsByPath
require: { for (const key in builds) {
builtins: { if (!builds.hasOwnProperty(key)) {
fs: require.resolve("empty-module"), continue
}, }
transforms: [ console.log("Built", key)
{
transform: "lasso-babel-transform",
config: {
extensions: [".js", ".es6"], // Enabled file extensions. Default: [".js", ".es6"]
babelOptions: {
presets: [ "es2015" ]
}
}
}
]
} }
})
// 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
View File
@@ -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
View File
@@ -5,6 +5,7 @@ const config = {
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: "",
+31
View File
@@ -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,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
@@ -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 {
+6 -1
View File
@@ -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
}
} }
} }
+1 -1
View File
@@ -1 +1 @@
<div class="menu-gap"></div> <div class=["menu-gap", input.class]></div>
+1 -1
View File
@@ -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)
+5
View File
@@ -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
} }
+3 -1
View File
@@ -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)>
+24
View File
@@ -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;
}
} }
+4 -1
View File
@@ -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) {
+1 -1
View File
@@ -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;
+5
View File
@@ -0,0 +1,5 @@
<div class=["right-menu", input.class]>
<div class="menu-body">
<include(input.renderBody) />
</div>
</div>
+26
View File
@@ -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;
}
}
+16 -2
View File
@@ -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;
+3 -3
View File
@@ -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);
+11 -1
View File
@@ -30,4 +30,14 @@ nav.layout-navbar {
color: white; color: white;
} }
} }
} }
@media screen and (max-width: 700px) {
nav.layout-navbar {
position: static;
margin-left: 0;
width: 100%;
font-size: 1.25em;
}
}
+6
View File
@@ -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 />
@@ -17,8 +17,6 @@
onDesc="Log new posts in this channel." onDesc="Log new posts in this channel."
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,119 @@
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.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 "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",
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,16 @@
module.exports = class {
onCreate(input) {
this.state = {
characters: input.characters,
modal: null,
}
}
open(modal) {
this.state.modal = modal
}
close() {
this.state.modal = null
}
}
@@ -0,0 +1,12 @@
<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 input.changes)>
<change data=change />
</for>
</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") />
@@ -0,0 +1,36 @@
.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;
}
}
}
@@ -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>
@@ -2,8 +2,9 @@
<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-gap /> <menu-link key="history" selected=input.selected.changes icon="H" href="/data/changes/">History</menu-link>
<if-permitted user=input.user permission=["member", "character.add", "channel.add"]> <if-permitted user=input.user permission=["member", "character.add", "channel.add"]>
<menu-gap />
<menu-header>Add</menu-header> <menu-header>Add</menu-header>
<if-permitted user=input.user permission=["character.add", "member"]> <if-permitted user=input.user permission=["character.add", "member"]>
<menu-link dark key="characters_add" icon="+" on-click("emit", "open", "character.add")>Character</menu-link> <menu-link dark key="characters_add" icon="+" on-click("emit", "open", "character.add")>Character</menu-link>
@@ -4,7 +4,7 @@
<p class="color-error">${state.error}</p> <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 />
+9
View File
@@ -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>
@@ -19,15 +19,11 @@ 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
} }
} }
@@ -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>
@@ -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) {
@@ -9,6 +10,22 @@ module.exports = class {
modal: null, modal: null,
removed: false, removed: false,
} }
}
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()
}
} }
open(modal) { open(modal) {
@@ -30,8 +47,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 +57,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 +79,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,11 +94,69 @@ 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) {
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
} }
@@ -16,12 +16,13 @@
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
last=(post.position === state.log.posts.length) last=(post.position === state.log.posts.length)
@@ -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 {
@@ -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>
@@ -17,4 +17,14 @@ 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,13 @@ module.exports = class {
nameSuffix: "", nameSuffix: "",
modal: null, modal: null,
removed: false, removed: false,
multipart: false,
anchored: false,
prevWasText: false,
} }
this.mounted = false;
this.updatePost(input) this.updatePost(input)
} }
@@ -38,18 +47,58 @@ module.exports = class {
return return
} }
const timediff = Date.parse(input.post.time) - Date.parse(input.prev.time)
this.state.multipart = (input.post.nick == input.prev.nick && input.prev.multipart !== false) && (Math.abs(timediff) < 60000);
this.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.replace(/\x02/g, "**").replace(/\x1D/g, "*").replace(/\`/g, "\\`").trim();
if (input.post.kind === "text" && !this.state.text.includes("\"")) { if (this.state.text.startsWith("|")) {
this.state.text = '"' + this.state.text + '"' this.state.name = ""
this.state.text = this.state.text.slice(1).trim();
}
if (input.post.kind === "text" && !this.state.text.includes("\"") && !this.state.text.includes("\''") && !this.state.text.includes("|")) {
const colonIndex = this.state.text.indexOf(": ");
if (colonIndex != -1 && colonIndex < this.state.text.indexOf(" ")) {
this.state.text = this.state.text.replace(": ", ": \"") + "\"";
} else {
this.state.text = '"' + this.state.text + '"'
}
}
if (this.state.text.charAt(0) == this.state.text.charAt(0).toUpperCase()) {
this.state.name = ""
} }
if (!input.post.nick.startsWith("=")) { if (!input.post.nick.startsWith("=")) {
@@ -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]>
<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,7 +15,7 @@
</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))>
@@ -24,7 +26,8 @@
<markdown class="post-content post-scene" source=state.text /> <markdown 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 + " " : "");
<markdown 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 /> <markdown class="post-content post-text" source=state.text />
@@ -1,16 +1,28 @@
div.post { div.post {
margin-top: 1em;
margin-bottom: 1ch; margin-bottom: 1ch;
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,38 @@ div.post {
div.annotation { div.annotation {
margin-top: 0.33em; margin-top: 0.33em;
} }
}
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;
}
}
}
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: -0.9em;
div.post-meta {
span {
display: none;
}
} }
} }
+1 -1
View File
@@ -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,83 @@
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: "",
},
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.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,44 @@
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>
</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>
<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>
@@ -0,0 +1,10 @@
const moment = require("moment")
module.exports = class {
onCreate(input) {
this.state = {
expand: false,
dateStr: moment(input.log.date).format("MMM D, YYYY"),
}
}
}
@@ -0,0 +1,22 @@
$ const fullNames = (input.log.characters.length < 4);
<tr key=input.log.shortId class="logs-table-row color-text">
<td class="date color-primary">
<a href=("/logs/"+input.log.id)>${state.dateStr}</a>
</td>
<td class="channel color-menu">
<a on-click("emit", "addfilter", "channels", input.log.channelName)>${input.log.channelName}</a>
</td>
<td class="event color-tag-event">
<a on-click("emit", "addfilter", "events", input.log.eventName)>${(input.log.eventName || "")}</a>
</td>
<td class="characters color-menu">
<span for(character in input.log.characters | status-var=loop)>
<a on-click("emit", "addfilter", "characters", character.id) class="name">${fullNames ? character.name : character.shortName}</a>
<span class="comma" if(loop.getIndex() < (loop.getLength() - 1))>, </span>
</span>
</td>
</tr>
<tr if(state.expand) key=(input.log.shortId + "_expand") class="logs-table-row-expanded">
</tr>
@@ -0,0 +1,31 @@
tr.logs-table-row {
td {
padding: 0.25em 1ch;
text-align: center;
border-bottom: 1px solid rgba(51, 51, 51, 0.5);
a {
color: inherit;
}
}
td.expand {
width: 0%;
}
td.date {
width: 16%;
}
td.channel {
width: 14%;
}
td.event {
width: 18%;
}
td.character {
a.name {
white-space: nowrap;
}
}
}
@@ -0,0 +1,11 @@
import moment from "moment"
<table class="logs-table color-highlight-dark color-primary">
<tr key="_header">
<th>Date</th>
<th>Channel</th>
<th class="event">Event</th>
<th>Characters</th>
</tr>
<logs-table-row for(log in input.logs) key=log.shortId log=log on-addfilter("emit", "addfilter") />
</table>
@@ -0,0 +1,25 @@
table.logs-table {
outline: 1px solid;
margin: 1em auto;
width: 90%;
max-width: 140ch;
a:hover {
cursor: pointer;
text-decoration: underline;
}
@media screen and (max-width: 1200px) {
td.event, th.event {
display: none;
}
}
@media screen and (max-width: 700px) {
td.date {
white-space: normal !important;
}
font-size: 0.66em;
}
}
+46 -4
View File
@@ -1,4 +1,4 @@
const {logHeaderApi} = require("../../../../../rpdata/api/LogHeader") const { logHeaderApi } = require("../../../../../rpdata/api/LogHeader")
module.exports = class { module.exports = class {
onCreate(input) { onCreate(input) {
@@ -19,16 +19,23 @@ module.exports = class {
addFilter(type, filter) { addFilter(type, filter) {
if (type === "search") { if (type === "search") {
this.state.filter = Object.assign({}, this.state.filter, {search: filter}) if (filter.trim().length > 0) {
this.state.filter = Object.assign({}, this.state.filter, {search: filter})
} else {
this.state.filter = Object.assign({}, this.state.filter, {search: null})
}
} else { } else {
this.state.filter = Object.assign({}, this.state.filter, {[type]: (this.state.filter[type] || []).concat(filter)}) this.state.filter = Object.assign({}, this.state.filter, {[type]: (this.state.filter[type] || []).filter(f => f !== filter).concat(filter)})
} }
this.state.filter = Object.assign({}, this.state.filter, {limit: 0})
this.updateQuery(this.state.filter)
this.refresh() this.refresh()
} }
removeFilter(type, filter) { removeFilter(type, filter) {
if (type === "search") { if (type === "search") {
this.state.filter = Object.assign({}, this.state.filter, {search: null}) this.state.filter = Object.assign({}, this.state.filter, {search: null})
this.refresh() this.refresh()
@@ -42,6 +49,41 @@ module.exports = class {
this.refresh() this.refresh()
} }
} }
this.updateQuery(this.state.filter)
}
updateQuery(filter) {
const queries = []
if (filter.characters) {
queries.push("characters=" + filter.characters.join(","))
}
if (filter.channels) {
queries.push("channels=" + filter.channels.map(c => encodeURIComponent(c)).join(","))
}
if (filter.events) {
queries.push("events=" + filter.events.map(e => encodeURIComponent(e)).join(","))
}
if (filter.search) {
queries.push("search=" + encodeURIComponent(filter.search))
}
if (filter.limit) {
queries.push("limit=" + encodeURIComponent(filter.limit.toString()))
}
if (queries.length > 0) {
history.replaceState("", "", `/logs/?${queries.join("&")}`)
} else {
history.replaceState("", "", "/logs/")
}
}
clearLimit() {
this.state.filter = Object.assign({}, this.state.filter, {limit: 0})
this.refresh()
this.updateQuery(this.state.filter)
} }
refresh() { refresh() {
+10 -4
View File
@@ -1,7 +1,13 @@
<background src="/assets/images/bg.png" opacity=0.25 /> <background src="/assets/images/bg.png" opacity=0.25 />
<logs-menu key="menu" on-select("open") on-removefilter("removeFilter") selected=(input.selected || {}) user=input.user filter=state.filter characters=input.characters /> <logs-menu key="menu" on-select("open") selected=(input.selected || {}) user=input.user filter=state.filter characters=input.characters />
<main> <filter-menu on-add("addFilter") on-remove("removeFilter") filter=state.filter characters=input.characters channels=input.channels eventNames=input.eventNames />
<logs-list logs=state.logs /> <main class="logs-page with-right">
<logs-table logs=state.logs on-addfilter("addFilter") />
<if(state.filter.limit > 0)>
<div class="show-more-button" on-click("clearLimit")>
<span>Show All</span>
</div>
</if>
</main> </main>
<add-log-modal enabled=(state.modal === "log.add") on-close("close") /> <add-log-modal enabled=(state.modal === "log.add") on-close("close") />
<add-filter-modal enabled=(state.modal === "filter.add") characters=input.characters filter=state.filter on-add("addFilter") on-close("close") /> <import-log-modal enabled=(state.modal === "log.import") on-close("close") />
@@ -0,0 +1,17 @@
main.logs-page {
div.show-more-button {
margin: 1em 0;
padding: 0.5em 2em;
font-size: 2em;
text-align: center;
user-select: none;
color: #DA1;
}
div.show-more-button:hover {
cursor: pointer;
text-decoration: underline;
}
}
+7 -2
View File
@@ -1,6 +1,11 @@
<include("../layout", {title: "Logs", site: "logs"})> <include("../layout", {title: "Logs", site: "logs"})>
<@body> <@body>
<!-- Page needed to get component.js functionality --> <page logs=input.logs
<page logs=input.logs filter=input.filter characters=input.characters user=input.user selected=input.selected /> filter=input.filter
characters=input.characters
channels=input.channels
eventNames=input.eventNames
user=input.user
selected=input.selected />
</@body> </@body>
</include> </include>
@@ -11,7 +11,7 @@ module.exports = class {
} }
} }
updateState({kind, weak, value, updated}) { updateState({kind, weak, value, utc, updated}) {
this.state = {text: null, color: "color-menu", href: null, tooltip: null} this.state = {text: null, color: "color-menu", href: null, tooltip: null}
if (value == null || value == "") { if (value == null || value == "") {
@@ -20,7 +20,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
} }
@@ -10,6 +10,10 @@ module.exports = class {
this.emit("edit", data) this.emit("edit", data)
} }
moveChapter(data) {
this.emit("move", data)
}
removeChapter() { removeChapter() {
this.state.removed = true this.state.removed = true
this.emit("remove") this.emit("remove")
@@ -1,23 +1,25 @@
import moment from "moment"
<div class="story-chapter"> <div class="story-chapter">
<a class="anchor" id=input.chapter.id /> <a class="anchor" id=input.chapter.id />
<h1 if(input.chapter.title && !input.hideTitle) class="title color-primary">${input.chapter.title}</h1>
<div class="metadata"> <div class="metadata">
<div class="options color-menu"> <div class="options color-menu">
<if-permitted user=input.user author=input.chapter.author permission="chapter.edit"> <if-permitted user=input.user author=input.chapter.author permission="chapter.edit">
<a on-click("open", "edit") >Edit</a> <a on-click("open", "edit") >Edit</a>
</if-permitted> </if-permitted>
<if-permitted user=input.user author=input.chapter.author permission="chapter.move">
<a on-click("open", "move") >Move</a>
</if-permitted>
<if-permitted user=input.user author=input.chapter.author permission="chapter.remove"> <if-permitted user=input.user author=input.chapter.author permission="chapter.remove">
<a on-click("open", "remove") >Remove</a> <a on-click("open", "remove") >Remove</a>
</if-permitted> </if-permitted>
</div> </div>
<chapter-meta kind="title" value=input.chapter.title /> <chapter-meta kind="date" utc=true value=input.chapter.fictionalDate />
<chapter-meta kind="date" value=input.chapter.fictionalDate />
<chapter-meta weak kind="date" value=input.chapter.createdDate /> <chapter-meta weak kind="date" value=input.chapter.createdDate />
<chapter-meta weak kind="author" value=input.chapter.author /> <chapter-meta weak kind="author" value=input.chapter.author />
</div> </div>
<markdown class="chapter-content color-text" source=input.chapter.source /> <markdown class="chapter-content color-text" source=input.chapter.source />
<edit-chapter-modal enabled=(state.modal === "edit") chapter=input.chapter on-close("close") on-edit("updateChapter") /> <edit-chapter-modal enabled=(state.modal === "edit") chapter=input.chapter on-close("close") on-edit("updateChapter") />
<move-chapter-modal enabled=(state.modal === "move") chapter=input.chapter user=input.user on-close("close") on-move("moveChapter") />
<remove-chapter-modal enabled=(state.modal === "remove") chapter=input.chapter on-close("close") on-remove("removeChapter") /> <remove-chapter-modal enabled=(state.modal === "remove") chapter=input.chapter on-close("close") on-remove("removeChapter") />
</div> </div>
@@ -1,5 +1,12 @@
.story-chapter { .story-chapter {
padding-bottom: 1em; padding-bottom: 2em;
h1.title {
border-bottom: 1px solid rgba(34, 187, 255, 0.25);
font-size: 1.75em;
margin: 0;
padding-bottom: 0.125em;
}
.metadata { .metadata {
padding: 0; padding: 0;
@@ -15,8 +22,8 @@
.options { .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;
pointer-events: none; pointer-events: none;
@@ -33,6 +40,10 @@
a:hover { a:hover {
opacity: 1; opacity: 1;
} }
@media screen and (max-width: 700px) {
width: calc(100% - 6.5ch);
}
} }
div.chapter-content { div.chapter-content {
@@ -4,7 +4,7 @@
<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>IC Date</label> <label>IC Date</label>
<input key="icdate" placeholder="(Optional)" on-change("change", "fictionalDate") value=state.values.fictionalDate /> <input key="icdate" placeholder="(Optional)" on-change("change", "fictionalDate") value=state.values.fictionalDate />
@@ -4,7 +4,7 @@
<p key="error" class="color-error">${state.error}</p> <p key="error" class="color-error">${state.error}</p>
<label>Title</label> <label>Title</label>
<input key="title" placeholder="Title" class="big" on-change("change", "title") value=state.values.title /> <input key="title" autofocus placeholder="Title" class="big" on-change("change", "title") value=state.values.title />
<label>IC Date</label> <label>IC Date</label>
<input key="icdate" placeholder="IC Date" on-change("change", "fictionalDate") value=state.values.fictionalDate /> <input key="icdate" placeholder="IC Date" on-change("change", "fictionalDate") value=state.values.fictionalDate />
@@ -4,7 +4,7 @@
<p key="error" class="color-error">${state.error}</p> <p key="error" class="color-error">${state.error}</p>
<label>Title</label> <label>Title</label>
<input key="name" placeholder="Name" class="big" on-change("change", "name") value=state.values.name /> <input key="name" autofocus placeholder="Name" class="big" on-change("change", "name") value=state.values.name />
<label>IC Date</label> <label>IC Date</label>
<input key="icdate" placeholder="IC Date" on-change("change", "fictionalDate") value=state.values.fictionalDate /> <input key="icdate" placeholder="IC Date" on-change("change", "fictionalDate") value=state.values.fictionalDate />
@@ -0,0 +1,89 @@
const {storyApi} = require("../../../../../rpdata/api/Story")
const {chapterApi} = require("../../../../../rpdata/api/Chapter")
module.exports = class {
onCreate(input) {
this.state = {
error: null,
loading: false,
loadingStories: true,
stories: [],
selected: null,
}
this.first = false
}
change(key, ev) {
const value = ev.target.value
switch (key) {
case "selected": {
this.state.selected = Object.assign({}, this.state.stories.find(s => s.id === value))
break
}
}
}
open() {
const promises = []
if ((this.input.user.permissions || []).includes("chapter.move")) {
promises.push(storyApi.list())
} else {
promises.push(storyApi.list({author: this.input.user.name}))
promises.push(storyApi.list({open: true}))
}
this.state.loadingStories = true
Promise.all(promises).then(lists => {
this.state.stories = unique(Array.prototype.concat.call(...lists))
this.state.selected = this.state.stories[0]
}).catch(err => {
console.warn(err)
}).then(() => {
this.state.loadingStories = false
})
}
close() {
this.first = false
this.emit("close")
}
save() {
if (this.state.loading) {
return
}
const input = {id: this.input.chapter.id, storyId: this.state.selected.id}
this.state.loading = true
chapterApi.moveChapter(input).then(data => {
this.emit("move", input)
this.emit("close")
}).catch(errs => {
console.warn("Failed to edit:", errs)
this.state.error = "Failed to edit: " + errs[0].message
}).then(() => {
this.state.loading = false
})
}
}
function unique(stories) {
const ids = {}
const results = []
for (const story of stories) {
if (ids[story.id]) {
continue
}
ids[story.id] = true
results.push(story)
}
return results
}
@@ -0,0 +1,18 @@
$ const selected = (state.selected||{id:null});
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
<h1>Move Chapter</h1>
<p key="error" class="color-error">${state.error}</p>
<label>Target story</label>
<select key="story" on-change("change", "selected") autofocus value=selected.id>
<option for(story in state.stories) value=story.id selected=(selected.id === story.id)>${story.name}</option>
</select>
<if-permitted not permission="story.move">
<p>Only stories you are allowed to move the chapter to appear in the above list.</p>
</if-permitted>
<button disabled=state.loading on-click("save")>Move</button>
</modal>
@@ -24,6 +24,12 @@ module.exports = class {
this.refreshStory() this.refreshStory()
} }
moveChapter(chapter, data) {
if (data.storyId !== this.state.story.id) {
this.state.story = Object.assign({}, this.state.story, {chapters: this.state.story.chapters.filter(ch => ch.id !== chapter.id)})
}
}
updateChapter(id, data) { updateChapter(id, data) {
const index = this.state.story.chapters.findIndex(ch => ch.id === id) const index = this.state.story.chapters.findIndex(ch => ch.id === id)
if (index === -1) { if (index === -1) {
@@ -8,18 +8,22 @@
<a on-click("open", "story.edit") class="color-menu">Edit</a> <a on-click("open", "story.edit") class="color-menu">Edit</a>
</if-permitted> </if-permitted>
<if-permitted user=input.user author=state.story.author permission="story.remove"> <if-permitted user=input.user author=state.story.author permission="story.remove">
<a on-click("open", "story.remove") class="color-menu">Remove</a> <a on-click("open", "story.remove") class="color-menu">Remove</a>
</if-permitted> </if-permitted>
</if> </if>
</div> </div>
<story-tags tags=state.story.tags author=state.story.author user=(!state.removed ? input.user : null) on-select("open", "story.tags")/> <story-tags tags=state.story.tags open=state.story.open author=state.story.author user=(!state.removed ? input.user : null) on-select("open", "story.tags")/>
<annotation if(state.removed) level="error"> <annotation if(state.removed) level="error">
<p> <p>
This story has been removed. Your browser has not refreshed the page yet, This story has been removed. Your browser has not refreshed the page yet,
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>
<chapter for(chapter in state.story.chapters) key=chapter.id chapter=chapter user=(!state.removed ? input.user : null) on-edit("updateChapter", chapter.id) on-remove("removeChapter", chapter.id)/> <chapter for(chapter in state.story.chapters) key=chapter.id
chapter=chapter
user=(!state.removed ? input.user : null)
hideTitle=(state.story.chapters[0] === chapter && chapter.title === state.story.name)
on-edit("updateChapter", chapter.id) on-remove("removeChapter", chapter.id) on-move("moveChapter", chapter) />
</div> </div>
<create-chapter-modal storyId=state.story.id enabled=(state.modal === "chapter.add") chapter=input.chapter on-close("close") on-add("addChapter") /> <create-chapter-modal storyId=state.story.id enabled=(state.modal === "chapter.add") chapter=input.chapter on-close("close") on-add("addChapter") />
@@ -1,6 +1,6 @@
.story-content { .story-content {
width: 90%; width: 90%;
max-width: 75ch; max-width: 95ch;
margin: auto; margin: auto;
h1 { h1 {
@@ -16,7 +16,7 @@ module.exports = class {
if (chapter.title) { if (chapter.title) {
title = chapter.title title = chapter.title
} else if (chapter.fictionalDate && chapter.fictionalDate.getUTCFullYear() > 1) { } else if (chapter.fictionalDate && chapter.fictionalDate.getUTCFullYear() > 1) {
title = moment(chapter.fictionalDate).format("MMM D, YYYY") title = moment.utc(chapter.fictionalDate).format("MMM D, YYYY")
} }
return title return title
@@ -1,6 +1,14 @@
<div class="story-tags"> <div class="story-tags">
<a for(tag in input.tags) href=("/story/by-tag/" + tag.kind + "/" + encodeURIComponent(tag.name)) class=["tag", "color-tag-" + tag.kind.toLowerCase()]>${tag.name}</a> <a for(tag in input.tags) href=("/story/by-tag/" + tag.kind + "/" + encodeURIComponent(tag.name)) class=["tag", "color-tag-" + tag.kind.toLowerCase()]>${tag.name}</a>
<if-permitted user=input.user author=input.author permission="story.edit">
<a on-click("select", "edit") class="option color-menu">Manage Tags</a> <if(input.open)>
</if-permitted> <if-permitted user=input.user permission="member">
<a on-click("select", "edit") class="option color-menu">Manage Tags (Open)</a>
</if-permitted>
</if>
<else>
<if-permitted user=input.user author=input.author permission="story.edit">
<a on-click("select", "edit") class="option color-menu">Manage Tags</a>
</if-permitted>
</else>
</div> </div>
+1 -1
View File
@@ -1,6 +1,6 @@
<include("../layout", {title: "Stories", site: "story"})> <include("../layout", {title: "Stories", site: "story"})>
<@body> <@body>
<background src="/assets/images/bg.png" opacity=0.25 /> <background src="/assets/images/bg.png" opacity=0.125 />
<page story=input.story categories=input.categories user=input.user selected=input.selected /> <page story=input.story categories=input.categories user=input.user selected=input.selected />
</@body> </@body>
</include> </include>
@@ -4,7 +4,7 @@
<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="name" placeholder="Name" class="big" on-change("change", "name") value=state.values.name /> <input key="name" autofocus placeholder="Name" class="big" on-change("change", "name") value=state.values.name />
<label>IC Date</label> <label>IC Date</label>
<input key="icdate" placeholder="IC Date" on-change("change", "fictionalDate") value=state.values.fictionalDate /> <input key="icdate" placeholder="IC Date" on-change("change", "fictionalDate") value=state.values.fictionalDate />
@@ -1,6 +1,7 @@
<menu user=input.user> <menu user=input.user>
<menu-header>Story</menu-header> <menu-header>Story</menu-header>
<menu-link key="index" selected=input.selected.index icon="S" href="/story/">Stories</menu-link> <menu-link key="index" selected=input.selected.index icon="S" href="/story/">Stories</menu-link>
<menu-link if(input.user.loggedIn) key="unlisted" selected=input.selected.unlisted icon="S" href="/story/unlisted/">Unlisted</menu-link>
<menu-link selected=input.selected.tags icon="T" href="/story/tag-list/">Tags</menu-link> <menu-link selected=input.selected.tags icon="T" href="/story/tag-list/">Tags</menu-link>
<menu-gap if(input.menuAuthor || input.menuMonth || input.menuTags) /> <menu-gap if(input.menuAuthor || input.menuMonth || input.menuTags) />
<if(input.menuMonth != null)> <if(input.menuMonth != null)>
@@ -17,5 +18,4 @@
<menu-gap /> <menu-gap />
<menu-header>Categories</menu-header> <menu-header>Categories</menu-header>
<menu-link for(category in (input.categories||[])) key=(category.name) selected=(input.selected.category == category.name) href=("/story/by-category/"+category.name) icon=category.name.charAt(0)>${category.name}</menu-link> <menu-link for(category in (input.categories||[])) key=(category.name) selected=(input.selected.category == category.name) href=("/story/by-category/"+category.name) icon=category.name.charAt(0)>${category.name}</menu-link>
<menu-gap />
</menu> </menu>
@@ -9,7 +9,7 @@ div.tags-page {
} }
div.groups { div.groups {
column-count: 4; column-count: 5;
padding: 0.5em; padding: 0.5em;
div.group { div.group {
@@ -43,7 +43,15 @@ div.tags-page {
} }
} }
@media screen and (max-width: 1900px) { @media screen and (max-width: 2000px) {
div.tags-page {
div.groups {
column-count: 4;
}
}
}
@media screen and (max-width: 1400px) {
div.tags-page { div.tags-page {
div.groups { div.groups {
column-count: 3; column-count: 3;
@@ -51,7 +59,7 @@ div.tags-page {
} }
} }
@media screen and (max-width: 1300px) { @media screen and (max-width: 1000px) {
div.tags-page { div.tags-page {
div.groups { div.groups {
column-count: 2; column-count: 2;
@@ -59,7 +67,7 @@ div.tags-page {
} }
} }
@media screen and (max-width: 1000px) { @media screen and (max-width: 900px) {
div.tags-page { div.tags-page {
div.groups { div.groups {
column-count: 1; column-count: 1;
+2 -2
View File
@@ -3,7 +3,7 @@ const config = require("../config")
module.exports = (req, res, next) => { module.exports = (req, res, next) => {
if (res.marko) { if (res.marko) {
res.markoAsync = async(template, input) => { res.markoAsync = async(template, input) => {
const locals = Object.assign((res.locals || {}), input) const locals = Object.assign({}, (res.locals || {}), input)
try { try {
for (const key in locals) { for (const key in locals) {
@@ -12,7 +12,7 @@ module.exports = (req, res, next) => {
locals[key] = await value locals[key] = await value
} }
} }
if (locals.user.permissions != null) { if (locals.user.permissions != null) {
locals.user.permissions = await locals.user.permissions locals.user.permissions = await locals.user.permissions
} }
+1205 -48
View File
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@gisle/rpdata-frontend", "name": "@gisle/rpdata-frontend",
"version": "1.0.3", "version": "1.1.0",
"description": "RPData (www.aiterp.net) frontend/proxy", "description": "RPData (www.aiterp.net) frontend/proxy",
"main": "./index.js", "main": "./index.js",
"scripts": { "scripts": {
@@ -12,6 +12,7 @@
"homepage": "https://github.com/marko-js-samples/marko-express", "homepage": "https://github.com/marko-js-samples/marko-express",
"dependencies": { "dependencies": {
"@lasso/marko-taglib": "^1.0.12", "@lasso/marko-taglib": "^1.0.12",
"@marko/prebuild": "^4.0.5",
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"browser-refresh": "^1.7.3", "browser-refresh": "^1.7.3",
@@ -24,19 +25,26 @@
"express-http-proxy": "^1.2.0", "express-http-proxy": "^1.2.0",
"express-session": "^1.15.6", "express-session": "^1.15.6",
"graphql-query-compress": "^1.1.0", "graphql-query-compress": "^1.1.0",
"http-proxy-middleware": "^0.19.1",
"ip": "^1.1.5",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"jsonwebtoken": "^8.3.0", "jsonwebtoken": "^8.3.0",
"lasso": "^3.2.6", "lasso": "^3.2.6",
"lasso-babel-transform": "^1.0.2", "lasso-babel-transform": "^1.0.2",
"lasso-less": "^3.0.1", "lasso-less": "^3.0.1",
"lasso-marko": "^2.4.4", "lasso-marko": "^2.4.4",
"less": "^3.8.1",
"markdown-it": "^8.4.2", "markdown-it": "^8.4.2",
"markdown-it-container": "^2.0.0", "markdown-it-container": "^2.0.0",
"markdown-it-regexp": "^0.4.0", "markdown-it-regexp": "^0.4.0",
"marko": "^4.13.8", "marko": "^4.13.8",
"moment": "^2.22.2", "moment": "^2.22.2",
"moment-timezone": "^0.5.23",
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-auth0": "^1.0.0", "passport-auth0": "^1.0.0",
"pluralize": "^7.0.0" "pluralize": "^7.0.0"
},
"devDependencies": {
"@types/http-proxy-middleware": "^0.19.0"
} }
} }
+1 -6
View File
@@ -17,12 +17,7 @@ router.get("/login", saveReferer, passport.authenticate("auth0", {scope: "openid
res.redirect("/") res.redirect("/")
}) })
router.get("/error", (req, res) => { router.get("/callback", passport.authenticate("auth0"), (req, res) => {
console.error(req)
res.status(500).end("Something went wrong.")
})
router.get("/callback", passport.authenticate("auth0", { failureRedirect: "/auth/error" }), (req, res) => {
if (req.user == null) { if (req.user == null) {
throw new Error("user null"); throw new Error("user null");
} }
+15
View File
@@ -0,0 +1,15 @@
const express = require("express")
const router = express.Router()
const {changesApi} = require("../../rpdata/api/Change")
const changesTemplate = require("../../marko/page/data/changes.marko")
router.get("/", async(req, res) => {
res.markoAsync(changesTemplate, {
changes: changesApi.list({limit: 2000}),
selected: {changes: true},
})
})
module.exports = router
+23
View File
@@ -0,0 +1,23 @@
const fs = require("fs")
const express = require("express")
const less = require("less")
const router = express.Router()
let lessData = ""
let data = ""
for (const name of fs.readdirSync("./marko/global/", "utf8")) {
data += fs.readFileSync("./marko/global/"+name, {encoding:"utf8"}) + "\n\n"
}
data = less.render(data, (err, output) => {
if (err == null) {
lessData = output.css
}
})
router.get("/", (req, res) => {
res.contentType("text/css").end(lessData)
})
module.exports = router
+2
View File
@@ -1,3 +1,4 @@
const proxy = require("http-proxy-middleware")
const express = require("express") const express = require("express")
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
@@ -44,6 +45,7 @@ router.post("/", (req, res) => {
}) })
}) })
router.use("/", proxy(config.graphqlEndpoint, {ws: true}))
/** /**
* @param {string} user * @param {string} user

Some files were not shown because too many files have changed in this diff Show More