Implements thread list in Karax and backend.
This commit is contained in:
parent
2efd694660
commit
8910e55ad1
7 changed files with 276 additions and 57 deletions
66
forms.tmpl
66
forms.tmpl
|
|
@ -1,6 +1,6 @@
|
|||
#? stdtmpl | standard
|
||||
#
|
||||
#template `%`(idx: untyped): untyped =
|
||||
#template `!`(idx: untyped): untyped =
|
||||
# row[idx]
|
||||
#end template
|
||||
#
|
||||
|
|
@ -47,15 +47,15 @@
|
|||
<div>
|
||||
<div class="topic">
|
||||
<div>
|
||||
<a href="${c.genThreadUrl(threadid = %threadid)}"
|
||||
title="${xmlEncode(%name)}">${xmlEncode(%name)}</a>
|
||||
${genPagenumLocalNav(c, (%threadid).parseInt)}
|
||||
<a href="${c.genThreadUrl(threadid = !threadid)}"
|
||||
title="${xmlEncode(!name)}">${xmlEncode(!name)}</a>
|
||||
${genPagenumLocalNav(c, (!threadid).parseInt)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
#let users = getAllRows(db,
|
||||
# sql("select distinct name, email from person where id in " &
|
||||
# "(select author from post where thread = ?)"), %threadId)
|
||||
# "(select author from post where thread = ?)"), !threadId)
|
||||
<div class="users">
|
||||
<div>
|
||||
#for i in 0 .. min(6, users.len-1):
|
||||
|
|
@ -66,19 +66,19 @@
|
|||
|
||||
#let latestReplyAuthor = getValue(db, sql("select name from person where id = " &
|
||||
# "(select author from post where id = " &
|
||||
# "(select max(id) from post where thread = ?))"), %threadId)
|
||||
# "(select max(id) from post where thread = ?))"), !threadId)
|
||||
|
||||
#let replyProfileUrl = c.req.makeUri("profile/", false) &
|
||||
# xmlEncode(latestReplyAuthor)
|
||||
|
||||
# let posts = getValue(db, sql"select count(*) from post where thread = ?", %threadId)
|
||||
# let posts = getValue(db, sql"select count(*) from post where thread = ?", !threadId)
|
||||
<div class="detail">
|
||||
<div><div title="Views">${xmlEncode(%views)}</div></div>
|
||||
<div><div title="Views">${xmlEncode(!views)}</div></div>
|
||||
<div><div title="Posts">$posts</div></div>
|
||||
</div>
|
||||
|
||||
#let latestReplyDate = getValue(db, sql("SELECT strftime('%s', " &
|
||||
# "(select creation from post where id = (select max(id) from post where thread = ?)))"), %threadId)
|
||||
# "(select creation from post where id = (select max(id) from post where thread = ?)))"), !threadId)
|
||||
#let timeStr = formatTimestamp(latestReplyDate.parseInt())
|
||||
<div class="activity">
|
||||
<div>
|
||||
|
|
@ -150,28 +150,28 @@
|
|||
<div id="talk-thread">
|
||||
# for row in posts:
|
||||
# inc(count)
|
||||
<a name="${%postId}"></a>
|
||||
<div id="${%postId}">
|
||||
<a name="${!postId}"></a>
|
||||
<div id="${!postId}">
|
||||
<div class="author">
|
||||
<div>
|
||||
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName)
|
||||
<div class="avatar">${genGravatar(%userEmail)}</div>
|
||||
<a class="name" href="$profileUrl">${xmlEncode(%userName)}</a>
|
||||
#if c.userId == %postAuthor and c.currentPost.subject.len == 0:
|
||||
<hr/><a href="${c.genThreadUrl(%postId, "edit")}">Edit post</a>
|
||||
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(!userName)
|
||||
<div class="avatar">${genGravatar(!userEmail)}</div>
|
||||
<a class="name" href="$profileUrl">${xmlEncode(!userName)}</a>
|
||||
#if c.userId == !postAuthor and c.currentPost.subject.len == 0:
|
||||
<hr/><a href="${c.genThreadUrl(!postId, "edit")}">Edit post</a>
|
||||
#elif c.rank >= Moderator and c.currentPost.subject.len == 0:
|
||||
<hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit")}">Edit post</a>
|
||||
<hr/><a style="color: red;" href="${c.genThreadUrl(!postId, "edit")}">Edit post</a>
|
||||
#end if
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic">
|
||||
<div>
|
||||
#try:
|
||||
${(%postContent).rstToHtml}
|
||||
${(!postContent).rstToHtml}
|
||||
#except EParseError:
|
||||
# c.errorMsg = getCurrentExceptionMsg()
|
||||
#end
|
||||
<span class="date"><a href="${c.genThreadUrl(%postId, "", $threadId, $(c.pageNum))}">${xmlEncode(%postCreation)}</a></span>
|
||||
<span class="date"><a href="${c.genThreadUrl(!postId, "", $threadId, $(c.pageNum))}">${xmlEncode(!postCreation)}</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -421,43 +421,43 @@
|
|||
<div id="talk-thread" class="searchResults">
|
||||
# for row in results():
|
||||
# inc(count)
|
||||
# let isThread = %what == "0"
|
||||
# let isThread = !what == "0"
|
||||
# inc(whCount[isThread])
|
||||
# let postUrl = c.genThreadUrl(%postId,"",%threadId,"")
|
||||
# let threadUrl = c.genThreadUrl("","",%threadId)
|
||||
# let postUrl = c.genThreadUrl(!postId,"",!threadId,"")
|
||||
# let threadUrl = c.genThreadUrl("","",!threadId)
|
||||
# var headersDiffer = false
|
||||
<div>
|
||||
<div class="author">
|
||||
<div>
|
||||
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(%userName)
|
||||
<div><a href="$profileUrl">${genGravatar(%userEmail, 40)}</a></div>
|
||||
<div style="padding: 8px 0"><a href="$profileUrl">${xmlEncode(%userName)}</a></div>
|
||||
#if c.userId == %postAuthor and c.currentPost.subject.len == 0:
|
||||
<hr/><a href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a>
|
||||
#let profileUrl = c.req.makeUri("profile/", false) & xmlEncode(!userName)
|
||||
<div><a href="$profileUrl">${genGravatar(!userEmail, 40)}</a></div>
|
||||
<div style="padding: 8px 0"><a href="$profileUrl">${xmlEncode(!userName)}</a></div>
|
||||
#if c.userId == !postAuthor and c.currentPost.subject.len == 0:
|
||||
<hr/><a href="${c.genThreadUrl(!postId, "edit", !threadId)}">Edit post</a>
|
||||
#elif c.rank >= Moderator and c.currentPost.subject.len == 0:
|
||||
<hr/><a style="color: red;" href="${c.genThreadUrl(%postId, "edit", %threadId)}">Edit post</a>
|
||||
<hr/><a style="color: red;" href="${c.genThreadUrl(!postId, "edit", !threadId)}">Edit post</a>
|
||||
#end if
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic">
|
||||
<div>
|
||||
#if %postHeader != "":
|
||||
#if !postHeader != "":
|
||||
<div class="postTitle">
|
||||
<span class="titleHeader">Post:</span>
|
||||
<a href="${postUrl}">
|
||||
<span>${%postHeader}</span>
|
||||
<span>${!postHeader}</span>
|
||||
</a>
|
||||
</div>
|
||||
#end if
|
||||
#if not isThread:
|
||||
#try:
|
||||
${(%postContent).rstToHtml}
|
||||
${(!postContent).rstToHtml}
|
||||
#except EParseError:
|
||||
# c.errorMsg = getCurrentExceptionMsg()
|
||||
${xmlEncode(%postContent)}
|
||||
${xmlEncode(!postContent)}
|
||||
#end
|
||||
#end if
|
||||
<span class="date">${xmlEncode(%postCreation)}</span>
|
||||
<span class="date">${xmlEncode(!postCreation)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
64
forum.nim
64
forum.nim
|
|
@ -9,7 +9,9 @@
|
|||
import
|
||||
os, strutils, times, md5, strtabs, cgi, math, db_sqlite,
|
||||
scgi, jester, asyncdispatch, asyncnet, cache, sequtils,
|
||||
parseutils, utils, random, rst, ranks, recaptcha
|
||||
parseutils, utils, random, rst, ranks, recaptcha, json
|
||||
|
||||
import redesign/threadlist except User
|
||||
|
||||
when not defined(windows):
|
||||
import bcrypt # TODO
|
||||
|
|
@ -1024,6 +1026,66 @@ routes:
|
|||
additionalHeaders = genRSSHeaders(c), showRssLinks = true)
|
||||
resp data
|
||||
|
||||
get "/karax.html":
|
||||
resp readFile("redesign/karax.html")
|
||||
get "/nimforum.css":
|
||||
resp readFile("redesign/nimforum.css"), "text/css"
|
||||
get "/nimcache/forum.js":
|
||||
resp readFile("redesign/nimcache/forum.js"), "application/javascript"
|
||||
get "/images/crown.png":
|
||||
resp readFile("redesign/images/crown.png"), "image/png"
|
||||
|
||||
get "/threads.json":
|
||||
var
|
||||
start = 0
|
||||
count = 30
|
||||
parseInt(@"start", start, 0..1_000_000)
|
||||
parseInt(@"count", start, 0..1_000_000)
|
||||
|
||||
const threadsQuery =
|
||||
sql"""select id, name, views, strftime('%s', modified) from thread
|
||||
order by modified desc limit ?, ?;"""
|
||||
const postsQuery =
|
||||
sql"""select count(*), strftime('%s', creation) from post
|
||||
where thread = ?
|
||||
order by creation asc limit 1;"""
|
||||
const usersListQuery =
|
||||
sql"""select distinct name, email, strftime('%s',lastOnline)
|
||||
from person where id in
|
||||
(select author from post where thread = ?);"""
|
||||
|
||||
let thrCount = getValue(db, sql"select count(*) from thread;").parseInt()
|
||||
let moreCount = max(0, thrCount - (start + count))
|
||||
|
||||
var list = ThreadList(threads: @[], lastVisit: 0, moreCount: moreCount)
|
||||
for data in getAllRows(db, threadsQuery, start, count):
|
||||
let posts = getRow(db, postsQuery, data[0])
|
||||
|
||||
var thread = Thread(
|
||||
id: data[0].parseInt,
|
||||
topic: data[1],
|
||||
category: Category(id: "", color: "#ff0000"), # TODO
|
||||
users: @[],
|
||||
replies: posts[0].parseInt,
|
||||
views: data[2].parseInt,
|
||||
activity: data[3].parseInt,
|
||||
creation: posts[1].parseInt,
|
||||
isLocked: false,
|
||||
isSolved: false # TODO: ^ and this. Add a field to `post` to identify.
|
||||
)
|
||||
|
||||
# Gather the users list.
|
||||
for user in getAllRows(db, usersListQuery, thread.id):
|
||||
let isOnline = getTime().toUnix() - user[2].parseInt > (60*5)
|
||||
thread.users.add(threadlist.User(
|
||||
name: user[0],
|
||||
avatarUrl: user[1].getGravatarUrl(),
|
||||
isOnline: isOnline
|
||||
))
|
||||
list.threads.add(thread)
|
||||
|
||||
resp $(%list), "application/json"
|
||||
|
||||
get "/threadActivity.xml":
|
||||
createTFD()
|
||||
c.isThreadsList = true
|
||||
|
|
|
|||
40
main.tmpl
40
main.tmpl
|
|
@ -207,20 +207,20 @@
|
|||
<updated>${recent}</updated>
|
||||
# for row in rows(db, query, 10):
|
||||
<entry>
|
||||
<title>${xmlEncode(%name)}</title>
|
||||
<id>urn:entry:${%threadid}</id>
|
||||
# let url = c.genThreadUrl(threadid = %threadid,
|
||||
# pageNum = $(ceil(parseInt(%postCount) / PostsPerPage).int)) &
|
||||
# "#" & %postId
|
||||
<title>${xmlEncode(!name)}</title>
|
||||
<id>urn:entry:${!threadid}</id>
|
||||
# let url = c.genThreadUrl(threadid = !threadid,
|
||||
# pageNum = $(ceil(parseInt(!postCount) / PostsPerPage).int)) &
|
||||
# "#" & !postId
|
||||
<link rel="alternate" type="text/html"
|
||||
href="${c.req.makeUri(url)}"/>
|
||||
<published>${%threadDate}</published>
|
||||
<updated>${%threadDate}</updated>
|
||||
<author><name>${xmlEncode(%postAuthor)}</name></author>
|
||||
<published>${!threadDate}</published>
|
||||
<updated>${!threadDate}</updated>
|
||||
<author><name>${xmlEncode(!postAuthor)}</name></author>
|
||||
<content type="html"
|
||||
>Posts ${%postCount}, ${xmlEncode(%postAuthor)} said:
|
||||
>Posts ${!postCount}, ${xmlEncode(!postAuthor)} said:
|
||||
<p>
|
||||
${xmlEncode(rstToHtml(%postContent))}</content>
|
||||
${xmlEncode(rstToHtml(!postContent))}</content>
|
||||
</entry>
|
||||
# end for
|
||||
</feed>
|
||||
|
|
@ -256,20 +256,20 @@ ${xmlEncode(rstToHtml(%postContent))}</content>
|
|||
<updated>${recent}</updated>
|
||||
# for row in rows(db, query, 10):
|
||||
<entry>
|
||||
<title>${xmlEncode(%postHeader)}</title>
|
||||
<id>urn:entry:${%postId}</id>
|
||||
# let url = c.genThreadUrl(threadid = %postThread,
|
||||
# pageNum = $(ceil(parseInt(%postPosition) / PostsPerPage).int)) &
|
||||
# "#" & %postId
|
||||
<title>${xmlEncode(!postHeader)}</title>
|
||||
<id>urn:entry:${!postId}</id>
|
||||
# let url = c.genThreadUrl(threadid = !postThread,
|
||||
# pageNum = $(ceil(parseInt(!postPosition) / PostsPerPage).int)) &
|
||||
# "#" & !postId
|
||||
<link rel="alternate" type="text/html"
|
||||
href="${c.req.makeUri(url)}"/>
|
||||
<published>${%postRssDate}</published>
|
||||
<updated>${%postRssDate}</updated>
|
||||
<author><name>${xmlEncode(%postAuthor)}</name></author>
|
||||
<published>${!postRssDate}</published>
|
||||
<updated>${!postRssDate}</updated>
|
||||
<author><name>${xmlEncode(!postAuthor)}</name></author>
|
||||
<content type="html"
|
||||
>On ${xmlEncode(%postHumanDate)}, ${xmlEncode(%postAuthor)} said:
|
||||
>On ${xmlEncode(!postHumanDate)}, ${xmlEncode(!postAuthor)} said:
|
||||
<p>
|
||||
${xmlEncode(rstToHtml(%postContent))}</content>
|
||||
${xmlEncode(rstToHtml(!postContent))}</content>
|
||||
</entry>
|
||||
# end for
|
||||
</feed>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,21 @@
|
|||
include karax/prelude
|
||||
import strformat, times, options, json
|
||||
|
||||
include karax/prelude
|
||||
import karax / [vstyles, kajax]
|
||||
|
||||
import threadlist, karaxutils
|
||||
|
||||
type
|
||||
State = ref object
|
||||
list: Option[ThreadList]
|
||||
|
||||
proc newState(): State =
|
||||
State(
|
||||
list: none[ThreadList]()
|
||||
)
|
||||
|
||||
const
|
||||
baseUrl = "http://localhost:5000/"
|
||||
|
||||
proc genHeader(): VNode =
|
||||
result = buildHtml(header(id="main-navbar")):
|
||||
|
|
@ -34,9 +50,29 @@ proc genTopButtons(): VNode =
|
|||
section(class="navbar-section")
|
||||
|
||||
|
||||
proc createDom(): VNode =
|
||||
var state = newState()
|
||||
|
||||
proc onThreadList(httpStatus: int, response: kstring) =
|
||||
let parsed = parseJson($response)
|
||||
let list = to(parsed, ThreadList)
|
||||
|
||||
if state.list.isSome:
|
||||
state.list.get().threads.add(list.threads)
|
||||
state.list.get().moreCount = list.moreCount
|
||||
state.list.get().lastVisit = list.lastVisit
|
||||
else:
|
||||
state.list = some(list)
|
||||
|
||||
proc render(): VNode =
|
||||
if state.list.isNone:
|
||||
ajaxGet(baseUrl & "threads.json", @[], onThreadList)
|
||||
|
||||
result = buildHtml(tdiv()):
|
||||
genHeader()
|
||||
genTopButtons()
|
||||
if state.list.isNone:
|
||||
tdiv(class="loading loading-lg")
|
||||
else:
|
||||
genThreadList(state.list.get())
|
||||
|
||||
setRenderer createDom
|
||||
setRenderer render
|
||||
1
redesign/forum.nim.cfg
Normal file
1
redesign/forum.nim.cfg
Normal file
|
|
@ -0,0 +1 @@
|
|||
-d:js
|
||||
5
redesign/karaxutils.nim
Normal file
5
redesign/karaxutils.nim
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
proc class*(classes: varargs[tuple[name: string, present: bool]],
|
||||
defaultClasses: string = ""): string =
|
||||
result = defaultClasses & " "
|
||||
for class in classes:
|
||||
if class.present: result.add(class.name & " ")
|
||||
115
redesign/threadlist.nim
Normal file
115
redesign/threadlist.nim
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import strformat, times
|
||||
|
||||
type
|
||||
User* = object
|
||||
name*: string
|
||||
avatarUrl*: string
|
||||
isOnline*: bool
|
||||
|
||||
Category* = object
|
||||
id*: string
|
||||
color*: string
|
||||
|
||||
Thread* = object
|
||||
id*: int
|
||||
topic*: string
|
||||
category*: Category
|
||||
users*: seq[User]
|
||||
replies*: int
|
||||
views*: int
|
||||
activity*: int64 ## Unix timestamp
|
||||
creation*: int64 ## Unix timestamp
|
||||
isLocked*: bool
|
||||
isSolved*: bool
|
||||
|
||||
ThreadList* = ref object
|
||||
threads*: seq[Thread]
|
||||
lastVisit*: int64 ## Unix timestamp
|
||||
moreCount*: int ## How many more threads are left
|
||||
|
||||
when defined(js):
|
||||
include karax/prelude
|
||||
import karax / [vstyles]
|
||||
|
||||
import karaxutils
|
||||
|
||||
proc genUserAvatars(users: seq[User]): VNode =
|
||||
result = buildHtml(td):
|
||||
for user in users:
|
||||
figure(class="avatar avatar-sm"):
|
||||
img(src=user.avatarUrl, title=user.name)
|
||||
if user.isOnline:
|
||||
italic(class="avatar-presense online")
|
||||
|
||||
proc renderActivity(activity: int64): string =
|
||||
let currentTime = getTime()
|
||||
let activityTime = fromUnix(activity)
|
||||
let duration = currentTime - activityTime
|
||||
if duration.days > 300:
|
||||
return activityTime.local().format("MMM yyyy")
|
||||
elif duration.days > 30 and duration.days < 300:
|
||||
return activityTime.local().format("MMM dd")
|
||||
elif duration.days != 0:
|
||||
return $duration.days & "d"
|
||||
elif duration.hours != 0:
|
||||
return $duration.hours & "h"
|
||||
elif duration.minutes != 0:
|
||||
return $duration.minutes & "m"
|
||||
else:
|
||||
return $duration.seconds & "s"
|
||||
|
||||
proc genThread(thread: Thread, isNew: bool, noBorder: bool): VNode =
|
||||
result = buildHtml():
|
||||
tr(class=class({"no-border": noBorder})):
|
||||
td():
|
||||
if thread.isLocked:
|
||||
italic(class="fas fa-lock fa-xs")
|
||||
text thread.topic
|
||||
td():
|
||||
tdiv(class="triangle",
|
||||
style=style(
|
||||
(StyleAttr.borderBottom, kstring"0.6rem solid " & thread.category.color)
|
||||
)):
|
||||
text thread.category.id
|
||||
genUserAvatars(thread.users)
|
||||
td(): text $thread.replies
|
||||
td(class=class({
|
||||
"views-text": thread.views < 999,
|
||||
"popular-text": thread.views > 999 and thread.views < 5000,
|
||||
"super-popular-text": thread.views > 5000
|
||||
})):
|
||||
if thread.views > 999:
|
||||
text fmt"{thread.views/1000:.1f}"
|
||||
else:
|
||||
text $thread.views
|
||||
td(class=class({"text-success": isNew, "text-gray": not isNew})): # TODO: Colors.
|
||||
text renderActivity(thread.activity)
|
||||
|
||||
proc genThreadList*(list: ThreadList): VNode =
|
||||
result = buildHtml():
|
||||
section(class="container grid-xl"): # TODO: Rename to `.thread-list`.
|
||||
table(class="table"):
|
||||
thead():
|
||||
tr:
|
||||
th(text "Topic")
|
||||
th(text "Category")
|
||||
th(text "Users")
|
||||
th(text "Replies")
|
||||
th(text "Views")
|
||||
th(text "Activity")
|
||||
tbody():
|
||||
for i in 0 ..< list.threads.len:
|
||||
let thread = list.threads[i]
|
||||
let isLastVisit =
|
||||
i+1 < list.threads.len and list.threads[i].activity < list.lastVisit
|
||||
let isNew = thread.creation < list.lastVisit
|
||||
genThread(thread, isNew, noBorder=isLastVisit)
|
||||
if isLastVisit:
|
||||
tr(class="last-visit-separator"):
|
||||
td(colspan="6"):
|
||||
span(text "last visit")
|
||||
|
||||
if list.moreCount > 0:
|
||||
tr(class="load-more-separator"):
|
||||
td(colspan="6"):
|
||||
span(text "load more threads")
|
||||
Loading…
Add table
Add a link
Reference in a new issue