├── .gitignore ├── .env.example ├── src ├── data │ └── votes.json ├── app.ts ├── config │ └── fluvio.ts └── routes │ └── vote.ts ├── tsconfig.json ├── package.json ├── LICENSE ├── README.md └── public ├── vote.html └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | \node_modules 2 | .env -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=8000 2 | FLUVIO_TOPIC=vote-topic -------------------------------------------------------------------------------- /src/data/votes.json: -------------------------------------------------------------------------------- 1 | { 2 | "A": [ 3 | "Anand", 4 | "Anan", 5 | "Kom1" 6 | ], 7 | "B": [ 8 | "Anon" 9 | ], 10 | "C": [ 11 | "Kom" 12 | ], 13 | "D": [], 14 | "E": [] 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "CommonJS", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "resolveJsonModule": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import dotenv from "dotenv"; 4 | import path from "path"; 5 | import voteRoutes from "./routes/vote"; 6 | import { getVotes, initializeFluvio } from "./config/fluvio"; 7 | 8 | dotenv.config(); 9 | const port = process.env.PORT || 8000; 10 | 11 | const app = express(); 12 | app.use(express.json()); 13 | app.use(cors()); 14 | app.use(express.static(path.join(__dirname, "../public"))); 15 | 16 | app.use("/api/vote", voteRoutes); 17 | 18 | app.listen(port, () => { 19 | console.log(`Server running on port ${port}`); 20 | initializeFluvio(); 21 | }); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "start": "tsc && node dist/app.js", 8 | "dev": "ts-node-dev src/app.ts" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "devDependencies": { 15 | "@types/cors": "^2.8.17", 16 | "@types/express": "^4.17.21", 17 | "@types/node": "^22.4.0", 18 | "@types/ws": "^8.5.12", 19 | "nodemon": "^3.1.4", 20 | "ts-node": "^10.9.2", 21 | "ts-node-dev": "^2.0.0", 22 | "typescript": "^5.5.4" 23 | }, 24 | "dependencies": { 25 | "@fluvio/client": "^0.14.9", 26 | "cors": "^2.8.5", 27 | "dotenv": "^16.4.5", 28 | "express": "^4.19.2", 29 | "ws": "^8.18.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 K Om Senapati 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/config/fluvio.ts: -------------------------------------------------------------------------------- 1 | import Fluvio, { Offset } from "@fluvio/client"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | const FLUVIO_TOPIC = process.env.FLUVIO_TOPIC || "vote-topic"; 7 | const PARTITION = 0; 8 | 9 | export const initializeFluvio = async () => { 10 | try { 11 | const fluvio = new Fluvio(); 12 | await fluvio.connect(); 13 | console.log("Fluvio initialized"); 14 | } catch (error) { 15 | console.error("Failed to initialize Fluvio:", error); 16 | } 17 | }; 18 | 19 | export const getConsumer = async () => { 20 | const fluvio = new Fluvio(); 21 | await fluvio.connect(); 22 | const consumer = await fluvio.partitionConsumer(FLUVIO_TOPIC, PARTITION); 23 | return consumer; 24 | }; 25 | 26 | const getProducer = async () => { 27 | const fluvio = new Fluvio(); 28 | await fluvio.connect(); 29 | const producer = await fluvio.topicProducer(FLUVIO_TOPIC); 30 | return producer; 31 | }; 32 | 33 | export const sendVote = async (candidate: string) => { 34 | const producer = await getProducer(); 35 | await producer.send("candidate", JSON.stringify(candidate)); 36 | }; 37 | 38 | export const getVotes = async () => { 39 | const votes: string[] = []; 40 | const consumer = await getConsumer(); 41 | 42 | await consumer.stream(Offset.FromEnd(), async (record) => { 43 | votes.push(record.valueString()); 44 | }); 45 | return votes; 46 | }; 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Real-Time Vote App** 🎉 2 | 3 | ![Real-Time Vote App](https://socialify.git.ci/kom-senapati/real-time-voting-app/image?font=KoHo&name=1&owner=1&pattern=Diagonal%20Stripes&theme=Light) 4 | 5 | ## **Description:** 6 | 7 | The Real-Time Vote App provides a seamless voting experience with live updates using Fluvio and Server-Sent Events (SSE). Users can cast their votes in real-time and watch as the results update dynamically. This application showcases modern web technologies and real-time data handling, making voting engaging and immediate. 8 | 9 | ## **Tech Stack:** 10 | 11 | [![My Skills](https://skillicons.dev/icons?i=ts,nodejs,expressjs,html,tailwindcss)](https://skillicons.dev) 12 | 13 | - Chart JS 14 | - Fluvio 15 | 16 | ## **Features:** 17 | 18 | - **Real-Time Voting:** 🗳️ See live updates on voting results as votes are cast. 19 | - **Dynamic Charting:** 📊 View and interact with real-time charts using Chart.js. 20 | 21 |
22 |

Screenshots:

23 | 24 | ![image](https://github.com/user-attachments/assets/9acee59c-70b8-4241-90ed-110c942e079d) 25 | ![image](https://github.com/user-attachments/assets/d086a687-9d91-4aec-9430-eb7a9cfcfe7b) 26 | ![image](https://github.com/user-attachments/assets/9b1ca939-faf4-41c3-b41c-0f37333fed90) 27 | 28 |
29 | 30 | ## **Demo Video:** 31 | 32 | [![YouTube](http://i.ytimg.com/vi/vIrSBFoPjvk/hqdefault.jpg)](https://www.youtube.com/watch?v=vIrSBFoPjvk) 33 | 34 | ## **Getting Started:** 35 | 36 | To get started with the Real-Time Vote App: 37 | 38 | 1. **Install Fluvio**: Open Terminal and run the following command: 39 | 40 | ```bash 41 | curl -fsS https://hub.infinyon.cloud/install/install.sh | bash 42 | ``` 43 | 44 | 2. **Add Fluvio to your path**: 45 | 46 | ```bash 47 | echo 'export PATH="${HOME}/.fvm/bin:${HOME}/.fluvio/bin:${PATH}"' >> ~/.bashrc 48 | echo 'source "${HOME}/.fvm/env"' >> ~/.bashrc 49 | ``` 50 | 51 | 3. **Source the new .bashrc file**: 52 | 53 | ```bash 54 | source ~/.bashrc 55 | ``` 56 | 57 | 4. **Start the Fluvio cluster and create a topic**: Follow the instructions at [Fluvio GitHub](https://github.com/infinyon/fluvio): 58 | 59 | ```bash 60 | fluvio cluster start 61 | fluvio topic create vote-topic 62 | ``` 63 | 64 | 5. **Install dependencies**: 65 | 66 | ```bash 67 | npm install 68 | ``` 69 | 70 | 6. **Run the development server**: 71 | ```bash 72 | npm run dev 73 | ``` 74 | 75 | ## **About the Author:** 76 | 77 | Hi, I'm K Om Senapati! 👋 78 | Connect with me on [LinkedIn](https://www.linkedin.com/in/kom-senapati/), [X](https://x.com/kom_senapati) and check out my other projects on [GitHub](https://github.com/kom-senapati). 79 | -------------------------------------------------------------------------------- /src/routes/vote.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import { getConsumer, sendVote } from "../config/fluvio"; 3 | import { Offset, type Record } from "@fluvio/client"; 4 | import fs from "fs"; 5 | import path from "path"; 6 | 7 | const router = express.Router(); 8 | 9 | const votesFilePath = path.join(__dirname, "../data/votes.json"); 10 | 11 | interface VotesData { 12 | [candidate: string]: string[]; 13 | } 14 | 15 | interface SubmitRequestBody { 16 | candidate?: string; 17 | user: string; 18 | action: "create" | "update" | "delete"; 19 | } 20 | 21 | router.post( 22 | "/submit", 23 | async (req: Request<{}, {}, SubmitRequestBody>, res: Response) => { 24 | let { candidate, user, action } = req.body; 25 | let oldParty = ""; 26 | 27 | if (!candidate && action !== "delete") { 28 | return res.status(400).send({ error: "Candidate is required." }); 29 | } 30 | 31 | if (!user) { 32 | return res.status(400).send({ error: "User is required." }); 33 | } 34 | 35 | if (!["create", "update", "delete"].includes(action)) { 36 | return res.status(400).send({ error: "Invalid action." }); 37 | } 38 | 39 | try { 40 | const votesData: VotesData = JSON.parse( 41 | fs.readFileSync(votesFilePath, "utf8") 42 | ); 43 | const hasVoted = Object.values(votesData).some((voters: string[]) => 44 | voters.includes(user) 45 | ); 46 | 47 | if (action === "create") { 48 | if (hasVoted) { 49 | return res.status(400).send({ error: "User has already voted." }); 50 | } 51 | 52 | if (!votesData[candidate!]) { 53 | votesData[candidate!] = []; 54 | } 55 | votesData[candidate!].push(user); 56 | } else if (action === "update") { 57 | if (!hasVoted) { 58 | return res.status(400).send({ error: "User has not voted yet." }); 59 | } 60 | 61 | for (const key of Object.keys(votesData)) { 62 | if (votesData[key].includes(user)) { 63 | if (votesData[key].includes(user)) { 64 | votesData[key] = votesData[key].filter( 65 | (voter: string) => voter !== user 66 | ); 67 | oldParty = key; 68 | break; 69 | } 70 | } 71 | } 72 | 73 | if (!votesData[candidate!]) { 74 | votesData[candidate!] = []; 75 | } 76 | votesData[candidate!].push(user); 77 | } else if (action === "delete") { 78 | if (!hasVoted) { 79 | return res.status(400).send({ error: "User has not voted yet." }); 80 | } 81 | 82 | for (const key of Object.keys(votesData)) { 83 | if (votesData[key].includes(user)) { 84 | votesData[key] = votesData[key].filter( 85 | (voter: string) => voter !== user 86 | ); 87 | candidate = key; 88 | } 89 | } 90 | } 91 | 92 | fs.writeFileSync(votesFilePath, JSON.stringify(votesData, null, 2)); 93 | 94 | const eventData = `${user}:${action}:${candidate}:${oldParty}`; 95 | await sendVote(JSON.stringify(eventData)); 96 | 97 | res 98 | .status(200) 99 | .send({ message: "Vote operation completed successfully." }); 100 | } catch (error) { 101 | console.error("Error submitting vote:", error); 102 | res.status(500).send({ error: "Failed to perform vote operation." }); 103 | } 104 | } 105 | ); 106 | 107 | router.get("/stream", async (_req: Request, res: Response) => { 108 | try { 109 | res.setHeader("Access-Control-Allow-Origin", "*"); 110 | res.setHeader("Cache-Control", "no-cache"); 111 | res.setHeader("Content-Type", "text/event-stream;"); 112 | res.setHeader("Connection", "keep-alive"); 113 | res.setHeader("X-Accel-Buffering", "no"); 114 | res.flushHeaders(); 115 | 116 | const consumer = await getConsumer(); 117 | await consumer.stream(Offset.FromEnd(), async (record: Record) => { 118 | const eventData = record.valueString(); 119 | res.write(`data: ${eventData}\n\n`); 120 | }); 121 | 122 | res.on("close", () => { 123 | res.end(); 124 | }); 125 | } catch (error) { 126 | console.log(error); 127 | return res.status(500).json({ 128 | error: error, 129 | }); 130 | } 131 | }); 132 | 133 | router.get("/", async (_req: Request, res: Response) => { 134 | try { 135 | const votesData = JSON.parse(fs.readFileSync(votesFilePath, "utf8")); 136 | 137 | const voteCounts: { [key: string]: number } = {}; 138 | for (const candidate in votesData) { 139 | voteCounts[candidate] = votesData[candidate].length; 140 | } 141 | 142 | res.status(200).send(voteCounts); 143 | } catch (error) { 144 | console.error("Error fetching vote counts:", error); 145 | res.status(500).send({ error: "Failed to fetch vote counts." }); 146 | } 147 | }); 148 | 149 | export default router; 150 | -------------------------------------------------------------------------------- /public/vote.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Real-Time Vote App 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 43 | 44 | 45 |
46 |

Vote for Your Favorite Party

47 | 48 |
49 | 55 |
56 | 57 |
58 | 59 |
60 | 61 |
62 | 73 |
74 | 75 |
76 | 83 | 90 | 97 |
98 |
99 | 100 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Real-Time Vote App - Landing Page 7 | 8 | 12 | 163 | 164 | 165 | 166 | 231 | 232 |
238 |
239 |
242 |

243 | Welcome to the Real-Time Vote App 244 |

245 |

246 | This app provides real-time voting results using Fluvio and 247 | Server-Sent Events (SSE). 248 |

249 | 250 | 268 | 269 |
270 |
271 | 272 |
273 |
274 |

Features

275 |
276 |
277 |

Real-Time Chart

278 |

279 | Display real-time voting data using dynamic charts that update 280 | instantly. 281 |

282 |
283 |
284 |

Fluvio for Real-Time

285 |

286 | Utilize Fluvio, a powerful real-time data platform for live vote 287 | streaming. 288 |

289 |
290 |
291 |

JSON Management

292 |

293 | Efficiently manage votes using JSON for data handling and 294 | processing. 295 |

296 |
297 |
298 |
299 |
300 | 301 |
302 |
303 |

Tech Stack

304 |
305 | Node.js 310 | TypeScript 315 | HTML 320 | Tailwind CSS 325 |
326 | 331 | Fluvio 332 | 333 |
334 |
335 | 336 |
337 |
340 |

Open Source

341 |

342 | Our project is open source! Contribute and star us on GitHub. 343 |

344 | 394 |
395 |
396 | 397 |
398 |
399 |

Contact Me

400 |

401 | For more information or inquiries, feel free to reach out. 402 |

403 | 404 | 557 | 558 |
559 |
560 | 561 | 564 | 565 | 578 | 579 | 580 | --------------------------------------------------------------------------------