From 9ee1d589ebc78117103099f6c185241de0e01415 Mon Sep 17 00:00:00 2001
From: Akif9748 <akif9748@gmail.com>
Date: Thu, 25 May 2023 19:28:05 +0300
Subject: [PATCH] Better pagination and rewiretten threads etc.

---
 README.md                                     |   3 +
 src/index.js                                  |   4 +-
 src/themes/bootstrap_black/public/main.css    |  45 -----
 src/themes/bootstrap_black/views/admin.ejs    | 124 +++++++++++++
 .../bootstrap_black/views/categories.ejs      |  74 ++++++++
 .../bootstrap_black/views/create_category.ejs |  59 ++++++
 .../bootstrap_black/views/create_thread.ejs   |  69 +++++++
 src/themes/bootstrap_black/views/thread.ejs   | 168 ++++++++++++++++++
 src/themes/bootstrap_black/views/threads.ejs  |  83 +++++++++
 src/themes/bootstrap_black/views/users.ejs    |  51 ++++++
 10 files changed, 633 insertions(+), 47 deletions(-)
 create mode 100644 src/themes/bootstrap_black/views/admin.ejs
 create mode 100644 src/themes/bootstrap_black/views/categories.ejs
 create mode 100644 src/themes/bootstrap_black/views/create_category.ejs
 create mode 100644 src/themes/bootstrap_black/views/create_thread.ejs
 create mode 100644 src/themes/bootstrap_black/views/thread.ejs
 create mode 100644 src/themes/bootstrap_black/views/threads.ejs
 create mode 100644 src/themes/bootstrap_black/views/users.ejs

diff --git a/README.md b/README.md
index f6208a7..0391f07 100644
--- a/README.md
+++ b/README.md
@@ -58,8 +58,11 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn
 - 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
diff --git a/src/index.js b/src/index.js
index 2df22dd..c2124a2 100644
--- a/src/index.js
+++ b/src/index.js
@@ -23,8 +23,6 @@ app.ips = [];
 app.set("view engine", "ejs");
 app.set("limits", limits);
 
-if (RLS.enabled) app.use(RL(RLS.windowMs, RLS.max));
-
 for (const theme of fs.readdirSync(join(__dirname, "themes")))
     app.use(`/themes/${theme}`, express.static(join(__dirname, "themes", theme, "public")));
 
@@ -70,6 +68,8 @@ app.use(express.static(join(__dirname, "public")), express.json(), express.urlen
     }
 );
 
+if (RLS.enabled) app.use(RL(RLS.windowMs, RLS.max));
+
 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`);
 
diff --git a/src/themes/bootstrap_black/public/main.css b/src/themes/bootstrap_black/public/main.css
index b98806f..77443b7 100644
--- a/src/themes/bootstrap_black/public/main.css
+++ b/src/themes/bootstrap_black/public/main.css
@@ -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
diff --git a/src/themes/bootstrap_black/views/admin.ejs b/src/themes/bootstrap_black/views/admin.ejs
new file mode 100644
index 0000000..62aa5ea
--- /dev/null
+++ b/src/themes/bootstrap_black/views/admin.ejs
@@ -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">&times;</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>
\ No newline at end of file
diff --git a/src/themes/bootstrap_black/views/categories.ejs b/src/themes/bootstrap_black/views/categories.ejs
new file mode 100644
index 0000000..8086e65
--- /dev/null
+++ b/src/themes/bootstrap_black/views/categories.ejs
@@ -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>
\ No newline at end of file
diff --git a/src/themes/bootstrap_black/views/create_category.ejs b/src/themes/bootstrap_black/views/create_category.ejs
new file mode 100644
index 0000000..12aae29
--- /dev/null
+++ b/src/themes/bootstrap_black/views/create_category.ejs
@@ -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>
\ No newline at end of file
diff --git a/src/themes/bootstrap_black/views/create_thread.ejs b/src/themes/bootstrap_black/views/create_thread.ejs
new file mode 100644
index 0000000..9d9db3a
--- /dev/null
+++ b/src/themes/bootstrap_black/views/create_thread.ejs
@@ -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>
\ No newline at end of file
diff --git a/src/themes/bootstrap_black/views/thread.ejs b/src/themes/bootstrap_black/views/thread.ejs
new file mode 100644
index 0000000..415c89e
--- /dev/null
+++ b/src/themes/bootstrap_black/views/thread.ejs
@@ -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>
\ No newline at end of file
diff --git a/src/themes/bootstrap_black/views/threads.ejs b/src/themes/bootstrap_black/views/threads.ejs
new file mode 100644
index 0000000..b2b0f5e
--- /dev/null
+++ b/src/themes/bootstrap_black/views/threads.ejs
@@ -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>
\ No newline at end of file
diff --git a/src/themes/bootstrap_black/views/users.ejs b/src/themes/bootstrap_black/views/users.ejs
new file mode 100644
index 0000000..16661e7
--- /dev/null
+++ b/src/themes/bootstrap_black/views/users.ejs
@@ -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>
\ No newline at end of file