add topic/thread list view
This commit is contained in:
		@@ -12,6 +12,8 @@ local Avatars = models.Avatars
 | 
			
		||||
local Topics = models.Topics
 | 
			
		||||
local Threads = models.Threads
 | 
			
		||||
 | 
			
		||||
local THREADS_PER_PAGE = 10
 | 
			
		||||
 | 
			
		||||
local ThreadCreateError = {
 | 
			
		||||
  OK = 0,
 | 
			
		||||
  GUEST = 1,
 | 
			
		||||
@@ -53,8 +55,10 @@ app:post("topic_create", "/create", function(self)
 | 
			
		||||
    description = topic_description,
 | 
			
		||||
    slug = slug,
 | 
			
		||||
  })
 | 
			
		||||
  
 | 
			
		||||
  util.inject_infobox(self, "Topic created.")
 | 
			
		||||
 | 
			
		||||
  return {redirect_to = self:url_for("all_topics")}
 | 
			
		||||
  return {redirect_to = self:url_for("topic", {slug = topic.slug})}
 | 
			
		||||
end)
 | 
			
		||||
 | 
			
		||||
app:get("topic", "/:slug", function(self)
 | 
			
		||||
@@ -64,11 +68,51 @@ app:get("topic", "/:slug", function(self)
 | 
			
		||||
  if not topic then
 | 
			
		||||
    return {status = 404}
 | 
			
		||||
  end
 | 
			
		||||
  local threads_count = Threads:count(db.clause({
 | 
			
		||||
    topic_id = topic.id
 | 
			
		||||
  }))
 | 
			
		||||
  self.topic = topic
 | 
			
		||||
  self.threads_list = db.query("SELECT * FROM threads WHERE topic_id = ? ORDER BY is_stickied DESC, created_at DESC", topic.id)
 | 
			
		||||
 | 
			
		||||
  self.pages = math.ceil(threads_count / THREADS_PER_PAGE)
 | 
			
		||||
  self.page = math.max(1, math.min(tonumber(self.params.page) or 1, self.pages))
 | 
			
		||||
  -- self.threads_list = db.query("SELECT * FROM threads WHERE topic_id = ? ORDER BY is_stickied DESC, created_at DESC", topic.id)
 | 
			
		||||
  self.threads_list = db.query([[
 | 
			
		||||
    SELECT
 | 
			
		||||
      threads.title, threads.slug, threads.created_at, threads.is_locked, threads.is_stickied,
 | 
			
		||||
      users.username AS started_by,
 | 
			
		||||
      u.username AS latest_post_username,
 | 
			
		||||
      ph.content AS latest_post_content,
 | 
			
		||||
      posts.created_at AS latest_post_created_at,
 | 
			
		||||
      posts.id AS latest_post_id
 | 
			
		||||
    FROM
 | 
			
		||||
      threads
 | 
			
		||||
    JOIN users ON users.id = threads.user_id
 | 
			
		||||
    JOIN (
 | 
			
		||||
      SELECT
 | 
			
		||||
        posts.thread_id,
 | 
			
		||||
        posts.id,
 | 
			
		||||
        posts.user_id,
 | 
			
		||||
        posts.created_at,
 | 
			
		||||
        posts.current_revision_id,
 | 
			
		||||
        ROW_NUMBER() OVER (PARTITION BY posts.thread_id ORDER BY posts.created_at DESC) AS rn
 | 
			
		||||
      FROM
 | 
			
		||||
        posts
 | 
			
		||||
    ) posts ON posts.thread_id = threads.id AND posts.rn = 1
 | 
			
		||||
    JOIN
 | 
			
		||||
      post_history ph ON ph.id = posts.current_revision_id
 | 
			
		||||
    JOIN
 | 
			
		||||
      users u ON u.id = posts.user_id
 | 
			
		||||
    WHERE
 | 
			
		||||
      threads.topic_id = ?
 | 
			
		||||
    ORDER BY
 | 
			
		||||
      threads.is_stickied DESC,
 | 
			
		||||
      threads.created_at DESC
 | 
			
		||||
    LIMIT ? OFFSET ?
 | 
			
		||||
  ]], topic.id, THREADS_PER_PAGE, (self.page - 1) * THREADS_PER_PAGE)
 | 
			
		||||
  
 | 
			
		||||
  local user = util.get_logged_in_user_or_transient(self)
 | 
			
		||||
  print(topic.is_locked, type(topic.is_locked))
 | 
			
		||||
  self.me = user
 | 
			
		||||
  
 | 
			
		||||
  self.ThreadCreateError = ThreadCreateError
 | 
			
		||||
  self.thread_create_error = ThreadCreateError.OK
 | 
			
		||||
  if user:is_logged_in_guest() then
 | 
			
		||||
@@ -79,7 +123,7 @@ app:get("topic", "/:slug", function(self)
 | 
			
		||||
    self.thread_create_error = ThreadCreateError.TOPIC_LOCKED
 | 
			
		||||
  end
 | 
			
		||||
  
 | 
			
		||||
  self.page_title = "all threads in " .. topic.name
 | 
			
		||||
  self.page_title = "browsing topic " .. topic.name
 | 
			
		||||
  
 | 
			
		||||
  return {render = "topics.topic"}
 | 
			
		||||
end)
 | 
			
		||||
 
 | 
			
		||||
@@ -86,6 +86,8 @@ body {
 | 
			
		||||
 | 
			
		||||
.thread-title {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  font-size: 1.5rem;
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post {
 | 
			
		||||
@@ -187,6 +189,7 @@ body {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button, input[type="submit"], .linkbutton {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  @include button($button_color);
 | 
			
		||||
  
 | 
			
		||||
  &.critical {
 | 
			
		||||
@@ -205,6 +208,10 @@ input[type="file"]::file-selector-button {
 | 
			
		||||
  margin: 10px 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
p {
 | 
			
		||||
  margin: 15px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pagebutton {
 | 
			
		||||
  @include button($button_color);
 | 
			
		||||
  padding: 5px 5px;
 | 
			
		||||
@@ -277,3 +284,52 @@ input[type="text"], input[type="password"] {
 | 
			
		||||
  min-width: 60px;
 | 
			
		||||
  padding-right: 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.thread {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 96px 1.6fr 96px;
 | 
			
		||||
  grid-template-rows: 1fr;
 | 
			
		||||
  gap: 0px 0px;
 | 
			
		||||
  grid-auto-flow: row;
 | 
			
		||||
  min-height: 96px;
 | 
			
		||||
  grid-template-areas:
 | 
			
		||||
    "thread-sticky-container thread-info-container thread-locked-container";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.thread-sticky-container {
 | 
			
		||||
  grid-area: thread-sticky-container;
 | 
			
		||||
  border: 2px outset $light;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.thread-locked-container {
 | 
			
		||||
  grid-area: thread-locked-container;
 | 
			
		||||
  border: 2px outset $light;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.contain-svg {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.contain-svg > svg {
 | 
			
		||||
  height: 50%;
 | 
			
		||||
  width: 50%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.thread-info-container {
 | 
			
		||||
  grid-area: thread-info-container;
 | 
			
		||||
  background-color: $accent_color;
 | 
			
		||||
  padding: 5px 20px;
 | 
			
		||||
  border-top: 1px solid black;
 | 
			
		||||
  border-bottom: 1px solid black;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.thread-info-post-preview {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  display: inline;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -58,6 +58,8 @@ body {
 | 
			
		||||
 | 
			
		||||
.thread-title {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  font-size: 1.5rem;
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post {
 | 
			
		||||
@@ -153,6 +155,7 @@ body {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button, input[type=submit], .linkbutton {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  background-color: rgb(177, 206, 204.5);
 | 
			
		||||
}
 | 
			
		||||
button:hover, input[type=submit]:hover, .linkbutton:hover {
 | 
			
		||||
@@ -192,6 +195,10 @@ input[type=file]::file-selector-button:active {
 | 
			
		||||
  background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
p {
 | 
			
		||||
  margin: 15px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pagebutton {
 | 
			
		||||
  background-color: rgb(177, 206, 204.5);
 | 
			
		||||
  padding: 5px 5px;
 | 
			
		||||
@@ -267,3 +274,51 @@ input[type=text], input[type=password] {
 | 
			
		||||
  min-width: 60px;
 | 
			
		||||
  padding-right: 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.thread {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 96px 1.6fr 96px;
 | 
			
		||||
  grid-template-rows: 1fr;
 | 
			
		||||
  gap: 0px 0px;
 | 
			
		||||
  grid-auto-flow: row;
 | 
			
		||||
  min-height: 96px;
 | 
			
		||||
  grid-template-areas: "thread-sticky-container thread-info-container thread-locked-container";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.thread-sticky-container {
 | 
			
		||||
  grid-area: thread-sticky-container;
 | 
			
		||||
  border: 2px outset rgb(217.26, 220.38, 213.42);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.thread-locked-container {
 | 
			
		||||
  grid-area: thread-locked-container;
 | 
			
		||||
  border: 2px outset rgb(217.26, 220.38, 213.42);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.contain-svg {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.contain-svg > svg {
 | 
			
		||||
  height: 50%;
 | 
			
		||||
  width: 50%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.thread-info-container {
 | 
			
		||||
  grid-area: thread-info-container;
 | 
			
		||||
  background-color: #c1ceb1;
 | 
			
		||||
  padding: 5px 20px;
 | 
			
		||||
  border-top: 1px solid black;
 | 
			
		||||
  border-bottom: 1px solid black;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.thread-info-post-preview {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
  display: inline;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								svg-icons/sticky.etlua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								svg-icons/sticky.etlua
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
<!-- https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license -->
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
<path d="M13 20H6C4.89543 20 4 19.1046 4 18V6C4 4.89543 4.89543 4 6 4H18C19.1046 4 20 4.89543 20 6V13M13 20L20 13M13 20V14C13 13.4477 13.4477 13 14 13H20" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
 | 
			
		||||
</svg>
 | 
			
		||||
@@ -1,33 +1,69 @@
 | 
			
		||||
<h1><%= topic.name %></h1>
 | 
			
		||||
<h2><%= topic.description %></h2>
 | 
			
		||||
<% render("views.common.topnav") -%>
 | 
			
		||||
<% if infobox then %>
 | 
			
		||||
  <% render("views.common.infobox", infobox) %>
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
<nav class="darkbg">
 | 
			
		||||
  <h1 class="thread-title">All threads in "<%= topic.name %>"</h1>
 | 
			
		||||
  <span><%= topic.description %></span>
 | 
			
		||||
  <div>
 | 
			
		||||
  <% if thread_create_error == ThreadCreateError.OK then %>
 | 
			
		||||
    <a class="linkbutton" href=<%= url_for("thread_create", nil, {topic_id = topic.id}) %>>New thread</a>
 | 
			
		||||
  <% elseif thread_create_error == ThreadCreateError.GUEST then %>
 | 
			
		||||
    <p>Your account is still pending confirmation by a moderator. You are not able to create a new thread or post at this time.</p>
 | 
			
		||||
  <% elseif thread_create_error == ThreadCreateError.LOGGED_OUT then %>
 | 
			
		||||
    <p>Only logged in users can create threads. <a href="<%= url_for("user_signup") %>">Sign up</a> or <a href="<%= url_for("user_login")%>">log in</a> to create a thread.</p>
 | 
			
		||||
  <% else %>
 | 
			
		||||
    <p>This topic is locked.</p>
 | 
			
		||||
  <% end %>
 | 
			
		||||
  <% if me:is_mod() then %>
 | 
			
		||||
    <a class="linkbutton" href="<%= url_for("topic_edit", {slug = topic.slug}) %>">Edit topic</a>
 | 
			
		||||
    <form class="modform" method="post" action="<%= url_for("topic_edit", {slug = topic.slug}) %>">
 | 
			
		||||
      <input type="hidden" name="is_locked" value="<%= not ntob(topic.is_locked) %>">
 | 
			
		||||
      <input class="warn" type="submit" id="lock" value="<%= ntob(topic.is_locked) and "Unlock topic" or "Lock topic" %>">
 | 
			
		||||
    </form>
 | 
			
		||||
  <% end %>
 | 
			
		||||
  </div>
 | 
			
		||||
</nav>
 | 
			
		||||
 | 
			
		||||
<% if #threads_list == 0 then %>
 | 
			
		||||
  <p>There are no threads in this topic.</p>
 | 
			
		||||
<% else %>
 | 
			
		||||
  <ul>
 | 
			
		||||
    <% for _, thread in ipairs(threads_list) do %>
 | 
			
		||||
      <li>
 | 
			
		||||
      <a href="<%= url_for("thread", {slug = thread.slug}) %>"><%= thread.title %></a><% if ntob(thread.is_stickied) then %> - pinned<% end %>
 | 
			
		||||
      </li>
 | 
			
		||||
    <% end %>
 | 
			
		||||
  </ul>
 | 
			
		||||
  <% for _, thread in ipairs(threads_list) do %>
 | 
			
		||||
    <% local is_stickied = ntob(thread.is_stickied) %>
 | 
			
		||||
    <% local is_locked = ntob(thread.is_locked) %>
 | 
			
		||||
    <div class="thread">
 | 
			
		||||
      <div class="thread-sticky-container contain-svg">
 | 
			
		||||
        <% if is_stickied then -%>
 | 
			
		||||
          <% render("svg-icons.sticky") %>
 | 
			
		||||
          <i>Stickied</i>
 | 
			
		||||
        <% end -%>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="thread-info-container">
 | 
			
		||||
        <span>
 | 
			
		||||
          <span class="thread-title"><a href="<%= url_for("thread", {slug = thread.slug}) %>"><%= thread.title %></a></span>
 | 
			
		||||
          •
 | 
			
		||||
          Started by <a href=<%= url_for("user", {username = thread.started_by}) %>><%= thread.started_by %></a>
 | 
			
		||||
          on <%= os.date("%c", thread.created_at) %>
 | 
			
		||||
        </span>
 | 
			
		||||
        <span>
 | 
			
		||||
          Latest post by <a href="<%= url_for("user", {username = thread.latest_post_username}) %>"><%= thread.latest_post_username %></a>
 | 
			
		||||
          <a href="<%= url_for("thread", {slug = thread.slug}, {after = thread.latest_post_id}) .. "#post-" .. thread.latest_post_id %>">on <%= os.date("%c", thread.latest_post_created_at) %></a>:
 | 
			
		||||
        </span>
 | 
			
		||||
        <span class="thread-info-post-preview">
 | 
			
		||||
          <%- thread.latest_post_content %>
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="thread-locked-container contain-svg">
 | 
			
		||||
        <% if is_locked then -%>
 | 
			
		||||
          <% render("svg-icons.lock") %>
 | 
			
		||||
          <i>Locked</i>
 | 
			
		||||
        <% end -%>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  <% end %>
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
<% if thread_create_error == ThreadCreateError.OK then %>
 | 
			
		||||
<a href=<%= url_for("thread_create", nil, {topic_id = topic.id}) %>>New thread</a>
 | 
			
		||||
<% elseif thread_create_error == ThreadCreateError.GUEST then %>
 | 
			
		||||
<p>Your account is still pending confirmation by a moderator. You are not able to create a new thread or post at this time.</p>
 | 
			
		||||
<% elseif thread_create_error == ThreadCreateError.LOGGED_OUT then %>
 | 
			
		||||
<p>Only logged in users can create threads. <a href="<%= url_for("user_signup") %>">Sign up</a> or <a href="<%= url_for("user_login")%>">log in</a> to create a thread.</p>
 | 
			
		||||
<% else %>
 | 
			
		||||
<p>This topic is locked.</p>
 | 
			
		||||
<% end %>
 | 
			
		||||
 | 
			
		||||
<% if me:is_mod() then %>
 | 
			
		||||
  <br>
 | 
			
		||||
  <a href="<%= url_for("topic_edit", {slug = topic.slug}) %>">Edit topic</a>
 | 
			
		||||
  <form method="post" action="<%= url_for("topic_edit", {slug = topic.slug}) %>">
 | 
			
		||||
  <input type="hidden" name="is_locked" value="<%= not ntob(topic.is_locked) %>">
 | 
			
		||||
  <p><%= "This topic is " .. (ntob(topic.is_locked) and "" or "un") .. "locked." %></p>
 | 
			
		||||
  <input type="submit" id="lock" value="<%= ntob(topic.is_locked) and "Unlock" or "Lock" %>">
 | 
			
		||||
  </form>
 | 
			
		||||
<% end %>
 | 
			
		||||
<nav id="bottomnav">
 | 
			
		||||
  <% render("views.common.pagination", {page_count = pages, current_page = page}) %>
 | 
			
		||||
</nav>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user