Add categories page
This commit is contained in:
parent
5a4f44b4ee
commit
9d19d70558
6 changed files with 187 additions and 41 deletions
|
|
@ -22,6 +22,7 @@ table th {
|
|||
// Custom styles.
|
||||
// - Navigation bar.
|
||||
$navbar-height: 60px;
|
||||
$default-category-color: #98c766;
|
||||
$logo-height: $navbar-height - 20px;
|
||||
|
||||
.navbar-button {
|
||||
|
|
@ -166,6 +167,33 @@ $logo-height: $navbar-height - 20px;
|
|||
}
|
||||
}
|
||||
|
||||
.thread-list {
|
||||
@extend .container;
|
||||
@extend .grid-xl;
|
||||
}
|
||||
|
||||
.category-list {
|
||||
@extend .thread-list;
|
||||
|
||||
|
||||
.category-title {
|
||||
@extend .thread-title;
|
||||
a, a:hover {
|
||||
color: lighten($body-font-color, 10%);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.category-description {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
#categories-list .category {
|
||||
border-left: 6px solid;
|
||||
border-left-color: $default-category-color;
|
||||
}
|
||||
|
||||
$super-popular-color: #f86713;
|
||||
$popular-color: darken($super-popular-color, 25%);
|
||||
$threads-meta-color: #545d70;
|
||||
|
|
@ -217,7 +245,7 @@ $threads-meta-color: #545d70;
|
|||
.category-color {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 0.3rem solid #98c766;
|
||||
border: 0.3rem solid $default-category-color;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
|
@ -726,8 +754,3 @@ hr {
|
|||
margin-top: $control-padding-y*2;
|
||||
}
|
||||
}
|
||||
|
||||
// - Hide features that have not been implemented yet.
|
||||
#main-buttons > section.navbar-section:nth-child(1) {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -806,14 +806,28 @@ routes:
|
|||
var
|
||||
start = getInt(@"start", 0)
|
||||
count = getInt(@"count", 30)
|
||||
categoryId = getInt(@"categoryId", -1)
|
||||
|
||||
var
|
||||
categorySection = ""
|
||||
categoryArgs: seq[string] = @[$start, $count]
|
||||
countQuery = sql"select count(*) from thread;"
|
||||
countArgs: seq[string] = @[]
|
||||
|
||||
|
||||
if categoryId != -1:
|
||||
categorySection = "c.id == ? and "
|
||||
countQuery = sql"select count(*) from thread t, category c where category == c.id and c.id == ?;"
|
||||
countArgs.add($categoryId)
|
||||
categoryArgs.insert($categoryId, 0)
|
||||
|
||||
const threadsQuery =
|
||||
sql"""select t.id, t.name, views, strftime('%s', modified), isLocked,
|
||||
"""select t.id, t.name, views, strftime('%s', modified), isLocked,
|
||||
c.id, c.name, c.description, c.color,
|
||||
u.name, u.email, strftime('%s', u.lastOnline),
|
||||
strftime('%s', u.previousVisitAt), u.status, u.isDeleted
|
||||
from thread t, category c, person u
|
||||
where t.isDeleted = 0 and category = c.id and
|
||||
where t.isDeleted = 0 and category = c.id and $#
|
||||
u.status <> 'Spammer' and u.status <> 'Troll' and
|
||||
u.id in (
|
||||
select u.id from post p, person u
|
||||
|
|
@ -823,11 +837,11 @@ routes:
|
|||
)
|
||||
order by modified desc limit ?, ?;"""
|
||||
|
||||
let thrCount = getValue(db, sql"select count(*) from thread;").parseInt()
|
||||
let thrCount = getValue(db, countQuery, countArgs).parseInt()
|
||||
let moreCount = max(0, thrCount - (start + count))
|
||||
|
||||
var list = ThreadList(threads: @[], moreCount: moreCount)
|
||||
for data in getAllRows(db, threadsQuery, start, count):
|
||||
for data in getAllRows(db, sql(threadsQuery % categorySection), categoryArgs):
|
||||
let thread = selectThread(data[0 .. 8], selectUser(data[9 .. ^1]))
|
||||
list.threads.add(thread)
|
||||
|
||||
|
|
|
|||
83
src/frontend/categorylist.nim
Normal file
83
src/frontend/categorylist.nim
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import strformat, times, options, json, httpcore, sugar, strutils, uri
|
||||
|
||||
import category
|
||||
|
||||
when defined(js):
|
||||
include karax/prelude
|
||||
import karax / [vstyles, kajax, kdom]
|
||||
|
||||
import karaxutils, error, user, mainbuttons
|
||||
|
||||
type
|
||||
State = ref object
|
||||
list: Option[CategoryList]
|
||||
loading: bool
|
||||
status: HttpCode
|
||||
|
||||
proc newState(): State =
|
||||
State(
|
||||
list: none[CategoryList](),
|
||||
loading: false,
|
||||
status: Http200
|
||||
)
|
||||
var
|
||||
state = newState()
|
||||
|
||||
proc genCategory(category: Category, noBorder = false): VNode =
|
||||
result = buildHtml():
|
||||
tr(class=class({"no-border": noBorder})):
|
||||
td(style=style((StyleAttr.borderLeftColor, kstring("#" & category.color))), class="category"):
|
||||
h4(class="category-title"):
|
||||
a(href=makeUri("/c/" & $category.id)):
|
||||
tdiv():
|
||||
tdiv(class="category-name"):
|
||||
text category.name
|
||||
tdiv(class="category-description"):
|
||||
text category.description
|
||||
td(class="topics"):
|
||||
text "Topics"
|
||||
|
||||
proc onCategoriesRetrieved(httpStatus: int, response: kstring) =
|
||||
state.loading = false
|
||||
state.status = httpStatus.HttpCode
|
||||
if state.status != Http200: return
|
||||
|
||||
let parsed = parseJson($response)
|
||||
let list = to(parsed, CategoryList)
|
||||
|
||||
if state.list.isSome:
|
||||
state.list.get().categories.add(list.categories)
|
||||
else:
|
||||
state.list = some(list)
|
||||
|
||||
proc renderCategories(): VNode =
|
||||
if state.status != Http200:
|
||||
return renderError("Couldn't retrieve threads.", state.status)
|
||||
|
||||
if state.list.isNone:
|
||||
if not state.loading:
|
||||
state.loading = true
|
||||
ajaxGet(makeUri("categories.json"), @[], onCategoriesRetrieved)
|
||||
|
||||
return buildHtml(tdiv(class="loading loading-lg"))
|
||||
|
||||
let list = state.list.get()
|
||||
|
||||
return buildHtml():
|
||||
section(class="category-list"):
|
||||
table(id="categories-list", class="table"):
|
||||
thead():
|
||||
tr:
|
||||
th(text "Category")
|
||||
th(text "Topics")
|
||||
tbody():
|
||||
for i in 0 ..< list.categories.len:
|
||||
let category = list.categories[i]
|
||||
|
||||
let isLastCategory = i+1 == list.categories.len
|
||||
genCategory(category, noBorder=isLastCategory)
|
||||
|
||||
proc renderCategoryList*(currentUser: Option[User]): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
renderMainButtons(currentUser)
|
||||
renderCategories()
|
||||
|
|
@ -5,6 +5,7 @@ include karax/prelude
|
|||
import jester/[patterns]
|
||||
|
||||
import threadlist, postlist, header, profile, newthread, error, about
|
||||
import categorylist
|
||||
import resetpassword, activateemail, search
|
||||
import karaxutils
|
||||
|
||||
|
|
@ -81,6 +82,14 @@ proc render(): VNode =
|
|||
result = buildHtml(tdiv()):
|
||||
renderHeader()
|
||||
route([
|
||||
r("/categories",
|
||||
(params: Params) =>
|
||||
(renderCategoryList(getLoggedInUser()))
|
||||
),
|
||||
r("/c/@id",
|
||||
(params: Params) =>
|
||||
(renderThreadList(getLoggedInUser(), params["id"].parseInt))
|
||||
),
|
||||
r("/newthread",
|
||||
(params: Params) =>
|
||||
(render(state.newThread, getLoggedInUser()))
|
||||
|
|
|
|||
37
src/frontend/mainbuttons.nim
Normal file
37
src/frontend/mainbuttons.nim
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import options
|
||||
import user
|
||||
|
||||
when defined(js):
|
||||
include karax/prelude
|
||||
import karax / [vstyles, kajax, kdom]
|
||||
|
||||
import karaxutils, error, user
|
||||
|
||||
let buttons = [
|
||||
(name: "Latest", url: makeUri("/"), id: "latest-btn"),
|
||||
(name: "Categories", url: makeUri("/categories"), id: "categories-btn"),
|
||||
]
|
||||
|
||||
proc renderMainButtons*(currentUser: Option[User]): VNode =
|
||||
result = buildHtml():
|
||||
section(class="navbar container grid-xl", id="main-buttons"):
|
||||
section(class="navbar-section"):
|
||||
#[tdiv(class="dropdown"):
|
||||
a(href="#", class="btn dropdown-toggle"):
|
||||
text "Filter "
|
||||
italic(class="fas fa-caret-down")
|
||||
ul(class="menu"):
|
||||
li: text "community"
|
||||
li: text "dev" ]#
|
||||
|
||||
for btn in buttons:
|
||||
let active = btn.url == window.location.href
|
||||
a(id=btn.id, href=btn.url):
|
||||
button(class=class({"btn-primary": active, "btn-link": not active}, "btn")):
|
||||
text btn.name
|
||||
section(class="navbar-section"):
|
||||
if currentUser.isSome():
|
||||
a(id="new-thread-btn", href=makeUri("/newthread"), onClick=anchorCB):
|
||||
button(class="btn btn-secondary"):
|
||||
italic(class="fas fa-plus")
|
||||
text " New Thread"
|
||||
|
|
@ -29,7 +29,7 @@ when defined(js):
|
|||
include karax/prelude
|
||||
import karax / [vstyles, kajax, kdom]
|
||||
|
||||
import karaxutils, error, user
|
||||
import karaxutils, error, user, mainbuttons
|
||||
|
||||
type
|
||||
State = ref object
|
||||
|
|
@ -65,27 +65,6 @@ when defined(js):
|
|||
|
||||
return true
|
||||
|
||||
proc genTopButtons(currentUser: Option[User]): VNode =
|
||||
result = buildHtml():
|
||||
section(class="navbar container grid-xl", id="main-buttons"):
|
||||
section(class="navbar-section"):
|
||||
tdiv(class="dropdown"):
|
||||
a(href="#", class="btn dropdown-toggle"):
|
||||
text "Filter "
|
||||
italic(class="fas fa-caret-down")
|
||||
ul(class="menu"):
|
||||
li: text "community"
|
||||
li: text "dev"
|
||||
button(class="btn btn-primary"): text "Latest"
|
||||
button(class="btn btn-link"): text "Most Active"
|
||||
button(class="btn btn-link"): text "Categories"
|
||||
section(class="navbar-section"):
|
||||
if currentUser.isSome():
|
||||
a(id="new-thread-btn", href=makeUri("/newthread"), onClick=anchorCB):
|
||||
button(class="btn btn-secondary"):
|
||||
italic(class="fas fa-plus")
|
||||
text " New Thread"
|
||||
|
||||
proc genUserAvatars(users: seq[User]): VNode =
|
||||
result = buildHtml(td):
|
||||
for user in users:
|
||||
|
|
@ -168,10 +147,10 @@ when defined(js):
|
|||
else:
|
||||
state.list = some(list)
|
||||
|
||||
proc onLoadMore(ev: Event, n: VNode) =
|
||||
proc onLoadMore(ev: Event, n: VNode, categoryId: int) =
|
||||
state.loading = true
|
||||
let start = state.list.get().threads.len
|
||||
ajaxGet(makeUri("threads.json?start=" & $start), @[], onThreadList)
|
||||
ajaxGet(makeUri("threads.json?start=" & $start & "&categoryId=" & $categoryId), @[], onThreadList)
|
||||
|
||||
proc getInfo(
|
||||
list: seq[Thread], i: int, currentUser: Option[User]
|
||||
|
|
@ -196,20 +175,20 @@ when defined(js):
|
|||
isNew: thread.creation > previousVisitAt
|
||||
)
|
||||
|
||||
proc genThreadList(currentUser: Option[User]): VNode =
|
||||
proc genThreadList(currentUser: Option[User], categoryId: int): VNode =
|
||||
if state.status != Http200:
|
||||
return renderError("Couldn't retrieve threads.", state.status)
|
||||
|
||||
if state.list.isNone:
|
||||
if not state.loading:
|
||||
state.loading = true
|
||||
ajaxGet(makeUri("threads.json"), @[], onThreadList)
|
||||
ajaxGet(makeUri("threads.json?categoryId=" & $categoryId), @[], onThreadList)
|
||||
|
||||
return buildHtml(tdiv(class="loading loading-lg"))
|
||||
|
||||
let list = state.list.get()
|
||||
result = buildHtml():
|
||||
section(class="container grid-xl"): # TODO: Rename to `.thread-list`.
|
||||
section(class="thread-list"):
|
||||
table(class="table", id="threads-list"):
|
||||
thead():
|
||||
tr:
|
||||
|
|
@ -239,10 +218,11 @@ when defined(js):
|
|||
td(colspan="6"):
|
||||
tdiv(class="loading loading-lg")
|
||||
else:
|
||||
td(colspan="6", onClick=onLoadMore):
|
||||
td(colspan="6",
|
||||
onClick = (ev: Event, n: VNode) => (onLoadMore(ev, n, categoryId))):
|
||||
span(text "load more threads")
|
||||
|
||||
proc renderThreadList*(currentUser: Option[User]): VNode =
|
||||
proc renderThreadList*(currentUser: Option[User], categoryId = -1): VNode =
|
||||
result = buildHtml(tdiv):
|
||||
genTopButtons(currentUser)
|
||||
genThreadList(currentUser)
|
||||
renderMainButtons(currentUser)
|
||||
genThreadList(currentUser, categoryId)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue