Compare commits

...

5 Commits

14 changed files with 182 additions and 26 deletions

3
.gitignore vendored
View File

@ -3,5 +3,6 @@ nginx.conf.compiled
db.*.sqlite
.vscode/
.local/
static/avatars/
static/avatars/*
!static/avatars/default.webp
secrets.lua

View File

@ -22,7 +22,27 @@ local ThreadCreateError = {
}
app:get("all_topics", "", function(self)
self.topic_list = db.query("select * from topics limit 25;")
self.topic_list = db.query([[
SELECT
topics.name, topics.slug, topics.description, topics.is_locked,
users.username AS latest_thread_username,
threads.title AS latest_thread_title,
threads.slug AS latest_thread_slug,
threads.created_at AS latest_thread_created_at
FROM
topics
LEFT JOIN (
SELECT
*,
row_number() OVER (PARTITION BY threads.topic_id ORDER BY threads.created_at DESC) as rn
FROM
threads
) threads ON threads.topic_id = topics.id AND threads.rn = 1
LEFT JOIN
users on users.id = threads.user_id
ORDER BY
topics.sort_order ASC
]])
self.me = util.get_logged_in_user_or_transient(self)
return {render = "topics.topics"}
end)
@ -50,10 +70,12 @@ app:post("topic_create", "/create", function(self)
local time = os.time()
local slug = lapis_util.slugify(topic_name) .. "-" .. time
local topic_count = Topics:count()
local topic = Topics:create({
name = topic_name,
description = topic_description,
slug = slug,
sort_order = topic_count + 1,
})
util.inject_infobox(self, "Topic created.")

View File

@ -150,8 +150,10 @@ app:post("user_clear_avatar", "/:username/clear_avatar", function(self)
if me.id ~= target_user.id then
return {redirect_to = self:url_for("user", {username = self.params.username})}
end
local old_avatar_id = target_user.avatar_id
util.destroy_avatar(old_avatar_id)
target_user:update({
avatar_id = db.NULL,
avatar_id = 1,
})
util.inject_infobox(self, "Avatar cleared.")
return {redirect_to = self:url_for("user_settings", {username = self.params.username})}

View File

@ -4,6 +4,17 @@ local constants = require("constants")
local alphabet = "-_@0123456789abcdefghijklmnopqrstuvwABCDEFGHIJKLMNOPQRSTUVWXYZ"
local function create_default_avatar()
if models.Avatars:count() > 0 then
print("default avatar must exist")
return
end
models.Avatars:create({
file_path = "/avatars/default.webp",
uploaded_at = os.time(),
})
end
local function create_admin()
local username = "admin"
local root_count = models.Users:count("username = ?", username)
@ -44,5 +55,6 @@ local function create_deleted_user()
})
end
create_default_avatar()
create_admin()
create_deleted_user()

View File

@ -52,4 +52,15 @@ return {
[6] = function ()
schema.drop_column("post_history", "user_id")
end,
[7] = function ()
db.query('DROP INDEX "idx_users_avatar"')
schema.drop_column("users", "avatar_id")
schema.add_column("users", "avatar_id", "REFERENCES avatars(id) DEFAULT 1")
end,
[8] = function ()
schema.add_column("topics", "sort_order", types.integer{default = 0})
db.query("UPDATE topics SET sort_order = (SELECT COUNT(*) FROM topics t2 WHERE t2.ROWID <= topics.ROWID)")
end
}

View File

@ -25,7 +25,7 @@ function Users_mt:is_logged_in_guest()
end
function Users_mt:is_default_avatar()
return self.avatar_id == nil
return self.avatar_id == 1
end
function Users_mt:is_logged_in()

View File

@ -35,6 +35,10 @@ $button_color: color.adjust($accent_color, $hue: 90);
&:active {
background-color: color.scale($color, $lightness: -10%, $saturation: -70%);
}
&:disabled {
background-color: color.scale($color, $lightness: 30%, $saturation: -90%);
}
}
@mixin navbar($color) {
@ -338,3 +342,27 @@ input[type="text"], input[type="password"], textarea, select {
text-overflow: ellipsis;
display: inline;
}
.topic {
display: grid;
grid-template-columns: 1.5fr 64px;
grid-template-rows: 1fr;
gap: 0px 0px;
grid-auto-flow: row;
grid-template-areas:
"topic-info-container topic-locked-container";
}
.topic-info-container {
grid-area: topic-info-container;
background-color: $accent_color;
padding: 5px 20px;
border: 1px solid black;
display: flex;
flex-direction: column;
}
.topic-locked-container {
grid-area: topic-locked-container;
border: 2px outset $light;
}

BIN
static/avatars/default.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -164,6 +164,9 @@ button:hover, input[type=submit]:hover, .linkbutton:hover {
button:active, input[type=submit]:active, .linkbutton:active {
background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323);
}
button:disabled, input[type=submit]:disabled, .linkbutton:disabled {
background-color: rgb(209.535, 211.565, 211.46);
}
button.critical, input[type=submit].critical, .linkbutton.critical {
color: white;
background-color: red;
@ -174,6 +177,9 @@ button.critical:hover, input[type=submit].critical:hover, .linkbutton.critical:h
button.critical:active, input[type=submit].critical:active, .linkbutton.critical:active {
background-color: rgb(149.175, 80.325, 80.325);
}
button.critical:disabled, input[type=submit].critical:disabled, .linkbutton.critical:disabled {
background-color: rgb(174.675, 156.825, 156.825);
}
button.warn, input[type=submit].warn, .linkbutton.warn {
background-color: #fbfb8d;
}
@ -183,6 +189,9 @@ button.warn:hover, input[type=submit].warn:hover, .linkbutton.warn:hover {
button.warn:active, input[type=submit].warn:active, .linkbutton.warn:active {
background-color: rgb(198.3813559322, 198.3813559322, 154.4186440678);
}
button.warn:disabled, input[type=submit].warn:disabled, .linkbutton.warn:disabled {
background-color: rgb(217.55, 217.55, 209.85);
}
input[type=file]::file-selector-button {
background-color: rgb(177, 206, 204.5);
@ -194,6 +203,9 @@ input[type=file]::file-selector-button:hover {
input[type=file]::file-selector-button:active {
background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323);
}
input[type=file]::file-selector-button:disabled {
background-color: rgb(209.535, 211.565, 211.46);
}
p {
margin: 15px 0;
@ -213,6 +225,9 @@ p {
.pagebutton:active {
background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323);
}
.pagebutton:disabled {
background-color: rgb(209.535, 211.565, 211.46);
}
.currentpage {
border: none;
@ -326,3 +341,26 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
text-overflow: ellipsis;
display: inline;
}
.topic {
display: grid;
grid-template-columns: 1.5fr 64px;
grid-template-rows: 1fr;
gap: 0px 0px;
grid-auto-flow: row;
grid-template-areas: "topic-info-container topic-locked-container";
}
.topic-info-container {
grid-area: topic-info-container;
background-color: #c1ceb1;
padding: 5px 20px;
border: 1px solid black;
display: flex;
flex-direction: column;
}
.topic-locked-container {
grid-area: topic-locked-container;
border: 2px outset rgb(217.26, 220.38, 213.42);
}

View File

@ -34,9 +34,6 @@ util.TransientUser = {
}
function util.get_user_avatar_url(req, user)
if not user.avatar_id then
return "/avatars/default.webp"
end
return Avatars:find(user.avatar_id).file_path
end
@ -72,6 +69,29 @@ function util.validate_and_create_image(input_image, filename)
return true
end
function util.destroy_avatar(avatar_id)
if avatar_id == 1 then
print("won't delete default avatar")
return
end
local avatar = Avatars:find(avatar_id)
if not avatar then
return
end
local file_path = "static" .. avatar.file_path
local f = io.open(file_path, "r")
if not f then
print("can't open avatar file")
else
f:close()
os.remove(file_path)
avatar:delete()
end
end
function util.get_logged_in_user(req)
if req.session.session_key == nil then
return nil

View File

@ -1,7 +1,7 @@
<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">
<img src="<%= post.avatar_path %>" class="avatar">
</a>
<a href="<%= url_for("user", {username = post.username}) %>" class="username-link"><%= post.username %></a>
<% if post.status ~= "" then %>

View File

@ -1,6 +1,10 @@
<h1>Create topic</h1>
<form method="post">
<input type="text" name="name" id="name" placeholder="Topic name" required><br>
<textarea id="description" name="description" placeholder="Topic description" required></textarea><br>
<input type="submit" value="Create topic">
</form>
<div class="darkbg settings-container">
<h1>Create topic</h1>
<form method="post">
<label for=name>Name</label>
<input type="text" name="name" id="name" required><br>
<label for=description>Description</label>
<textarea id="description" name="description" required rows=5></textarea><br>
<input type="submit" value="Create topic">
</form>
</div>

View File

@ -1,16 +1,33 @@
<h1>Topics</h1>
<nav class="darkbg">
<h1 class="thread-title">All topics</h1>
<% if me:is_mod() then %>
<a class="linkbutton" href="<%= url_for("topic_create") %>">Create new topic</a>
<% end %>
</nav>
<% if #topic_list == 0 then %>
<p>There are no topics.</p>
<p>There are no topics.</p>
<% else %>
<ul>
<% for i, v in ipairs(topic_list) do %>
<li>
<a href=<%= url_for("topic", {slug = v.slug}) %>><%= v.name %></a> - <%= v.description %>
</li>
<% for _, topic in ipairs(topic_list) do %>
<% local is_locked = ntob(topic.is_locked) %>
<div class="topic">
<div class="topic-info-container">
<a href=<%= url_for("topic", {slug = topic.slug}) %>><%= topic.name %></a>
<%= topic.description %>
<% if topic.latest_thread_username then %>
<span>
Latest thread: <a href="<%= url_for("thread", {slug = topic.latest_thread_slug}) %>"><%= topic.latest_thread_title %></a> by <a href="<%= url_for("user", {username = topic.latest_thread_username}) %>"><%= topic.latest_thread_username %></a> on <%= os.date("%c", topic.latest_thread_created_at) %>
</span>
<% else %>
<i>No threads yet.</i>
<% end %>
</div>
<div class="topic-locked-container contain-svg">
<% if is_locked then -%>
<% render("svg-icons.lock") %>
<i>Locked</i>
<% end -%>
</div>
</div>
<% end %>
<% end %>
</ul>
<% if me:is_mod() then %>
<a href="<%= url_for("topic_create") %>">Create new topic</a>
<% end %>

View File

@ -1,3 +1,4 @@
<% local disable_avatar = me:is_logged_in_guest() %>
<div class="darkbg settings-container">
<h1>User settings</h1>
<% if infobox then %>
@ -7,7 +8,7 @@
<img src="<%= avatar_url(me) %>">
<input id="file" type="file" name="avatar" accept="image/*" required>
<div>
<input type="submit" value="Update avatar">
<input type="submit" value="Update avatar" <%= disable_avatar and "disabled=disabled" %>>
<% if not me:is_default_avatar() then %>
<input type="submit" value="Clear avatar" formaction="<%= url_for("user_clear_avatar", {username = me.username}) %>" formnovalidate>
<% end %>