Compare commits
16 Commits
96922fdd76
...
with-docke
Author | SHA1 | Date | |
---|---|---|---|
ca23415288 | |||
d4ab245297
|
|||
a28572003e
|
|||
511687c8c3
|
|||
7d761bae2e
|
|||
7f10dde1ea
|
|||
9438d3704b
|
|||
16127983ab
|
|||
1cb9262ad7
|
|||
9b42d05174
|
|||
fd261ec8c0
|
|||
f8da57224f
|
|||
24c210e395
|
|||
f18e31811c
|
|||
f5ba312032
|
|||
8e7b167bc2
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -3,5 +3,8 @@ nginx.conf.compiled
|
|||||||
db.*.sqlite
|
db.*.sqlite
|
||||||
.vscode/
|
.vscode/
|
||||||
.local/
|
.local/
|
||||||
static/avatars/
|
static/avatars/*
|
||||||
|
!static/avatars/default.webp
|
||||||
secrets.lua
|
secrets.lua
|
||||||
|
|
||||||
|
.first_launch.*
|
||||||
|
41
README.md
41
README.md
@ -5,16 +5,41 @@ porous forum
|
|||||||
Released under [CNPLv7+](https://thufie.lain.haus/NPL.html).
|
Released under [CNPLv7+](https://thufie.lain.haus/NPL.html).
|
||||||
Please read the [full terms](./LICENSE.md) for proper wording.
|
Please read the [full terms](./LICENSE.md) for proper wording.
|
||||||
|
|
||||||
# deps
|
# installing & first time setup
|
||||||
this is all off the top of my head so if you try to run it got help you
|
1. first, install OpenResty. instructions for linux can be found [here](https://openresty.org/en/linux-packages.html).
|
||||||
|
2. then, install LuaJIT and Lua 5.1 (usually called `lua5.1` in package managers)
|
||||||
|
3. then, install [LuaRocks](https://luarocks.org) (prefer your package manager instead of a local install recommended by the guide)
|
||||||
|
4. add luarocks search dirs to path:
|
||||||
|
|
||||||
- lapis
|
```bash
|
||||||
- lsqlite3
|
# in .bashrc (or other shell equivalent)
|
||||||
- [magick](https://github.com/leafo/magick)
|
eval "$(luarocks --lua-version 5.1 path)"
|
||||||
- bcrypt
|
```
|
||||||
- luaossl
|
5. clone repo
|
||||||
|
6. install the dependencies:
|
||||||
|
|
||||||
i think thats it
|
```bash
|
||||||
|
$ luarocks --local --lua-version 5.1 build --only-deps
|
||||||
|
```
|
||||||
|
7. create a file named `secrets.lua` in the project directory.
|
||||||
|
use the `secrets.lua.example` file as reference, and generate a cryptographically secure random key, for example, with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ openssl rand -hex 32
|
||||||
|
```
|
||||||
|
8. run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ start.sh production
|
||||||
|
```
|
||||||
|
the script will perform some necessary first time setup (and create a hidden file in the folder to ensure it won't do so again). it will create an administrator account and print the credentials to the console; **this will only happen once**. make sure you save them somewhere. the administrator account is the only one that can promote other users to moderator.
|
||||||
|
(note the `production` argument. if called with no arguments, `start.sh` will run in a development environment, which uses a separate database.)
|
||||||
|
|
||||||
|
this app is made with the assumption that it is being reverse-proxied. as such, you may want to change the port to something other than the default `8080`. you can do that in [`config.lua`]([./config.lua]).
|
||||||
|
|
||||||
|
after the first time setup is complete, everything is ready to go. put the app behind your reverse proxy and serve it on the web. the app does not run in https by itself, but the reverse proxy can be set up to do that.
|
||||||
|
|
||||||
|
once you are able to navigate to the forum, you can log in as the administrator account. other people may also sign up, but they are not able to post until manually verified by an administrator or a moderator. the administrator can promote regular users to moderator.
|
||||||
|
|
||||||
# icons
|
# icons
|
||||||
the icons in the `icons/` folder are by [Gabriele Malaspina](https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license)
|
the icons in the `icons/` folder are by [Gabriele Malaspina](https://www.figma.com/community/file/1136337054881623512/iconcino-v2-0-0-free-icons-cc0-1-0-license)
|
||||||
|
1
app.lua
1
app.lua
@ -31,6 +31,7 @@ app:before_filter(inject_methods)
|
|||||||
app:include("apps.users", {path = "/user"})
|
app:include("apps.users", {path = "/user"})
|
||||||
app:include("apps.topics", {path = "/topics"})
|
app:include("apps.topics", {path = "/topics"})
|
||||||
app:include("apps.threads", {path = "/threads"})
|
app:include("apps.threads", {path = "/threads"})
|
||||||
|
app:include("apps.mod", {path = "/mod"})
|
||||||
|
|
||||||
app:get("/", function(self)
|
app:get("/", function(self)
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
return {redirect_to = self:url_for("all_topics")}
|
||||||
|
23
apps/mod.lua
Normal file
23
apps/mod.lua
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
local app = require("lapis").Application()
|
||||||
|
|
||||||
|
local util = require("util")
|
||||||
|
|
||||||
|
local models = require("models")
|
||||||
|
local Users = models.Users
|
||||||
|
|
||||||
|
app:get("user_list", "/list", function(self)
|
||||||
|
self.me = util.get_logged_in_user(self)
|
||||||
|
if not self.me then
|
||||||
|
return {redirect_to = self:url_for("all_topics")}
|
||||||
|
end
|
||||||
|
|
||||||
|
if not self.me:is_mod() then
|
||||||
|
return {redirect_to = self:url_for("all_topics")}
|
||||||
|
end
|
||||||
|
|
||||||
|
self.users = Users:select("")
|
||||||
|
|
||||||
|
return {render = "mod.user-list"}
|
||||||
|
end)
|
||||||
|
|
||||||
|
return app
|
@ -69,6 +69,11 @@ app:get("thread", "/:slug", function(self)
|
|||||||
end
|
end
|
||||||
self.thread = thread
|
self.thread = thread
|
||||||
|
|
||||||
|
local post_count = Posts:count(db.clause({
|
||||||
|
thread_id = thread.id
|
||||||
|
}))
|
||||||
|
self.pages = math.max(math.ceil(post_count / POSTS_PER_PAGE), 1)
|
||||||
|
|
||||||
if self.params.after then
|
if self.params.after then
|
||||||
local after_id = tonumber(self.params.after)
|
local after_id = tonumber(self.params.after)
|
||||||
local post_position = Posts:count(db.clause({
|
local post_position = Posts:count(db.clause({
|
||||||
@ -77,13 +82,9 @@ app:get("thread", "/:slug", function(self)
|
|||||||
}))
|
}))
|
||||||
self.page = math.floor((post_position - 1) / POSTS_PER_PAGE) + 1
|
self.page = math.floor((post_position - 1) / POSTS_PER_PAGE) + 1
|
||||||
else
|
else
|
||||||
self.page = tonumber(self.params.page) or 1
|
self.page = math.max(1, math.min(tonumber(self.params.page) or 1, self.pages))
|
||||||
end
|
end
|
||||||
|
|
||||||
local post_count = Posts:count(db.clause({
|
|
||||||
thread_id = thread.id
|
|
||||||
}))
|
|
||||||
self.pages = math.ceil(post_count / POSTS_PER_PAGE)
|
|
||||||
-- self.page = math.max(1, math.min(self.page, self.pages))
|
-- self.page = math.max(1, math.min(self.page, self.pages))
|
||||||
local posts = db.query([[
|
local posts = db.query([[
|
||||||
SELECT
|
SELECT
|
||||||
|
@ -12,6 +12,8 @@ local Avatars = models.Avatars
|
|||||||
local Topics = models.Topics
|
local Topics = models.Topics
|
||||||
local Threads = models.Threads
|
local Threads = models.Threads
|
||||||
|
|
||||||
|
local THREADS_PER_PAGE = 10
|
||||||
|
|
||||||
local ThreadCreateError = {
|
local ThreadCreateError = {
|
||||||
OK = 0,
|
OK = 0,
|
||||||
GUEST = 1,
|
GUEST = 1,
|
||||||
@ -20,7 +22,27 @@ local ThreadCreateError = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app:get("all_topics", "", function(self)
|
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)
|
self.me = util.get_logged_in_user_or_transient(self)
|
||||||
return {render = "topics.topics"}
|
return {render = "topics.topics"}
|
||||||
end)
|
end)
|
||||||
@ -48,13 +70,17 @@ app:post("topic_create", "/create", function(self)
|
|||||||
local time = os.time()
|
local time = os.time()
|
||||||
local slug = lapis_util.slugify(topic_name) .. "-" .. time
|
local slug = lapis_util.slugify(topic_name) .. "-" .. time
|
||||||
|
|
||||||
|
local topic_count = Topics:count()
|
||||||
local topic = Topics:create({
|
local topic = Topics:create({
|
||||||
name = topic_name,
|
name = topic_name,
|
||||||
description = topic_description,
|
description = topic_description,
|
||||||
slug = slug,
|
slug = slug,
|
||||||
|
sort_order = topic_count + 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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)
|
end)
|
||||||
|
|
||||||
app:get("topic", "/:slug", function(self)
|
app:get("topic", "/:slug", function(self)
|
||||||
@ -64,11 +90,51 @@ app:get("topic", "/:slug", function(self)
|
|||||||
if not topic then
|
if not topic then
|
||||||
return {status = 404}
|
return {status = 404}
|
||||||
end
|
end
|
||||||
|
local threads_count = Threads:count(db.clause({
|
||||||
|
topic_id = topic.id
|
||||||
|
}))
|
||||||
self.topic = topic
|
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.max(math.ceil(threads_count / THREADS_PER_PAGE), 1)
|
||||||
|
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)
|
local user = util.get_logged_in_user_or_transient(self)
|
||||||
print(topic.is_locked, type(topic.is_locked))
|
|
||||||
self.me = user
|
self.me = user
|
||||||
|
|
||||||
self.ThreadCreateError = ThreadCreateError
|
self.ThreadCreateError = ThreadCreateError
|
||||||
self.thread_create_error = ThreadCreateError.OK
|
self.thread_create_error = ThreadCreateError.OK
|
||||||
if user:is_logged_in_guest() then
|
if user:is_logged_in_guest() then
|
||||||
@ -79,7 +145,7 @@ app:get("topic", "/:slug", function(self)
|
|||||||
self.thread_create_error = ThreadCreateError.TOPIC_LOCKED
|
self.thread_create_error = ThreadCreateError.TOPIC_LOCKED
|
||||||
end
|
end
|
||||||
|
|
||||||
self.page_title = "all threads in " .. topic.name
|
self.page_title = "browsing topic " .. topic.name
|
||||||
|
|
||||||
return {render = "topics.topic"}
|
return {render = "topics.topic"}
|
||||||
end)
|
end)
|
||||||
|
@ -150,9 +150,11 @@ app:post("user_clear_avatar", "/:username/clear_avatar", function(self)
|
|||||||
if me.id ~= target_user.id then
|
if me.id ~= target_user.id then
|
||||||
return {redirect_to = self:url_for("user", {username = self.params.username})}
|
return {redirect_to = self:url_for("user", {username = self.params.username})}
|
||||||
end
|
end
|
||||||
|
local old_avatar_id = target_user.avatar_id
|
||||||
target_user:update({
|
target_user:update({
|
||||||
avatar_id = db.NULL,
|
avatar_id = 1,
|
||||||
})
|
})
|
||||||
|
util.destroy_avatar(old_avatar_id)
|
||||||
util.inject_infobox(self, "Avatar cleared.")
|
util.inject_infobox(self, "Avatar cleared.")
|
||||||
return {redirect_to = self:url_for("user_settings", {username = self.params.username})}
|
return {redirect_to = self:url_for("user_settings", {username = self.params.username})}
|
||||||
end)
|
end)
|
||||||
|
@ -2,6 +2,7 @@ local config = require("lapis.config")
|
|||||||
local secrets = require("secrets")
|
local secrets = require("secrets")
|
||||||
|
|
||||||
config({"development", "production"}, {
|
config({"development", "production"}, {
|
||||||
|
port = 8080,
|
||||||
server = "nginx",
|
server = "nginx",
|
||||||
code_cache = "off",
|
code_cache = "off",
|
||||||
num_workers = "1",
|
num_workers = "1",
|
||||||
@ -21,4 +22,5 @@ config("production", {
|
|||||||
sqlite = {
|
sqlite = {
|
||||||
database = "db.prod.sqlite"
|
database = "db.prod.sqlite"
|
||||||
},
|
},
|
||||||
|
session_name = "porom_session_s"
|
||||||
})
|
})
|
||||||
|
@ -4,6 +4,17 @@ local constants = require("constants")
|
|||||||
|
|
||||||
local alphabet = "-_@0123456789abcdefghijklmnopqrstuvwABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
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 function create_admin()
|
||||||
local username = "admin"
|
local username = "admin"
|
||||||
local root_count = models.Users:count("username = ?", username)
|
local root_count = models.Users:count("username = ?", username)
|
||||||
@ -44,5 +55,6 @@ local function create_deleted_user()
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_default_avatar()
|
||||||
create_admin()
|
create_admin()
|
||||||
create_deleted_user()
|
create_deleted_user()
|
||||||
|
13
docker-compose.yaml
Normal file
13
docker-compose.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Generate a random secret key
|
||||||
|
# export PROD_SECRET_KEY=$(openssl rand -hex 32)
|
||||||
|
# Start the container
|
||||||
|
# docker-compose up
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
porom:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- PROD_SECRET_KEY=${PROD_SECRET_KEY}
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
36
dockerfile
Normal file
36
dockerfile
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# HOW TO:
|
||||||
|
#
|
||||||
|
# Generate a random secret key & build the Docker image
|
||||||
|
# ```sh
|
||||||
|
# SECRET_KEY=$(openssl rand -hex 32) docker build --build-arg PROD_SECRET_KEY="$SECRET_KEY" -t porom:latest .
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# Then run the container
|
||||||
|
# ```sh
|
||||||
|
# docker run -d -p 8080:8080 --name porom porom:latest
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
FROM openresty/openresty:alpine-fat
|
||||||
|
COPY ./nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
|
||||||
|
COPY . /usr/local/openresty/nginx/html
|
||||||
|
WORKDIR /usr/local/openresty/nginx/html
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
make \
|
||||||
|
git \
|
||||||
|
make \
|
||||||
|
gcc \
|
||||||
|
g++ \
|
||||||
|
musl-dev \
|
||||||
|
libffi-dev \
|
||||||
|
openssl-dev \
|
||||||
|
sqlite-dev \
|
||||||
|
imagemagick-dev \
|
||||||
|
lua5.1 \
|
||||||
|
lua5.1-dev
|
||||||
|
RUN eval "$(luarocks --lua-version 5.1 path)"
|
||||||
|
RUN luarocks --lua-version 5.1 build --only-deps
|
||||||
|
ARG PROD_SECRET_KEY
|
||||||
|
RUN echo "return { key = \"${PROD_SECRET_KEY}\",}" > /usr/local/openresty/nginx/html/secrets.lua
|
||||||
|
EXPOSE 8080
|
||||||
|
RUN chmod +x /usr/local/openresty/nginx/html/start.sh
|
||||||
|
ENTRYPOINT ["/usr/local/openresty/nginx/html/start.sh", "production"]
|
@ -52,4 +52,15 @@ return {
|
|||||||
[6] = function ()
|
[6] = function ()
|
||||||
schema.drop_column("post_history", "user_id")
|
schema.drop_column("post_history", "user_id")
|
||||||
end,
|
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
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ function Users_mt:is_logged_in_guest()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Users_mt:is_default_avatar()
|
function Users_mt:is_default_avatar()
|
||||||
return self.avatar_id == nil
|
return self.avatar_id == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
function Users_mt:is_logged_in()
|
function Users_mt:is_logged_in()
|
||||||
|
25
porom-dev-1.rockspec
Normal file
25
porom-dev-1.rockspec
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package = "porom"
|
||||||
|
version = "dev-1"
|
||||||
|
|
||||||
|
source = {
|
||||||
|
url = "ssh://gitea@git.poto.cafe:222/yagich/porom.git"
|
||||||
|
}
|
||||||
|
|
||||||
|
description = {
|
||||||
|
summary = "Homegrown forum software",
|
||||||
|
homepage = "",
|
||||||
|
license = "CNPLv7+"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies = {
|
||||||
|
"lua ~> 5.1",
|
||||||
|
"lapis == 1.16.0",
|
||||||
|
"lsqlite3",
|
||||||
|
"magick",
|
||||||
|
"bcrypt",
|
||||||
|
"luaossl",
|
||||||
|
}
|
||||||
|
|
||||||
|
build = {
|
||||||
|
type = "none"
|
||||||
|
}
|
@ -35,6 +35,10 @@ $button_color: color.adjust($accent_color, $hue: 90);
|
|||||||
&:active {
|
&:active {
|
||||||
background-color: color.scale($color, $lightness: -10%, $saturation: -70%);
|
background-color: color.scale($color, $lightness: -10%, $saturation: -70%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: color.scale($color, $lightness: 30%, $saturation: -90%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin navbar($color) {
|
@mixin navbar($color) {
|
||||||
@ -46,7 +50,7 @@ $button_color: color.adjust($accent_color, $hue: 90);
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
margin: 20px;
|
margin: 20px 100px;
|
||||||
background-color: $main_bg;
|
background-color: $main_bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,6 +90,8 @@ body {
|
|||||||
|
|
||||||
.thread-title {
|
.thread-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post {
|
.post {
|
||||||
@ -187,6 +193,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button, input[type="submit"], .linkbutton {
|
button, input[type="submit"], .linkbutton {
|
||||||
|
display: inline-block;
|
||||||
@include button($button_color);
|
@include button($button_color);
|
||||||
|
|
||||||
&.critical {
|
&.critical {
|
||||||
@ -205,6 +212,10 @@ input[type="file"]::file-selector-button {
|
|||||||
margin: 10px 10px;
|
margin: 10px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.pagebutton {
|
.pagebutton {
|
||||||
@include button($button_color);
|
@include button($button_color);
|
||||||
padding: 5px 5px;
|
padding: 5px 5px;
|
||||||
@ -245,13 +256,18 @@ input[type="file"]::file-selector-button {
|
|||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"], input[type="password"] {
|
input[type="text"], input[type="password"], textarea, select {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: $lighter;
|
resize: vertical;
|
||||||
|
background-color: color.scale($accent_color, $lightness: 40%);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: color.scale($accent_color, $lightness: 60%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.infobox {
|
.infobox {
|
||||||
@ -277,3 +293,76 @@ input[type="text"], input[type="password"] {
|
|||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
padding-right: 15px;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
return {
|
return {
|
||||||
key = PROD_SECRET_KEY_HERE,
|
key = "PROD_SECRET_KEY_HERE",
|
||||||
}
|
}
|
||||||
|
28
start.sh
Executable file
28
start.sh
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
start() {
|
||||||
|
lapis serve
|
||||||
|
}
|
||||||
|
|
||||||
|
first_launch() {
|
||||||
|
echo "Setting up for the first time"
|
||||||
|
touch ".first_launch.$LAPIS_ENVIRONMENT"
|
||||||
|
lua5.1 schema.lua
|
||||||
|
lapis migrate
|
||||||
|
lua5.1 create_default_accounts.lua
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
export LAPIS_ENVIRONMENT="development"
|
||||||
|
echo "WARN: no environment passed, assuming default (development)"
|
||||||
|
else
|
||||||
|
export LAPIS_ENVIRONMENT="$1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting in $LAPIS_ENVIRONMENT"
|
||||||
|
|
||||||
|
if ! [ -f ".first_launch.$LAPIS_ENVIRONMENT" ]; then
|
||||||
|
first_launch
|
||||||
|
fi
|
||||||
|
|
||||||
|
start
|
BIN
static/avatars/default.webp
Normal file
BIN
static/avatars/default.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
103
static/style.css
103
static/style.css
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
margin: 20px;
|
margin: 20px 100px;
|
||||||
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
|
background-color: rgb(173.5214173228, 183.6737007874, 161.0262992126);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +58,8 @@ body {
|
|||||||
|
|
||||||
.thread-title {
|
.thread-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post {
|
.post {
|
||||||
@ -153,6 +155,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button, input[type=submit], .linkbutton {
|
button, input[type=submit], .linkbutton {
|
||||||
|
display: inline-block;
|
||||||
background-color: rgb(177, 206, 204.5);
|
background-color: rgb(177, 206, 204.5);
|
||||||
}
|
}
|
||||||
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
button:hover, input[type=submit]:hover, .linkbutton:hover {
|
||||||
@ -161,6 +164,9 @@ button:hover, input[type=submit]:hover, .linkbutton:hover {
|
|||||||
button:active, input[type=submit]:active, .linkbutton:active {
|
button:active, input[type=submit]:active, .linkbutton:active {
|
||||||
background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323);
|
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 {
|
button.critical, input[type=submit].critical, .linkbutton.critical {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: red;
|
background-color: red;
|
||||||
@ -171,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 {
|
button.critical:active, input[type=submit].critical:active, .linkbutton.critical:active {
|
||||||
background-color: rgb(149.175, 80.325, 80.325);
|
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 {
|
button.warn, input[type=submit].warn, .linkbutton.warn {
|
||||||
background-color: #fbfb8d;
|
background-color: #fbfb8d;
|
||||||
}
|
}
|
||||||
@ -180,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 {
|
button.warn:active, input[type=submit].warn:active, .linkbutton.warn:active {
|
||||||
background-color: rgb(198.3813559322, 198.3813559322, 154.4186440678);
|
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 {
|
input[type=file]::file-selector-button {
|
||||||
background-color: rgb(177, 206, 204.5);
|
background-color: rgb(177, 206, 204.5);
|
||||||
@ -191,6 +203,13 @@ input[type=file]::file-selector-button:hover {
|
|||||||
input[type=file]::file-selector-button:active {
|
input[type=file]::file-selector-button:active {
|
||||||
background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
.pagebutton {
|
.pagebutton {
|
||||||
background-color: rgb(177, 206, 204.5);
|
background-color: rgb(177, 206, 204.5);
|
||||||
@ -206,6 +225,9 @@ input[type=file]::file-selector-button:active {
|
|||||||
.pagebutton:active {
|
.pagebutton:active {
|
||||||
background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323);
|
background-color: rgb(166.6881496063, 178.0118503937, 177.4261417323);
|
||||||
}
|
}
|
||||||
|
.pagebutton:disabled {
|
||||||
|
background-color: rgb(209.535, 211.565, 211.46);
|
||||||
|
}
|
||||||
|
|
||||||
.currentpage {
|
.currentpage {
|
||||||
border: none;
|
border: none;
|
||||||
@ -237,13 +259,17 @@ input[type=file]::file-selector-button:active {
|
|||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=text], input[type=password] {
|
input[type=text], input[type=password], textarea, select {
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: rgb(229.84, 231.92, 227.28);
|
resize: vertical;
|
||||||
|
background-color: rgb(217.8, 225.6, 208.2);
|
||||||
|
}
|
||||||
|
input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus {
|
||||||
|
background-color: rgb(230.2, 235.4, 223.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infobox {
|
.infobox {
|
||||||
@ -267,3 +293,74 @@ input[type=text], input[type=password] {
|
|||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
padding-right: 15px;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
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>
|
28
util.lua
28
util.lua
@ -34,9 +34,6 @@ util.TransientUser = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function util.get_user_avatar_url(req, user)
|
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
|
return Avatars:find(user.avatar_id).file_path
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -72,6 +69,29 @@ function util.validate_and_create_image(input_image, filename)
|
|||||||
return true
|
return true
|
||||||
end
|
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)
|
function util.get_logged_in_user(req)
|
||||||
if req.session.session_key == nil then
|
if req.session.session_key == nil then
|
||||||
return nil
|
return nil
|
||||||
@ -149,9 +169,7 @@ function util.transfer_and_delete_user(user)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function util.pop_infobox(req)
|
function util.pop_infobox(req)
|
||||||
print("1")
|
|
||||||
if not req.session.infobox then return end
|
if not req.session.infobox then return end
|
||||||
print("2")
|
|
||||||
req.infobox = req.session.infobox
|
req.infobox = req.session.infobox
|
||||||
req.session.infobox = nil
|
req.session.infobox = nil
|
||||||
end
|
end
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
<link rel="stylesheet" href="<%= "/static/style.css?" .. math.random(1, 100) %>">
|
<link rel="stylesheet" href="<%= "/static/style.css?" .. math.random(1, 100) %>">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<% render("views.common.topnav") -%>
|
||||||
<% content_for("inner") %>
|
<% content_for("inner") %>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
11
views/common/bbcode_help.etlua
Normal file
11
views/common/bbcode_help.etlua
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<details>
|
||||||
|
<summary>Supported babycode tags</summary>
|
||||||
|
<ul>
|
||||||
|
<li>[b]<b>bold</b>[/b]</li>
|
||||||
|
<li>[i]<i>italic</i>[/i]</li>
|
||||||
|
<li>[s]<del>strikethrough</del>[/s]</li>
|
||||||
|
<li>[url=https://example.com]<a href="https://example.com">labeled URL</a>[/url]</li>
|
||||||
|
<li>[url]<a href="https://unlabeled-url.example.com">https://unlabeled-url.example.com</a>[/url]</li>
|
||||||
|
<li>[code]<code>code block</code>[/code]</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
@ -7,6 +7,10 @@
|
|||||||
<span>
|
<span>
|
||||||
<% if me and me:is_logged_in() then -%>
|
<% if me and me:is_logged_in() then -%>
|
||||||
Welcome, <a href="<%= url_for("user", {username = me.username}) %>"><%= me.username %></a>
|
Welcome, <a href="<%= url_for("user", {username = me.username}) %>"><%= me.username %></a>
|
||||||
|
<% if me:is_mod() then %>
|
||||||
|
•
|
||||||
|
<a href="<%= url_for("user_list") %>">User list</a>
|
||||||
|
<% end %>
|
||||||
<% else -%>
|
<% else -%>
|
||||||
Welcome, guest. Please <a href="<%= url_for("user_signup") %>">sign up</a> or <a href="<%= url_for("user_login") %>">log in</a>
|
Welcome, guest. Please <a href="<%= url_for("user_signup") %>">sign up</a> or <a href="<%= url_for("user_login") %>">log in</a>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
|
8
views/mod/user-list.etlua
Normal file
8
views/mod/user-list.etlua
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<div class="darkbg settings-container">
|
||||||
|
<h1>All users</h1>
|
||||||
|
<ul>
|
||||||
|
<% for _, user in ipairs(users) do %>
|
||||||
|
<li><a href="<%= url_for("user", {username = user.username}) %>"><%= user.username %></a></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
@ -1,13 +1,17 @@
|
|||||||
<h1>New thread</h1>
|
<div class="darkbg settings-container">
|
||||||
<form method="post">
|
<h1>New thread</h1>
|
||||||
<label for="topic_id">Topic:</label>
|
<form method="post">
|
||||||
<select name="topic_id", id="topic_id" autocomplete="off">
|
<label for="topic_id">Topic</label>
|
||||||
<% for _, topic in ipairs(all_topics) do %>
|
<select name="topic_id", id="topic_id" autocomplete="off">
|
||||||
<option value="<%= topic.id %>" <%- params.topic_id == tostring(topic.id) and "selected" or "" %>><%= topic.name %></value>
|
<% for _, topic in ipairs(all_topics) do %>
|
||||||
<% end %>
|
<option value="<%= topic.id %>" <%- params.topic_id == tostring(topic.id) and "selected" or "" %>><%= topic.name %></value>
|
||||||
</select><br>
|
<% end %>
|
||||||
<label for="title">Thread title:</label>
|
</select><br>
|
||||||
<input type="text" id="title" name="title" required><br>
|
<label for="title">Thread title</label>
|
||||||
<textarea id="initial_post" name="initial_post" placeholder="Post body" required></textarea><br>
|
<input type="text" id="title" name="title" placeholder="Required" required>
|
||||||
<input type="submit" value="Create thread">
|
<label for="initial_post">Post body</label>
|
||||||
</form>
|
<textarea id="initial_post" name="initial_post" placeholder="Required" rows=5 required></textarea>
|
||||||
|
<% render "views.common.bbcode_help" %>
|
||||||
|
<input type="submit" value="Create thread">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class="post" id="post-<%= post.id %>">
|
<div class="post" id="post-<%= post.id %>">
|
||||||
<div class="usercard">
|
<div class="usercard">
|
||||||
<a href="<%= url_for("user", {username = post.username}) %>" style="display: contents;">
|
<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>
|
||||||
<a href="<%= url_for("user", {username = post.username}) %>" class="username-link"><%= post.username %></a>
|
<a href="<%= url_for("user", {username = post.username}) %>" class="username-link"><%= post.username %></a>
|
||||||
<% if post.status ~= "" then %>
|
<% if post.status ~= "" then %>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
<% render("views.common.topnav") -%>
|
|
||||||
<% local is_locked = ntob(thread.is_locked) %>
|
<% local is_locked = ntob(thread.is_locked) %>
|
||||||
<main>
|
<main>
|
||||||
<nav class="darkbg">
|
<nav class="darkbg">
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
<h1>Create topic</h1>
|
<div class="darkbg settings-container">
|
||||||
<form method="post">
|
<h1>Create topic</h1>
|
||||||
<input type="text" name="name" id="name" placeholder="Topic name" required><br>
|
<form method="post">
|
||||||
<textarea id="description" name="description" placeholder="Topic description" required></textarea><br>
|
<label for=name>Name</label>
|
||||||
<input type="submit" value="Create topic">
|
<input type="text" name="name" id="name" required><br>
|
||||||
</form>
|
<label for=description>Description</label>
|
||||||
|
<textarea id="description" name="description" required rows=5></textarea><br>
|
||||||
|
<input type="submit" value="Create topic">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<h1>Editing topic <%= topic.name %></h1>
|
<div class="darkbg settings-container">
|
||||||
<form method="post">
|
<h1>Editing topic <%= topic.name %></h1>
|
||||||
<input type="text" name="name" id="name" value="<%= topic.name %>" placeholder="Topic name" required><br>
|
<form method="post">
|
||||||
<textarea id="description" name="description" value="<%= topic.description %>" placeholder="Topic description"></textarea><br>
|
<label for=name>Name</label>
|
||||||
<input type="checkbox" id="is_locked" name="is_locked" value="<%= ntob(topic.is_locked) %>">
|
<input type="text" name="name" id="name" value="<%= topic.name %>" placeholder="Topic name" required>
|
||||||
<label for="is_locked">Locked</label><br>
|
<label for=description>Description</label>
|
||||||
<input type="submit" value="Save changes">
|
<textarea id="description" name="description" placeholder="Topic description" rows=4><%= topic.description %></textarea>
|
||||||
</form>
|
<input type="submit" value="Save changes">
|
||||||
<form method="get" action="<%= url_for("topic", {slug = topic.slug}) %>">
|
<a class="linkbutton" href="<%= url_for("topic", {slug = topic.slug}) %>">Cancel</a><br>
|
||||||
<input type="submit" value="Cancel">
|
<i>Note: to preserve history, you cannot change the topic URL.</i>
|
||||||
</form>
|
</form>
|
||||||
<i>Note: to preserve history, you cannot change the topic URL.</i>
|
</div>
|
||||||
|
@ -1,33 +1,68 @@
|
|||||||
<h1><%= topic.name %></h1>
|
<% if infobox then %>
|
||||||
<h2><%= topic.description %></h2>
|
<% 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 %>
|
<% if #threads_list == 0 then %>
|
||||||
<p>There are no threads in this topic.</p>
|
<p>There are no threads in this topic.</p>
|
||||||
<% else %>
|
<% else %>
|
||||||
<ul>
|
<% for _, thread in ipairs(threads_list) do %>
|
||||||
<% for _, thread in ipairs(threads_list) do %>
|
<% local is_stickied = ntob(thread.is_stickied) %>
|
||||||
<li>
|
<% local is_locked = ntob(thread.is_locked) %>
|
||||||
<a href="<%= url_for("thread", {slug = thread.slug}) %>"><%= thread.title %></a><% if ntob(thread.is_stickied) then %> - pinned<% end %>
|
<div class="thread">
|
||||||
</li>
|
<div class="thread-sticky-container contain-svg">
|
||||||
<% end %>
|
<% if is_stickied then -%>
|
||||||
</ul>
|
<% 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 %>
|
<% end %>
|
||||||
|
|
||||||
<% if thread_create_error == ThreadCreateError.OK then %>
|
<nav id="bottomnav">
|
||||||
<a href=<%= url_for("thread_create", nil, {topic_id = topic.id}) %>>New thread</a>
|
<% render("views.common.pagination", {page_count = pages, current_page = page}) %>
|
||||||
<% elseif thread_create_error == ThreadCreateError.GUEST then %>
|
</nav>
|
||||||
<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 %>
|
|
||||||
|
@ -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 %>
|
<% if #topic_list == 0 then %>
|
||||||
<p>There are no topics.</p>
|
<p>There are no topics.</p>
|
||||||
<% else %>
|
<% else %>
|
||||||
<ul>
|
<% for _, topic in ipairs(topic_list) do %>
|
||||||
<% for i, v in ipairs(topic_list) do %>
|
<% local is_locked = ntob(topic.is_locked) %>
|
||||||
<li>
|
<div class="topic">
|
||||||
<a href=<%= url_for("topic", {slug = v.slug}) %>><%= v.name %></a> - <%= v.description %>
|
<div class="topic-info-container">
|
||||||
</li>
|
<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 %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
|
||||||
<% if me:is_mod() then %>
|
|
||||||
<a href="<%= url_for("topic_create") %>">Create new topic</a>
|
|
||||||
<% end %>
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
<% render("views.common.topnav") -%>
|
|
||||||
<div class="darkbg settings-container">
|
<div class="darkbg settings-container">
|
||||||
<h1>Are you sure you want to delete your account, <%= me.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>This cannot be undone. This will not delete your posts, only anonymize them.</p>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
<% render("views.common.topnav") -%>
|
|
||||||
<div class="darkbg login-container">
|
<div class="darkbg login-container">
|
||||||
<h1>Log In</h1>
|
<h1>Log In</h1>
|
||||||
<% if infobox then %>
|
<% if infobox then %>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<% render("views.common.topnav") -%>
|
<% local disable_avatar = me:is_logged_in_guest() %>
|
||||||
<div class="darkbg settings-container">
|
<div class="darkbg settings-container">
|
||||||
<h1>User settings</h1>
|
<h1>User settings</h1>
|
||||||
<% if infobox then %>
|
<% if infobox then %>
|
||||||
@ -8,7 +8,7 @@
|
|||||||
<img src="<%= avatar_url(me) %>">
|
<img src="<%= avatar_url(me) %>">
|
||||||
<input id="file" type="file" name="avatar" accept="image/*" required>
|
<input id="file" type="file" name="avatar" accept="image/*" required>
|
||||||
<div>
|
<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 %>
|
<% if not me:is_default_avatar() then %>
|
||||||
<input type="submit" value="Clear avatar" formaction="<%= url_for("user_clear_avatar", {username = me.username}) %>" formnovalidate>
|
<input type="submit" value="Clear avatar" formaction="<%= url_for("user_clear_avatar", {username = me.username}) %>" formnovalidate>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
<% render("views.common.topnav") -%>
|
|
||||||
<div class="darkbg login-container">
|
<div class="darkbg login-container">
|
||||||
<h1>Sign up</h1>
|
<h1>Sign up</h1>
|
||||||
<% if infobox then %>
|
<% if infobox then %>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
<% render("views.common.topnav") -%>
|
|
||||||
<% if infobox then %>
|
<% if infobox then %>
|
||||||
<% render("views.common.infobox", pop_infobox) %>
|
<% render("views.common.infobox", pop_infobox) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
Reference in New Issue
Block a user