Added forum post editing

This commit is contained in:
Eduard Urbach 2017-07-06 16:54:10 +02:00
parent 9de86a83f9
commit 5070600964
13 changed files with 139 additions and 78 deletions

View File

@ -1,5 +1,5 @@
component Postable(post arn.Postable, highlightAuthorID string) component Postable(post arn.Postable, user *arn.User, highlightAuthorID string)
.post.mountable(id=post.ID(), data-highlight=post.Author().ID == highlightAuthorID) .post.mountable(id=strings.ToLower(post.Type()) + "-" + toString(post.ID()), data-highlight=post.Author().ID == highlightAuthorID, data-api="/api/" + strings.ToLower(post.Type()) + "/" + post.ID())
.post-author .post-author
Avatar(post.Author()) Avatar(post.Author())
@ -9,34 +9,38 @@ component Postable(post arn.Postable, highlightAuthorID string)
.post-content .post-content
div(id="render-" + post.ID())!= post.HTML() div(id="render-" + post.ID())!= post.HTML()
//- if user && user.ID === post.authorId if user != nil && user.ID == post.Author().ID
//- textarea.post-input.hidden(id="source-" + post.ID)= post.text textarea.post-input.hidden(id="source-" + post.ID())= post.Text()
//- a.post-save.hidden(id="save-" + post.ID, onclick=`$.saveEdit("${type.toLowerCase()}", "${post.ID}")`) .buttons.hidden(id="edit-toolbar-" + post.ID())
//- i.fa.fa-save a.button.post-save.action(data-action="savePost", data-trigger="click", data-id=post.ID())
//- span Save Icon("save")
span Save
a.button.post-cancel-edit.action(data-action="editPost", data-trigger="click", data-id=post.ID())
Icon("close")
span Cancel
.post-toolbar(id="toolbar-" + post.ID()) .post-toolbar(id="toolbar-" + post.ID())
.spacer .spacer
.post-likes(id="likes-" + post.ID(), title="Likes")= len(post.Likes()) .post-likes(id="likes-" + post.ID(), title="Likes")= len(post.Likes())
//- if user != nil if user != nil
//- if user.ID !== post.authorId //- if user.ID !== post.authorId
//- - var liked = post.likes && post.likes.indexOf(user.ID) !== -1 //- - var liked = post.likes && post.likes.indexOf(user.ID) !== -1
//- a.post-tool.post-like(id="like-" + post.ID, onclick=`$.like("${type.toLowerCase()}", "${post.ID}")`, title="Like", class=liked ? "hidden" : ") //- a.post-tool.post-like(id="like-" + post.ID, onclick=`$.like("${type.toLowerCase()}", "${post.ID}")`, title="Like", class=liked ? "hidden" : ")
//- i.fa.fa-thumbs-up.fa-fw //- i.fa.fa-thumbs-up.fa-fw
//- a.post-tool.post-unlike(id="unlike-" + post.ID, onclick=`$.unlike("${type.toLowerCase()}", "${post.ID}")`, title="Unlike", class=!liked ? "hidden" : ") //- a.post-tool.post-unlike(id="unlike-" + post.ID, onclick=`$.unlike("${type.toLowerCase()}", "${post.ID}")`, title="Unlike", class=!liked ? "hidden" : ")
//- i.fa.fa-thumbs-down.fa-fw //- i.fa.fa-thumbs-down.fa-fw
//- if type === "Posts" || type === "Threads" if user.ID == post.Author().ID
//- if user.ID === post.authorId a.post-tool.post-edit.action(data-action="editPost", data-trigger="click", data-id=post.ID(), title="Edit")
//- a.post-tool.post-edit(onclick=`$.edit("${post.ID}")`, title="Edit") RawIcon("pencil")
//- i.fa.fa-pencil.fa-fw
if post.Type() != "Thread" if post.Type() != "Thread"
a.post-tool.post-permalink.ajax(href=post.Link(), title="Permalink") a.post-tool.post-permalink.ajax(href=post.Link(), title="Permalink")
Icon("link") RawIcon("link")
//- if type === "Messages" && user && (user.ID === post.authorId || user.ID === post.recipientId) //- if type === "Messages" && user && (user.ID === post.authorId || user.ID === post.recipientId)
//- a.post-tool.post-delete(onclick=`if(confirm("Do you really want to delete this ${typeSingular.toLowerCase()} from ${post.author.nick}?")) $.delete${typeSingular}("${post.ID}")`, title="Delete") //- a.post-tool.post-delete(onclick=`if(confirm("Do you really want to delete this ${typeSingular.toLowerCase()} from ${post.author.nick}?")) $.delete${typeSingular}("${post.ID}")`, title="Delete")

View File

@ -1,5 +1,5 @@
component PostableList(postables []arn.Postable) component PostableList(postables []arn.Postable, user *arn.User)
.thread .thread
.posts .posts
each post in postables each post in postables
Postable(post, "") Postable(post, user, "")

View File

