mirror of
https://github.com/Wolvan/poll.horse.git
synced 2024-11-22 13:07:58 +01:00
Make frontend js-less
The frontend should work without having JS enabled. JS will enhance the experience but should under no circumstance be necessary. To achieve this, the entire entry system has been turned into a form that posts its values to a new backend path specifically made to take form responses. Instead of returning an API response, it also redirects the browser to either the voting page on successful creation or the frontpage with a bunch of get parameters that are used to prefill the form in a server-side rendering process. An error parameter is also given but there is no way to display said error for now.
This commit is contained in:
parent
862fe9d2f2
commit
9fa3eabad1
6 changed files with 138 additions and 59 deletions
|
@ -17,33 +17,27 @@
|
||||||
<main>
|
<main>
|
||||||
<section class="notepad">
|
<section class="notepad">
|
||||||
<div class="notepad-border"></div>
|
<div class="notepad-border"></div>
|
||||||
|
<form action="{{ BACKEND_BASE_PATH }}/_backend/poll-form" method="POST">
|
||||||
<section class="poll-title">
|
<section class="poll-title">
|
||||||
<input id="poll-title" type="text" maxlength="300" placeholder="Enter your question here">
|
<input id="poll-title" name="poll-title" type="text" maxlength="300" placeholder="Enter your question here" value="{{ FORM_TITLE }}">
|
||||||
</section>
|
</section>
|
||||||
<section id="poll-options" class="poll-options">
|
<section id="poll-options" class="poll-options">
|
||||||
<div class="poll-option">
|
{{ FORM_OPTION_DIVS }}
|
||||||
<input type="text" maxlength="300" placeholder="Enter your option here">
|
|
||||||
</div>
|
|
||||||
<div class="poll-option">
|
|
||||||
<input type="text" maxlength="300" placeholder="Enter your option here">
|
|
||||||
</div>
|
|
||||||
<div class="poll-option">
|
|
||||||
<input type="text" maxlength="300" placeholder="Enter your option here">
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
<section class="poll-footer">
|
<section class="poll-footer">
|
||||||
<select id="dupe-check">
|
<select id="dupe-check" name="dupe-check">
|
||||||
<option value="ip" selected>IP-based duplication checking</option>
|
<option value="ip" {{ FORM_DUPECHECK_IP }}>IP-based duplication checking</option>
|
||||||
<option value="cookie">Cookie-based duplication checking</option>
|
<option value="cookie" {{ FORM_DUPECHECK_COOKIE }}>Cookie-based duplication checking</option>
|
||||||
<option value="none">No duplication checking</option>
|
<option value="none" {{ FORM_DUPECHECK_NONE }}>No duplication checking</option>
|
||||||
</select>
|
</select>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="multiple">
|
<input type="checkbox" name="multi-select" {{ FORM_MULTI_SELECT }} id="multiple">
|
||||||
<span>Allow multiple answers to be selected</span>
|
<span>Allow multiple answers to be selected</span>
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<button id="create-poll">Create poll</button>
|
<input type="submit" name="submit" value="Create poll" id="create-poll">
|
||||||
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -88,6 +88,7 @@ main .notepad-border {
|
||||||
}
|
}
|
||||||
/* #endregion notepad */
|
/* #endregion notepad */
|
||||||
/* #region notepad-footer */
|
/* #region notepad-footer */
|
||||||
|
main .notepad .poll-footer input[type="submit"],
|
||||||
main .notepad .poll-footer button {
|
main .notepad .poll-footer button {
|
||||||
padding: 0 30px;
|
padding: 0 30px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
@ -100,6 +101,7 @@ main .notepad .poll-footer button {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
main .notepad .poll-footer input[type="submit"]:hover,
|
||||||
main .notepad .poll-footer button:hover {
|
main .notepad .poll-footer button:hover {
|
||||||
background-color: #b6b6b6;
|
background-color: #b6b6b6;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
type DupeCheckMode = "none" | "ip" | "cookie";
|
||||||
type BasePoll = {
|
type BasePoll = {
|
||||||
id: string,
|
id: string,
|
||||||
title: string,
|
title: string,
|
||||||
dupeCheckMode: "none" | "ip" | "cookie",
|
dupeCheckMode: DupeCheckMode,
|
||||||
multiSelect: boolean,
|
multiSelect: boolean,
|
||||||
captcha: boolean,
|
captcha: boolean,
|
||||||
creationTime: Date,
|
creationTime: Date,
|
||||||
|
@ -30,5 +30,6 @@ type PollResult = {
|
||||||
export {
|
export {
|
||||||
FrontendPoll,
|
FrontendPoll,
|
||||||
BackendPoll,
|
BackendPoll,
|
||||||
PollResult
|
PollResult,
|
||||||
|
DupeCheckMode
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Router } from "express";
|
||||||
import persist from "node-persist";
|
import persist from "node-persist";
|
||||||
import { program } from "commander";
|
import { program } from "commander";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
import { BackendPoll as Poll } from "./Poll";
|
import { BackendPoll as Poll, DupeCheckMode } from "./Poll";
|
||||||
|
|
||||||
function randomString(length = 10, charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789") {
|
function randomString(length = 10, charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789") {
|
||||||
let result = "";
|
let result = "";
|
||||||
|
@ -60,39 +60,96 @@ export default async function init(router: Router): Promise<void> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/poll", async (req, res) => {
|
async function createPoll(pollData: {
|
||||||
try {
|
title: string,
|
||||||
const options = req.body.options;
|
options: string[],
|
||||||
if (!Array.isArray(options) || options.filter(i => i).length < 2)
|
dupeCheckMode: DupeCheckMode,
|
||||||
return res.status(400).json({ error: "Options must be an array and have at least 2 entries" });
|
multiSelect: boolean,
|
||||||
|
captcha: boolean
|
||||||
|
}): Promise<Poll | string> {
|
||||||
|
if (!Array.isArray(pollData.options) || pollData.options.filter(i => i).length < 2)
|
||||||
|
return "Options must be an array and have at least 2 entries";
|
||||||
|
|
||||||
let id = randomString(8);
|
let id = randomString(8);
|
||||||
while (await polls.getItem(id)) id = randomString(6);
|
while (await polls.getItem(id)) id = randomString(6);
|
||||||
await polls.setItem(id, {});
|
await polls.setItem(id, {});
|
||||||
const dupeCheckMode = ["none", "ip", "cookie"].includes((req.body.dupeCheckMode || "").toLowerCase()) ? (req.body.dupeCheckMode || "").toLowerCase() : "ip";
|
|
||||||
|
const dupeCheckMode = (
|
||||||
|
["none", "ip", "cookie"].includes((pollData.dupeCheckMode || "").toLowerCase()) ?
|
||||||
|
(pollData.dupeCheckMode || "").toLowerCase() : "ip"
|
||||||
|
) as DupeCheckMode;
|
||||||
const dupeData =
|
const dupeData =
|
||||||
dupeCheckMode === "none" ? null :
|
dupeCheckMode === "none" ? null :
|
||||||
dupeCheckMode === "ip" ? [] :
|
dupeCheckMode === "ip" ? [] :
|
||||||
dupeCheckMode === "cookie" ? randomString(16) : null;
|
dupeCheckMode === "cookie" ? randomString(16) : null;
|
||||||
const poll: Poll = {
|
const poll: Poll = {
|
||||||
id,
|
id,
|
||||||
title: (req.body.title || "").trim().slice(0, 300),
|
title: (pollData.title || "").trim().slice(0, 300),
|
||||||
options: (() => {
|
options: (() => {
|
||||||
const result: { [option: string]: number } = {};
|
const result: { [option: string]: number } = {};
|
||||||
for (const option of options.map(i => i.trim().slice(0, 300))) {
|
for (const option of pollData.options.map(i => i.trim().slice(0, 300))) {
|
||||||
if (option) result[option] = 0;
|
if (option) result[option] = 0;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
})(),
|
})(),
|
||||||
dupeCheckMode,
|
dupeCheckMode,
|
||||||
dupeData,
|
dupeData,
|
||||||
multiSelect: req.body.multiSelect || false,
|
multiSelect: pollData.multiSelect || false,
|
||||||
captcha: req.body.captcha || false,
|
captcha: pollData.captcha || false,
|
||||||
creationTime: new Date()
|
creationTime: new Date()
|
||||||
};
|
};
|
||||||
await polls.setItem(id, poll);
|
await polls.setItem(id, poll);
|
||||||
res.json({
|
return poll;
|
||||||
id: id
|
}
|
||||||
|
|
||||||
|
router.post("/poll", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const poll = await createPoll({
|
||||||
|
title: (req.body.title || "").trim().slice(0, 300),
|
||||||
|
options: req.body.options,
|
||||||
|
dupeCheckMode: req.body.dupeCheckMode,
|
||||||
|
multiSelect: req.body.multiSelect || false,
|
||||||
|
captcha: req.body.captcha || false,
|
||||||
});
|
});
|
||||||
|
if (typeof poll !== "string") res.json({
|
||||||
|
id: poll.id
|
||||||
|
});
|
||||||
|
else res.status(400).json({
|
||||||
|
error: poll
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
if (error instanceof Error) res.status(500).json({
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
else res.status(500).json({
|
||||||
|
error: error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router.post("/poll-form", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const poll = await createPoll({
|
||||||
|
title: (req.body["poll-title"] || "").trim().slice(0, 300),
|
||||||
|
options: req.body["poll-option"],
|
||||||
|
dupeCheckMode: req.body["dupe-check"],
|
||||||
|
multiSelect: req.body["multi-select"] === "on",
|
||||||
|
captcha: req.body["captcha"] === "on",
|
||||||
|
});
|
||||||
|
if (typeof poll !== "string") res.redirect("/" + poll.id);
|
||||||
|
else res.redirect(`/?error=${
|
||||||
|
encodeURIComponent(poll)
|
||||||
|
}&title=${
|
||||||
|
encodeURIComponent(req.body["poll-title"])
|
||||||
|
}&options=${
|
||||||
|
encodeURIComponent((req.body["poll-option"] || []).join("\uFFFE"))
|
||||||
|
}&dupecheck=${
|
||||||
|
encodeURIComponent(req.body["dupe-check"])
|
||||||
|
}&multiselect=${
|
||||||
|
req.body["multi-select"] === "on"
|
||||||
|
}&captcha=${
|
||||||
|
req.body["captcha"] === "on"
|
||||||
|
}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error instanceof Error) res.status(500).json({
|
if (error instanceof Error) res.status(500).json({
|
||||||
|
|
|
@ -126,5 +126,29 @@ export default function init(router: Router): void {
|
||||||
res.redirect(`/`);
|
res.redirect(`/`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
router.get("/", (req, res) => displayPage(req, res, "index.html"));
|
|
||||||
|
router.get("/", (req, res) => {
|
||||||
|
const options = (typeof req.query.options === "string" ? req.query.options.split("\uFFFE") : [])
|
||||||
|
.filter(i => i)
|
||||||
|
.concat(Array(3).fill(""))
|
||||||
|
.slice(0, 3);
|
||||||
|
const pollOptionDivs = options.map(option => `
|
||||||
|
<div class="poll-option">
|
||||||
|
<input type="text" name="poll-option" maxlength="300" placeholder="Enter your option here" value="${option}">
|
||||||
|
</div>
|
||||||
|
`).join("");
|
||||||
|
|
||||||
|
displayPage(req, res, "index.html", {
|
||||||
|
"BACKEND_BASE_PATH": (program.opts().backendBaseUrl || ""),
|
||||||
|
"FORM_SUBMISSION_ERROR": req.query.error,
|
||||||
|
"FORM_SUBMISSION_ERROR_SHOWN_CLASS": req.query.error ? "error-visible" : "",
|
||||||
|
"FORM_TITLE": req.query.title || "",
|
||||||
|
"FORM_DUPECHECK_IP": req.query.dupecheck === "ip" ? "selected" : "",
|
||||||
|
"FORM_DUPECHECK_COOKIE": req.query.dupecheck === "cookie" ? "selected" : "",
|
||||||
|
"FORM_DUPECHECK_NONE": req.query.dupecheck === "none" ? "selected" : "",
|
||||||
|
"FORM_MULTI_SELECT": req.query.multiselect === "true" ? "checked" : "",
|
||||||
|
"FORM_CAPTCHA": req.query.captcha === "true" ? "checked" : "",
|
||||||
|
"FORM_OPTION_DIVS": pollOptionDivs
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
|
@ -19,6 +19,7 @@ async function main(): Promise<void> {
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
app.use(cookiepaser());
|
app.use(cookiepaser());
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue