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)
.post.mountable(id=post.ID(), data-highlight=post.Author().ID == highlightAuthorID)
component Postable(post arn.Postable, user *arn.User, highlightAuthorID string)
.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
Avatar(post.Author())
@ -9,34 +9,38 @@ component Postable(post arn.Postable, highlightAuthorID string)
.post-content
div(id="render-" + post.ID())!= post.HTML()
//- if user && user.ID === post.authorId
//- textarea.post-input.hidden(id="source-" + post.ID)= post.text
//- a.post-save.hidden(id="save-" + post.ID, onclick=`$.saveEdit("${type.toLowerCase()}", "${post.ID}")`)
//- i.fa.fa-save
//- span Save
if user != nil && user.ID == post.Author().ID
textarea.post-input.hidden(id="source-" + post.ID())= post.Text()
.buttons.hidden(id="edit-toolbar-" + post.ID())
a.button.post-save.action(data-action="savePost", data-trigger="click", data-id=post.ID())
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())
.spacer
.post-likes(id="likes-" + post.ID(), title="Likes")= len(post.Likes())
//- if user != nil
//- if user.ID !== post.authorId
//- - var liked = post.likes && post.likes.indexOf(user.ID) !== -1
if user != nil
//- if user.ID !== post.authorId
//- - 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" : ")
//- i.fa.fa-thumbs-up.fa-fw
//- 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
//- 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
//- 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
//- if type === "Posts" || type === "Threads"
//- if user.ID === post.authorId
//- a.post-tool.post-edit(onclick=`$.edit("${post.ID}")`, title="Edit")
//- i.fa.fa-pencil.fa-fw
if user.ID == post.Author().ID
a.post-tool.post-edit.action(data-action="editPost", data-trigger="click", data-id=post.ID(), title="Edit")
RawIcon("pencil")
if post.Type() != "Thread"
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)
//- 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
.posts
each post in postables
Postable(post, "")
Postable(post, user, "")

View File

@ -6,16 +6,18 @@ import (
"github.com/aerogo/aero"
"github.com/animenotifier/arn"
"github.com/animenotifier/notify.moe/components"
"github.com/animenotifier/notify.moe/utils"
)
// Get post.
func Get(ctx *aero.Context) string {
id := ctx.Get("id")
user := utils.GetUser(ctx)
post, err := arn.GetPost(id)
if err != nil {
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)
Postable(post.ToPostable(), "")
component Post(post *arn.Post, user *arn.User)
Postable(post.ToPostable(), user, "")
.side-note
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
h2.page-title= len(postables), " latest posts by ", postables[0].Author().Nick
PostableList(postables)
PostableList(postables, user)
else
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)
.posts
Postable(thread.ToPostable(), thread.Author().ID)
Postable(thread.ToPostable(), user, thread.Author().ID)
each post in posts
Postable(post.ToPostable(), thread.Author().ID)
Postable(post.ToPostable(), user, thread.Author().ID)
// Reply
if user != nil

View File

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

View File

@ -1,6 +1,7 @@
import { Application } from "./Application"
import { AnimeNotifier } from "./AnimeNotifier"
import { Diff } from "./Diff"
import { findAll } from "./Utils"
// Save new data from an input field
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
}
// 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) {
input.contentEditable = "false"
} else {
input.disabled = true
}
fetch(apiObject.dataset.api, {
let apiEndpoint = arn.findAPIEndpoint(input)
fetch(apiEndpoint, {
method: "POST",
body: JSON.stringify(obj),
credentials: "same-origin"
@ -82,40 +69,46 @@ export function soon() {
export function diff(arn: AnimeNotifier, element: HTMLElement) {
let url = element.dataset.url || (element as HTMLAnchorElement).getAttribute("href")
arn.diff(url).then(() => {
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
arn.diff(url).then(() => arn.scrollTo(element))
}
let scroll = () => {
let time = Date.now()
let progress = (time - timeStart) / duration
// Edit post
export function editPost(arn: AnimeNotifier, element: HTMLElement) {
let postId = element.dataset.id
if(progress > 1.0) {
progress = 1.0
}
let render = arn.app.find("render-" + postId)
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)
arn.app.content.parentElement.scrollTop = newScroll
if(!render.classList.contains("hidden")) {
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) {
window.requestAnimationFrame(scroll)
}
}
// Save post
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
@ -129,7 +122,7 @@ export function forumReply(arn: AnimeNotifier) {
tags: []
}
arn.post("/api/post/new", post)
arn.post("/api/new/post", post)
.then(() => arn.reloadContent())
.then(() => textarea.value = "")
.catch(console.error)
@ -147,7 +140,7 @@ export function createThread(arn: AnimeNotifier) {
tags: [category.value]
}
arn.post("/api/thread/new", thread)
arn.post("/api/new/thread", thread)
.then(() => arn.app.load("/forum/" + thread.tags[0]))
.catch(console.error)
}
@ -168,7 +161,7 @@ export function createSoundTrack(arn: AnimeNotifier, button: HTMLButtonElement)
button.innerText = "Adding..."
button.disabled = true
arn.post("/api/soundtrack/new", soundtrack)
arn.post("/api/new/soundtrack", soundtrack)
.then(() => arn.app.load("/music"))
.catch(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) {
if(e.state) {
this.app.load(e.state, {

View File

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

View File

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

View File

@ -1,6 +1,5 @@
mixin input-focus
:focus
color black
border 1px solid input-focus-border-color
// TODO: Replace with alpha(main-color, 20%) function
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{
creatable,
updatable,
},
"Post": []reflect.Type{
creatable,
updatable,
},
"SoundTrack": []reflect.Type{
creatable,