@ -6,16 +6,18 @@ import (
"github.com/aerogo/aero" "github.com/aerogo/aero"
"github.com/animenotifier/arn" "github.com/animenotifier/arn"
"github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/components"
"github.com/animenotifier/notify.moe/utils"
) )
// Get post. // Get post.
func Get(ctx *aero.Context) string { func Get(ctx *aero.Context) string {
id := ctx.Get("id") id := ctx.Get("id")
user := utils.GetUser(ctx)
post, err := arn.GetPost(id) post, err := arn.GetPost(id)
if err != nil { if err != nil {
return ctx.Error(http.StatusNotFound, "Post not found", err) return ctx.Error(http.StatusNotFound, "Post not found", err)
} }
return ctx.HTML(components.Post(post)) return ctx.HTML(components.Post(post, user))
} }

View File

@ -1,5 +1,5 @@
component Post(post *arn.Post) component Post(post *arn.Post, user *arn.User)
Postable(post.ToPostable(), "") Postable(post.ToPostable(), user, "")
.side-note .side-note
a.ajax(href=post.Thread().Link())= post.Thread().Title a.ajax(href=post.Thread().Link())= post.Thread().Title

View File

@ -3,6 +3,6 @@ component LatestPosts(postables []arn.Postable, viewUser *arn.User, user *arn.Us
if len(postables) > 0 if len(postables) > 0
h2.page-title= len(postables), " latest posts by ", postables[0].Author().Nick h2.page-title= len(postables), " latest posts by ", postables[0].Author().Nick
PostableList(postables) PostableList(postables, user)
else else
p.no-data.mountable= viewUser.Nick + " hasn't written any posts yet." p.no-data.mountable= viewUser.Nick + " hasn't written any posts yet."

View File

@ -3,10 +3,10 @@ component Thread(thread *arn.Thread, posts []*arn.Post, user *arn.User)
#thread.thread(data-id=thread.ID) #thread.thread(data-id=thread.ID)
.posts .posts
Postable(thread.ToPostable(), thread.Author().ID) Postable(thread.ToPostable(), user, thread.Author().ID)
each post in posts each post in posts
Postable(post.ToPostable(), thread.Author().ID) Postable(post.ToPostable(), user, thread.Author().ID)
// Reply // Reply
if user != nil if user != nil

View File

@ -43,7 +43,7 @@ func init() {
} }
if requestURI == "/dark-flame-master" { if requestURI == "/dark-flame-master" {
ctx.SetURI("/api/analytics/new") ctx.SetURI("/api/new/analytics")
return return
} }
}) })

View File

