initial commit
This commit is contained in:
commit
f219a2fe41
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
data.db
|
||||
.sass-cache
|
||||
node_modules
|
||||
package-lock.json
|
||||
sessions
|
||||
build
|
24
README.md
Normal file
24
README.md
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Web Forum Experiment
|
||||
|
||||
This is a weekend project I made to gain a better understanding of databases.
|
||||
It's a simple message board forum where users can create threads and messages within those threads.
|
||||
It is written in JavaScript using the [Express framework](https://expressjs.com/), with SQLite for a database.
|
||||
I don't intend to develop this any further.
|
||||
|
||||
## Installing Dependencies
|
||||
```
|
||||
npm install
|
||||
```
|
||||
## Running
|
||||
If you have GNU make installed:
|
||||
```
|
||||
make dev
|
||||
```
|
||||
or if not:
|
||||
```
|
||||
npx nodemon --ignore ./sessions/ -e js,mjs,json,scss index.js
|
||||
```
|
||||
Then you can go to `localhost:3333` in your browser to view the forum.
|
||||
|
||||
|
||||
![Screenshot](screenshot.png)
|
8
buildstyle.js
Normal file
8
buildstyle.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import sass from "sass";
|
||||
import { writeFileSync, } from "fs";
|
||||
|
||||
const style = sass.compile("./css/main.scss").css;
|
||||
|
||||
writeFileSync("static/out.css", style, err => {
|
||||
console.log(err);
|
||||
});
|
160
css/main.scss
Normal file
160
css/main.scss
Normal file
|
@ -0,0 +1,160 @@
|
|||
@import 'variables';
|
||||
|
||||
$asideWidth: 150px;
|
||||
$mainWidth: 1200px;
|
||||
$borderSize: 5px;
|
||||
$totalWidth: $asideWidth+$mainWidth+(4*$borderSize);
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--background-color);
|
||||
background-image: url("/background.png");
|
||||
background-repeat: repeat;
|
||||
background-size: 100px 100px;
|
||||
font-family: $content-font;
|
||||
}
|
||||
|
||||
|
||||
div#actionBarRight {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
div#actionBar {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
display: flex;
|
||||
border: 5px ridge var(--fourth-color);
|
||||
border-bottom: none;
|
||||
background-color: lightgray;
|
||||
font-family: sans-serif;
|
||||
align-items: flex-start;
|
||||
|
||||
div {
|
||||
a {
|
||||
font-size: large;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
display: block;
|
||||
padding-inline: 20px;
|
||||
padding-block: 8px;
|
||||
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--fourth-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div#content {
|
||||
margin-inline: auto;
|
||||
max-width: $mainWidth;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
header {
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
font-size: 4rem;
|
||||
background: linear-gradient(red, orange);
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-stroke: 1px red;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
header a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#messageContainer:first-child .messageMeta {
|
||||
// background: linear-gradient(rgba(255, 0, 0, 1), rgba(255, 166, 0, 1));
|
||||
padding-block: 10px;
|
||||
font-size: 15px;
|
||||
text-decoration: none;
|
||||
text-decoration-style: none;
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
.message {
|
||||
background-color: var(--tertiary-color);
|
||||
// padding: 10px;
|
||||
margin-bottom: 5px;
|
||||
padding: 0 0 10px 0;
|
||||
margin-top: 15px;
|
||||
|
||||
.messageMeta {
|
||||
font-family: $clean-font;
|
||||
user-select: none;
|
||||
background: rgb(190, 190, 190);
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
font-size: 13px;
|
||||
padding-block: 5px;
|
||||
padding-left: 5px;
|
||||
|
||||
}
|
||||
|
||||
.messageContent {
|
||||
display: flex;
|
||||
padding-block: 10px;
|
||||
|
||||
|
||||
.userInfo {
|
||||
font-family: $clean-font;
|
||||
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
max-width: 170px;
|
||||
font-size: 15px;
|
||||
border-right: 2px ridge gainsboro;
|
||||
text-align: center;
|
||||
|
||||
#userName {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.messageBody {
|
||||
width: 100%;
|
||||
|
||||
h3 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0px 0px 0px 0px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
main {
|
||||
background-color: var(--main-color);
|
||||
border: 5px ridge var(--border-color);
|
||||
padding-block: 10px;
|
||||
padding-inline: 20px;
|
||||
}
|
17
css/variables.scss
Normal file
17
css/variables.scss
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Light mode colors
|
||||
:root {
|
||||
--faded-text-color: #303030;
|
||||
--background-color: #6d695c;
|
||||
--main-color: #E0E0E0;
|
||||
--border-color: white;
|
||||
--tertiary-color: lightgray;
|
||||
--fourth-color: darkgray;
|
||||
--fun-color: #4CAF50;
|
||||
--angry-color: red;
|
||||
--text-color: #242424;
|
||||
--darker-color: #ebebeb;
|
||||
}
|
||||
|
||||
|
||||
$clean-font: sans-serif;
|
||||
$content-font: Charter, 'Bitstream Charter', 'Sitka Text', Cambria, serif;
|
155
index.js
Normal file
155
index.js
Normal file
|
@ -0,0 +1,155 @@
|
|||
import bcrypt from "bcrypt";
|
||||
import sqlite3 from "sqlite3";
|
||||
import { open } from "sqlite";
|
||||
import express from 'express';
|
||||
import session from "express-session";
|
||||
|
||||
import sfs from "session-file-store";
|
||||
let FileStore = sfs(session);
|
||||
const app = express();
|
||||
const PORT = 3333;
|
||||
const SALT_COUNT = 12;
|
||||
|
||||
|
||||
|
||||
const dbPromise = open({
|
||||
filename: "data.db",
|
||||
driver: sqlite3.Database,
|
||||
});
|
||||
|
||||
var fileStoreOptions = {};
|
||||
|
||||
app.set('view engine', 'ejs');
|
||||
app.use(
|
||||
express.urlencoded({ extended: true }),
|
||||
express.static('static'),
|
||||
session({
|
||||
store: new FileStore(fileStoreOptions),
|
||||
secret: "process.env.SESSION_SECRET", cookie: { maxAge: 60000 },
|
||||
resave: true,
|
||||
saveUninitialized: true
|
||||
}),
|
||||
|
||||
);
|
||||
|
||||
app.get('/thread/:threadId/', async (req, res) => {
|
||||
try {
|
||||
const db = await dbPromise;
|
||||
const threadId = req.params.threadId;
|
||||
const thread = await db.get('SELECT * FROM Thread WHERE Thread.id=?', threadId);
|
||||
if (!thread) throw "";
|
||||
|
||||
const messages = await db.all('SELECT Message.*, User.creationdate as accountCreated, User.username FROM Message LEFT JOIN User WHERE Message.author=User.id AND Message.thread=?', threadId);
|
||||
res.render('pages/thread', { threadinfo: thread, messages: messages, session: req.session });
|
||||
} catch {
|
||||
res.redirect('back');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.get('/', async (req, res) => {
|
||||
const db = await dbPromise;
|
||||
const threads = await db.all('SELECT * FROM Thread');
|
||||
res.render('pages/index', {
|
||||
threads: threads,
|
||||
session: req.session,
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/login", async (req, res) => {
|
||||
if (req.session.logged_in) {
|
||||
return res.redirect('/');
|
||||
}
|
||||
res.render('pages/login');
|
||||
});
|
||||
|
||||
app.get("/logout", async (req, res) => {
|
||||
req.session.logged_in = false;
|
||||
req.session.user_id = null;
|
||||
res.redirect("/");
|
||||
});
|
||||
|
||||
app.post("/login", async (req, res) => {
|
||||
try {
|
||||
const db = await dbPromise;
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) { return res.render('pages/login', { error: "Username or password not provided" }); }
|
||||
const user = await db.get(`SELECT password,id FROM User WHERE username = ?`, username);
|
||||
if (!user) return res.render('pages/login', { error: "Incorrect username or password" });
|
||||
const compared = await bcrypt.compare(password, user.password);
|
||||
if (compared) {
|
||||
req.session.logged_in = true;
|
||||
req.session.user_id = user.id;
|
||||
res.redirect("/");
|
||||
} else {
|
||||
return res.render('pages/login', { error: "Incorrect username or password" });
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return res.render('pages/login', { error: "An error ocurred." });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/register", async (req, res) => {
|
||||
try {
|
||||
const db = await dbPromise;
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) { return res.render('pages/login', { error: "Username or password not provided" }); }
|
||||
if (username.length < 4) { return res.render('pages/login', { error: "Username must be at least 4 characters long" }); }
|
||||
if (password.length < 6) { return res.render('pages/login', { error: "Password must be at least 6 characters long" }); }
|
||||
const passwordHash = await bcrypt.hash(password, SALT_COUNT);
|
||||
const result = await db.run('INSERT INTO User (username,password,creationdate) VALUES (?,?,unixepoch())', username, passwordHash);
|
||||
const userID = result.lastID;
|
||||
req.session.user_id = userID;
|
||||
req.session.logged_in = true;
|
||||
res.redirect("/");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
if (err.errno) {
|
||||
switch (err.errno) {
|
||||
case 19:
|
||||
return res.render('pages/login', { error: "Username already exists" });
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
return res.render('pages/login', { error: "An error ocurred." });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/message/:threadId/", async (req, res) => {
|
||||
try {
|
||||
const threadId = req.params.threadId;
|
||||
const db = await dbPromise;
|
||||
const thread = await db.get('SELECT * FROM Thread WHERE Thread.id=?', threadId);
|
||||
const messageText = req.body.messageText;
|
||||
const subjectText = req.body.subjectText;
|
||||
const userID = req.session.user_id;
|
||||
await db.run("INSERT INTO Message (text,author,subject,creationdate,thread) VALUES (?,?,?,unixepoch(),?);", messageText, userID, subjectText, threadId);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
res.redirect('back');
|
||||
});
|
||||
|
||||
app.post("/newthread", async (req, res) => {
|
||||
try {
|
||||
const db = await dbPromise;
|
||||
const threadName = req.body.threadName;
|
||||
const threadDesc = req.body.threadDesc;
|
||||
const userID = req.session.user_id;
|
||||
await db.run("INSERT INTO Thread (name,author,description,creationdate) VALUES (?,?,?,unixepoch());", threadName, userID, threadDesc);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
res.redirect("/");
|
||||
});
|
||||
|
||||
const setup = async () => {
|
||||
const db = await dbPromise;
|
||||
await db.migrate();
|
||||
app.listen(PORT, () => {
|
||||
console.log("listening on http://localhost:" + PORT);
|
||||
});
|
||||
};
|
||||
setup();
|
17
makefile
Normal file
17
makefile
Normal file
|
@ -0,0 +1,17 @@
|
|||
dev:
|
||||
npx nodemon --ignore ./sessions/ -e js,mjs,json,scss index.js
|
||||
|
||||
build: style
|
||||
- mkdir build
|
||||
cp -r static build/
|
||||
cp -r views build/
|
||||
cp index.js build/index.js
|
||||
|
||||
clean:
|
||||
-rm -r .sass-cache
|
||||
-rm -r sessions
|
||||
-rm -r build
|
||||
-rm data.db
|
||||
|
||||
style:
|
||||
node buildstyle.js
|
32
migrations/001-initial-schema.sql
Normal file
32
migrations/001-initial-schema.sql
Normal file
|
@ -0,0 +1,32 @@
|
|||
-- Up
|
||||
|
||||
CREATE TABLE Thread(
|
||||
id INTEGER PRIMARY KEY,
|
||||
author INTEGER NOT NULL,
|
||||
name STRING NOT NULL,
|
||||
creationdate INTEGER,
|
||||
description STRING
|
||||
);
|
||||
|
||||
CREATE TABLE Message (
|
||||
id INTEGER PRIMARY KEY,
|
||||
text STRING NOT NULL,
|
||||
subject STRING,
|
||||
creationdate INTEGER NOT NULL,
|
||||
thread INTEGER NOT NULL,
|
||||
author INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE User (
|
||||
id INTEGER PRIMARY KEY,
|
||||
username STRING UNIQUE NOT NULL,
|
||||
password STRING NOT NULL,
|
||||
creationdate INTEGER NOT NULL
|
||||
);
|
||||
|
||||
|
||||
-- Down
|
||||
|
||||
DROP TABLE Message;
|
||||
DROP TABLE User;
|
||||
DROP TABLE Thread;
|
24
package.json
Normal file
24
package.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "forum",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"author": "Alexander Bass",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.0.3",
|
||||
"ejs": "^3.1.9",
|
||||
"esm": "^3.2.25",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"session-file-store": "^1.5.0",
|
||||
"sqlite": "^4.1.2",
|
||||
"sqlite3": "^5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.22",
|
||||
"sass": "^1.61.0"
|
||||
}
|
||||
}
|
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 181 KiB |
BIN
static/background.png
Normal file
BIN
static/background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
158
static/out.css
Normal file
158
static/out.css
Normal file
|
@ -0,0 +1,158 @@
|
|||
:root {
|
||||
--faded-text-color: #303030;
|
||||
--background-color: #6d695c;
|
||||
--main-color: #E0E0E0;
|
||||
--border-color: white;
|
||||
--tertiary-color: lightgray;
|
||||
--fourth-color: darkgray;
|
||||
--fun-color: #4CAF50;
|
||||
--angry-color: red;
|
||||
--text-color: #242424;
|
||||
--link-color: #2980b9;
|
||||
--link-visited-color: #9b59b6;
|
||||
--darker-color: #ebebeb;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--background-color);
|
||||
background-image: url("/background.png");
|
||||
background-repeat: repeat;
|
||||
background-size: 100px 100px;
|
||||
font-family: Charter, "Bitstream Charter", "Sitka Text", Cambria, serif;
|
||||
}
|
||||
|
||||
aside {
|
||||
width: 150px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
background: linear-gradient(red, orange);
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
-webkit-background-clip: text;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
div#actionBarRight {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
div#actionBar {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
display: flex;
|
||||
border: 5px ridge var(--fourth-color);
|
||||
border-bottom: none;
|
||||
background-color: lightgray;
|
||||
font-family: sans-serif;
|
||||
align-items: flex-start;
|
||||
}
|
||||
div#actionBar div a {
|
||||
font-size: large;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
display: block;
|
||||
padding-inline: 20px;
|
||||
padding-block: 8px;
|
||||
}
|
||||
div#actionBar div:hover {
|
||||
background-color: var(--fourth-color);
|
||||
}
|
||||
|
||||
div#content {
|
||||
margin-inline: auto;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
header {
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
font-size: 4rem;
|
||||
background: linear-gradient(red, orange);
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-stroke: 1px red;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
header a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#messageContainer:first-child .messageMeta {
|
||||
padding-block: 10px;
|
||||
font-size: 15px;
|
||||
text-decoration: none;
|
||||
text-decoration-style: none;
|
||||
}
|
||||
|
||||
.message {
|
||||
background-color: var(--tertiary-color);
|
||||
margin-bottom: 5px;
|
||||
padding: 0 0 10px 0;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.message .messageMeta {
|
||||
font-family: sans-serif;
|
||||
user-select: none;
|
||||
background: rgb(190, 190, 190);
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
font-size: 13px;
|
||||
padding-block: 5px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.message .messageContent {
|
||||
display: flex;
|
||||
padding-block: 10px;
|
||||
}
|
||||
.message .messageContent .userInfo {
|
||||
font-family: sans-serif;
|
||||
display: inline-block;
|
||||
margin-right: 16px;
|
||||
max-width: 170px;
|
||||
font-size: 15px;
|
||||
border-right: 2px ridge gainsboro;
|
||||
text-align: center;
|
||||
}
|
||||
.message .messageContent .userInfo #userName {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.message .messageContent .messageBody {
|
||||
width: 100%;
|
||||
}
|
||||
.message .messageContent .messageBody h3 {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.message .messageContent .messageBody p {
|
||||
margin: 0px 0px 0px 0px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
main {
|
||||
background-color: var(--main-color);
|
||||
border: 5px ridge var(--border-color);
|
||||
padding-block: 10px;
|
||||
padding-inline: 20px;
|
||||
}
|
46
views/pages/board.ejs
Normal file
46
views/pages/board.ejs
Normal file
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<%- include('../partials/head'); %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<%- include('../partials/header') -%>
|
||||
<div id="content">
|
||||
<%- include('../partials/top'); %>
|
||||
<main>
|
||||
|
||||
<%
|
||||
if(locals.login){
|
||||
%>
|
||||
<div style="background-color: green;">You are logged in</div>
|
||||
<% } else {; %>
|
||||
<div style="background-color:red">You are not logged in</div>
|
||||
<% } %>
|
||||
<% messages.forEach((m) => { %>
|
||||
|
||||
<strong><%= m.username %></strong>:
|
||||
<%= m.text %>
|
||||
<br>
|
||||
<% }); %>
|
||||
</ul>
|
||||
<%
|
||||
if(locals.login){
|
||||
%>
|
||||
<form id="submitForm" action="/message" method="post">
|
||||
<label for="subjectText">Subject:</label>
|
||||
<input type="text" name="subjectText" id="">
|
||||
<label for="messageText">message:</label>
|
||||
<textarea name="messageText" id="" cols="30" rows="10"></textarea>
|
||||
<button type="submit">send</button>
|
||||
</form>
|
||||
<% }; %>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
<footer>
|
||||
<%- include('../partials/footer'); %>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
34
views/pages/index.ejs
Normal file
34
views/pages/index.ejs
Normal file
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<%- include('../partials/head'); %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<%- include('../partials/header') -%>
|
||||
<div id="content">
|
||||
|
||||
<%- include('../partials/top'); %>
|
||||
|
||||
<main>
|
||||
|
||||
<% threads.forEach((t) => { %>
|
||||
<h2><a href="/thread/<%= t.id %>/"><%= t.name %></a></h2>
|
||||
<%= t.description %>
|
||||
<br>
|
||||
<%- include('../partials/format_date',{format_date:t.creationdate}); %>
|
||||
<br>
|
||||
<% }); %>
|
||||
|
||||
|
||||
|
||||
<%- include('../partials/create_thread'); %>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
<footer>
|
||||
<%- include('../partials/footer'); %>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
53
views/pages/login.ejs
Normal file
53
views/pages/login.ejs
Normal file
|
@ -0,0 +1,53 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<%- include('../partials/head'); %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<%- include('../partials/header'); %>
|
||||
</header>
|
||||
<div id="content">
|
||||
<%- include('../partials/top'); %>
|
||||
<main>
|
||||
<%
|
||||
if(locals.error){
|
||||
%>
|
||||
<div style="background-color: red;">Could not login: <%= error %></div>
|
||||
<% }; %>
|
||||
<%
|
||||
if(locals.success){
|
||||
%>
|
||||
<div style="background-color: green; color:white;"><%= success %></div>
|
||||
<% }; %>
|
||||
<h1>Login</h1>
|
||||
<form id="loginForm" action="/login" method="post">
|
||||
|
||||
<label for="usernameField">Username:</label>
|
||||
<input type="text" name="username" id="usernameField">
|
||||
<label for="passwordField">Password</label>
|
||||
<input type="text" name="password" id="passwordField">
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
<h1>Register</h1>
|
||||
<form id="registerForm" action="/register" method="post">
|
||||
|
||||
<label for="usernameField">Username:</label>
|
||||
<input type="text" name="username" id="usernameField">
|
||||
<label for="passwordField">Password</label>
|
||||
<input type="text" name="password" id="passwordField">
|
||||
<button type="submit">Register</button>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<%- include('../partials/footer'); %>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
42
views/pages/thread.ejs
Normal file
42
views/pages/thread.ejs
Normal file
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<%- include('../partials/head'); %>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<%- include('../partials/header') -%>
|
||||
<div id="content">
|
||||
|
||||
<%- include('../partials/top'); %>
|
||||
|
||||
<main>
|
||||
<h1><%= threadinfo.name %></h1>
|
||||
<h2><%= threadinfo.description %> <%- include('../partials/format_date',{format_date:threadinfo.creationdate}); %></h2>
|
||||
<% messages.forEach((m) => { %>
|
||||
<div id="messageContainer">
|
||||
<%- include('../partials/message',{m:m}); %>
|
||||
</div>
|
||||
<% }); %>
|
||||
<%
|
||||
if(locals.session.logged_in){
|
||||
%>
|
||||
<form id="submitForm" action="/message/<%= threadinfo.id %>" method="post">
|
||||
<label for="subjectText">Subject:</label>
|
||||
<input type="text" name="subjectText" id="">
|
||||
<label for="messageText">message:</label>
|
||||
<textarea name="messageText" id="" cols="30" rows="10"></textarea>
|
||||
<button type="submit">send</button>
|
||||
</form>
|
||||
<% } else {; %>
|
||||
<p>You must login to post a message.</p>
|
||||
<% } %>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
<footer>
|
||||
<%- include('../partials/footer'); %>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
13
views/partials/create_thread.ejs
Normal file
13
views/partials/create_thread.ejs
Normal file
|
@ -0,0 +1,13 @@
|
|||
<%
|
||||
if(locals.session.logged_in){
|
||||
%>
|
||||
<form id="submitForm" action="/newthread" method="post">
|
||||
<label for="threadName">Name:</label>
|
||||
<input type="text" name="threadName" id="">
|
||||
<label for="threadDesc">Description</label>
|
||||
<textarea name="threadDesc" id="" cols="30" rows="10"></textarea>
|
||||
<button type="submit">Create New Thread</button>
|
||||
</form>
|
||||
<% } else {; %>
|
||||
<p>You must login to create a thread.</p>
|
||||
<% } %>
|
0
views/partials/footer.ejs
Normal file
0
views/partials/footer.ejs
Normal file
7
views/partials/format_date.ejs
Normal file
7
views/partials/format_date.ejs
Normal file
|
@ -0,0 +1,7 @@
|
|||
<% const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; %>
|
||||
<%
|
||||
const date = new Date(format_date*1000);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth();
|
||||
const day = date.getDate();
|
||||
%><%= months[month] %> <%= `${day}` %> <%= year %>
|
3
views/partials/head.ejs
Normal file
3
views/partials/head.ejs
Normal file
|
@ -0,0 +1,3 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>ForumExperiment.Example</title>
|
||||
<link rel="stylesheet" href="/out.css">
|
3
views/partials/header.ejs
Normal file
3
views/partials/header.ejs
Normal file
|
@ -0,0 +1,3 @@
|
|||
<header>
|
||||
<a href="/">Forum Experiment</a>
|
||||
</header>
|
23
views/partials/message.ejs
Normal file
23
views/partials/message.ejs
Normal file
|
@ -0,0 +1,23 @@
|
|||
<div class="message">
|
||||
<div class="messageMeta">
|
||||
|
||||
<div id="messageDate"><%- include('./format_date',{format_date:m.creationdate}); %></div>
|
||||
</div>
|
||||
<div class="messageContent">
|
||||
<div class="userInfo">
|
||||
<div id="userName">
|
||||
<%= m.username %>
|
||||
</div>
|
||||
|
||||
Member since <%- include('./format_date',{format_date:m.accountCreated}); %>
|
||||
</div>
|
||||
<div class="messageBody">
|
||||
<% if (m.subject){ %>
|
||||
<h3><%= m.subject %></h3>
|
||||
<% }; %>
|
||||
<p>
|
||||
<%= m.text %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
9
views/partials/top.ejs
Normal file
9
views/partials/top.ejs
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div id="actionBar">
|
||||
<div><a href="/">Home</a> </div>
|
||||
<% if(locals.session?.logged_in){ %>
|
||||
<div id="actionBarRight"><a href="/logout">logout</a> </div>
|
||||
<% } else { %>
|
||||
<div id="actionBarRight"><a href="/login">Login</a> </div>
|
||||
<% }; %>
|
||||
</ul>
|
||||
</div>
|
Loading…
Reference in a new issue