Compare commits
10 Commits
00c56f1417
...
2eddb70d63
Author | SHA1 | Date | |
---|---|---|---|
2eddb70d63 | |||
3bd474d7fe | |||
82b25946a0 | |||
a1055b0c43 | |||
7cc16047cb | |||
8c7ef09567 | |||
f1f218fc75 | |||
8609c33f00 | |||
9b689a08e2 | |||
c473d2b1a0 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,5 +3,5 @@ nginx.conf.compiled
|
||||
db.*.sqlite
|
||||
.vscode/
|
||||
.local/
|
||||
static/
|
||||
static/avatars/
|
||||
secrets.lua
|
||||
|
4
app.lua
4
app.lua
@ -25,8 +25,8 @@ app:include("apps.users", {path = "/user"})
|
||||
app:include("apps.topics", {path = "/topics"})
|
||||
app:include("apps.threads", {path = "/threads"})
|
||||
|
||||
app:get("/", function()
|
||||
return "Welcome to Lapis " .. require("lapis.version")
|
||||
app:get("/", function(self)
|
||||
return {redirect_to = self:url_for("all_topics")}
|
||||
end)
|
||||
|
||||
return app
|
||||
|
@ -9,6 +9,8 @@ local Topics = models.Topics
|
||||
local Threads = models.Threads
|
||||
local Posts = models.Posts
|
||||
|
||||
local POSTS_PER_PAGE = 10
|
||||
|
||||
app:get("thread_create", "/create", function(self)
|
||||
local user = util.get_logged_in_user(self)
|
||||
if not user then
|
||||
@ -20,6 +22,8 @@ app:get("thread_create", "/create", function(self)
|
||||
return "how did you get here?"
|
||||
end
|
||||
self.all_topics = all_topics
|
||||
self.page_title = "creating thread"
|
||||
self.me = user
|
||||
return {render = "threads.create"}
|
||||
end)
|
||||
|
||||
@ -57,7 +61,6 @@ app:post("thread_create", "/create", function(self)
|
||||
end)
|
||||
|
||||
app:get("thread", "/:slug", function(self)
|
||||
local posts_per_page = 10
|
||||
local thread = Threads:find({
|
||||
slug = self.params.slug
|
||||
})
|
||||
@ -65,14 +68,26 @@ app:get("thread", "/:slug", function(self)
|
||||
return {status = 404}
|
||||
end
|
||||
self.thread = thread
|
||||
|
||||
if self.params.after then
|
||||
local after_id = tonumber(self.params.after)
|
||||
local post_position = Posts:count(db.clause({
|
||||
thread_id = thread.id,
|
||||
{"id <= ?", after_id},
|
||||
}))
|
||||
self.page = math.floor((post_position - 1) / POSTS_PER_PAGE) + 1
|
||||
else
|
||||
self.page = tonumber(self.params.page) or 1
|
||||
end
|
||||
|
||||
local post_count = Posts:count(db.clause({
|
||||
thread_id = thread.id
|
||||
}))
|
||||
self.pages = math.ceil(post_count / posts_per_page)
|
||||
self.page = tonumber(self.params.page) or 1
|
||||
self.pages = math.ceil(post_count / POSTS_PER_PAGE)
|
||||
-- self.page = math.max(1, math.min(self.page, self.pages))
|
||||
local posts = db.query([[
|
||||
SELECT
|
||||
posts.id, post_history.content, users.username, avatars.file_path AS avatar_path
|
||||
posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, avatars.file_path AS avatar_path
|
||||
FROM
|
||||
posts
|
||||
JOIN
|
||||
@ -86,10 +101,13 @@ app:get("thread", "/:slug", function(self)
|
||||
ORDER BY
|
||||
posts.created_at ASC
|
||||
LIMIT ? OFFSET ?
|
||||
]], thread.id, posts_per_page, (self.page - 1) * posts_per_page)
|
||||
]], thread.id, POSTS_PER_PAGE, (self.page - 1) * POSTS_PER_PAGE)
|
||||
self.topic = Topics:find(thread.topic_id)
|
||||
self.user = util.get_logged_in_user_or_transient(self)
|
||||
self.me = util.get_logged_in_user_or_transient(self)
|
||||
self.posts = posts
|
||||
|
||||
self.page_title = thread.title
|
||||
|
||||
return {render = "threads.thread"}
|
||||
end)
|
||||
|
||||
@ -115,11 +133,15 @@ app:post("thread", "/:slug", function(self)
|
||||
|
||||
local post_content = self.params.post_content
|
||||
local post = util.create_post(thread.id, user.id, post_content)
|
||||
local post_count = Posts:count(db.clause({
|
||||
thread_id = thread.id
|
||||
}))
|
||||
local last_page = math.ceil(post_count / POSTS_PER_PAGE)
|
||||
if not post then
|
||||
return {redirect_to = self:url_for("thread", {slug = thread.slug})}
|
||||
return {redirect_to = self:url_for("thread", {slug = thread.slug}, {page = last_page}) .. "#latest-post"}
|
||||
end
|
||||
|
||||
return {redirect_to = self:url_for("thread", {slug = thread.slug})}
|
||||
return {redirect_to = self:url_for("thread", {slug = thread.slug}, {page = last_page}) .. "#latest-post"}
|
||||
end)
|
||||
|
||||
return app
|
||||
|
@ -21,7 +21,7 @@ local ThreadCreateError = {
|
||||
|
||||
app:get("all_topics", "", function(self)
|
||||
self.topic_list = db.query("select * from topics limit 25;")
|
||||
self.user = util.get_logged_in_user(self) or util.TransientUser
|
||||
self.me = util.get_logged_in_user_or_transient(self)
|
||||
return {render = "topics.topics"}
|
||||
end)
|
||||
|
||||
@ -31,6 +31,9 @@ app:get("topic_create", "/create", function(self)
|
||||
return {status = 403}
|
||||
end
|
||||
|
||||
self.page_title = "creating topic"
|
||||
self.me = user
|
||||
|
||||
return {render = "topics.create"}
|
||||
end)
|
||||
|
||||
@ -65,7 +68,7 @@ app:get("topic", "/:slug", function(self)
|
||||
self.threads_list = db.query("SELECT * FROM threads WHERE topic_id = ? ORDER BY is_stickied DESC, created_at DESC", topic.id)
|
||||
local user = util.get_logged_in_user_or_transient(self)
|
||||
print(topic.is_locked, type(topic.is_locked))
|
||||
self.user = user
|
||||
self.me = user
|
||||
self.ThreadCreateError = ThreadCreateError
|
||||
self.thread_create_error = ThreadCreateError.OK
|
||||
if user:is_logged_in_guest() then
|
||||
@ -76,6 +79,8 @@ app:get("topic", "/:slug", function(self)
|
||||
self.thread_create_error = ThreadCreateError.TOPIC_LOCKED
|
||||
end
|
||||
|
||||
self.page_title = "all threads in " .. topic.name
|
||||
|
||||
return {render = "topics.topic"}
|
||||
end)
|
||||
|
||||
@ -91,6 +96,9 @@ app:get("topic_edit", "/:slug/edit", function(self)
|
||||
return {redirect_to = self:url_for("all_topics")}
|
||||
end
|
||||
self.topic = topic
|
||||
self.me = user
|
||||
self.page_title = "editing topic " .. topic.name
|
||||
|
||||
return {render = "topics.edit"}
|
||||
end)
|
||||
|
||||
|
@ -71,8 +71,7 @@ app:get("user", "/:username", function(self)
|
||||
self.session.flash = {}
|
||||
end
|
||||
|
||||
-- local me = validate_session(self.session.session_key) or TransientUser
|
||||
local me = util.get_logged_in_user(self) or util.TransientUser
|
||||
local me = util.get_logged_in_user_or_transient(self)
|
||||
self.user = user
|
||||
self.me = me
|
||||
|
||||
@ -83,6 +82,26 @@ app:get("user", "/:username", function(self)
|
||||
return {status = 404}
|
||||
end
|
||||
end
|
||||
|
||||
self.latest_posts = db.query([[
|
||||
SELECT
|
||||
posts.id, posts.created_at, post_history.content, post_history.edited_at, threads.title AS thread_title, topics.name as topic_name, threads.slug as thread_slug
|
||||
FROM
|
||||
posts
|
||||
JOIN
|
||||
post_history ON posts.current_revision_id = post_history.id
|
||||
JOIN
|
||||
threads ON posts.thread_id = threads.id
|
||||
JOIN
|
||||
topics ON threads.topic_id = topics.id
|
||||
WHERE
|
||||
posts.user_id = ?
|
||||
ORDER BY posts.created_at DESC
|
||||
LIMIT 10
|
||||
]], user.id)
|
||||
|
||||
self.page_title = user.username .. "'s profile"
|
||||
|
||||
return {render = "user.user"}
|
||||
end)
|
||||
|
||||
@ -128,7 +147,9 @@ app:get("user_delete_confirm", "/:username/delete_confirm", function(self)
|
||||
self.err = self.session.flash.error
|
||||
self.session.flash = {}
|
||||
end
|
||||
self.user = target_user
|
||||
self.me = target_user
|
||||
self.page_title = "confirm deletion"
|
||||
|
||||
return {render = "user.delete_confirm"}
|
||||
end)
|
||||
|
||||
@ -206,7 +227,9 @@ app:get("user_settings", "/:username/settings", function(self)
|
||||
self.flash_msg = flash.error
|
||||
end
|
||||
end
|
||||
self.user = target_user
|
||||
self.me = target_user
|
||||
self.page_title = "settings"
|
||||
|
||||
return {render = "user.settings"}
|
||||
end)
|
||||
|
||||
@ -245,6 +268,9 @@ app:get("user_login", "/login", function(self)
|
||||
self.err = self.session.flash.error
|
||||
self.session.flash = {}
|
||||
end
|
||||
|
||||
self.page_title = "log in"
|
||||
|
||||
return {render = "user.login"}
|
||||
end)
|
||||
|
||||
@ -287,6 +313,9 @@ app:get("user_signup", "/signup", function(self)
|
||||
self.err = self.session.flash.error
|
||||
self.session.flash = {}
|
||||
end
|
||||
|
||||
self.page_title = "sign up"
|
||||
|
||||
return {render = "user.signup"}
|
||||
end)
|
||||
|
||||
@ -425,4 +454,4 @@ app:post("guest_user", "/guest_user/:user_id", function(self)
|
||||
return {redirect_to = self:url_for("user", {username = target_user.username})}
|
||||
end)
|
||||
|
||||
return app
|
||||
return app
|
||||
|
@ -1,15 +1,5 @@
|
||||
local babycode = {}
|
||||
|
||||
local _escape_html = function(text)
|
||||
return text:gsub("[&<>\"']", {
|
||||
["&"] = "&",
|
||||
["<"] = "<",
|
||||
[">"] = ">",
|
||||
['"'] = """,
|
||||
["'"] = "'"
|
||||
})
|
||||
end
|
||||
|
||||
---renders babycode to html
|
||||
---@param s string input babycode
|
||||
---@param escape_html fun(s: string): string function that escapes html
|
||||
@ -21,7 +11,8 @@ function babycode.to_html(s, escape_html)
|
||||
local code_count = 0
|
||||
local text = s:gsub("%[code%](.-)%[/code%]", function(code)
|
||||
code_count = code_count + 1
|
||||
code_blocks[code_count] = code
|
||||
-- strip leading and trailing newlines, preserve others
|
||||
code_blocks[code_count] = code:gsub("^%s*(.-)%s*$", "%1")
|
||||
return "\1CODE:"..code_count.."\1"
|
||||
end)
|
||||
|
||||
@ -48,14 +39,14 @@ function babycode.to_html(s, escape_html)
|
||||
return url
|
||||
end)
|
||||
|
||||
-- normalize newlines, replace them with <br>
|
||||
text = text:gsub("\r?\n\r?\n+", "<br>"):gsub("\r?\n", "<br>")
|
||||
|
||||
-- replace code block placeholders back with their original contents
|
||||
text = text:gsub("\1CODE:(%d+)\1", function(n)
|
||||
return "<pre><code>"..code_blocks[tonumber(n)].."</code></pre>"
|
||||
end)
|
||||
|
||||
-- finally, normalize newlines replace them with <br>
|
||||
text = text:gsub("\r?\n\r?\n+", "<br>"):gsub("\r?\n", "<br>")
|
||||
|
||||
return text
|
||||
end
|
||||
|
||||
|
@ -28,6 +28,10 @@ function Users_mt:is_default_avatar()
|
||||
return self.avatar_id == nil
|
||||
end
|
||||
|
||||
function Users_mt:is_logged_in()
|
||||
return true
|
||||
end
|
||||
|
||||
local ret = {
|
||||
Users = Users,
|
||||
Topics = Model:extend("topics"),
|
||||
|
223
sass/style.scss
Normal file
223
sass/style.scss
Normal file
@ -0,0 +1,223 @@
|
||||
/* src: */
|
||||
|
||||
@use "sass:color";
|
||||
|
||||
$accent_color: #c1ceb1;
|
||||
|
||||
$dark_bg: color.scale($accent_color, $lightness: -25%, $saturation: -97%);
|
||||
$dark2: color.scale($accent_color, $lightness: -30%, $saturation: -60%);
|
||||
|
||||
$light: color.scale($accent_color, $lightness: 40%, $saturation: -60%);
|
||||
|
||||
$main_bg: color.scale($accent_color, $lightness: -10%, $saturation: -40%);
|
||||
$button_color: color.adjust($accent_color, $hue: 90);
|
||||
|
||||
%button-base {
|
||||
cursor: default;
|
||||
color: black;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
padding: 5px 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
@mixin button($color) {
|
||||
@extend %button-base;
|
||||
background-color: $color;
|
||||
|
||||
&:hover {
|
||||
background-color: color.scale($color, $lightness: 20%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: color.scale($color, $lightness: -10%, $saturation: -70%);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin navbar($color) {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
background-color: $color;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 20px;
|
||||
background-color: $main_bg;
|
||||
}
|
||||
|
||||
.big {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
#topnav {
|
||||
@include navbar($accent_color);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#bottomnav {
|
||||
@include navbar($dark_bg);
|
||||
}
|
||||
|
||||
.darkbg {
|
||||
padding-bottom: 10px;
|
||||
padding-left: 10px;
|
||||
background-color: $dark_bg;
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
column-gap: 15px;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
display: inline;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.thread-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.post {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"usercard post-content-container";
|
||||
border: 2px outset $dark2;
|
||||
}
|
||||
|
||||
.usercard {
|
||||
grid-area: usercard;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 10px;
|
||||
border: 4px outset $light;
|
||||
background-color: $dark_bg;
|
||||
border-right: solid 2px;
|
||||
}
|
||||
|
||||
.post-content-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 0.2fr 2.5fr;
|
||||
gap: 0px 0px;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"post-info"
|
||||
"post-content";
|
||||
grid-area: post-content-container;
|
||||
}
|
||||
|
||||
.post-info {
|
||||
grid-area: post-info;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 20px;
|
||||
align-items: center;
|
||||
border-top: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
grid-area: post-content;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
.user-posts {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"user-page-usercard user-posts-container";
|
||||
border: 2px outset $dark2;
|
||||
}
|
||||
|
||||
.user-page-usercard {
|
||||
grid-area: user-page-usercard;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 10px;
|
||||
border: 4px outset $light;
|
||||
background-color: $dark_bg;
|
||||
border-right: solid 2px;
|
||||
}
|
||||
|
||||
.user-posts-container {
|
||||
grid-area: user-posts-container;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 0.2fr 2.5fr;
|
||||
gap: 0px 0px;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
"post-info"
|
||||
"post-content";
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
object-fit: contain;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.username-link {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.user-status {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button, input[type="submit"], .linkbutton {
|
||||
@include button($button_color);
|
||||
|
||||
&.critical {
|
||||
color: white;
|
||||
@include button(red);
|
||||
}
|
||||
|
||||
&.warn {
|
||||
@include button(#fbfb8d);
|
||||
}
|
||||
}
|
||||
|
||||
// not sure why this one has to be separate, but if it's included in the rule above everything breaks
|
||||
input[type="file"]::file-selector-button {
|
||||
@include button($button_color);
|
||||
}
|
||||
|
||||
.pagebutton {
|
||||
@include button($button_color);
|
||||
padding: 5px 5px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.currentpage {
|
||||
@extend %button-base;
|
||||
border: none;
|
||||
padding: 5px 5px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modform {
|
||||
display: inline;
|
||||
}
|
216
static/style.css
Normal file
216
static/style.css
Normal file
@ -0,0 +1,216 @@
|
||||
/* src: */
|
||||
.currentpage, .pagebutton, input[type=file]::file-selector-button, button.warn, input[type=submit].warn, .linkbutton.warn, button.critical, input[type=submit].critical, .linkbutton.critical, button, input[type=submit], .linkbutton {
|
||||
cursor: default;
|
||||
color: black;
|
||||
font-size: 0.9rem;
|
||||
text-decoration: none;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
padding: 5px 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 20px;
|
||||
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
|
||||
}
|
||||
|
||||
.big {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
#topnav {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
background-color: #c1ceb1;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#bottomnav {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016);
|
||||
}
|
||||
|
||||
.darkbg {
|
||||
padding-bottom: 10px;
|
||||
padding-left: 10px;
|
||||
background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016);
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
column-gap: 15px;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
display: inline;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.thread-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.post {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: "usercard post-content-container";
|
||||
border: 2px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
||||
}
|
||||
|
||||
.usercard {
|
||||
grid-area: usercard;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 10px;
|
||||
border: 4px outset rgb(217.26, 220.38, 213.42);
|
||||
background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016);
|
||||
border-right: solid 2px;
|
||||
}
|
||||
|
||||
.post-content-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 0.2fr 2.5fr;
|
||||
gap: 0px 0px;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: "post-info" "post-content";
|
||||
grid-area: post-content-container;
|
||||
}
|
||||
|
||||
.post-info {
|
||||
grid-area: post-info;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 20px;
|
||||
align-items: center;
|
||||
border-top: 1px solid black;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
grid-area: post-content;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
.user-posts {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: "user-page-usercard user-posts-container";
|
||||
border: 2px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
||||
}
|
||||
|
||||
.user-page-usercard {
|
||||
grid-area: user-page-usercard;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 10px;
|
||||
border: 4px outset rgb(217.26, 220.38, 213.42);
|
||||
background-color: rgb(143.7039271654, 144.3879625984, 142.8620374016);
|
||||
border-right: solid 2px;
|
||||
}
|
||||
|
||||
.user-posts-container {
|
||||
grid-area: user-posts-container;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 0.2fr 2.5fr;
|
||||
gap: 0px 0px;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas: "post-info" "post-content";
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
object-fit: contain;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.username-link {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.user-status {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button, input[type=submit], .linkbutton {
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
}
|
||||
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
||||
background-color: rgb(192.6, 215.8, 214.6);
|
||||
}
|
||||
button:active, input[type=submit]:active, .linkbutton:active {
|
||||
background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323);
|
||||
}
|
||||
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||
color: white;
|
||||
background-color: red;
|
||||
}
|
||||
button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:hover {
|
||||
background-color: #ff3333;
|
||||
}
|
||||
button.critical:active, input[type=submit].critical:active, .linkbutton.critical:active {
|
||||
background-color: rgb(149.175, 80.325, 80.325);
|
||||
}
|
||||
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||
background-color: #fbfb8d;
|
||||
}
|
||||
button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover {
|
||||
background-color: rgb(251.8, 251.8, 163.8);
|
||||
}
|
||||
button.warn:active, input[type=submit].warn:active, .linkbutton.warn:active {
|
||||
background-color: rgb(198.3813559322, 198.3813559322, 154.4186440678);
|
||||
}
|
||||
|
||||
input[type=file]::file-selector-button {
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
}
|
||||
input[type=file]::file-selector-button:hover {
|
||||
background-color: rgb(192.6, 215.8, 214.6);
|
||||
}
|
||||
input[type=file]::file-selector-button:active {
|
||||
background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323);
|
||||
}
|
||||
|
||||
.pagebutton {
|
||||
background-color: rgb(177, 206, 204.5);
|
||||
padding: 5px 5px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.pagebutton:hover {
|
||||
background-color: rgb(192.6, 215.8, 214.6);
|
||||
}
|
||||
.pagebutton:active {
|
||||
background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323);
|
||||
}
|
||||
|
||||
.currentpage {
|
||||
border: none;
|
||||
padding: 5px 5px;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modform {
|
||||
display: inline;
|
||||
}
|
3
util.lua
3
util.lua
@ -26,6 +26,9 @@ util.TransientUser = {
|
||||
is_logged_in_guest = function (self)
|
||||
return false
|
||||
end,
|
||||
is_logged_in = function (self)
|
||||
return false
|
||||
end,
|
||||
username = "Deleted User",
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,13 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Porom</title>
|
||||
<% if page_title then %>
|
||||
<title>Porom - <%= page_title %></title>
|
||||
<% else %>
|
||||
<title>Porom</title>
|
||||
<% end %>
|
||||
<% math.randomseed(os.time()) %>
|
||||
<link rel="stylesheet" href="<%= "/static/style.css?" .. math.random(1, 100) %>">
|
||||
</head>
|
||||
<body>
|
||||
<% content_for("inner") %>
|
||||
|
27
views/common/pagination.etlua
Normal file
27
views/common/pagination.etlua
Normal file
@ -0,0 +1,27 @@
|
||||
<% local left_start = math.max(1, current_page - 5) %>
|
||||
<% local right_end = math.min(page_count, current_page + 5) %>
|
||||
|
||||
<div class="pager">
|
||||
<span>Page:</span>
|
||||
<% if current_page > 5 then %>
|
||||
<a href="?page=1" class="pagebutton">1</a>
|
||||
<% if left_start > 2 then %>
|
||||
<span class="currentpage">…</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% for i = left_start, current_page - 1 do%>
|
||||
<a href="?page=<%= i %>" class="pagebutton"><%= i %></a>
|
||||
<% end %>
|
||||
<% if page_count > 0 then %>
|
||||
<span class="currentpage"><%= current_page %></span>
|
||||
<% end %>
|
||||
<% for i = current_page + 1, right_end do %>
|
||||
<a href="?page=<%= i %>" class="pagebutton"><%= i %></a>
|
||||
<% end %>
|
||||
<% if right_end < page_count then %>
|
||||
<% if right_end < page_count - 1 then %>
|
||||
<span class="currentpage">…</span>
|
||||
<% end %>
|
||||
<a href="?page=<%= page_count %>" class="pagebutton"><%= page_count %></a>
|
||||
<% end %>
|
||||
</div>
|
13
views/common/topnav.etlua
Normal file
13
views/common/topnav.etlua
Normal file
@ -0,0 +1,13 @@
|
||||
<nav id="topnav">
|
||||
<span>
|
||||
<h1 class="site-title">Porom</h1>
|
||||
<a href="<%= url_for("all_topics") %>">All topics</a>
|
||||
</span>
|
||||
<span>
|
||||
<% if me:is_logged_in() then -%>
|
||||
Welcome, <a href="<%= url_for("user", {username = me.username}) %>"><%= me.username %></a>
|
||||
<% else -%>
|
||||
Welcome, guest. Please <a href="<%= url_for("user_signup") %>">sign up</a> or <a href="<%= url_for("user_login") %>">log in</a>
|
||||
<% end -%>
|
||||
</span>
|
||||
</nav>
|
26
views/threads/post.etlua
Normal file
26
views/threads/post.etlua
Normal file
@ -0,0 +1,26 @@
|
||||
<div class="post" id="post-<%= post.id %>">
|
||||
<div class="usercard">
|
||||
<a href="<%= url_for("user", {username = post.username}) %>" style="display: contents;">
|
||||
<img src="<%= post.avatar_path or "/avatars/default.webp" %>" class="avatar">
|
||||
</a>
|
||||
<a href="<%= url_for("user", {username = post.username}) %>" class="username-link"><%= post.username %></a>
|
||||
<% if post.status ~= "" then %>
|
||||
<em class="user-status"><%= post.status %></em>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="post-content-container"<%= is_latest and 'id=latest-post' or "" %>>
|
||||
<div class="post-info">
|
||||
<div><a href="<%= "#post-" .. post.id %>" title="Permalink"><i>
|
||||
<% if tonumber(post.edited_at) > tonumber(post.created_at) then -%>
|
||||
Edited at <%= os.date("%c", post.edited_at) %>
|
||||
<% else -%>
|
||||
Posted at <%= os.date("%c", post.created_at) %>
|
||||
<% end -%>
|
||||
</i></a></div>
|
||||
<div><button>Reply</button></div>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<%- post.content %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,27 +1,22 @@
|
||||
<h1><%= thread.title %></h1>
|
||||
<p>Posted under <a href="<%= url_for("topic", {slug = topic.slug}) %>"><%= topic.name %></a>
|
||||
<% for _, post in ipairs(posts) do %>
|
||||
<div id="post-<%= post.id %>">
|
||||
<img src="<%= post.avatar_path or "/avatars/default.webp" %>"><br>
|
||||
<a href="<%= url_for("user", {username = post.username}) %>"><%= post.username %></a>
|
||||
<div><p><%- post.content %></p></div>
|
||||
<a href="#post-<%= post.id %>">permalink</a>
|
||||
</div>
|
||||
<% end %>
|
||||
<% render("views.common.topnav") -%>
|
||||
<main>
|
||||
<nav class="darkbg">
|
||||
<h1 class="thread-title"><%= thread.title %></h1>
|
||||
<span>Posted in <a href="<%= url_for("topic", {slug = topic.slug}) %>"><%= topic.name %></a></span>
|
||||
</nav>
|
||||
<% for i, post in ipairs(posts) do %>
|
||||
<% render("views.threads.post", {post = post, is_latest = i == #posts}) %>
|
||||
<% end %>
|
||||
</main>
|
||||
|
||||
<% if not user:is_guest() then %>
|
||||
<nav id="bottomnav">
|
||||
<% render("views.common.pagination", {page_count = pages, current_page = page}) %>
|
||||
</nav>
|
||||
|
||||
<% if not me:is_guest() then %>
|
||||
<h1>Respond to "<%= thread.title %>"</h1>
|
||||
<form method="post">
|
||||
<textarea id="post_content" name="post_content" placeholder="Response body" required></textarea><br>
|
||||
<input type="submit" value="Reply">
|
||||
</form>
|
||||
<% end %>
|
||||
<span>
|
||||
<% for i = 1, math.max(pages, 1) do %>
|
||||
<% if i == page then %>
|
||||
<%= tostring(i)%>
|
||||
<% else %>
|
||||
<a href="?page=<%= i %>"><%= tostring(i)%></a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</span>
|
||||
|
@ -22,7 +22,7 @@
|
||||
<p>This topic is locked.</p>
|
||||
<% end %>
|
||||
|
||||
<% if user:is_mod() then %>
|
||||
<% 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}) %>">
|
||||
|
@ -11,6 +11,6 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% if user:is_mod() then %>
|
||||
<% if me:is_mod() then %>
|
||||
<a href="<%= url_for("topic_create") %>">Create new topic</a>
|
||||
<% end %>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<h1>Are you sure you want to delete your account, <%= user.username %>?</h1>
|
||||
<h1>Are you sure you want to delete your account, <%= me.username %>?</h1>
|
||||
<p>This cannot be undone. This will not delete your posts, only anonymize them.</p>
|
||||
<p>If you are sure, please type your password below.</p>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<h2><%= err %></h2>
|
||||
<% end %>
|
||||
|
||||
<form method="post" action="<%= url_for("user_delete", {username = user.username}) %>">
|
||||
<form method="post" action="<%= url_for("user_delete", {username = me.username}) %>">
|
||||
<input type="password" name="password" id="password" autocomplete="current-password" placeholder="Password" required><br>
|
||||
<input type="submit" value="Delete my account (NO UNDO)">
|
||||
<input class="critical" type="submit" value="Delete my account (NO UNDO)">
|
||||
</form>
|
||||
|
@ -2,19 +2,19 @@
|
||||
<% if flash_msg then %>
|
||||
<h2><%= flash_msg %></h2>
|
||||
<% end %>
|
||||
<form method="post" action="<%= url_for("user_set_avatar", {username = user.username}) %>" enctype="multipart/form-data">
|
||||
<img src="<%= avatar_url(user) %>"><br>
|
||||
<input type="file" name="avatar" accept="image/*"><br>
|
||||
<form method="post" action="<%= url_for("user_set_avatar", {username = me.username}) %>" enctype="multipart/form-data">
|
||||
<img src="<%= avatar_url(me) %>"><br>
|
||||
<input id="file" type="file" name="avatar" accept="image/*">
|
||||
<input type="submit" value="Update avatar">
|
||||
<% if not user:is_default_avatar() then %>
|
||||
<input type="submit" value="Clear avatar" formaction="<%= url_for("user_clear_avatar", {username = user.username}) %>">
|
||||
<% if not me:is_default_avatar() then %>
|
||||
<input type="submit" value="Clear avatar" formaction="<%= url_for("user_clear_avatar", {username = me.username}) %>">
|
||||
<% end %>
|
||||
<br>
|
||||
</form>
|
||||
<form method="post" action="">
|
||||
<label for="status">Status</label>
|
||||
<input type="text" id="status" name="status" value="<%= user.status %>" maxlength="30"><br>
|
||||
<input type="submit" value="Save">
|
||||
<input type="text" id="status" name="status" value="<%= me.status %>" maxlength="30"><br>
|
||||
<input type="submit" value="Save status">
|
||||
</form>
|
||||
<br>
|
||||
<a href="<%= url_for("user_delete_confirm", {username = user.username}) %>">Delete account</a>
|
||||
<a class="linkbutton critical" href="<%= url_for("user_delete_confirm", {username = me.username}) %>">Delete account</a>
|
||||
|
@ -1,41 +1,74 @@
|
||||
<% if just_logged_in then %>
|
||||
<h1>Logged in successfully.</h1>
|
||||
<% end %>
|
||||
<img src="<%= avatar_url(user) %>">
|
||||
<h1><%= user.username %></h1>
|
||||
<h2><%= PermissionLevelString[user.permission] %></h2>
|
||||
<% if user:is_guest() and user_is_me then %>
|
||||
<h2>You are a guest. An Moderator needs to approve your account before you will be able to post.</h2>
|
||||
<% render("views.common.topnav") -%>
|
||||
<div class="darkbg">
|
||||
<h1 class="thread-title">Latest posts by <i><%= user.username %></i></h1>
|
||||
<div>
|
||||
User permission: <i><%= PermissionLevelString[user.permission] %></i>
|
||||
</div>
|
||||
<% if user_is_me then -%>
|
||||
<div class="user-actions">
|
||||
<a class="linkbutton" href="<%= url_for("user_settings", {username = user.username}) %>">Settings</a>
|
||||
<form method="post" action="<%= url_for("user_logout", {user_id = me.id}) %>">
|
||||
<input class="warn" type="submit" value="Log out">
|
||||
</form>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% --[[ duplicating code, maybe i'll refactor the post subview later to work anywhere <clown emoji>]] %>
|
||||
<% for i, post in ipairs(latest_posts) do %>
|
||||
<div class="user-posts">
|
||||
<div class="user-page-usercard">
|
||||
<img class="avatar" src="<%= avatar_url(user) %>">
|
||||
<b class="big"><%= user.username %></b>
|
||||
<em class="user-status"><%= user.status %></em>
|
||||
</div>
|
||||
<div class="user-posts-container">
|
||||
<div class="post-info">
|
||||
<div><a href="<%= url_for("thread", {slug = post.thread_slug}, {after = post.id}) .. "#post-" .. post.id %>" title="Permalink"><i>
|
||||
<% if tonumber(post.edited_at) > tonumber(post.created_at) then -%>
|
||||
Edited in <%= post.thread_title %> at <%= os.date("%c", post.edited_at) %>
|
||||
<% else -%>
|
||||
Posted in <%= post.thread_title %> at <%= os.date("%c", post.created_at) %>
|
||||
<% end -%>
|
||||
</i></a></div>
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<%- post.content %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if user_is_me then %>
|
||||
<a href="<%= url_for("user_settings", {username = user.username}) %>">Settings</a>
|
||||
<form method="post" action="<%= url_for("user_logout", {user_id = me.id}) %>">
|
||||
<input type="submit" value="Log out">
|
||||
</form>
|
||||
|
||||
<% if user:is_guest() and user_is_me then %>
|
||||
<h2>You are a guest. A Moderator needs to approve your account before you will be able to post.</h2>
|
||||
<% end %>
|
||||
|
||||
<% if me:is_mod() and not user:is_system() then %>
|
||||
<h1>Moderator controls</h2>
|
||||
<% if user:is_guest() then %>
|
||||
<p>This user is a guest. They signed up on <%= os.date("%c", user.created_at) %>.</p>
|
||||
<form method="post" action="<%= url_for("confirm_user", {user_id = user.id}) %>">
|
||||
<input type="submit" value="Confirm user">
|
||||
</form>
|
||||
<% else %> <% --[[ user is not guest ]] %>
|
||||
<p>This user signed up on <%= os.date("%c", user.created_at) %> and was confirmed on <%= os.date("%c", user.confirmed_on) %>.</p>
|
||||
<% if user.id ~= me.id and user.permission < me.permission then %>
|
||||
<form method="post" action="<%= url_for("guest_user", {user_id = user.id}) %>">
|
||||
<input type="submit" value="Demote user to guest (soft ban)">
|
||||
</form>
|
||||
<% end %>
|
||||
<% if me:is_admin() and not user:is_mod() then %>
|
||||
<form method="post" action="<%= url_for("mod_user", {user_id = user.id}) %>">
|
||||
<input type="submit" value="Promote user to moderator">
|
||||
<div class="darkbg">
|
||||
<h1>Moderator controls</h2>
|
||||
<% if user:is_guest() then %>
|
||||
<p>This user is a guest. They signed up on <%= os.date("%c", user.created_at) %>.</p>
|
||||
<form class="modform" method="post" action="<%= url_for("confirm_user", {user_id = user.id}) %>">
|
||||
<input type="submit" value="Confirm user">
|
||||
</form>
|
||||
<% elseif me:is_admin() then %>
|
||||
<form method="post" action="<%= url_for("demod_user", {user_id = user.id}) %>">
|
||||
<input type="submit" value="Demote user to regular user">
|
||||
<% else %> <% --[[ user is not guest ]] %>
|
||||
<p>This user signed up on <%= os.date("%c", user.created_at) %> and was confirmed on <%= os.date("%c", user.confirmed_on) %>.</p>
|
||||
<% if user.permission < me.permission then %>
|
||||
<form class="modform" method="post" action="<%= url_for("guest_user", {user_id = user.id}) %>">
|
||||
<input class="warn" type="submit" value="Demote user to guest (soft ban)">
|
||||
</form>
|
||||
<% end %>
|
||||
<% if me:is_admin() and not user:is_mod() then %>
|
||||
<form class="modform" method="post" action="<%= url_for("mod_user", {user_id = user.id}) %>">
|
||||
<input class="warn" type="submit" value="Promote user to moderator">
|
||||
</form>
|
||||
<% elseif user:is_mod() and user.permission < me.permission then %>
|
||||
<form class="modform" method="post" action="<%= url_for("demod_user", {user_id = user.id}) %>">
|
||||
<input class="critical" type="submit" value="Demote user to regular user">
|
||||
</form>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
Loading…
Reference in New Issue
Block a user