@ -1,6 +1,7 @@
import { Application } from "./Application" import { Application } from "./Application"
import { AnimeNotifier } from "./AnimeNotifier" import { AnimeNotifier } from "./AnimeNotifier"
import { Diff } from "./Diff" import { Diff } from "./Diff"
import { findAll } from "./Utils"
// Save new data from an input field // Save new data from an input field
export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaElement) { export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaElement) {
@ -20,29 +21,15 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE
obj[input.dataset.field] = value obj[input.dataset.field] = value
} }
// console.log(input.type, input.dataset.api, obj, JSON.stringify(obj))
let apiObject: HTMLElement
let parent = input as HTMLElement
while(parent = parent.parentElement) {
if(parent.dataset.api !== undefined) {
apiObject = parent
break
}
}
if(!apiObject) {
throw "API object not found"
}
if(isContentEditable) { if(isContentEditable) {
input.contentEditable = "false" input.contentEditable = "false"
} else { } else {
input.disabled = true input.disabled = true
} }
fetch(apiObject.dataset.api, { let apiEndpoint = arn.findAPIEndpoint(input)
fetch(apiEndpoint, {
method: "POST", method: "POST",
body: JSON.stringify(obj), body: JSON.stringify(obj),
credentials: "same-origin" credentials: "same-origin"
@ -82,40 +69,46 @@ export function soon() {
export function diff(arn: AnimeNotifier, element: HTMLElement) { export function diff(arn: AnimeNotifier, element: HTMLElement) {
let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href") let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href")
arn.diff(url).then(() => { arn.diff(url).then(() => arn.scrollTo(element))
const duration = 250.0 }
const steps = 60
const interval = duration / steps
const fullSin = Math.PI / 2
const contentPadding = 24
let target = element
let scrollHandle: number
let oldScroll = arn.app.content.parentElement.scrollTop
let newScroll = 0
let finalScroll = Math.max(target.offsetTop - contentPadding, 0)
let scrollDistance = finalScroll - oldScroll
let timeStart = Date.now()
let timeEnd = timeStart + duration
let scroll = () => { // Edit post
let time = Date.now() export function editPost(arn: AnimeNotifier, element: HTMLElement) {
let progress = (time - timeStart) / duration let postId = element.dataset.id
if(progress > 1.0) { let render = arn.app.find("render-" + postId)
progress = 1.0 let toolbar = arn.app.find("toolbar-" + postId)
} let source = arn.app.find("source-" + postId)
let edit = arn.app.find("edit-toolbar-" + postId)
newScroll = oldScroll + scrollDistance * Math.sin(progress * fullSin) if(!render.classList.contains("hidden")) {
arn.app.content.parentElement.scrollTop = newScroll render.classList.add("hidden")
toolbar.classList.add("hidden")
source.classList.remove("hidden")
edit.classList.remove("hidden")
} else {
render.classList.remove("hidden")
toolbar.classList.remove("hidden")
source.classList.add("hidden")
edit.classList.add("hidden")
}
}
if(time < timeEnd && newScroll != finalScroll) { // Save post
window.requestAnimationFrame(scroll) export function savePost(arn: AnimeNotifier, element: HTMLElement) {
} let postId = element.dataset.id
} let source = arn.app.find("source-" + postId) as HTMLTextAreaElement
let text = source.value
window.requestAnimationFrame(scroll) let updates = {
}) Text: text,
}
let apiEndpoint = arn.findAPIEndpoint(element)
arn.post(apiEndpoint, updates)
.then(() => arn.reloadContent())
.catch(console.error)
} }
// Forum reply // Forum reply
@ -129,7 +122,7 @@ export function forumReply(arn: AnimeNotifier) {
tags: [] tags: []
} }
arn.post("/api/post/new", post) arn.post("/api/new/post", post)
.then(() => arn.reloadContent()) .then(() => arn.reloadContent())
.then(() => textarea.value = "") .then(() => textarea.value = "")
.catch(console.error) .catch(console.error)
@ -147,7 +140,7 @@ export function createThread(arn: AnimeNotifier) {
tags: [category.value] tags: [category.value]
} }
arn.post("/api/thread/new", thread) arn.post("/api/new/thread", thread)
.then(() => arn.app.load("/forum/" + thread.tags[0])) .then(() => arn.app.load("/forum/" + thread.tags[0]))
.catch(console.error) .catch(console.error)
} }
@ -168,7 +161,7 @@ export function createSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement)
button.innerText = "Adding..." button.innerText = "Adding..."
button.disabled = true button.disabled = true
arn.post("/api/soundtrack/new", soundtrack) arn.post("/api/new/soundtrack", soundtrack)
.then(() => arn.app.load("/music")) .then(() => arn.app.load("/music"))
.catch(err => { .catch(err => {
console.error(err) console.error(err)

View File

@ -321,6 +321,58 @@ export class AnimeNotifier {
}) })
} }
scrollTo(target: HTMLElement) {
const duration = 250.0
const steps = 60
const interval = duration / steps
const fullSin = Math.PI / 2
const contentPadding = 24
let scrollHandle: number
let oldScroll = this.app.content.parentElement.scrollTop
let newScroll = 0
let finalScroll = Math.max(target.offsetTop - contentPadding, 0)
let scrollDistance = finalScroll - oldScroll
let timeStart = Date.now()
let timeEnd = timeStart + duration
let scroll = () => {
let time = Date.now()
let progress = (time - timeStart) / duration
if(progress > 1.0) {
progress = 1.0
}
newScroll = oldScroll + scrollDistance * Math.sin(progress * fullSin)
this.app.content.parentElement.scrollTop = newScroll
if(time < timeEnd && newScroll != finalScroll) {
window.requestAnimationFrame(scroll)
}
}
window.requestAnimationFrame(scroll)
}
findAPIEndpoint(element: HTMLElement) {
let apiObject: HTMLElement
let parent = element
while(parent = parent.parentElement) {
if(parent.dataset.api !== undefined) {
apiObject = parent
break
}
}
if(!apiObject) {
throw "API object not found"
}
return apiObject.dataset.api
}
onPopState(e: PopStateEvent) { onPopState(e: PopStateEvent) {
if(e.state) { if(e.state) {
this.app.load(e.state, { this.app.load(e.state, {

View File

@ -32,5 +32,8 @@ a
img img
backface-visibility hidden backface-visibility hidden
.hidden
display none !important
.spacer .spacer
flex 1 flex 1

View File

@ -105,6 +105,12 @@
.post-unlike .post-unlike
color rgb(255, 32, 12) !important color rgb(255, 32, 12) !important
.post-save
//
.post-input
min-height 200px
// Old // Old
// #posts // #posts

View File

@ -1,6 +1,5 @@
mixin input-focus mixin input-focus
:focus :focus
color black
border 1px solid input-focus-border-color border 1px solid input-focus-border-color
// TODO: Replace with alpha(main-color, 20%) function // TODO: Replace with alpha(main-color, 20%) function
box-shadow 0 0 6px rgba(248, 165, 130, 0.2) box-shadow 0 0 6px rgba(248, 165, 130, 0.2)

View File

@ -201,9 +201,11 @@ var interfaceImplementations = map[string][]reflect.Type{
}, },
"Thread": []reflect.Type{ "Thread": []reflect.Type{
creatable, creatable,
updatable,
}, },
"Post": []reflect.Type{ "Post": []reflect.Type{
creatable, creatable,
updatable,
}, },
"SoundTrack": []reflect.Type{ "SoundTrack": []reflect.Type{
creatable, creatable,