story-content: Added edit story modal, added toggle component.
This commit is contained in:
@@ -23,7 +23,9 @@ div.overlay {
|
||||
label {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
margin-left: 0.5ch;
|
||||
font-size: 0.75em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
input {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = class {
|
||||
set(value) {
|
||||
if (value !== this.input.value) {
|
||||
this.emit("change", {target: {value}})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="toggle">
|
||||
<div class="toggle-pill color-primary">
|
||||
<a on-click("set", false) class=["toggle-option", !input.value ? "color-highlight-primary" : "color-primary"]>${input.off}</a>
|
||||
<a on-click("set", true) class=["toggle-option", input.value ? "color-highlight-primary" : "color-primary"]>${input.on}</a>
|
||||
</div>
|
||||
<div class="toggle-content color-text">${input.value ? input.onDesc : input.offDesc}</div>
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
div.toggle {
|
||||
div.toggle-pill {
|
||||
display: inline-block;
|
||||
margin: 0.5em;
|
||||
margin-bottom: 0;
|
||||
|
||||
a.toggle-option {
|
||||
padding: 0.125em 0.5ch;
|
||||
display: inline-block;
|
||||
width: 8ch;
|
||||
text-align: center;
|
||||
outline: 0.5px dotted;
|
||||
cursor: pointer;
|
||||
opacity: 0.75;
|
||||
}
|
||||
a.toggle-option.color-highlight-primary {
|
||||
outline: 1px solid;
|
||||
}
|
||||
a.toggle-option:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
div.toggle-content {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
const moment = require("moment")
|
||||
|
||||
const {storyApi} = require("../../../../../rpdata/api/Story")
|
||||
|
||||
module.exports = class {
|
||||
onCreate() {
|
||||
this.state = {
|
||||
error: null,
|
||||
values: {
|
||||
name: "",
|
||||
fictionalDate: "",
|
||||
category: "",
|
||||
open: "",
|
||||
listed: "",
|
||||
},
|
||||
loading: false,
|
||||
}
|
||||
|
||||
this.filled = false
|
||||
}
|
||||
|
||||
onInput(input) {
|
||||
if (!this.filled) {
|
||||
this.state.values = {
|
||||
name: input.story.name,
|
||||
category: input.story.category,
|
||||
open: input.story.open,
|
||||
listed: input.story.listed,
|
||||
}
|
||||
|
||||
if (input.story.fictionalDate != null) {
|
||||
this.state.values.fictionalDate = moment.utc(input.story.fictionalDate).format("MMM D, YYYY")
|
||||
}
|
||||
|
||||
this.filled = true
|
||||
}
|
||||
}
|
||||
|
||||
change(key, ev) {
|
||||
this.state.values[key] = ev.target.value
|
||||
this.state.values = Object.assign({}, this.state.values)
|
||||
}
|
||||
|
||||
save() {
|
||||
const values = this.state.values
|
||||
|
||||
let fictionalDate = new Date(values.fictionalDate + " UTC")
|
||||
if (values.fictionalDate != "") {
|
||||
if (Number.isNaN(fictionalDate)) {
|
||||
this.state.error = `Could not parse ${values.fictionalDate} as date`
|
||||
return
|
||||
}
|
||||
} else {
|
||||
fictionalDate = null
|
||||
}
|
||||
|
||||
const input = {id: this.input.story.id, name: values.name, category: values.category, open: values.open, listed: values.listed}
|
||||
if (fictionalDate != null) {
|
||||
input.fictionalDate = fictionalDate
|
||||
} else {
|
||||
input.clearFictionalDate = true
|
||||
}
|
||||
|
||||
this.state.loading = true
|
||||
storyApi.edit(input).then(data => {
|
||||
this.emit("edit", data)
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
close() {
|
||||
this.emit("close")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<modal class="modal color-text nolabel" key="modal2" enabled=(input.enabled) closable on-close("close") >
|
||||
<h1>Edit Story</h1>
|
||||
|
||||
<p key="error" class="color-error">${state.error}</p>
|
||||
|
||||
<label>Title</label>
|
||||
<input key="name" placeholder="Name" class="big" on-change("change", "name") value=state.values.name />
|
||||
|
||||
<label>IC Date</label>
|
||||
<input key="icdate" placeholder="IC Date" on-change("change", "fictionalDate") value=state.values.fictionalDate />
|
||||
|
||||
<label>Category</label>
|
||||
<select key="category" placeholder="Kind" on-change("change", "category")>
|
||||
<option for(category in input.categories) value=category.name selected=(state.values.category === category.name)>${category.name}</option>
|
||||
</select>
|
||||
|
||||
<label>Options</label>
|
||||
<toggle value=state.values.listed on="Listed" off="Unlisted"
|
||||
onDesc="The story will be visible on the front page."
|
||||
offDesc="A direct link is required to view this story."
|
||||
on-change("change", "listed") />
|
||||
<toggle value=state.values.open on="Open" off="Closed"
|
||||
onDesc="Other authors can add chapters to this story."
|
||||
offDesc="Only you can add chapters to this story."
|
||||
on-change("change", "open") />
|
||||
|
||||
<button disabled=state.loading on-click("save")>Save</button>
|
||||
</modal>
|
||||
@@ -15,6 +15,10 @@ module.exports = class {
|
||||
this.state.modal = null
|
||||
}
|
||||
|
||||
updateStory(data) {
|
||||
this.state.story = Object.assign(Object.assign({}, this.state.story), data)
|
||||
}
|
||||
|
||||
addChapter(chapter) {
|
||||
this.state.story.chapters.push(chapter)
|
||||
this.refreshStory()
|
||||
|
||||
@@ -18,5 +18,6 @@
|
||||
|
||||
<create-chapter-modal storyId=state.story.id enabled=(state.modal === "chapter.add") chapter=input.chapter on-close("close") on-add("addChapter") />
|
||||
<edit-story-tags-modal enabled=(state.modal === "story.tags") story=state.story on-tags("updateStoryTags") on-close("close") />
|
||||
<edit-story-modal enabled=(state.modal === "story.edit") story=state.story categories=input.categories on-edit("updateStory") on-close("close") />
|
||||
<remove-story-modal enabled=(state.modal === "story.remove") story=state.story on-remove("timeToDie") on-close("close") />
|
||||
</main>
|
||||
@@ -14,7 +14,6 @@
|
||||
margin-bottom: 1em;
|
||||
|
||||
h1 {
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<include("../layout", {title: "Stories", site: "story"})>
|
||||
<@body>
|
||||
<background src="/assets/images/bg.png" opacity=0.25 />
|
||||
<page story=input.story user=input.user selected=input.selected />
|
||||
<page story=input.story categories=input.categories user=input.user selected=input.selected />
|
||||
</@body>
|
||||
</include>
|
||||
@@ -11,6 +11,10 @@ module.exports = (req, res, next) => {
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
if (JSON.stringify(err) === "{}") {
|
||||
return next(err)
|
||||
}
|
||||
|
||||
return res.status(404).json(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const viewTemplate = require("../../marko/page/story-content/view.marko")
|
||||
module.exports = async(req, res, next) => {
|
||||
res.markoAsync(viewTemplate, {
|
||||
story: storyApi.find(req.params.id),
|
||||
categories: storyApi.categories(),
|
||||
selected: {index: true},
|
||||
})
|
||||
}
|
||||
+35
-2
@@ -10,17 +10,21 @@ class Story {
|
||||
* @param {string} name
|
||||
* @param {string} author
|
||||
* @param {"Info"|"News"|"Document"|"Background"|"Story"} category Story's category
|
||||
* @param {boolean} listed
|
||||
* @param {boolean} open
|
||||
* @param {string} createdDate
|
||||
* @param {string} updatedDate
|
||||
* @param {string} fictionalDate
|
||||
* @param {{kind:string, name:string}[]} tags
|
||||
* @param {Chapter[]} chapters
|
||||
*/
|
||||
constructor(id, name, author, category, createdDate, updatedDate, fictionalDate, tags, chapters) {
|
||||
constructor(id, name, author, category, listed, open, createdDate, updatedDate, fictionalDate, tags, chapters) {
|
||||
this.id = id
|
||||
this.name = name
|
||||
this.author = author
|
||||
this.category = category
|
||||
this.listed = listed
|
||||
this.open = open
|
||||
this.createdDate = new Date(createdDate)
|
||||
this.updatedDate = new Date(updatedDate)
|
||||
this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null
|
||||
@@ -69,13 +73,15 @@ const storyApi = {
|
||||
kind
|
||||
name
|
||||
}
|
||||
listed
|
||||
open
|
||||
createdDate
|
||||
fictionalDate
|
||||
updatedDate
|
||||
}
|
||||
}
|
||||
`, {filter}).then(({stories}) => {
|
||||
return stories.map(d => new Story(d.id, d.name, d.author, d.category, d.createdDate, d.updatedDate, d.fictionalDate, d.tags))
|
||||
return stories.map(d => new Story(d.id, d.name, d.author, d.category, d.listed, d.open, d.createdDate, d.updatedDate, d.fictionalDate, d.tags))
|
||||
})
|
||||
},
|
||||
|
||||
@@ -95,6 +101,8 @@ const storyApi = {
|
||||
kind
|
||||
name
|
||||
}
|
||||
listed
|
||||
open
|
||||
createdDate
|
||||
fictionalDate
|
||||
updatedDate
|
||||
@@ -112,6 +120,7 @@ const storyApi = {
|
||||
`, {id}).then(({story}) => {
|
||||
return new Story(
|
||||
story.id, story.name, story.author, story.category,
|
||||
story.listed, story.open,
|
||||
story.createdDate, story.updatedDate, story.fictionalDate,
|
||||
story.tags, story.chapters
|
||||
)
|
||||
@@ -185,6 +194,30 @@ const storyApi = {
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Call `editStory(input)` mutation, returns the changable fields.
|
||||
*
|
||||
* @param {{id:string, name:string, category:string, author:string, open:boolean, listed:boolean, fictionalDate:Date, clearFictionalDate:boolean}} input
|
||||
* @returns {Promise<{id:string, name:string, category:string, author:string, open:boolean, listed:boolean, fictionalDate:Date}>}
|
||||
*/
|
||||
edit(input) {
|
||||
return query(`
|
||||
mutation EditStory($input: StoryEditInput!) {
|
||||
editStory(input:$input) {
|
||||
id
|
||||
name
|
||||
category
|
||||
author
|
||||
open
|
||||
listed
|
||||
fictionalDate
|
||||
}
|
||||
}
|
||||
`, {input}, {permissions: ["member", "story.edit"]}).then(({editStory}) => {
|
||||
return editStory
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Call `removeStory(input)` mutation, returns the id.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user