Compare commits
14 commits
667c0222e9
...
922b8e5732
Author | SHA1 | Date | |
---|---|---|---|
922b8e5732 | |||
9ee1d589eb | |||
|
83ae8507c0 | ||
e3b6345196 | |||
17486e4288 | |||
8e1eff30f5 | |||
a77bee5fde | |||
31b0e86d09 | |||
dced1b0a02 | |||
2a8e26b7b2 | |||
78e78c0694 | |||
edf9fc83dc | |||
080e1e6ced | |||
adb19e2e34 |
|
@ -1,6 +1,7 @@
|
|||
MONGO_DB_URL = mongodb://localhost:27017/akf-forum
|
||||
SECRET = secret
|
||||
DISCORD_SECRET = yourDiscordSecret
|
||||
DISCORD_ID = yourDiscordId
|
||||
EMAIL_USER =
|
||||
EMAIL_PASS =
|
||||
EMAIL_SERVICE =
|
|
@ -20,7 +20,7 @@
|
|||
"browser": true
|
||||
},
|
||||
"files": [
|
||||
"public/**"
|
||||
"src/public/js/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
31
README.md
|
@ -6,18 +6,19 @@ A Node.js based forum software.
|
|||
- Run `npm i` to install **dependencies**.
|
||||
- Enter your database credentials in `.env`.
|
||||
- Run `npm start` for run it.
|
||||
- Go /setup page for setup your forum.
|
||||
- Go `/setup` page for setup your forum.
|
||||
|
||||
### Extra (If you are not use `setup` page)
|
||||
Run `node util/reset` to **reset the database** for duplicate key errors, and run `node util/admin` for give admin perms to first member.
|
||||
Edit `config.json` for default theme color (`black` or `white`) of users, and forum name, meta description, character limits, discord auth enabler, global ratelimit.
|
||||
Edit `config.json` for default theme for users, forum name, meta description, character limits, discord auth enabler, global ratelimit etc.
|
||||
|
||||
### How to install theme:
|
||||
- Copy your theme to `public/themes` folder.
|
||||
- Copy your theme to `src/themes` folder.
|
||||
Additional note for themes: If a theme has not got any .ejs file, it will use default theme's .ejs files. default theme is in themes folder, named as `common`.
|
||||
|
||||
### DISCORD AUTH:
|
||||
`"discord_auth": "your_app_id"` in config.json.
|
||||
Add your app secret to `.env` as `DISCORD_SECRET`.
|
||||
`"discord_auth": true` in config.json.
|
||||
Add your app secret and app id to `.env` as `DISCORD_SECRET` and `DISCORD_ID`.
|
||||
Create a redirect url in discord developer portal:
|
||||
`https://forum_url.com/auth/discord`
|
||||
|
||||
|
@ -39,14 +40,8 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn
|
|||
|
||||
## Screenshot
|
||||
|
||||
### Thread Page w/Black Theme
|
||||
![black-theme](https://user-images.githubusercontent.com/70021050/187899782-2ff010aa-0d39-4fc2-b00c-19bcf1623c8a.png)
|
||||
### Threads Page w/Default Theme
|
||||
![light-theme](https://user-images.githubusercontent.com/70021050/186941146-f9a8fbf8-9b2b-4028-afc8-81cff559d9fb.png)
|
||||
<details>
|
||||
<summary><b>Mobile view</b></summary>
|
||||
<img src="https://user-images.githubusercontent.com/70021050/187901065-fd75ef85-56e3-42ce-8b34-cb8d799a6517.png"></img>
|
||||
</details>
|
||||
### Thread Page w/Bootstrap theme
|
||||
![image](https://github.com/Akif9748/akf-forum/assets/70021050/1ad4ad8e-d000-46a6-834e-7d76cdddda60)
|
||||
|
||||
## TO-DO list
|
||||
- Profile Message or DM
|
||||
|
@ -59,11 +54,15 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn
|
|||
- change category name
|
||||
- _id
|
||||
- add support for transition around gravatar
|
||||
- forum setup page rewrite and directly open a router
|
||||
- login w/ email
|
||||
- BETTER SETUP PAGE
|
||||
- add used open source libraries to README.md
|
||||
- send public to common/public
|
||||
- user.ejs for per theme
|
||||
- categori search title like thread search
|
||||
|
||||
### front-end
|
||||
- working reset button
|
||||
- better pagination
|
||||
- text alling center body
|
||||
- add a css file for CodeMirror in threads / send message ok
|
||||
- old contents / titles add to forum interface
|
||||
|
@ -92,4 +91,4 @@ threads:
|
|||
- V3: New Theme
|
||||
- V2: Backend fix, mongoose is fixed. Really big fix.
|
||||
- V1: Mongoose added.
|
||||
- V0: Birth with quick.db
|
||||
- V0: Birth with quick.db
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"max": 25,
|
||||
"windowMs": 60000
|
||||
},
|
||||
"discord_auth": "",
|
||||
"discord_auth": false,
|
||||
"default_thread_state": "OPEN",
|
||||
"default_user_state": "ACTIVE",
|
||||
"email_auth": false,
|
||||
|
|
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "akf-forum",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.6",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "akf-forum",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.6",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.0",
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "akf-forum",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.6",
|
||||
"description": "A Node.js based forum software",
|
||||
"main": "index.js",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node .",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
<% if(user?.admin || user?.id === member.id){ %>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#userMenu" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="userMenu">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<% if (!member.discordID && discord && user?.id === member.id) { %>
|
||||
<a href="<%=discord%>" class="btn-outline-primary">Discord auth</a>
|
||||
<% } else if(member.discordID && user?.id === member.id) { %>
|
||||
<a class="btn-primary" id="un_discord">Unauth Discord</a>
|
||||
<% } %>
|
||||
<% if (member.hideLastSeen) {%>
|
||||
<a id="last_unhide" class="btn-primary">Unhide last seen</a>
|
||||
<% } else { %>
|
||||
<a id="last_hide" class="btn-outline-primary">Hide last seen</a>
|
||||
<% } %>
|
||||
<% if (member.deleted) {%>
|
||||
<h1>This user has been deleted!</h1>
|
||||
<a id="undelete" class="btn-primary">Undelete user</a>
|
||||
<% } else if (user?.admin){ %>
|
||||
<a id="delete" class="btn-outline-primary">Delete user</a>
|
||||
<% } %>
|
||||
<a class="btn-outline-primary" href="/users/<%=member.id%>/edit">Edit user</a>
|
||||
<a href="/users/<%=member.id%>/avatar" class="btn-outline-primary">Upload avatar</a>
|
||||
</ul>
|
||||
|
||||
<script type="module">
|
||||
import request from "../../js/request.js";
|
||||
|
||||
|
||||
document.addEventListener("click", async e => {
|
||||
if (e.target.id == "delete") {
|
||||
const response = await request("/api/users/<%= member.id %>", "DELETE");
|
||||
if (response.state !== "DELETED") return
|
||||
alert("User is deleted!");
|
||||
location.reload()
|
||||
} else if (e.target.id == "undelete") {
|
||||
const response = await request("/api/users/<%= member.id %>/", "PATCH", {
|
||||
deleted: false
|
||||
});
|
||||
if (response.state == "DELETED") return;
|
||||
alert("User is undeleted successfully!");
|
||||
location.reload()
|
||||
} else if (e.target.id == "un_discord") {
|
||||
const response = await fetch("/auth/discord/", {
|
||||
method: "DELETE"
|
||||
});
|
||||
alert(await response.text());
|
||||
location.reload()
|
||||
} else if (e.target.id.startsWith("last_")) {
|
||||
let hideLastSeen = e.target.id.replace("last_", "") == "hide" ? true : false;
|
||||
const response = await request("/api/users/<%= member.id %>/", "PATCH", {
|
||||
hideLastSeen
|
||||
});
|
||||
alert(`Last seen is ${!hideLastSeen?"un":""}hided!`);
|
||||
location.reload()
|
||||
|
||||
} else if (e.target.id == "toogle")
|
||||
document.getElementById('user-edit').classList.toggle('no-active')
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</nav>
|
||||
<% if (user.admin) { %>
|
||||
<select>
|
||||
<option selected>IP LIST</option>
|
||||
<% for(const ip of member.ips) { %>
|
||||
<option><%= ip %></option>
|
||||
<% } %>
|
||||
</select>
|
||||
<% } %>
|
||||
|
||||
<% } %>
|
|
@ -1,44 +0,0 @@
|
|||
<div class="footer">
|
||||
<% if (user){ %>
|
||||
<select id="theme_select">
|
||||
<% for(const theme of dataset.themes){%>
|
||||
<option value="<%= theme.codename %>"><%= theme.name %></option>
|
||||
<% } %>
|
||||
</select>
|
||||
<script>
|
||||
const theme_select = document.getElementById("theme_select");
|
||||
theme_select.querySelector(`option[value=<%= user.theme.codename %>]`).selected = true;
|
||||
theme_select.addEventListener("change", async e => {
|
||||
const codename = e.target.value;
|
||||
await fetch('/api/users/<%= user.id %>', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
theme: {
|
||||
codename
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
const theme = await fetch("/api/themes/" + codename).then(res => res.json());
|
||||
const txt = "Theme changed to:\n" +
|
||||
"Name: " + theme.name + "\n" +
|
||||
"Description: " + theme.description + "\n" +
|
||||
"Author: " + theme.author + "\n";
|
||||
alert(txt);
|
||||
location.reload();
|
||||
});
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
<a href="https://github.com/Akif9748/akf-forum" style="color: white;"> This website is powered by
|
||||
<span style="color: #ffbf00;">akf-forum</span>
|
||||
</a>
|
||||
<div>
|
||||
<span style="color:white">Coders</span> <br>
|
||||
<div style="text-align:center;">
|
||||
<a href="https://github.com/Akif9748/" style="color: #ffbf00;">Akif</a><br><a href="#" style="color:#ffbf00;">Tokmak</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,45 +0,0 @@
|
|||
|
||||
<% if (user?.admin){ %>
|
||||
<div class="admin-bar">
|
||||
<a href="/admin" class="admin-bar">Click here to reach admin panel</a>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="header">
|
||||
<a class="logo" href="/"><%= dataset.forum_name.toUpperCase() %> <span>FORUM</span></a>
|
||||
<div class="buttons">
|
||||
|
||||
<% if (user){ %>
|
||||
<a href="<%=user.getLink() %>" class="btn-outline-primary">
|
||||
<div class="box-username"><%= user.name %>
|
||||
<div class="avatar"><img src="<%=user.avatar %>"></div>
|
||||
</div>
|
||||
</a>
|
||||
<a id="logout" href="/login" class="btn-primary">Logout</a>
|
||||
<% } else { %>
|
||||
|
||||
<a id="login" href="/login" class="btn-primary">Login</a>
|
||||
<a href="/register" class="btn-outline-primary">Register</a>
|
||||
<script>
|
||||
document.getElementById("login").href += "?redirect=" + location.pathname;
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu">
|
||||
<a href="/threads/create/" class="menu-item">Create Thread</a>
|
||||
<a href="/categories" class="menu-item">Categories</a>
|
||||
<a href="/threads" class="menu-item">Threads</a>
|
||||
<a href="/users" class="menu-item">Users</a>
|
||||
<a href="/search" class="menu-item">Search</a>
|
||||
<script>
|
||||
const menuItems = document.getElementsByClassName("menu-item");
|
||||
|
||||
for (let i = 0; i < menuItems.length; i++)
|
||||
if (window.location.pathname.includes(menuItems[i].getAttribute("href"))) {
|
||||
menuItems[i].classList.add("active-menu");
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
</div>
|
|
@ -1,72 +0,0 @@
|
|||
<% if(user?.admin || user?.id === member.id){ %>
|
||||
<details>
|
||||
<summary class="btn-outline-primary">User Menu:</summary>
|
||||
|
||||
<% if (!member.discordID && discord && user?.id === member.id) { %>
|
||||
<a href="<%=discord%>" class="btn-outline-primary">DC auth</a>
|
||||
<% } else if(member.discordID && user?.id === member.id) { %>
|
||||
<a class="btn-outline-primary" id="un_discord">Unauth DC!</a>
|
||||
<% } %>
|
||||
<a href="/users/<%=member.id%>/avatar" class="btn-outline-primary">Upload avatar</a>
|
||||
<a class="btn-outline-primary" id="toogle">Edit user!</a>
|
||||
<script type="module">
|
||||
import request from "/js/request.js";
|
||||
|
||||
const form = document.getElementById("form");
|
||||
|
||||
document.addEventListener("click", async e => {
|
||||
if (e.target.id == "delete") {
|
||||
const response = await request("/api/users/<%= member.id %>", "DELETE");
|
||||
if (response.state !== "DELETED") return
|
||||
alert("User is deleted!");
|
||||
location.reload()
|
||||
} else if (e.target.id == "undelete") {
|
||||
const response = await request("/api/users/<%= member.id %>/", "PATCH", {
|
||||
deleted: false
|
||||
});
|
||||
if (response.state == "DELETED") return;
|
||||
alert("User is undeleted successfully!");
|
||||
location.reload()
|
||||
} else if (e.target.id == "un_discord") {
|
||||
const response = await fetch("/auth/discord/", {
|
||||
method: "DELETE"
|
||||
});
|
||||
alert(await response.text());
|
||||
location.reload()
|
||||
} else if (e.target.id.startsWith("last_")) {
|
||||
let hideLastSeen = e.target.id.replace("last_", "") == "hide" ? true : false;
|
||||
const response = await request("/api/users/<%= member.id %>/", "PATCH", {
|
||||
hideLastSeen
|
||||
});
|
||||
alert(`Last seen is ${!hideLastSeen?"un":""}hided!`);
|
||||
location.reload()
|
||||
|
||||
} else if (e.target.id == "toogle")
|
||||
document.getElementById('user-edit').classList.toggle('no-active')
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<% if (member.hideLastSeen) {%>
|
||||
<a id="last_unhide" class="btn-primary">Unhide last seen! </a>
|
||||
<% } else { %>
|
||||
<a id="last_hide" class="btn-outline-primary">Hide last seen! </a>
|
||||
<% } %>
|
||||
|
||||
<% if (member.deleted) {%>
|
||||
<h1>This user has been deleted!</h1>
|
||||
<a id="undelete" class="btn-primary">Undelete user! </a>
|
||||
<% } else if (user?.admin){ %>
|
||||
<a id="delete" class="btn-outline-primary">Delete user! </a>
|
||||
<% } %>
|
||||
<% if (user?.admin) {%>
|
||||
<h2>IP adresses of the user:</h2>
|
||||
<select>
|
||||
<% for(const ip of member.ips) { %>
|
||||
<option><%= ip %></option>
|
||||
<% } %>
|
||||
</select>
|
||||
|
||||
<% } %>
|
||||
</details>
|
||||
<% } %>
|
|
@ -1 +0,0 @@
|
|||
<%- include(dataset.getFile("common/footer")) %>
|
|
@ -1 +0,0 @@
|
|||
<%- include(dataset.getFile("common/navbar")) %>
|
|
@ -1 +0,0 @@
|
|||
<%- include(dataset.getFile("common/usermenu")) %>
|
|
@ -1 +0,0 @@
|
|||
<%- include(dataset.getFile("common/footer")) %>
|
|
@ -1 +0,0 @@
|
|||
<%- include(dataset.getFile("common/navbar")) %>
|
|
@ -1 +0,0 @@
|
|||
<%- include(dataset.getFile("common/usermenu")) %>
|
45
routes/.js
|
@ -1,45 +0,0 @@
|
|||
const { UserModel, ThreadModel, MessageModel } = require("../models")
|
||||
const { Router } = require("express");
|
||||
const app = Router();
|
||||
const fs = require("fs");
|
||||
|
||||
app.get("/", async (req, res) => {
|
||||
|
||||
const
|
||||
mem = process.memoryUsage().heapUsed / Math.pow(2, 20),
|
||||
users = await UserModel.count({ deleted: false }),
|
||||
threads = await ThreadModel.count({ state: "OPEN" }),
|
||||
messages = await MessageModel.count({ deleted: false });
|
||||
|
||||
res.reply("index", { mem, users, threads, messages })
|
||||
|
||||
})
|
||||
|
||||
app.get("/setup", async (req, res) => {
|
||||
if (await UserModel.exists({ admin: true })) return res.error(400, "You have already setuped the site.");
|
||||
res.reply("setup");
|
||||
})
|
||||
app.post("/setup", async (req, res) => {
|
||||
if (await UserModel.exists({ admin: true })) return res.error(400, "You have already setuped the site.");
|
||||
let original = {};
|
||||
|
||||
try {
|
||||
original = JSON.parse(fs.readFileSync("./config.json", "utf8"));
|
||||
} catch (e) {
|
||||
try {
|
||||
original = JSON.parse(fs.readFileSync("./config.json.example", "utf8"));
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
const content = req.body;
|
||||
|
||||
for (const key in content)
|
||||
if (key in original && content[key])
|
||||
original[key] = content[key];
|
||||
|
||||
fs.writeFileSync("./config.json", JSON.stringify(original,null,4));
|
||||
require.cache[require.resolve("../config.json")] = require("../config.json");
|
||||
res.redirect("/register");
|
||||
})
|
||||
|
||||
module.exports = app;
|
|
@ -1,6 +1,6 @@
|
|||
require("dotenv").config();
|
||||
const
|
||||
{ def_theme, forum_name, description, limits, global_ratelimit: RLS, discord_auth, host } = require("./config.json"),
|
||||
{ def_theme, forum_name, description, limits, global_ratelimit: RLS, discord_auth, host } = require("../config.json"),
|
||||
{ UserModel, BanModel } = require("./models"),
|
||||
port = process.env.PORT || 3000,
|
||||
mongoose = require("mongoose"),
|
||||
|
@ -23,7 +23,11 @@ app.ips = [];
|
|||
app.set("view engine", "ejs");
|
||||
app.set("limits", limits);
|
||||
|
||||
app.use(express.static("public"), express.json(), express.urlencoded({ extended: true }), IP(),
|
||||
for (const theme of fs.readdirSync(join(__dirname, "themes")))
|
||||
app.use(`/themes/${theme}`, express.static(join(__dirname, "themes", theme, "public")));
|
||||
|
||||
|
||||
app.use(express.static(join(__dirname, "public")), express.json(), express.urlencoded({ extended: true }), IP(),
|
||||
SES({ secret: process.env.SECRET, store: MS.create({ clientPromise: DB, stringify: false }), resave: false, saveUninitialized: false }),
|
||||
async (req, res, next) => {
|
||||
if (app.ips.includes(req.clientIp)) return res.status(403).send("You are banned from this forum.");
|
||||
|
@ -37,15 +41,19 @@ app.use(express.static("public"), express.json(), express.urlencoded({ extended:
|
|||
if (!themes.some(t => t.codename === theme.codename))
|
||||
theme = def_theme;
|
||||
|
||||
res.reply = (page, options = {}, status = 200) => res.status(status).render(page, {
|
||||
dataset: {
|
||||
themes, theme, forum_name, description,
|
||||
getFile: file => join(__dirname, "public", "themes", file),
|
||||
},
|
||||
user: req.user,
|
||||
...options
|
||||
});
|
||||
|
||||
res.reply = (page, options = {}, status = 200) => {
|
||||
const road = join(__dirname, "themes", theme.codename, "views", `${page}.ejs`);
|
||||
const renderpage = fs.existsSync(road) ? road : join(__dirname, "themes", "common", "views", `${page}.ejs`);
|
||||
return res.status(status).render(renderpage, {
|
||||
dataset: {
|
||||
themes, theme, forum_name, description,
|
||||
getFile: file => join(__dirname, "themes", file),
|
||||
},
|
||||
user: req.user,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
res.error = (type, error) => res.reply("error", { type, error }, type);
|
||||
|
||||
|
@ -60,14 +68,14 @@ app.use(express.static("public"), express.json(), express.urlencoded({ extended:
|
|||
}
|
||||
);
|
||||
|
||||
if (discord_auth)
|
||||
app.set("discord_auth", `https://discord.com/api/oauth2/authorize?client_id=${discord_auth}&redirect_uri=${host}%2Fauth%2Fdiscord&response_type=code&scope=identify`);
|
||||
|
||||
if (RLS.enabled) app.use(RL(RLS.windowMs, RLS.max));
|
||||
|
||||
for (const file of fs.readdirSync("./routes"))
|
||||
if (discord_auth)
|
||||
app.set("DISCORD_AUTH_URL", `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_ID}&redirect_uri=${host}%2Fauth%2Fdiscord&response_type=code&scope=identify`);
|
||||
|
||||
for (const file of fs.readdirSync(join(__dirname, "routes")))
|
||||
app.use("/" + file.replace(".js", ""), require(`./routes/${file}`));
|
||||
|
||||
app.all("*", (req, res) => res.error(404, "This page does not exist on this forum."));
|
||||
|
||||
app.listen(port, () => console.log(`${forum_name}-forum on port:`, port));
|
||||
app.listen(port, () => console.log(`${forum_name} on port:`, port));
|
|
@ -1,12 +1,12 @@
|
|||
const RL = require('express-rate-limit');
|
||||
const nodemailer = require("nodemailer");
|
||||
const config = require("./config.json");
|
||||
const config = require("../config.json");
|
||||
const crypto = require("crypto");
|
||||
const { readdirSync } = require('fs');
|
||||
|
||||
const { join } = require('path');
|
||||
require("dotenv").config();
|
||||
module.exports = {
|
||||
themes: readdirSync("./public/themes").filter(f => f !== "common").map(f => require(`./public/themes/${f}`)),
|
||||
themes: readdirSync(join(__dirname, "themes")).filter(f => f !== "common").map(f => require(`./themes/${f}`)),
|
||||
threadEnum: ["OPEN", "APPROVAL", "DELETED"],
|
||||
userEnum: ["ACTIVE", "APPROVAL", "DELETED", "BANNED"],
|
||||
RL(windowMs = 60_000, max = 1) {
|
|
@ -1,6 +1,6 @@
|
|||
const mongoose = require("mongoose");
|
||||
const cache = require("./cache");
|
||||
const { limits } = require("../config.json");
|
||||
const { limits } = require("../../config.json");
|
||||
|
||||
const schema = new mongoose.Schema({
|
||||
id: { type: String, unique: true },
|
|
@ -1,7 +1,7 @@
|
|||
const mongoose = require("mongoose");
|
||||
const cache = require("./cache")
|
||||
const MessageModel = require("./Message");
|
||||
const { limits, default_thread_state } = require("../config.json");
|
||||
const { limits, default_thread_state } = require("../../config.json");
|
||||
const { threadEnum } = require("../lib");
|
||||
const schema = new mongoose.Schema({
|
||||
id: { type: String, unique: true },
|
|
@ -1,5 +1,5 @@
|
|||
const mongoose = require("mongoose")
|
||||
const { def_theme, limits, default_user_state } = require("../config.json");
|
||||
const { def_theme, limits, default_user_state } = require("../../config.json");
|
||||
const { userEnum } = require("../lib");
|
||||
|
||||
const schema = new mongoose.Schema({
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
src/public/images/avatars/0.jpg
Normal file
After Width: | Height: | Size: 704 KiB |
BIN
src/public/images/avatars/1.jpg
Normal file
After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
17
src/routes/.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
const { UserModel, ThreadModel, MessageModel, CategoryModel } = require("../models")
|
||||
const { Router } = require("express");
|
||||
const app = Router();
|
||||
|
||||
app.get("/", async (req, res) => {
|
||||
|
||||
const categories = await CategoryModel.count(),
|
||||
users = await UserModel.count({ deleted: false }),
|
||||
threads = await ThreadModel.count({ state: "OPEN" }),
|
||||
messages = await MessageModel.count({ deleted: false }),
|
||||
newestMember = await UserModel.findOne({ deleted: false }, "name").sort({ time: -1 });
|
||||
|
||||
res.reply("index", { categories, users, threads, messages, newestMember: newestMember.name });
|
||||
|
||||
});
|
||||
|
||||
module.exports = app;
|
|
@ -3,7 +3,7 @@ const app = Router();
|
|||
const fs = require("fs");
|
||||
const bcrypt = require("bcrypt");
|
||||
const { UserModel } = require("../../models");
|
||||
|
||||
const{join}=require("path");
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
res.error = (status, error) => res.status(status).json({ error });
|
||||
|
@ -31,7 +31,7 @@ app.use(async (req, res, next) => {
|
|||
|
||||
app.get("/me", (req, res) => res.complate(req.user))
|
||||
|
||||
for (const file of fs.readdirSync("./routes/api/routes"))
|
||||
for (const file of fs.readdirSync(join(__dirname, "routes")))
|
||||
app.use("/" + file.replace(".js", ""), require(`./routes/${file}`));
|
||||
|
||||
app.all("*", (req, res) => res.error(400, "Bad request"));
|
|
@ -16,10 +16,10 @@ app.get("/", (req, res) => {
|
|||
}
|
||||
});
|
||||
app.put("/", (req, res) => {
|
||||
const write= req.query.text ? req.body : JSON.stringify(req.body, null, 4)
|
||||
fs.writeFileSync("./config.json",write );
|
||||
require.cache[require.resolve("../../../config.json")] = require("../../../config.json");
|
||||
res.complate(require("../../../config.json"));
|
||||
const write = req.query.text ? req.body : JSON.stringify(req.body, null, 4)
|
||||
fs.writeFileSync("./config.json", write);
|
||||
require.cache[require.resolve("../../../../config.json")] = require("../../../../config.json");
|
||||
res.complate(require("../../../../config.json"));
|
||||
});
|
||||
|
||||
module.exports = app;
|
|
@ -3,7 +3,7 @@ const { Router } = require("express");
|
|||
const multer = require("multer");
|
||||
const { themes } = require("../../../lib")
|
||||
const app = Router();
|
||||
|
||||
const { join } = require("path");
|
||||
app.param("id", async (req, res, next, id) => {
|
||||
req.member = await UserModel.get(id, req.user.admin ? "+lastSeen +ips" : "");
|
||||
|
||||
|
@ -80,7 +80,7 @@ app.post("/:id/ban", async (req, res) => {
|
|||
});
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination:'./public/images/avatars',
|
||||
destination: join(__dirname, "..", "..", "..", "public", "images", "avatars"),
|
||||
filename: function (req, _file, cb) {
|
||||
cb(null, req.member.id + ".jpg")
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ const storage = multer.diskStorage({
|
|||
const upload = multer({ storage })
|
||||
|
||||
app.post("/:id/avatar", upload.single('avatar'), async (req, res) => {
|
||||
|
||||
|
||||
const { member } = req;
|
||||
|
||||
if (req.user.id !== member.id && !req.user.admin) return res.error(403, "You have not got permission for this.");
|
|
@ -2,10 +2,10 @@ const { Router } = require("express")
|
|||
const { UserModel } = require("../models");
|
||||
const fetch = require("node-fetch");
|
||||
const app = Router();
|
||||
const { host, discord_auth, email_auth } = require("../config.json")
|
||||
const { host, email_auth } = require("../../config.json")
|
||||
|
||||
app.get("/discord", async (req, res) => {
|
||||
const client_id = discord_auth;
|
||||
const client_id = process.env.DISCORD_ID;
|
||||
if (!client_id) return res.error(404, "Discord auth is disabled")
|
||||
const { code } = req.query;
|
||||
if (!code) return res.error(400, "No code provided");
|
|
@ -3,7 +3,7 @@ const { Router } = require("express");
|
|||
const app = Router();
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
app.get("/", (req, res) => res.reply("login", { redirect: req.query.redirect, user: null, discord: req.app.get("discord_auth") }));
|
||||
app.get("/", (req, res) => res.reply("login", { redirect: req.query.redirect, user: null, discord: req.app.get("DISCORD_AUTH_URL") }));
|
||||
|
||||
app.post("/", async (req, res) => {
|
||||
req.session.userID = null;
|
|
@ -3,8 +3,8 @@ const { Router } = require("express")
|
|||
const bcrypt = require("bcrypt");
|
||||
const { RL, transporter, emailRegEx, getGravatar } = require('../lib');
|
||||
const app = Router();
|
||||
const { email_auth, forum_name, host } = require("../config.json");
|
||||
app.get("/", (req, res) => res.reply("register", { user: null, discord: req.app.get("discord_auth"), mail: email_auth }));
|
||||
const { email_auth, forum_name, host } = require("../../config.json");
|
||||
app.get("/", (req, res) => res.reply("register", { user: null, discord: req.app.get("DISCORD_AUTH_URL"), mail: email_auth }));
|
||||
|
||||
app.post("/", RL(24 * 60 * 60_000, 5), async (req, res) => {
|
||||
|
34
src/routes/setup.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
const { UserModel } = require("../models")
|
||||
const { Router } = require("express");
|
||||
const app = Router();
|
||||
const fs = require("fs");
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
if (await UserModel.exists({ admin: true })) return res.error(400, "You have already setuped the site.");
|
||||
next();
|
||||
});
|
||||
app.get("/", async (req, res) => res.reply("setup"))
|
||||
app.post("/", async (req, res) => {
|
||||
let original = {};
|
||||
|
||||
try {
|
||||
original = JSON.parse(fs.readFileSync("./config.json", "utf8"));
|
||||
} catch (e) {
|
||||
try {
|
||||
original = JSON.parse(fs.readFileSync("./config.json.example", "utf8"));
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
const content = req.body;
|
||||
|
||||
for (const key in content)
|
||||
if (key in original && content[key])
|
||||
original[key] = content[key];
|
||||
|
||||
fs.writeFileSync("./config.json", JSON.stringify(original, null, 4));
|
||||
require.cache[require.resolve("../../config.json")] = require("../../config.json");
|
||||
res.redirect("/register");
|
||||
})
|
||||
|
||||
module.exports = app;
|
|
@ -28,7 +28,7 @@ app.get("/:id", async (req, res) => {
|
|||
|
||||
const message = await MessageModel.count({ authorID: id });
|
||||
const thread = await ThreadModel.count({ authorID: id });
|
||||
res.reply("user", { member, counts: { message, thread }, discord: req.app.get("discord_auth") })
|
||||
res.reply("user", { member, counts: { message, thread }, discord: req.app.get("DISCORD_AUTH_URL") })
|
||||
}
|
||||
else res.error(404, `We don't have any user with id ${id}.`);
|
||||
|
|
@ -293,51 +293,6 @@ a {
|
|||
}
|
||||
|
||||
|
||||
/***********************************
|
||||
PAGINATION
|
||||
***********************************/
|
||||
.pagination {
|
||||
box-shadow: 0 0 5px 0 var(--box-shadow);
|
||||
margin: 10px auto;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pagination .back,
|
||||
.pagination .after {
|
||||
color: var(--second);
|
||||
font-size: 26px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.pagination .numbers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.pagination .number {
|
||||
color: var(--second);
|
||||
font-size: 22px;
|
||||
border: 0 0 5px var(--second);
|
||||
padding: 8px;
|
||||
border-radius: 2px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.pagination .number.active {
|
||||
color: var(--main);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
/************************************
|
||||
Threads
|
124
src/themes/bootstrap_black/views/admin.ejs
Normal file
|
@ -0,0 +1,124 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Admin Panel!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<style>
|
||||
table {
|
||||
font-family: arial, sans-serif;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border: 1px solid #dddddd;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
color: var(--anti);
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #dddddd;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h2>Welcome to the admin panel of the forum, <%= user.name %>!</h1>
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLabel">Banned users</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="document.getElementById('exampleModal').style.display = 'none';">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table>
|
||||
<tr>
|
||||
<th>IP</th>
|
||||
<th>Reason</th>
|
||||
<th>AuthorID</th>
|
||||
</tr>
|
||||
<% for (const ban of bans) { %>
|
||||
<tr>
|
||||
<td><%=ban.ip%></td>
|
||||
<td><%=ban.reason%></td>
|
||||
<td><%=ban.authorID%></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn-primary" onclick="ban();">IP BAN</a>
|
||||
<a class="btn-outline-primary" onclick="unban();">REMOVE IP BAN</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<button onclick="window.location.href = '/categories/create';" class="btn-primary">Create Category</button>
|
||||
<button onclick="window.location.href = '/admin/config';" class="btn-primary">Edit config</button>
|
||||
|
||||
<button onclick="document.getElementById('exampleModal').style.display = 'block';" class="btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#exampleModal" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
Banned users
|
||||
</button>
|
||||
<div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Admin list</th>
|
||||
</tr>
|
||||
<% for (const admin of admins) { %>
|
||||
<tr>
|
||||
<td><a style="color: var(--anti);" href="<%= admin.getLink() %>"><%= admin.name %></a></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</table>
|
||||
<ul>
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import request from "../../js/request.js";
|
||||
|
||||
window.unban = async function() {
|
||||
const ip = prompt("Enter ip to unban");
|
||||
if (!ip) return;
|
||||
const response = await request("/api/bans/" + ip, "DELETE");
|
||||
if (response)
|
||||
alert("IP unbanned!");
|
||||
else
|
||||
alert("IP is not unbanned!");
|
||||
location.reload();
|
||||
}
|
||||
window.ban = async function() {
|
||||
const ip = prompt("Enter ip to ban");
|
||||
if (!ip) return;
|
||||
|
||||
const response = await request("/api/bans/" + ip);
|
||||
if (response)
|
||||
alert("IP banned!");
|
||||
else
|
||||
alert("IP is not banned!");
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
74
src/themes/bootstrap_black/views/categories.ejs
Normal file
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Category list!" }) %>
|
||||
|
||||
<body>
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<div class="container my-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 text-white bg-info mb-0 p-4 rounded-top"><%= "Categories" %></h2>
|
||||
|
||||
<table class="table table-striped table-bordered table-responsive-lg">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col" class="topic-col">Topic</th>
|
||||
<th scope="col">Description</th>
|
||||
<% if (user?.admin){ %> <th scope="col" class="last-post-col">Action</th> <% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% categories.forEach(category=>{ %>
|
||||
<tr>
|
||||
<td>
|
||||
<h3 class="h6">
|
||||
<a href="<%= category.getLink() %>"><%= category.name %></a>
|
||||
</h3>
|
||||
</td>
|
||||
<td>
|
||||
<div><%= category.desp %></div>
|
||||
</td>
|
||||
<% if (user?.admin){ %>
|
||||
<td>
|
||||
<a class="btn-danger" onclick="fetch('/api/categories/<%= category.id %>/',{method:'DELETE'})"><i class="bx bx-trash bx-sm"></i></a>
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% }); %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<% if(typeof page === "number"){ %>
|
||||
<div class="mb-3 clearfix">
|
||||
<nav aria-label="Navigate post pages" class="float-lg-right">
|
||||
<ul class="pagination pagination-sm mb-lg-0">
|
||||
<% if (page > 0){ %>
|
||||
<li class="page-item"><a href="/categories?page=<%= page-1 %>" class="page-link">Back</a></li>
|
||||
<% } %>
|
||||
<% for(let i=0; i < pages; i++){ %>
|
||||
<li class="page-item <%= i==page?'active':'' %>"><a href="/categories?page=<%= i %>" class="page-link"><%= i+1 %>
|
||||
<% if (i==page){ %>
|
||||
<span class="sr-only">(current)</span>
|
||||
<% } %>
|
||||
</a></li>
|
||||
<% } %>
|
||||
<% if (pages-1 > page) { %>
|
||||
<li class="page-item"><a href="/categories?page=<%= page+1 %>" class="page-link">Next</a></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
59
src/themes/bootstrap_black/views/create_category.ejs
Normal file
|
@ -0,0 +1,59 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Create Category!" }) %>
|
||||
|
||||
<body>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
|
||||
<script src="/libs/simplemde/simplemde.min.js"></script>
|
||||
|
||||
<div class="container my-3">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 text-white bg-info mb-3 p-4 rounded">Create new category</h2>
|
||||
<form class="mb-3">
|
||||
<div class="form-group">
|
||||
<label for="topic">Name</label>
|
||||
<input type="text" class="form-control" id="title" placeholder="Give your category a name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="comment">Description</label>
|
||||
<textarea id="textarea"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
<button type="reset" class="btn btn-danger">Reset</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script src="/js/editor.js"></script>
|
||||
|
||||
|
||||
<script type="module">
|
||||
const simplemde = editor("category-create");
|
||||
|
||||
import request from "../../js/request.js";
|
||||
|
||||
document.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
const response = await request("/api/categories/", "POST", {
|
||||
name: document.getElementById("title").value,
|
||||
desp: simplemde.value()
|
||||
});
|
||||
|
||||
simplemde.clearAutosavedValue();
|
||||
|
||||
if (response)
|
||||
window.location.href = "/categories/" + response.id;
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
69
src/themes/bootstrap_black/views/create_thread.ejs
Normal file
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Create thread!" }) %>
|
||||
|
||||
<body>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
|
||||
<script src="/libs/simplemde/simplemde.min.js"></script>
|
||||
|
||||
|
||||
<div class="container my-3">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 text-white bg-info mb-3 p-4 rounded">Create new thread</h2>
|
||||
<form class="mb-3">
|
||||
<div class="form-group">
|
||||
<label for="topic">Title</label>
|
||||
<input type="text" class="form-control" id="title" placeholder="Give your thread a title." required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="comment">Comment:</label>
|
||||
<textarea id="textarea"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-check-label">
|
||||
Category:
|
||||
</label>
|
||||
|
||||
<select id="category" class="input">
|
||||
<% for (const category of categories) { %>
|
||||
<option value="<%= category.id %>"><%= category.name %></option>
|
||||
<% } %>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
<button type="reset" class="btn btn-danger">Reset</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/editor.js"></script>
|
||||
|
||||
|
||||
<script type="module">
|
||||
const simplemde = editor("thread-create");
|
||||
|
||||
import request from "../../js/request.js";
|
||||
|
||||
document.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
const response = await request("/api/threads/", "POST", {
|
||||
title: document.getElementById("title").value,
|
||||
content: simplemde.value(),
|
||||
category: document.getElementById("category").value
|
||||
});
|
||||
if (response) {
|
||||
simplemde.clearAutosavedValue();
|
||||
window.location.href = "/threads/" + response.id;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
124
src/themes/bootstrap_black/views/index.ejs
Normal file
|
@ -0,0 +1,124 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: "Welcome to the "+dataset.forum_name+"-forum!" }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<%- include("extra/navbar") %>
|
||||
|
||||
|
||||
<div class="container my-3">
|
||||
<nav class="breadcrumb">
|
||||
<span class="breadcrumb-item active">
|
||||
<% if (user) { %>
|
||||
Welcome, <%= user.name %>
|
||||
<% } else { %>
|
||||
Welcome, Guest! <a href="/register">You can press to register.</a>
|
||||
<% } %>
|
||||
</span>
|
||||
</nav>
|
||||
<div class="row">
|
||||
<div class="col-12 col-xl-9">
|
||||
<div class="category">
|
||||
|
||||
<h2 class="h4 text-white bg-danger mb-0 p-4 rounded-top">Forum category</h2>
|
||||
<table class="table table-striped table-bordered table-responsive">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col" class="forum-col">Forum</th>
|
||||
<th scope="col">Topics</th>
|
||||
<th scope="col">Posts</th>
|
||||
<th scope="col" class="last-post-col">Last post</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="thread">
|
||||
<td>
|
||||
<h3 class="h5 mb-0"><a href="#0" class="text-uppercase">Forum name</a></h3>
|
||||
<p class="mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit. In laoreet pellentesque lorem sed elementum.</p>
|
||||
</td>
|
||||
<td>
|
||||
<div>5</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>18</div>
|
||||
</td>
|
||||
<td>
|
||||
<h4 class="h6 mb-0 font-weight-bold"><a href="#0">Post name</a></h4>
|
||||
<div>by <a href="#0">Author name</a></div>
|
||||
<div>05 Apr 2017, 20:07</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xl-3">
|
||||
<aside>
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-6 col-xl-12">
|
||||
<div class="card mb-3 mb-sm-0 mb-xl-3">
|
||||
<div class="card-body">
|
||||
<h2 class="h4 card-title">Members online</h2>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li><a href="/users">You</a></li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-8">Total:</dt>
|
||||
<dd class="col-4 mb-0">-</dd>
|
||||
</dl>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-8">Members:</dt>
|
||||
<dd class="col-4 mb-0">-</dd>
|
||||
</dl>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-8">Guests:</dt>
|
||||
<dd class="col-4 mb-0">-</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-xl-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
|
||||
<h2 class="h4 card-title">Forum statistics</h2>
|
||||
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-8">Total forums:</dt>
|
||||
<dd class="col-4 mb-0"><%= categories %></dd>
|
||||
</dl>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-8">Total threads:</dt>
|
||||
<dd class="col-4 mb-0"><%= threads %></dd>
|
||||
</dl>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-8">Total messages:</dt>
|
||||
<dd class="col-4 mb-0"><%= messages %></dd>
|
||||
</dl>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-8">Total members:</dt>
|
||||
<dd class="col-4 mb-0"><%= users %></dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div>Newest member:</div>
|
||||
<div><a href="#0"><%= newestMember %></a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include("extra/footer") %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
168
src/themes/bootstrap_black/views/thread.ejs
Normal file
|
@ -0,0 +1,168 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: thread.title }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"></script>
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
|
||||
<script src="/libs/simplemde/simplemde.min.js"></script>
|
||||
|
||||
<div style="text-align:center;padding:8px">
|
||||
<a href="/categories/<%= thread.categoryID %>" class="title" id="title"><%= thread.title %></a>
|
||||
<div class="date">
|
||||
<%= new Date(thread.time).toLocaleString() %> • Views: <%= thread.views %>
|
||||
</div>
|
||||
<div class="date">
|
||||
<a style="color: var(--anti);" href="/users/<%= thread.author.id %>"><%= thread.author.name %></a> <%= "• "+(thread.edited ? "Edited" : "Not edited")%>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="text-align:center;padding:8px">
|
||||
<% if (user && (user.id === thread.authorID || user.admin ) && !thread.deleted){ %>
|
||||
<a onclick="delete_thread('<%= thread.id %>')" class="btn-outline-primary">DELETE</a>
|
||||
<a onclick="edit_thread('<%= thread.id %>')" class="btn-outline-primary">EDIT</a>
|
||||
<% } else if (thread.deleted) { %>
|
||||
<h4 class="title" style="display:inline; font-size: 20px;">This thread has been deleted</h3>
|
||||
<a onclick="undelete_thread('<%= thread.id %>')" class="btn-primary">UNDELETE</a>
|
||||
<% }; %>
|
||||
</div>
|
||||
|
||||
<div id="messages">
|
||||
|
||||
<% messages.filter(Boolean).forEach(message=>{ %>
|
||||
|
||||
<div class="message" id="message-<%= message.id %>">
|
||||
<div class="left">
|
||||
<img src="<%= message.author.avatar %>" />
|
||||
<div class="username"><a href="/users/<%=message.author.id %>"><%=message.author.name %></a></div>
|
||||
<div class="date">
|
||||
<%= new Date(message.time).toLocaleDateString() %>
|
||||
</div>
|
||||
<div class="date">
|
||||
<%= new Date(message.time).toLocaleTimeString() %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content"><%= message.content %></div>
|
||||
<% if(user){ %>
|
||||
<% if(user.id === message.authorID || user.admin){ %>
|
||||
|
||||
<div class="dots" modal="#modal-<%=message.id %>">
|
||||
<% if (message.deleted){ %>
|
||||
<i class='bx bx-trash bx-sm' id="deleted" style="color: var(--important);"></i>
|
||||
<% } %>
|
||||
<% if (message.edited){ %>
|
||||
<i class='bx bx-pencil bx-sm' id="edited" style="color: GREEN;"></i>
|
||||
<% } %>
|
||||
<i class='bx bx-dots-horizontal-rounded'></i>
|
||||
</div>
|
||||
|
||||
<div class="dots-menu" id="modal-<%=message.id %>">
|
||||
<% if (!message.deleted){ %>
|
||||
<a onclick="delete_message('<%=message.id %>');">Delete</a>
|
||||
<a onclick="edit_message('<%=message.id %>');">Edit</a>
|
||||
<% }else { %>
|
||||
<a onclick="undelete_message('<%=message.id %>');">Undelete</a>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="reactions">
|
||||
<div <% if (message.react.like.includes(user?.id)) { %> style="color: var(--main)" <% } %>>
|
||||
<i onclick='react("<%= message.id %>","like");' class='bx bx-like'></i>
|
||||
<div id="like"><%=message.react.like.length %></div>
|
||||
</div>
|
||||
<div <% if (message.react.dislike.includes(user?.id)) { %> style="color: var(--main)" <% } %>>
|
||||
<i onclick='react("<%= message.id %>","dislike");' class='bx bx-dislike'></i>
|
||||
<div id="dislike"><%=message.react.dislike.length %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% }; %>
|
||||
|
||||
</div>
|
||||
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const converter = new showdown.Converter();
|
||||
for (const message of document.querySelectorAll(".message")) {
|
||||
const content = message.querySelector(".content");
|
||||
content.innerHTML = converter.makeHtml(content.rawText = content.innerHTML);
|
||||
}
|
||||
</script>
|
||||
<script src="/js/modal.js"></script>
|
||||
<% if (user){ %>
|
||||
<script type="module" src="/js/thread.js"></script>
|
||||
|
||||
<div class="message" id="send-div">
|
||||
|
||||
<form id="send" style="width:100%">
|
||||
<textarea rows="4" id="textarea"></textarea>
|
||||
<input name="page" type="hidden" value="<%= page %>"></input>
|
||||
<button class="btn-primary">Send!</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/editor.js"></script>
|
||||
|
||||
<script type="module">
|
||||
const simplemde = editor("thread-<%= thread.id %>");
|
||||
|
||||
import request from "../../js/request.js";
|
||||
|
||||
document.getElementById("send").addEventListener("submit", async e => {
|
||||
|
||||
e.preventDefault();
|
||||
const res = await request("/api/messages", "POST", {
|
||||
threadID: "<%= thread.id %>",
|
||||
content: simplemde.value()
|
||||
})
|
||||
simplemde.clearAutosavedValue();
|
||||
let tp = Number("<%= thread.pages %>")
|
||||
let tm = Number("<%= thread.count %>")
|
||||
if (tp * 10 === tm) tp++;
|
||||
if (res) location.href = `/threads/<%= thread.id %>?page=${tp-1}`;
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.fa {
|
||||
color: var(--main);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<% }%>
|
||||
|
||||
<div class="mb-3 clearfix">
|
||||
<nav aria-label="Navigate post pages" class="float-lg-right">
|
||||
<ul class="pagination pagination-sm mb-lg-0">
|
||||
<% if (page > 0){ %>
|
||||
<li class="page-item"><a href="/<%= thread.getLink() %>?page=<%= page-1 %>" class="page-link">Back</a></li>
|
||||
<% } %>
|
||||
<% for(let i=0; i < thread.pages; i++){ %>
|
||||
<li class="page-item <%= i==page?'active':'' %>"><a href="/<%= thread.getLink() %>?page=<%= i %>" class="page-link"><%= i+1 %>
|
||||
<% if (i==page){ %>
|
||||
<span class="sr-only">(current)</span>
|
||||
<% } %>
|
||||
</a></li>
|
||||
<% } %>
|
||||
<% if (thread.pages-1 > page) { %>
|
||||
<li class="page-item"><a href="/<%= thread.getLink() %>?page=<%= page+1 %>" class="page-link">Next</a></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
83
src/themes/bootstrap_black/views/threads.ejs
Normal file
|
@ -0,0 +1,83 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Thread list!" }) %>
|
||||
|
||||
<body style="text-align: center;">
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<div class="container my-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 text-white bg-info mb-0 p-4 rounded-top"><%= title || "Threads" %></h2>
|
||||
<h3 class="h6 text-white bg-info mb-0 p-4 rounded-top"><%= desp %></h2>
|
||||
|
||||
<table class="table table-striped table-bordered table-responsive-lg">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col" class="topic-col">Topic</th>
|
||||
<th scope="col" class="created-col">Created</th>
|
||||
<th scope="col">Statistics</th>
|
||||
<% if (user?.admin){ %> <th scope="col" class="last-post-col">Action</th> <% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<% threads.forEach(thread=>{ %>
|
||||
<tr>
|
||||
<td>
|
||||
<h3 class="h6">
|
||||
<% if (thread.deleted) { %> <span class="badge badge-primary">[DELETED]</span><% } %>
|
||||
<a href="<%= thread.getLink() %>"><%= thread.title %></a>
|
||||
</h3>
|
||||
</td>
|
||||
<td>
|
||||
<div> </div>
|
||||
<div class="avatar">by <a href="<%= thread.getLink() %>"><%= thread.author.name %></a><img src="<%=thread.author.avatar %>"></div>
|
||||
<div><%= new Date(thread.time).toLocaleString() %></div>
|
||||
</td>
|
||||
<td>
|
||||
<div><%= thread.messages.length %> messages</div>
|
||||
<div><%= thread.views %> views</div>
|
||||
</td>
|
||||
<% if (user?.admin){ %>
|
||||
<td>
|
||||
<% if (!thread.deleted){ %>
|
||||
<a class="btn-danger" onclick="fetch('/api/threads/<%= thread.id %>/',{method:'DELETE'})"><i class="bx bx-trash bx-sm"></i></a>
|
||||
<% } %>
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 clearfix">
|
||||
<nav aria-label="Navigate post pages" class="float-lg-right">
|
||||
<ul class="pagination pagination-sm mb-lg-0">
|
||||
<% if (page > 0){ %>
|
||||
<li class="page-item"><a href="/threads?page=<%= page-1 %>" class="page-link">Back</a></li>
|
||||
<% } %>
|
||||
<% for(let i=0; i < pages; i++){ %>
|
||||
<li class="page-item <%= i==page?'active':'' %>"><a href="/threads?page=<%= i %>" class="page-link"><%= i+1 %>
|
||||
<% if (i==page){ %>
|
||||
<span class="sr-only">(current)</span>
|
||||
<% } %>
|
||||
</a></li>
|
||||
<% } %>
|
||||
<% if (pages-1 > page) { %>
|
||||
<li class="page-item"><a href="/threads?page=<%= page+1 %>" class="page-link">Next</a></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<a href="/threads/create" class="btn btn-lg btn-primary">New Thread</a>
|
||||
</div>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
164
src/themes/bootstrap_black/views/user.ejs
Normal file
|
@ -0,0 +1,164 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: member.name }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"></script>
|
||||
|
||||
<div class="usercontent">
|
||||
|
||||
|
||||
<% if(user?.admin || user?.id === member.id){ %>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#userMenu" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="userMenu">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<% if (!member.discordID && discord && user?.id === member.id) { %>
|
||||
<a href="<%=discord%>" class="btn-outline-primary">Discord auth</a>
|
||||
<% } else if(member.discordID && user?.id === member.id) { %>
|
||||
<a class="btn-primary" id="un_discord">Unauth Discord</a>
|
||||
<% } %>
|
||||
<% if (member.hideLastSeen) {%>
|
||||
<a id="last_unhide" class="btn-primary">Unhide last seen</a>
|
||||
<% } else { %>
|
||||
<a id="last_hide" class="btn-outline-primary">Hide last seen</a>
|
||||
<% } %>
|
||||
<% if (member.deleted) {%>
|
||||
<h1>This user has been deleted!</h1>
|
||||
<a id="undelete" class="btn-primary">Undelete user</a>
|
||||
<% } else if (user?.admin){ %>
|
||||
<a id="delete" class="btn-outline-primary">Delete user</a>
|
||||
<% } %>
|
||||
<a class="btn-outline-primary" href="/users/<%=member.id%>/edit">Edit user</a>
|
||||
<a href="/users/<%=member.id%>/avatar" class="btn-outline-primary">Upload avatar</a>
|
||||
</ul>
|
||||
|
||||
<script type="module">
|
||||
import request from "../../js/request.js";
|
||||
|
||||
|
||||
document.addEventListener("click", async e => {
|
||||
if (e.target.id == "delete") {
|
||||
const response = await request("/api/users/<%= member.id %>", "DELETE");
|
||||
if (response.state !== "DELETED") return
|
||||
alert("User is deleted!");
|
||||
location.reload()
|
||||
} else if (e.target.id == "undelete") {
|
||||
const response = await request("/api/users/<%= member.id %>/", "PATCH", {
|
||||
deleted: false
|
||||
});
|
||||
if (response.state == "DELETED") return;
|
||||
alert("User is undeleted successfully!");
|
||||
location.reload()
|
||||
} else if (e.target.id == "un_discord") {
|
||||
const response = await fetch("/auth/discord/", {
|
||||
method: "DELETE"
|
||||
});
|
||||
alert(await response.text());
|
||||
location.reload()
|
||||
} else if (e.target.id.startsWith("last_")) {
|
||||
let hideLastSeen = e.target.id.replace("last_", "") == "hide" ? true : false;
|
||||
const response = await request("/api/users/<%= member.id %>/", "PATCH", {
|
||||
hideLastSeen
|
||||
});
|
||||
alert(`Last seen is ${!hideLastSeen?"un":""}hided!`);
|
||||
location.reload()
|
||||
|
||||
} else if (e.target.id == "toogle")
|
||||
document.getElementById('user-edit').classList.toggle('no-active')
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</nav>
|
||||
<% if (user.admin) { %>
|
||||
<select>
|
||||
<option selected>IP LIST</option>
|
||||
<% for(const ip of member.ips) { %>
|
||||
<option><%= ip %></option>
|
||||
<% } %>
|
||||
</select>
|
||||
<% } %>
|
||||
|
||||
<% } %>
|
||||
|
||||
|
||||
<div class="userbox" style="justify-content:center;">
|
||||
<img style="width:150px;height:150px;border-radius:50%;" src="<%=member.avatar %>">
|
||||
</div>
|
||||
|
||||
<% if (member.admin) { %>
|
||||
<h2 class="userbox-value" style="align-self: center;">Admin</h2>
|
||||
<% } %>
|
||||
|
||||
<% if (member.about?.length) { %>
|
||||
<div class="userbox-value" id="about" style="
|
||||
margin: 10px auto;
|
||||
box-shadow: 0 0 5px 0 var(--second);
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
background: none;
|
||||
color: var(--anti);
|
||||
">
|
||||
<%= member.about %>
|
||||
</div>
|
||||
<script>
|
||||
const converter = new showdown.Converter();
|
||||
const about = document.getElementById("about")
|
||||
about.innerHTML = converter.makeHtml(about.innerText);
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
|
||||
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Name:</h2>
|
||||
<h2 class="userbox-value"><%= member.name %></h2>
|
||||
</div>
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Created at:</h2>
|
||||
<h2 class="userbox-value"><%= new Date(member.time).toLocaleString() %></h2>
|
||||
</div>
|
||||
<% if(!member.hideLastSeen || user?.admin) {%>
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Last seen at:</h2>
|
||||
<h2 class="userbox-value"><%= new Date(member.lastSeen).toLocaleString() %></h2>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Message:</h2>
|
||||
<a class="userbox-value" href="/search/messages?authorID=<%= member.id %>">
|
||||
<%= counts.message %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Thread:</h2>
|
||||
<a class="userbox-value" href="/search/threads?authorID=<%= member.id %>">
|
||||
<%= counts.thread %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
51
src/themes/bootstrap_black/views/users.ejs
Normal file
|
@ -0,0 +1,51 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "User list!" }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<div class="users">
|
||||
<% users.filter(member=> !member.deleted || user.admin ).forEach(member => { %>
|
||||
<div style="display:flex;justify-content:center;">
|
||||
<div class="user-box">
|
||||
<img src="<%= member.avatar %>" class="user-box-img">
|
||||
<div class="user-box-title"> <a href="<%= member.getLink() %>">
|
||||
<% if (member.deleted) { %> <span style="color: var(--important);">[DELETED]</span><% } %>
|
||||
<%= member.name %></a></div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
|
||||
</div>
|
||||
<% if(typeof page === "number"){ %>
|
||||
<div class="mb-3 clearfix">
|
||||
<nav aria-label="Navigate post pages" class="float-lg-right">
|
||||
<ul class="pagination pagination-sm mb-lg-0">
|
||||
<% if (page > 0){ %>
|
||||
<li class="page-item"><a href="/categories?page=<%= page-1 %>" class="page-link">Back</a></li>
|
||||
<% } %>
|
||||
<% for(let i=0; i < pages; i++){ %>
|
||||
<li class="page-item <%= i==page?'active':'' %>"><a href="/categories?page=<%= i %>" class="page-link"><%= i+1 %>
|
||||
<% if (i==page){ %>
|
||||
<span class="sr-only">(current)</span>
|
||||
<% } %>
|
||||
</a></li>
|
||||
<% } %>
|
||||
<% if (pages-1 > page) { %>
|
||||
<li class="page-item"><a href="/categories?page=<%= page+1 %>" class="page-link">Next</a></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,11 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: "Admin Panel!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Admin Panel!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<style>
|
||||
table {
|
||||
font-family: arial, sans-serif;
|
||||
|
@ -83,7 +84,7 @@
|
|||
}
|
||||
</script>
|
||||
</div>
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: "Avatar Upload Panel!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Avatar Upload Panel!" }) %>
|
||||
|
||||
<body style="text-align: center;">
|
||||
<link rel="stylesheet" href="/libs/cropper/cropper.css">
|
||||
|
@ -27,7 +27,8 @@
|
|||
</style>
|
||||
<script src="/libs/cropper/cropper.js"></script>
|
||||
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
|
||||
<div class="container">
|
||||
|
@ -107,7 +108,7 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<%- include("extra/meta", {title: "Thread list!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Category list!" }) %>
|
||||
|
||||
<body>
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<div class="threads">
|
||||
<% categories.forEach(category=>{ %>
|
||||
<a href="<%= category.getLink() %>">
|
||||
|
@ -48,7 +49,7 @@
|
|||
|
||||
</div>
|
||||
<% } %>
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: "Edit Forum Config!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Edit Forum Config!" }) %>
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<h1>Edit forum config</h1>
|
||||
<textarea rows="30" cols="75"><%= config %></textarea>
|
||||
<a onclick="send();" class="btn-primary">Edit config</a>
|
||||
|
@ -24,7 +25,7 @@
|
|||
|
||||
}
|
||||
</script>
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<%- include("extra/meta", {title: "Create Category!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Create Category!" }) %>
|
||||
|
||||
<body>
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
|
||||
<script src="/libs/simplemde/simplemde.min.js"></script>
|
||||
|
||||
|
@ -41,7 +42,7 @@
|
|||
|
||||
});
|
||||
</script>
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: "Create thread!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Create thread!" }) %>
|
||||
|
||||
<body >
|
||||
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
|
||||
<script src="/libs/simplemde/simplemde.min.js"></script>
|
||||
|
||||
|
@ -53,7 +54,7 @@
|
|||
|
||||
});
|
||||
</script>
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: member.name }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: member.name }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
|
||||
<script src="/libs/simplemde/simplemde.min.js"></script>
|
||||
|
||||
|
@ -47,7 +48,7 @@
|
|||
location.reload();
|
||||
});
|
||||
</script>
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
17
src/themes/common/views/error.ejs
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: type+" error!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<h1 style="color: var(--main);"><%= type %></h1>
|
||||
<h2 style="color: var(--second);"><%= error %></h2>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
44
src/themes/common/views/extra/footer.ejs
Normal file
|
@ -0,0 +1,44 @@
|
|||
<div class="footer">
|
||||
<% if (user){ %>
|
||||
<select id="theme_select">
|
||||
<% for(const theme of dataset.themes){%>
|
||||
<option value="<%= theme.codename %>"><%= theme.name %></option>
|
||||
<% } %>
|
||||
</select>
|
||||
<script>
|
||||
const theme_select = document.getElementById("theme_select");
|
||||
theme_select.querySelector(`option[value=<%= user.theme.codename %>]`).selected = true;
|
||||
theme_select.addEventListener("change", async e => {
|
||||
const codename = e.target.value;
|
||||
await fetch('/api/users/<%= user.id %>', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
theme: {
|
||||
codename
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
const theme = await fetch("/api/themes/" + codename).then(res => res.json());
|
||||
const txt = "Theme changed to:\n" +
|
||||
"Name: " + theme.name + "\n" +
|
||||
"Description: " + theme.description + "\n" +
|
||||
"Author: " + theme.author + "\n";
|
||||
alert(txt);
|
||||
location.reload();
|
||||
});
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
<a href="https://github.com/Akif9748/akf-forum" style="color: white;"> This website is powered by
|
||||
<span style="color: #ffbf00;">akf-forum</span>
|
||||
</a>
|
||||
<div>
|
||||
<span style="color:white">Coders</span> <br>
|
||||
<div style="text-align:center;">
|
||||
<a href="https://github.com/Akif9748/" style="color: #ffbf00;">Akif</a><br><a href="#" style="color:#ffbf00;">Tokmak</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
44
src/themes/common/views/extra/navbar.ejs
Normal file
|
@ -0,0 +1,44 @@
|
|||
<% if (user?.admin){ %>
|
||||
<div class="admin-bar">
|
||||
<a href="/admin" class="admin-bar">Click here to reach admin panel</a>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="header">
|
||||
<a class="logo" href="/"><%= dataset.forum_name.toUpperCase() %> <span>FORUM</span></a>
|
||||
<div class="buttons">
|
||||
|
||||
<% if (user){ %>
|
||||
<a href="<%=user.getLink() %>" class="btn-outline-primary">
|
||||
<div class="box-username"><%= user.name %>
|
||||
<div class="avatar"><img src="<%=user.avatar %>"></div>
|
||||
</div>
|
||||
</a>
|
||||
<a id="logout" href="/login" class="btn-primary">Logout</a>
|
||||
<% } else { %>
|
||||
|
||||
<a id="login" href="/login" class="btn-primary">Login</a>
|
||||
<a href="/register" class="btn-outline-primary">Register</a>
|
||||
<script>
|
||||
document.getElementById("login").href += "?redirect=" + location.pathname;
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu">
|
||||
<a href="/threads/create/" class="menu-item">Create Thread</a>
|
||||
<a href="/categories" class="menu-item">Categories</a>
|
||||
<a href="/threads" class="menu-item">Threads</a>
|
||||
<a href="/users" class="menu-item">Users</a>
|
||||
<a href="/search" class="menu-item">Search</a>
|
||||
<script>
|
||||
const menuItems = document.getElementsByClassName("menu-item");
|
||||
|
||||
for (let i = 0; i < menuItems.length; i++)
|
||||
if (window.location.pathname.includes(menuItems[i].getAttribute("href"))) {
|
||||
menuItems[i].classList.add("active-menu");
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
</div>
|
|
@ -1,13 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: "Welcome to the "+dataset.forum_name+"-forum!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Welcome to the "+dataset.forum_name+"-forum!" }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<link rel="stylesheet" href="/css/user.css" />
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<%- include("extra/navbar") %>
|
||||
|
||||
<div class="usercontent">
|
||||
<% if (user) { %>
|
||||
|
@ -44,12 +43,12 @@
|
|||
</div>
|
||||
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Memory usage:</h2>
|
||||
<h2 class="userbox-value"><%= mem.toFixed(2); %> MB</h2>
|
||||
<h2 class="userbox-title">Category count:</h2>
|
||||
<h2 class="userbox-value"><%= categories %></h2>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: "Log in!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Log in!" }) %>
|
||||
|
||||
<body style="text-align: center;">
|
||||
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<h1 class="title">Login</h1>
|
||||
|
||||
|
@ -20,7 +21,7 @@
|
|||
<% } %>
|
||||
<a href="/register" class="btn-outline-primary">Register</a>
|
||||
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title:"Message search!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title:"Message search!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<div id="messages">
|
||||
|
||||
|
@ -27,7 +28,7 @@
|
|||
|
||||
<% }); %>
|
||||
</div>
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: "Register!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Register!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<h1 class="title">Register</h1>
|
||||
|
||||
|
@ -24,7 +25,7 @@
|
|||
<a href="/login" class="btn-outline-primary">Login</a>
|
||||
|
||||
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: "Search page!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Search page!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<h1 class="title">Search</h1>
|
||||
|
||||
|
@ -27,7 +28,7 @@
|
|||
<input type="submit" value="Search" class="btn-primary" />
|
||||
</form>
|
||||
</div>
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: "Setup the Akf-forum!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Setup the Akf-forum!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<h1 style="color: var(--main);">Setup</h1>
|
||||
<h2 style="color: var(--second);">There is default settings for akf-forum, you not need to edit them, but you can! And, the first registered user will be admin.</h2>
|
||||
|
@ -21,10 +22,7 @@
|
|||
<input class="input" type="text" name="default_thread_state" value="ACTIVE" required>
|
||||
Domain of the forum, defaulty setted:
|
||||
<input class="input" type="text" name="host" id="domain" value="Akf-forum!" required>
|
||||
<hr>
|
||||
(Optional) Discord app ID for Discord login:
|
||||
<input class="input" type="text" name="discord_auth">
|
||||
|
||||
|
||||
<input type="submit" class="btn-primary" value="Setup">
|
||||
</form>
|
||||
<script>
|
|
@ -1,11 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: thread.title }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: thread.title }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"></script>
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
|
||||
|
@ -159,7 +160,7 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<%- include("extra/meta", {title: "Thread list!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Thread list!" }) %>
|
||||
|
||||
<body style="color: var(--anti); text-align: center;">
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<h1><%= title || "Threads" %></h1>
|
||||
<h2><%= desp %></h2>
|
||||
<div class="threads">
|
||||
|
@ -54,7 +55,7 @@
|
|||
|
||||
</div>
|
||||
<% } %>
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
151
src/themes/common/views/user.ejs
Normal file
|
@ -0,0 +1,151 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: member.name }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"></script>
|
||||
|
||||
<div class="usercontent">
|
||||
|
||||
<% if(user?.admin || user?.id === member.id){ %>
|
||||
<details>
|
||||
<summary class="btn-outline-primary">User Menu:</summary>
|
||||
|
||||
<% if (!member.discordID && discord && user?.id === member.id) { %>
|
||||
<a href="<%=discord%>" class="btn-outline-primary">DC auth</a>
|
||||
<% } else if(member.discordID && user?.id === member.id) { %>
|
||||
<a class="btn-outline-primary" id="un_discord">Unauth DC!</a>
|
||||
<% } %>
|
||||
<a href="/users/<%=member.id%>/avatar" class="btn-outline-primary">Upload avatar</a>
|
||||
<a class="btn-outline-primary" href="/users/<%=member.id%>/edit" id="toogle">Edit user!</a>
|
||||
<script type="module">
|
||||
import request from "/js/request.js";
|
||||
|
||||
const form = document.getElementById("form");
|
||||
|
||||
document.addEventListener("click", async e => {
|
||||
if (e.target.id == "delete") {
|
||||
const response = await request("/api/users/<%= member.id %>", "DELETE");
|
||||
if (response.state !== "DELETED") return
|
||||
alert("User is deleted!");
|
||||
location.reload()
|
||||
} else if (e.target.id == "undelete") {
|
||||
const response = await request("/api/users/<%= member.id %>/", "PATCH", {
|
||||
deleted: false
|
||||
});
|
||||
if (response.state == "DELETED") return;
|
||||
alert("User is undeleted successfully!");
|
||||
location.reload()
|
||||
} else if (e.target.id == "un_discord") {
|
||||
const response = await fetch("/auth/discord/", {
|
||||
method: "DELETE"
|
||||
});
|
||||
alert(await response.text());
|
||||
location.reload()
|
||||
} else if (e.target.id.startsWith("last_")) {
|
||||
let hideLastSeen = e.target.id.replace("last_", "") == "hide" ? true : false;
|
||||
const response = await request("/api/users/<%= member.id %>/", "PATCH", {
|
||||
hideLastSeen
|
||||
});
|
||||
alert(`Last seen is ${!hideLastSeen?"un":""}hided!`);
|
||||
location.reload()
|
||||
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<% if (member.hideLastSeen) {%>
|
||||
<a id="last_unhide" class="btn-primary">Unhide last seen! </a>
|
||||
<% } else { %>
|
||||
<a id="last_hide" class="btn-outline-primary">Hide last seen! </a>
|
||||
<% } %>
|
||||
|
||||
<% if (member.deleted) {%>
|
||||
<h1>This user has been deleted!</h1>
|
||||
<a id="undelete" class="btn-primary">Undelete user! </a>
|
||||
<% } else if (user?.admin){ %>
|
||||
<a id="delete" class="btn-outline-primary">Delete user! </a>
|
||||
<% } %>
|
||||
<% if (user?.admin) {%>
|
||||
<h2>IP adresses of the user:</h2>
|
||||
<select>
|
||||
<% for(const ip of member.ips) { %>
|
||||
<option><%= ip %></option>
|
||||
<% } %>
|
||||
</select>
|
||||
|
||||
<% } %>
|
||||
</details>
|
||||
<% } %>
|
||||
|
||||
|
||||
<div class="userbox" style="justify-content:center;">
|
||||
<img style="width:150px;height:150px;border-radius:50%;" src="<%=member.avatar %>">
|
||||
</div>
|
||||
|
||||
<% if (member.admin) { %>
|
||||
<h2 class="userbox-value" style="align-self: center;">Admin</h2>
|
||||
<% } %>
|
||||
|
||||
<% if (member.about?.length) { %>
|
||||
<div class="userbox-value" id="about" style="
|
||||
margin: 10px auto;
|
||||
box-shadow: 0 0 5px 0 var(--second);
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
background: none;
|
||||
color: var(--anti);
|
||||
">
|
||||
<%= member.about %>
|
||||
</div>
|
||||
<script>
|
||||
const converter = new showdown.Converter();
|
||||
const about = document.getElementById("about")
|
||||
about.innerHTML = converter.makeHtml(about.innerText);
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
|
||||
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Name:</h2>
|
||||
<h2 class="userbox-value"><%= member.name %></h2>
|
||||
</div>
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Created at:</h2>
|
||||
<h2 class="userbox-value"><%= new Date(member.time).toLocaleString() %></h2>
|
||||
</div>
|
||||
<% if(!member.hideLastSeen || user?.admin) {%>
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Last seen at:</h2>
|
||||
<h2 class="userbox-value"><%= new Date(member.lastSeen).toLocaleString() %></h2>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Message:</h2>
|
||||
<a class="userbox-value" href="/search/messages?authorID=<%= member.id %>">
|
||||
<%= counts.message %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Thread:</h2>
|
||||
<a class="userbox-value" href="/search/threads?authorID=<%= member.id %>">
|
||||
<%= counts.thread %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,13 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: "User list!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "User list!" }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
|
||||
<%- include("extra/navbar") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<div class="users">
|
||||
<% users.filter(member=> !member.deleted || user.admin ).forEach(member => { %>
|
||||
<div style="display:flex;justify-content:center;">
|
||||
|
@ -43,7 +44,7 @@
|
|||
|
||||
</div>
|
||||
<% } %>
|
||||
<%- include("extra/footer") %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
1
src/themes/default_black/views/extra/footer.ejs
Normal file
|
@ -0,0 +1 @@
|
|||
<%- include(dataset.getFile("common/views/extra/footer")) %>
|