Ansaribinhyder commited on
Commit
bb5c88a
·
verified ·
1 Parent(s): 66dca15

Upload 5 files

Browse files
Files changed (5) hide show
  1. .gitignore +3 -0
  2. README.md +49 -11
  3. package-lock.json +0 -0
  4. package.json +14 -0
  5. server.js +152 -0
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ node_modules/
2
+ data/votes.xlsx
3
+ .env
README.md CHANGED
@@ -1,11 +1,49 @@
1
- ---
2
- title: BOU 2026
3
- emoji: 🦀
4
- colorFrom: gray
5
- colorTo: red
6
- sdk: static
7
- pinned: false
8
- short_description: This Page is for Voting Purpose
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # BOU 2026 — Best Project voting app
2
+
3
+ A small self-hosted voting app for the 7 BOU 2026 projects. Every vote is written
4
+ as a row (Name, UID, Email, Project, Timestamp) into an Excel file on the server —
5
+ no external database needed.
6
+
7
+ ## What's included
8
+ - `server.js` — Node/Express backend. Handles voting and keeps `data/votes.xlsx` up to date.
9
+ - `public/index.html` — the page voters use.
10
+ - `public/admin.html` — organizer-only results page (tallies + Excel download), protected by an admin key.
11
+ - `data/votes.xlsx` — created automatically on the first vote. **Not included** — don't commit it, it contains personal data (names/UIDs/emails).
12
+
13
+ ## Requirements
14
+ This needs a **Node.js host with persistent disk storage** — it writes to a real
15
+ file, so it won't work on static hosting (GitHub Pages, S3, etc.) or on
16
+ serverless platforms whose filesystem resets between requests (e.g. Vercel/Netlify
17
+ functions). It runs fine on a VPS, an internal company server, Render/Railway
18
+ "web service" (not "static site"), or any machine you control. Node.js 18+ is recommended.
19
+
20
+ ## Setup
21
+ ```bash
22
+ npm install
23
+ ADMIN_KEY="pick-a-real-secret" npm start
24
+ ```
25
+ The app listens on port 3000 by default (override with `PORT=xxxx`).
26
+
27
+ - Voters go to: `http://your-host:3000/`
28
+ - You (organizer) go to: `http://your-host:3000/admin.html` and enter the `ADMIN_KEY`
29
+ you set above to see live results and download the Excel file.
30
+
31
+ **Set a real `ADMIN_KEY`** before sharing the link with anyone — anyone who knows
32
+ the key can see all voters' names/UIDs/emails and the current tally. Without it set,
33
+ the server starts with a placeholder key and prints a warning.
34
+
35
+ ## How voting works
36
+ - A voter fills in Name, UID, and Email, then picks one project and submits.
37
+ - Votes are matched by UID: if the same UID votes again, their row is **updated**
38
+ (their vote changes) rather than duplicated — so one person can't stack votes.
39
+ - Writes are queued in the server so two people voting at the same instant can't
40
+ corrupt the Excel file.
41
+
42
+ ## Changing the project list
43
+ Edit the `PROJECTS` array near the top of `server.js` (and nowhere else — the
44
+ voting page loads the list from the server automatically).
45
+
46
+ ## Privacy note
47
+ The Excel file holds personal data (name, UID, email) for every voter. Store it
48
+ somewhere access-controlled, delete it once the vote is over if you don't need
49
+ to retain it, and don't commit it to a public repo.
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "bou2026-vote",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Standalone team voting app for Best Project - BOU 2026. Logs Name, UID and Email to an Excel file.",
6
+ "main": "server.js",
7
+ "scripts": {
8
+ "start": "node server.js"
9
+ },
10
+ "dependencies": {
11
+ "express": "^4.19.2",
12
+ "exceljs": "^4.4.0"
13
+ }
14
+ }
server.js ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // BOU 2026 - Best Project voting app
2
+ // Serves the voting page and logs every vote (Name, UID, Email, Project, Timestamp)
3
+ // into an Excel file on disk, one row per voter (re-voting updates their row).
4
+
5
+ const path = require("path");
6
+ const fs = require("fs");
7
+ const express = require("express");
8
+ const ExcelJS = require("exceljs");
9
+
10
+ const PORT = process.env.PORT || 3000;
11
+ const ADMIN_KEY = process.env.ADMIN_KEY || "change-me"; // set a real secret before deploying
12
+ const VOTES_FILE = path.join(__dirname, "data", "votes.xlsx");
13
+
14
+ const PROJECTS = [
15
+ { id: "1", name: "Global Account – DAS (Adecco)", who: "Sathiaseelan Arumugam" },
16
+ { id: "2", name: "PCB & IC Package Team", who: "Syam" },
17
+ { id: "3", name: "Product Engineering / Embedded", who: "Munajat Abdul Hadi" },
18
+ { id: "4", name: "CIS", who: "Shunmuganainar Chockalingam" },
19
+ { id: "5", name: "ATE", who: "Kother" },
20
+ { id: "6", name: "Manufacturing POD", who: "Rohan Chowdhary" },
21
+ { id: "7", name: "Manufacturing", who: "Hasrul Hasni" },
22
+ ];
23
+ const PROJECT_IDS = new Set(PROJECTS.map((p) => p.id));
24
+
25
+ const HEADERS = ["Name", "UID", "Email", "Project ID", "Project Name", "Timestamp"];
26
+
27
+ // --- tiny write queue so concurrent votes never corrupt the Excel file ---
28
+ let writeQueue = Promise.resolve();
29
+ function withWriteLock(fn) {
30
+ const result = writeQueue.then(fn, fn);
31
+ writeQueue = result.catch(() => {});
32
+ return result;
33
+ }
34
+
35
+ async function loadWorkbook() {
36
+ const wb = new ExcelJS.Workbook();
37
+ if (fs.existsSync(VOTES_FILE)) {
38
+ await wb.xlsx.readFile(VOTES_FILE);
39
+ } else {
40
+ fs.mkdirSync(path.dirname(VOTES_FILE), { recursive: true });
41
+ const ws = wb.addWorksheet("Votes");
42
+ ws.addRow(HEADERS);
43
+ ws.getRow(1).font = { bold: true };
44
+ }
45
+ const ws = wb.getWorksheet("Votes") || wb.addWorksheet("Votes");
46
+ if (ws.rowCount === 0) {
47
+ ws.addRow(HEADERS);
48
+ ws.getRow(1).font = { bold: true };
49
+ }
50
+ return { wb, ws };
51
+ }
52
+
53
+ async function recordVote({ name, uid, email, projectId }) {
54
+ return withWriteLock(async () => {
55
+ const { wb, ws } = await loadWorkbook();
56
+ const project = PROJECTS.find((p) => p.id === projectId);
57
+
58
+ // find an existing row for this UID (column 2) so re-voting updates it
59
+ let existingRow = null;
60
+ ws.eachRow((row, rowNumber) => {
61
+ if (rowNumber === 1) return;
62
+ if (String(row.getCell(2).value) === String(uid)) existingRow = row;
63
+ });
64
+
65
+ const values = [name, uid, email, project.id, project.name, new Date().toISOString()];
66
+ if (existingRow) {
67
+ existingRow.values = [, ...values]; // keep 1-based column alignment
68
+ } else {
69
+ ws.addRow(values);
70
+ }
71
+
72
+ await wb.xlsx.writeFile(VOTES_FILE);
73
+ });
74
+ }
75
+
76
+ async function tallyResults() {
77
+ const { ws } = await loadWorkbook();
78
+ const counts = {};
79
+ let total = 0;
80
+ const voters = [];
81
+ ws.eachRow((row, rowNumber) => {
82
+ if (rowNumber === 1) return;
83
+ const projectId = String(row.getCell(4).value || "");
84
+ if (!projectId) return;
85
+ counts[projectId] = (counts[projectId] || 0) + 1;
86
+ total += 1;
87
+ voters.push({
88
+ name: row.getCell(1).value,
89
+ uid: row.getCell(2).value,
90
+ email: row.getCell(3).value,
91
+ projectId,
92
+ });
93
+ });
94
+ return { counts, total, voters };
95
+ }
96
+
97
+ // --- app ---
98
+ const app = express();
99
+ app.use(express.json());
100
+ app.use(express.static(path.join(__dirname, "public")));
101
+
102
+ app.get("/api/projects", (req, res) => res.json(PROJECTS));
103
+
104
+ app.post("/api/vote", async (req, res) => {
105
+ const { name, uid, email, projectId } = req.body || {};
106
+ if (!name || !uid || !email || !projectId) {
107
+ return res.status(400).json({ ok: false, error: "Name, UID, email and a project choice are all required." });
108
+ }
109
+ if (!/^\S+@\S+\.\S+$/.test(email)) {
110
+ return res.status(400).json({ ok: false, error: "That email address doesn't look valid." });
111
+ }
112
+ if (!PROJECT_IDS.has(String(projectId))) {
113
+ return res.status(400).json({ ok: false, error: "Unknown project." });
114
+ }
115
+ try {
116
+ await recordVote({ name: String(name).trim(), uid: String(uid).trim(), email: String(email).trim(), projectId: String(projectId) });
117
+ res.json({ ok: true });
118
+ } catch (err) {
119
+ console.error(err);
120
+ res.status(500).json({ ok: false, error: "Could not save your vote. Please try again." });
121
+ }
122
+ });
123
+
124
+ function requireAdmin(req, res, next) {
125
+ const key = req.query.key || req.headers["x-admin-key"];
126
+ if (key !== ADMIN_KEY) return res.status(401).json({ ok: false, error: "Invalid admin key." });
127
+ next();
128
+ }
129
+
130
+ app.get("/api/results", requireAdmin, async (req, res) => {
131
+ try {
132
+ const { counts, total } = await tallyResults();
133
+ res.json({ ok: true, projects: PROJECTS, counts, total });
134
+ } catch (err) {
135
+ console.error(err);
136
+ res.status(500).json({ ok: false, error: "Could not load results." });
137
+ }
138
+ });
139
+
140
+ app.get("/api/export", requireAdmin, async (req, res) => {
141
+ if (!fs.existsSync(VOTES_FILE)) {
142
+ return res.status(404).json({ ok: false, error: "No votes recorded yet." });
143
+ }
144
+ res.download(VOTES_FILE, "bou2026-votes.xlsx");
145
+ });
146
+
147
+ app.listen(PORT, () => {
148
+ console.log(`BOU 2026 voting app listening on port ${PORT}`);
149
+ if (ADMIN_KEY === "change-me") {
150
+ console.warn("WARNING: set a real ADMIN_KEY environment variable before sharing this app.");
151
+ }
152
+ });