Browse Source

data: Added character list/add/edit/remove

1.0
Gisle Aune 6 years ago
parent
commit
5b28ce4bc2
  1. 3
      marko/components/compact-list-item/index.marko
  2. 5
      marko/components/compact-list-item/style.less
  3. 5
      marko/components/compact-list-options/index.marko
  4. 28
      marko/components/compact-list-options/style.less
  5. 4
      marko/components/compact-list-property/index.marko
  6. 24
      marko/components/compact-list-property/style.less
  7. 3
      marko/components/compact-list/index.marko
  8. 13
      marko/components/compact-list/style.less
  9. 1
      marko/components/layout-navbar/index.marko
  10. 3
      marko/components/menu-link/style.less
  11. 34
      marko/global/colors.less
  12. 6
      marko/page/data/characters.marko
  13. 69
      marko/page/data/components/add-character-modal/component.js
  14. 22
      marko/page/data/components/add-character-modal/index.marko
  15. 5
      marko/page/data/components/character-list/index.marko
  16. 20
      marko/page/data/components/character/component.js
  17. 12
      marko/page/data/components/character/index.marko
  18. 36
      marko/page/data/components/characters-page/component.js
  19. 5
      marko/page/data/components/characters-page/index.marko
  20. 9
      marko/page/data/components/data-menu/index.marko
  21. 57
      marko/page/data/components/edit-character-modal/component.js
  22. 16
      marko/page/data/components/edit-character-modal/index.marko
  23. 42
      marko/page/data/components/remove-character-modal/component.js
  24. 9
      marko/page/data/components/remove-character-modal/index.marko
  25. 15
      routes/data/characters.js
  26. 8
      routes/data/index.js
  27. 96
      rpdata/api/Character.js
  28. 4
      server.js

3
marko/components/compact-list-item/index.marko

@ -0,0 +1,3 @@
<tr class="compact-list-item color-highlight-dark">
<include(input.renderBody)/>
</tr>

5
marko/components/compact-list-item/style.less

@ -0,0 +1,5 @@
tr.compact-list-item {
user-select: none;
outline: 0.05px dotted;
vertical-align: middle;
}

5
marko/components/compact-list-options/index.marko

@ -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>

28
marko/components/compact-list-options/style.less

@ -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%;
}

4
marko/components/compact-list-property/index.marko

@ -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>

24
marko/components/compact-list-property/style.less

@ -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%;
}

3
marko/components/compact-list/index.marko

@ -0,0 +1,3 @@
<table class="compact-list color-primary">
<include(input.renderBody)/>
</table>

13
marko/components/compact-list/style.less

@ -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;
}

1
marko/components/layout-navbar/index.marko

@ -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>

3
marko/components/menu-link/style.less

@ -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;
}

34
marko/global/colors.less

@ -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 {

6
marko/page/data/characters.marko

@ -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>

69
marko/page/data/components/add-character-modal/component.js

@ -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
})
}
}

22
marko/page/data/components/add-character-modal/index.marko

@ -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>

5
marko/page/data/components/character-list/index.marko

@ -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>

20
marko/page/data/components/character/component.js

@ -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
}
}

12
marko/page/data/components/character/index.marko

@ -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>

36
marko/page/data/components/characters-page/component.js

@ -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)
}
}

5
marko/page/data/components/characters-page/index.marko

@ -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") />

9
marko/page/data/components/data-menu/index.marko

@ -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>

57
marko/page/data/components/edit-character-modal/component.js

@ -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
})
}
}

16
marko/page/data/components/edit-character-modal/index.marko

@ -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>

42
marko/page/data/components/remove-character-modal/component.js

@ -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
})
}
}

9
marko/page/data/components/remove-character-modal/index.marko

@ -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>

15
routes/data/characters.js

@ -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

8
routes/data/index.js

@ -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

96
rpdata/api/Character.js

@ -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 }

4
server.js

@ -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/")

Loading…
Cancel
Save