diff --git a/frontend/html/index.html b/frontend/html/index.html index a8463c5..a4958c3 100644 --- a/frontend/html/index.html +++ b/frontend/html/index.html @@ -17,33 +17,27 @@
-
- -
-
-
- -
-
- -
-
- -
-
-
diff --git a/frontend/static/css/main.css b/frontend/static/css/main.css index d8baba9..953470e 100644 --- a/frontend/static/css/main.css +++ b/frontend/static/css/main.css @@ -88,6 +88,7 @@ main .notepad-border { } /* #endregion notepad */ /* #region notepad-footer */ +main .notepad .poll-footer input[type="submit"], main .notepad .poll-footer button { padding: 0 30px; height: 50px; @@ -100,6 +101,7 @@ main .notepad .poll-footer button { color: #fff; margin-right: 5px; } +main .notepad .poll-footer input[type="submit"]:hover, main .notepad .poll-footer button:hover { background-color: #b6b6b6; } diff --git a/src/Poll.ts b/src/Poll.ts index 33ab9bb..9097eb1 100644 --- a/src/Poll.ts +++ b/src/Poll.ts @@ -1,9 +1,9 @@ "use strict"; - +type DupeCheckMode = "none" | "ip" | "cookie"; type BasePoll = { id: string, title: string, - dupeCheckMode: "none" | "ip" | "cookie", + dupeCheckMode: DupeCheckMode, multiSelect: boolean, captcha: boolean, creationTime: Date, @@ -30,5 +30,6 @@ type PollResult = { export { FrontendPoll, BackendPoll, - PollResult + PollResult, + DupeCheckMode }; diff --git a/src/backend.ts b/src/backend.ts index 182774e..3d03f44 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -4,7 +4,7 @@ import { Router } from "express"; import persist from "node-persist"; import { program } from "commander"; import { resolve } from "path"; -import { BackendPoll as Poll } from "./Poll"; +import { BackendPoll as Poll, DupeCheckMode } from "./Poll"; function randomString(length = 10, charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789") { let result = ""; @@ -60,39 +60,96 @@ export default async function init(router: Router): Promise { } }); + async function createPoll(pollData: { + title: string, + options: string[], + dupeCheckMode: DupeCheckMode, + multiSelect: boolean, + captcha: boolean + }): Promise { + 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); + while (await polls.getItem(id)) id = randomString(6); + await polls.setItem(id, {}); + + const dupeCheckMode = ( + ["none", "ip", "cookie"].includes((pollData.dupeCheckMode || "").toLowerCase()) ? + (pollData.dupeCheckMode || "").toLowerCase() : "ip" + ) as DupeCheckMode; + const dupeData = + dupeCheckMode === "none" ? null : + dupeCheckMode === "ip" ? [] : + dupeCheckMode === "cookie" ? randomString(16) : null; + const poll: Poll = { + id, + title: (pollData.title || "").trim().slice(0, 300), + options: (() => { + const result: { [option: string]: number } = {}; + for (const option of pollData.options.map(i => i.trim().slice(0, 300))) { + if (option) result[option] = 0; + } + return result; + })(), + dupeCheckMode, + dupeData, + multiSelect: pollData.multiSelect || false, + captcha: pollData.captcha || false, + creationTime: new Date() + }; + await polls.setItem(id, poll); + return poll; + } + router.post("/poll", async (req, res) => { try { - const options = req.body.options; - if (!Array.isArray(options) || options.filter(i => i).length < 2) - return res.status(400).json({ error: "Options must be an array and have at least 2 entries" }); - let id = randomString(8); - while (await polls.getItem(id)) id = randomString(6); - await polls.setItem(id, {}); - const dupeCheckMode = ["none", "ip", "cookie"].includes((req.body.dupeCheckMode || "").toLowerCase()) ? (req.body.dupeCheckMode || "").toLowerCase() : "ip"; - const dupeData = - dupeCheckMode === "none" ? null : - dupeCheckMode === "ip" ? [] : - dupeCheckMode === "cookie" ? randomString(16) : null; - const poll: Poll = { - id, + const poll = await createPoll({ title: (req.body.title || "").trim().slice(0, 300), - options: (() => { - const result: { [option: string]: number } = {}; - for (const option of options.map(i => i.trim().slice(0, 300))) { - if (option) result[option] = 0; - } - return result; - })(), - dupeCheckMode, - dupeData, + options: req.body.options, + dupeCheckMode: req.body.dupeCheckMode, multiSelect: req.body.multiSelect || false, captcha: req.body.captcha || false, - creationTime: new Date() - }; - await polls.setItem(id, poll); - res.json({ - id: id }); + 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) { console.error(error); if (error instanceof Error) res.status(500).json({ diff --git a/src/frontend.ts b/src/frontend.ts index 71575f7..b6865e4 100644 --- a/src/frontend.ts +++ b/src/frontend.ts @@ -126,5 +126,29 @@ export default function init(router: Router): void { 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 => ` +
+ +
+ `).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 + }); + }); } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 0bf0811..05ebbb3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,6 +19,7 @@ async function main(): Promise { const app = express(); app.use(express.json()); + app.use(express.urlencoded({ extended: true })); app.use(compression()); app.use(cookiepaser());