data: Added character list/add/edit/remove
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<tr class="compact-list-item color-highlight-dark">
|
||||
<include(input.renderBody)/>
|
||||
</tr>
|
||||
@@ -0,0 +1,5 @@
|
||||
tr.compact-list-item {
|
||||
user-select: none;
|
||||
outline: 0.05px dotted;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<td class=["compact-list-options", input.short ? "short" : null, input.long ? "long" : null]>
|
||||
<div class="content color-menu">
|
||||
<include(input.renderBody)/>
|
||||
</div>
|
||||
</td>
|
||||
@@ -0,0 +1,28 @@
|
||||
td.compact-list-options {
|
||||
width: 20%;
|
||||
padding: 0 0.5ch;
|
||||
|
||||
|
||||
div.content {
|
||||
font-size: 1em;
|
||||
text-align: right;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
margin-right: 1ch;
|
||||
opacity: 0.5;
|
||||
}
|
||||
a:hover {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td.compact-list-options.short {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
td.compact-list-options.long {
|
||||
width: 30%;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<td class=["compact-list-property", input.short ? "short" : null, input.long ? "long" : null]>
|
||||
<div class="label color-menu">${input.label}</div>
|
||||
<div class=["value", input.primary ? "color-primary" : "color-text"]>${input.value}</div>
|
||||
</td>
|
||||
@@ -0,0 +1,24 @@
|
||||
td.compact-list-property {
|
||||
width: 20%;
|
||||
padding: 0 0.5ch;
|
||||
|
||||
div.label {
|
||||
font-size: 0.5em;
|
||||
line-height: 1em;
|
||||
padding-top: 0.5em;
|
||||
}
|
||||
|
||||
div.value {
|
||||
font-size: 0.75em;
|
||||
padding-bottom: 0.25em;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
td.compact-list-property.short {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
td.compact-list-property.long {
|
||||
width: 30%;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<table class="compact-list color-primary">
|
||||
<include(input.renderBody)/>
|
||||
</table>
|
||||
@@ -0,0 +1,13 @@
|
||||
table.compact-list {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 3em;
|
||||
|
||||
width: 90%;
|
||||
max-width: 100ch;
|
||||
padding-right: 1em;
|
||||
|
||||
margin: auto;
|
||||
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 0.5em;
|
||||
}
|
||||
@@ -6,5 +6,6 @@
|
||||
<div class="options">
|
||||
<a class=(input.site === "story" ? "color-primary" : "color-menu") href="/story/">Story</a>
|
||||
<a class=(input.site === "logs" ? "color-primary" : "color-menu") href="/logs/">Logs</a>
|
||||
<a class=(input.site === "data" ? "color-primary" : "color-menu") href="/data/">Data</a>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -2,6 +2,7 @@ div.menu-link {
|
||||
width: 100%;
|
||||
padding-left: 0.5ch;
|
||||
font-size: 1.1em;
|
||||
padding-bottom: 0.125em;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -10,7 +11,7 @@ div.menu-link {
|
||||
div.icon {
|
||||
display: inline-block;
|
||||
width: 3ch;
|
||||
padding: 0.15em 0.125em;
|
||||
padding: 0 0.125em;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,32 @@ body.theme-logs {
|
||||
}
|
||||
}
|
||||
|
||||
body.theme-data {
|
||||
.color-menu {
|
||||
color: #775;
|
||||
}
|
||||
.menu-link.selected {
|
||||
background-color: rgba(204, 204, 34, 0.226);
|
||||
}
|
||||
.menu-link:hover, .Menu > .head-menu > a:hover {
|
||||
background-color: rgba(204, 204, 34, 0.25);
|
||||
}
|
||||
.color-primary, button, input:focus, textarea:focus, select:focus, .modal h1, .modal h2, .modal label, .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content hr, .markdown-content a {
|
||||
color: #CC2;
|
||||
}
|
||||
.color-text, input, textarea, select {
|
||||
color: #BBB;
|
||||
}
|
||||
.color-highlight-dark {
|
||||
background-color: rgba(18, 8, 0, 0.50);
|
||||
}
|
||||
.color-highlight-primary {
|
||||
background-color: rgba(204, 204, 34, 0.25);
|
||||
color: #CC2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
div.markdown-content .orange, body.theme-logs div.markdown-content blockquote, body.theme-logs div.markdown-content table, body.theme-logs div.markdown-content pre {
|
||||
background-color: rgba(221, 170, 17, 0.125);
|
||||
color: #DA1;
|
||||
@@ -90,6 +116,14 @@ div.markdown-content .green, body.theme-mapp div.markdown-content blockquote, bo
|
||||
color: #1F8;
|
||||
}
|
||||
}
|
||||
div.markdown-content .yellow, body.theme-data div.markdown-content blockquote, body.theme-data div.markdown-content table, body.theme-data div.markdown-content pre {
|
||||
background-color: rgba(204, 204, 34, 0.25);
|
||||
color: #CC2;
|
||||
|
||||
h1, h2, h3 {
|
||||
color: #CC2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tag colors */
|
||||
.color-tag-event {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<include("../layout", {title: "Data", site: "data"})>
|
||||
<@body>
|
||||
<background src="/assets/images/bg.png" opacity=0.25 />
|
||||
<characters-page user=input.user characters=input.characters selected=input.selected />
|
||||
</@body>
|
||||
</include>
|
||||
@@ -0,0 +1,69 @@
|
||||
const moment = require("moment")
|
||||
|
||||
const {charactersApi} = require("../../../../../rpdata/api/Character")
|
||||
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
this.state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
values: {
|
||||
name: "",
|
||||
nick: "",
|
||||
shortName: "",
|
||||
author: "",
|
||||
description: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onInput(input) {
|
||||
this.state.values = Object.assign({}, this.state.values, {author: input.user.name})
|
||||
}
|
||||
|
||||
change(key, ev) {
|
||||
this.state.values[key] = ev.target.value
|
||||
}
|
||||
|
||||
open() {
|
||||
|
||||
}
|
||||
|
||||
close() {
|
||||
this.emit("close")
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this.state.loading) {
|
||||
return
|
||||
}
|
||||
|
||||
const values = Object.assign({}, this.state.values)
|
||||
if (values.name.length < 2) {
|
||||
this.state.error = "Name is too short (min: 2)"
|
||||
return
|
||||
}
|
||||
if (values.author == "") {
|
||||
delete values.author
|
||||
}
|
||||
|
||||
this.state.loading = true
|
||||
charactersApi.add(this.state.values).then((res) => {
|
||||
this.emit("added", res)
|
||||
this.emit("close")
|
||||
this.state.values = {
|
||||
name: "",
|
||||
nick: "",
|
||||
shortName: "",
|
||||
author: this.input.user.name,
|
||||
description: "",
|
||||
}
|
||||
}).catch(errs => {
|
||||
console.warn("Failed to edit:", errs)
|
||||
|
||||
this.state.error = "Failed to edit: " + errs[0].message
|
||||
}).then(() => {
|
||||
this.state.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
|
||||
<h1>Add Character</h1>
|
||||
|
||||
<p class="color-error">${state.error}</p>
|
||||
|
||||
<label>Full Name</label>
|
||||
<input key="name" placeholder="" class="big" on-change("change", "name") value=state.values.name />
|
||||
|
||||
<label>Short Name</label>
|
||||
<input key="shortName" placeholder="(Optional)" on-change("change", "shortName") value=state.values.shortName />
|
||||
|
||||
<label>IRC Nick</label>
|
||||
<input key="nick" placeholder="" class="big" on-change("change", "nick") value=state.values.nick />
|
||||
|
||||
<label>Author</label>
|
||||
<input key="author" placeholder=(input.user.name) on-change("change", "author") value=state.values.author />
|
||||
|
||||
<label>Description</label>
|
||||
<textarea key="description" placeholder="Description" on-change("change", "description") value=state.values.description />
|
||||
|
||||
<button disabled=state.loading on-click("save")>Save character!</button>
|
||||
</modal>
|
||||
@@ -0,0 +1,5 @@
|
||||
<compact-list>
|
||||
<compact-list-item for(character in input.characters) key=character.id id=character.id>
|
||||
<character data=character on-removed("emit", "removed", character) on-edited("emit", "edited", character) />
|
||||
</compact-list-item>
|
||||
</compact-list>
|
||||
@@ -0,0 +1,20 @@
|
||||
module.exports = class {
|
||||
onCreate() {
|
||||
this.state = {
|
||||
modal: null,
|
||||
deleted: false,
|
||||
}
|
||||
}
|
||||
|
||||
open(modal) {
|
||||
this.state.modal = modal
|
||||
}
|
||||
|
||||
close() {
|
||||
this.state.modal = null
|
||||
}
|
||||
|
||||
removed() {
|
||||
this.state.deleted = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<if (!state.deleted)>
|
||||
<compact-list-property primary short label="ID" value=input.data.id />
|
||||
<compact-list-property label="Nick" value=(input.data.nick) weak=("+" + (input.data.nicks.length - 1)) showWeak=(input.data.nicks.length > 0) />
|
||||
<compact-list-property long label="Name" value=input.data.name />
|
||||
<compact-list-property short label="Author" value=input.data.author />
|
||||
<compact-list-options long>
|
||||
<a key="edit" on-click("open", "edit", input.data)>Edit</a>
|
||||
<a key="remove" on-click("open", "remove", input.data)>Remove</a>
|
||||
</compact-list-options>
|
||||
<remove-character-modal enabled=(state.modal === "remove") character=input.data on-removed("emit", "removed") on-close("close") />
|
||||
<edit-character-modal enabled=(state.modal === "edit") character=input.data on-edited("emit", "edited") on-close("close") />
|
||||
</if>
|
||||
@@ -0,0 +1,36 @@
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
this.state = {
|
||||
characters: input.characters,
|
||||
modal: null,
|
||||
}
|
||||
}
|
||||
|
||||
open(modal) {
|
||||
this.state.modal = modal
|
||||
}
|
||||
|
||||
close() {
|
||||
this.state.modal = null
|
||||
}
|
||||
|
||||
characterRemoved({id}) {
|
||||
this.state.characters = this.state.characters.filter(c => c.id !== id)
|
||||
}
|
||||
|
||||
characterEdited(character, patch) {
|
||||
const characters = this.state.characters.slice()
|
||||
const index = characters.findIndex(c => c.id === character.id)
|
||||
if (index === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
characters[index] = Object.assign(characters[index], patch)
|
||||
|
||||
this.state.characters = characters
|
||||
}
|
||||
|
||||
characterAdded(character) {
|
||||
this.state.characters = this.state.characters.concat(character)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<data-menu categories=input.categories selected=(input.selected || {}) user=input.user on-open("open") />
|
||||
<main>
|
||||
<character-list characters=state.characters on-removed("characterRemoved") on-edited("characterEdited") />
|
||||
</main>
|
||||
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-added("characterAdded") on-close("close") />
|
||||
@@ -0,0 +1,9 @@
|
||||
<menu user=input.user>
|
||||
<menu-header>List</menu-header>
|
||||
<menu-link key="characters" selected=input.selected.characters icon="C" href="/data/characters/">Characters</menu-link>
|
||||
<menu-link key="channels" selected=input.selected.channels icon="#" href="/data/characters/">Channels</menu-link>
|
||||
<menu-gap />
|
||||
<menu-header>Options</menu-header>
|
||||
<menu-link dark key="characters_add" icon="+" on-click("emit", "open", "character.add")>Add Character</menu-link>
|
||||
<menu-link dark key="channels_add" icon="+" on-click("emit", "open", "channel.add")>Add Channel</menu-link>
|
||||
</menu>
|
||||
@@ -0,0 +1,57 @@
|
||||
const moment = require("moment")
|
||||
|
||||
const {charactersApi} = require("../../../../../rpdata/api/Character")
|
||||
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
this.state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
values: {
|
||||
name: "",
|
||||
shortName: "",
|
||||
description: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
change(key, ev) {
|
||||
this.state.values[key] = ev.target.value
|
||||
}
|
||||
|
||||
onInput(input) {
|
||||
this.state.values = {
|
||||
name: input.character.name,
|
||||
shortName: input.character.shortName,
|
||||
description: input.character.description,
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
|
||||
}
|
||||
|
||||
close() {
|
||||
this.emit("close")
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this.state.loading) {
|
||||
return
|
||||
}
|
||||
|
||||
const input = Object.assign({id: this.input.character.id}, this.state.values)
|
||||
|
||||
this.state.loading = true
|
||||
charactersApi.edit(input).then((res) => {
|
||||
this.emit("edited", res)
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
|
||||
<h1>Edit Character</h1>
|
||||
|
||||
<p class="color-error">${state.error}</p>
|
||||
|
||||
<label>Full Name</label>
|
||||
<input key="name" placeholder="" class="big" on-change("change", "name") value=state.values.name />
|
||||
|
||||
<label>Short Name</label>
|
||||
<input key="shortName" placeholder="E.g. first/given name" on-change("change", "shortName") value=state.values.shortName />
|
||||
|
||||
<label>Description</label>
|
||||
<textarea key="description" placeholder="Description" on-change("change", "description") value=state.values.description />
|
||||
|
||||
<button disabled=state.loading on-click("save")>Save character!</button>
|
||||
</modal>
|
||||
@@ -0,0 +1,42 @@
|
||||
const moment = require("moment")
|
||||
|
||||
const {charactersApi} = require("../../../../../rpdata/api/Character")
|
||||
|
||||
module.exports = class {
|
||||
onCreate(input) {
|
||||
this.state = {
|
||||
error: null,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
change(key, ev) {
|
||||
this.state.values[key] = ev.target.value
|
||||
}
|
||||
|
||||
open() {
|
||||
|
||||
}
|
||||
|
||||
close() {
|
||||
this.emit("close")
|
||||
}
|
||||
|
||||
doIt() {
|
||||
if (this.state.loading) {
|
||||
return
|
||||
}
|
||||
|
||||
this.state.loading = true
|
||||
charactersApi.remove({id: this.input.character.id}).then(() => {
|
||||
this.emit("removed")
|
||||
this.emit("close")
|
||||
}).catch(errs => {
|
||||
console.warn("Failed to delete:", errs)
|
||||
|
||||
this.state.error = "Failed to delete: " + errs[0].message
|
||||
}).then(() => {
|
||||
this.state.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) notification closable on-close("close") on-open("open") >
|
||||
<h1>Remove ${input.character.name}</h1>
|
||||
|
||||
<p class="color-error">${state.error}</p>
|
||||
|
||||
<p class="color-danger">This is irreversible!</p>
|
||||
|
||||
<button disabled=state.loading on-click("doIt")>Yes, do it!</button>
|
||||
</modal>
|
||||
@@ -0,0 +1,15 @@
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
|
||||
const {charactersApi} = require("../../rpdata/api/Character")
|
||||
|
||||
const charactersTemplate = require("../../marko/page/data/characters.marko")
|
||||
|
||||
router.get("/", async(req, res) => {
|
||||
res.markoAsync(charactersTemplate, {
|
||||
characters: charactersApi.list(),
|
||||
selected: {characters: true},
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -0,0 +1,8 @@
|
||||
const express = require("express")
|
||||
const router = express.Router()
|
||||
|
||||
router.get("/", async(req, res) => {
|
||||
return res.redirect("/data/characters/")
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
+95
-1
@@ -27,4 +27,98 @@ class Character {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Character }
|
||||
class ChracterAPI {
|
||||
/**
|
||||
* Call `characters(filter)` query
|
||||
*
|
||||
* @param {{ids:string[], nicks:string[], names:string[], author:string, search:string, logged:boolean}} filter
|
||||
* @returns {Promise<Story[]>}
|
||||
*/
|
||||
list(filter = {}) {
|
||||
if (filter.earliestFictionalDate != null && typeof(filter.earliestFictionalDate) !== "string") {
|
||||
filter.earliestFictionalDate = filter.earliestFictionalDate.toISOString()
|
||||
}
|
||||
if (filter.latestFictionalDate != null && typeof(filter.latestFictionalDate) !== "string") {
|
||||
filter.latestFictionalDate = filter.latestFictionalDate.toISOString()
|
||||
}
|
||||
|
||||
return query(`
|
||||
query ListCharacters($filter: CharactersFilter) {
|
||||
characters(filter: $filter) {
|
||||
id
|
||||
nicks
|
||||
author
|
||||
name
|
||||
shortName
|
||||
description
|
||||
}
|
||||
}
|
||||
`, {filter}).then(({characters}) => {
|
||||
return characters.map(d => Character.fromData(d))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `addCharacter(input)` mutation, returns addable fields.
|
||||
*
|
||||
* @param {{id:string, name:string, shortName:string, author:string, description:string}} input
|
||||
* @returns {Promise<Character>}
|
||||
*/
|
||||
add(input) {
|
||||
return query(`
|
||||
mutation AddCharacter($input: CharacterAddInput!) {
|
||||
addCharacter(input: $input) {
|
||||
id
|
||||
nicks
|
||||
author
|
||||
name
|
||||
shortName
|
||||
description
|
||||
}
|
||||
}
|
||||
`, {input}, {permissions: ["member", "character.add"]}).then(({addCharacter}) => {
|
||||
return Character.fromData(addCharacter)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `editCharacter(input)` mutation, returns editable fields.
|
||||
*
|
||||
* @param {{id:string, name:string, shortName:string, description:string}} input
|
||||
* @returns {Promise<{id:string, name:string, shortName:string, description:string}>}
|
||||
*/
|
||||
edit(input) {
|
||||
return query(`
|
||||
mutation EditCharacters($input: CharacterEditInput!) {
|
||||
editCharacter(input: $input) {
|
||||
id
|
||||
name
|
||||
shortName
|
||||
description
|
||||
}
|
||||
}
|
||||
`, {input}, {permissions: ["member", "character.edit"]}).then(({editCharacter}) => {
|
||||
return editCharacter
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `removeCharacter(input)` mutation
|
||||
*
|
||||
* @param {{id:string}} input
|
||||
* @returns {Promise<{id:string}>}
|
||||
*/
|
||||
remove(input) {
|
||||
return query(`
|
||||
mutation RemoveCharacters($input: CharacterRemoveInput!) {
|
||||
removeCharacter(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`, {input}, {permissions: ["member", "character.remove"]}).then(({removeCharacter}) => {
|
||||
return removeCharacter
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Character, charactersApi: new ChracterAPI }
|
||||
@@ -79,7 +79,9 @@ app.use("/story/tag-list/", require("./routes/story/tag-list"))
|
||||
app.use("/story/:id(S[0-9a-z]{15})/", require("./routes/story-content"))
|
||||
app.use("/logs/", require("./routes/logs"))
|
||||
app.use("/logs/:id([0-9]{4}-[0-1][0-9]-[0-3][0-9]_[0-9]{9}_[A-Za-z\,\']{2,})/", require("./routes/logs-content"))
|
||||
// 2018-03-10_035344747_Miner'sRespite
|
||||
app.use("/data/", require("./routes/data"))
|
||||
app.use("/data/characters/", require("./routes/data/characters"))
|
||||
|
||||
// Entry point
|
||||
app.get("/", function(req, res) {
|
||||
res.redirect("/story/")
|
||||
|
||||
Reference in New Issue
Block a user