Compare commits

..

14 Commits

Author SHA1 Message Date
TQY
37429aa7f2 translate ProfileImage component text to Chinese 2025-10-17 11:06:41 +08:00
TQY
bf6dff352e chinese 2025-10-17 10:43:04 +08:00
TQY
8c1c0abd8e update staleTime 2025-10-17 10:34:20 +08:00
root
efacf59414 init 2025-10-17 02:23:24 +00:00
Manik Maity
661f7cf6e9 Updated README.md with links 2024-11-15 20:10:51 +05:30
Manik Maity
5799fedbf3 Updated README.md 2024-11-15 20:05:40 +05:30
Manik Maity
4e8f7cffa6 Updated README.md 2024-11-15 20:02:16 +05:30
Manik Maity
9b0f53f742 Updated README.md 2024-11-15 20:01:13 +05:30
Manik Maity
698a84e903 Added options selected in frontend 2024-11-13 22:05:19 +05:30
Manik Maity
131b921d96 Added get voted details route 2024-11-13 21:43:10 +05:30
Manik Maity
a1319b8b2b FIX - Deploy bug in frontend 2024-11-13 21:22:46 +05:30
Manik Maity
3cc060b713 FIX - Deploy bug 2024-11-13 21:17:58 +05:30
Manik Maity
33e878d763 FIX - Deploy bug 2024-11-13 21:13:38 +05:30
Manik Maity
6101ec0821 FIX 2024-11-13 20:57:49 +05:30
35 changed files with 481 additions and 227 deletions

14
.env Normal file
View File

@@ -0,0 +1,14 @@
# 端口
PORT=3000
# MongoDB 连接地址,如果 MongoDB 在本机默认端口
DB_CONNECTION="mongodb://localhost:27017/livepoll"
# 密码加密复杂度
SALT_ROUNDS=6
# JWT 私钥(可以随便写一串随机字符)
JWT_PRIVATE="your-secret-jwt-key"
# 前端访问地址(群友打开的链接)
CLIENT_URL="http://110.42.109.143:3000"

117
DESIGN.md Normal file
View File

@@ -0,0 +1,117 @@
## System Design for LivePoll
#### 1. **Features**
- **User Authentication:** Users can log in, create polls, and vote. Authenticated users can bookmark polls.
- **Create Polls:** Users can create polls with options for others to vote on.
- **Real-Time Voting & Visualization:** Live updates for ongoing polls and dynamic visualizations using charts.
- **Bookmark Polls:** Users can bookmark polls to view or participate in later.
- **View Past Participation:** A history of polls the user participated in.
#### 2. **Frontend (React)**
- **React Components**:
- **Login/Register Page:** For user authentication.
- **Dashboard:** Displays polls created by the user and polls they bookmarked.
- **Poll Creation Page:** Allows users to create a new poll with title, description, and options.
- **Poll Display Page:** Shows the poll with voting options, live updates on votes, and a visualization of the voting progress (e.g., bar chart or pie chart).
- **Bookmarks Page:** Displays a list of bookmarked polls.
- **Real-Time Voting Updates**:
- Use **Socket.io** or **WebSockets** to enable real-time updates. When a user votes, the data is sent to the server and then broadcast to other connected clients, updating the visualization immediately.
#### 3. **Backend (Node.js + Express)**
- **API Endpoints**:
- **Auth Routes**:
- `POST /api/auth/register`: User registration.
- `POST /api/auth/login`: User login.
- **Poll Routes**:
- `POST /api/polls`: Create a new poll.
- `GET /api/polls/:pollId`: Get poll details, including live vote count.
- `POST /api/polls/:pollId/vote`: Submit a vote for a poll.
- `GET /api/polls/bookmarked`: Retrieve all bookmarked polls for a user.
- `POST /api/polls/:pollId/bookmark`: Bookmark a poll.
- **User Routes**:
- `GET /api/user/history`: Get a list of polls the user has participated in.
- **Real-Time Updates**:
- **Socket.io**: When a vote is cast, the server broadcasts the updated vote count to all connected clients subscribed to that poll. This ensures real-time updates on each client.
#### 4. **Database (MongoDB)**
- **Collections**:
- **Users**: Stores user information, bookmarks, and participated polls.
- **Polls**: Contains details of each poll, options, votes, and the creators user ID.
- **Votes**: Each document records a vote with a user ID, poll ID, and option ID.
- **Bookmarks**: Stores references to polls a user has bookmarked.
- **Schema Design**:
- **User Schema**:
```javascript
{
_id: ObjectId,
username: String,
password: String, // hashed
bookmarks: [pollId],
history: [pollId]
}
```
- **Poll Schema**:
```javascript
{
_id: ObjectId,
title: String,
description: String,
options: [
{ optionId: ObjectId, text: String, voteCount: Number }
],
creatorId: ObjectId,
createdAt: Date
}
```
- **Vote Schema**:
```javascript
{
_id: ObjectId,
userId: ObjectId,
pollId: ObjectId,
optionId: ObjectId
}
```
#### 5. **Real-Time Voting with Socket.io**
- **Workflow**:
- When a user votes on a poll, the frontend triggers a `vote` event via **Socket.io**.
- The server receives this event, updates the vote count in the database, and broadcasts the updated vote count to all clients connected to that poll.
- This enables real-time visualization as each client receives the updated poll data without refreshing the page.
#### 6. **Chart Visualization**
- **Frontend Visualization**:
- Use **Chart.js** or **D3.js** to create charts that display poll results.
- As Socket.io sends updates, the poll display page refreshes the chart data dynamically, showing live vote changes.
#### 7. **Bookmarking & Poll History**
- **Bookmarking a Poll**:
- When a user bookmarks a poll, the poll ID is added to their bookmarks in the `Users` collection.
- The bookmarks page fetches all polls that match the bookmarked poll IDs and displays them.
- **Poll History**:
- Each vote by a user is recorded, with the poll ID added to their `history` field in the `Users` collection.
- This allows users to view past polls they participated in.
### System Diagram
A basic diagram for this system would include:
1. **Frontend (React)**: Login/Register Page, Dashboard, Poll Creation Page, Poll Display Page with charts, Bookmark and History pages.
2. **Backend (Express)**: API endpoints for user actions (vote, create poll, bookmark), real-time voting handled by Socket.io.
3. **Database (MongoDB)**: Stores collections for users, polls, votes, and bookmarks.
4. **Socket.io/WebSocket**: Handles real-time updates from the server to the clients as votes come in.
## To go from deployed to local:
- change the backend .env url to `http://localhost:3000`
- chnage the axios base url to `http://localhost:3000/api/v1`
- change the io url in the vottingPage to `http://localhost:3000`

238
README.md
View File

@@ -1,113 +1,169 @@
## System Design for LivePoll
<div align="center">
<img height="100px" src="./images/imageGIF.gif"/>
<h1>LivePoll - Live Polling Platform</h1>
</div>
#### 1. **Features**
- **User Authentication:** Users can log in, create polls, and vote. Authenticated users can bookmark polls.
- **Create Polls:** Users can create polls with options for others to vote on.
- **Real-Time Voting & Visualization:** Live updates for ongoing polls and dynamic visualizations using charts.
- **Bookmark Polls:** Users can bookmark polls to view or participate in later.
- **View Past Participation:** A history of polls the user participated in.
LivePoll is an interactive web application designed to simplify the process of creating, participating in, and managing polls. It combines user-friendly features with real-time updates to deliver a seamless polling experience📊.
#### 2. **Frontend (React)**
## Features
- **React Components**:
- **Login/Register Page:** For user authentication.
- **Dashboard:** Displays polls created by the user and polls they bookmarked.
- **Poll Creation Page:** Allows users to create a new poll with title, description, and options.
- **Poll Display Page:** Shows the poll with voting options, live updates on votes, and a visualization of the voting progress (e.g., bar chart or pie chart).
- **Bookmarks Page:** Displays a list of bookmarked polls.
- User can signup and login using his credentials, used cookie based authentication with jwt.
- User can browse all the Polls created by other users in a pagination format and click on view to view the poll.
- In poll view page user can vote on the poll and see the result of the poll live with chart using sockt.io.
- User can bookmark the poll and see the bookmarked poll in bookmark page.
- In user dashboard user can see the their details and manage their poll.
- By clicking on the create poll button user can create a new poll and add options to the poll.
- Used react-toastify for showing the error and success message.
- Used chart.js and scocket.io-client for showing the poll result live in chart in poll view page.
- Used daisyui and tailwind for styling the UI of the application for responsive design.
- **Real-Time Voting Updates**:
- Use **Socket.io** or **WebSockets** to enable real-time updates. When a user votes, the data is sent to the server and then broadcast to other connected clients, updating the visualization immediately.
## Links
#### 3. **Backend (Node.js + Express)**
- [Live Website](https://live-poll-wine.vercel.app/) - Loading time may take few seconds initially (free tier).
- [Backend Routes Doc](https://livepoll-anjx.onrender.com/docs/)
- **API Endpoints**:
- **Auth Routes**:
- `POST /api/auth/register`: User registration.
- `POST /api/auth/login`: User login.
- **Poll Routes**:
- `POST /api/polls`: Create a new poll.
- `GET /api/polls/:pollId`: Get poll details, including live vote count.
- `POST /api/polls/:pollId/vote`: Submit a vote for a poll.
- `GET /api/polls/bookmarked`: Retrieve all bookmarked polls for a user.
- `POST /api/polls/:pollId/bookmark`: Bookmark a poll.
- **User Routes**:
- `GET /api/user/history`: Get a list of polls the user has participated in.
## Preview Images
- **Real-Time Updates**:
- **Socket.io**: When a vote is cast, the server broadcasts the updated vote count to all connected clients subscribed to that poll. This ensures real-time updates on each client.
### Home Page
#### 4. **Database (MongoDB)**
<img src="./images/Home.png"/>
- **Collections**:
- **Users**: Stores user information, bookmarks, and participated polls.
- **Polls**: Contains details of each poll, options, votes, and the creators user ID.
- **Votes**: Each document records a vote with a user ID, poll ID, and option ID.
- **Bookmarks**: Stores references to polls a user has bookmarked.
### Polls Page
- **Schema Design**:
- **User Schema**:
```javascript
{
_id: ObjectId,
username: String,
password: String, // hashed
bookmarks: [pollId],
history: [pollId]
}
```
- **Poll Schema**:
```javascript
{
_id: ObjectId,
title: String,
description: String,
options: [
{ optionId: ObjectId, text: String, voteCount: Number }
],
creatorId: ObjectId,
createdAt: Date
}
```
- **Vote Schema**:
```javascript
{
_id: ObjectId,
userId: ObjectId,
pollId: ObjectId,
optionId: ObjectId
}
<img src="./images/pollsPage.png"/>
### Login Page
<img src="./images/Screenshot 2024-11-14 101710.png"/>
### Signup Page
<img src="./images/signup.png"/>
### Poll Votting Page
<img src="./images/votingPage.png"/>
### Dashboard Page
<img src="./images/dashboard.png"/>
### Create Poll Page
<img src="./images/createPollPage.png"/>
### Bookmarks Page
<img src="./images/bookmark.png"/>
## Tech Stack
### Frontend
Framework & Routing: `ReactJS`, `React Router`
State Management: `Zustand`, `React Query`
Real-Time & Charts: `Socket.io-client`, `react-chartjs-2`
Styling: `TailwindCSS`, `DaisyUI`
Notifications & Icons: `React-Toastify`, `React Icons`
### Backend
Framework & Authentication: `Node.js`, `Express.js`, `JWT`, `bcrypt`
Validation & Documentation: `Zod`, `Swagger-jsdoc`
Real-Time Communication: `Socket.io`
Database & ORM: `Mongoose`
### Others
API Communication: `Axios`
## Installation and Setup
### Prerequisites
- Node.js and npm/yarn installed.
- MongoDB database set up locally or on a cloud provider.
### Steps
1. Clone the Repository
```bash
git clone https://github.com/ManikMaity/LivePoll.git
cd LivePoll
```
#### 5. **Real-Time Voting with Socket.io**
2. Backend Setup
- **Workflow**:
- When a user votes on a poll, the frontend triggers a `vote` event via **Socket.io**.
- The server receives this event, updates the vote count in the database, and broadcasts the updated vote count to all clients connected to that poll.
- This enables real-time visualization as each client receives the updated poll data without refreshing the page.
- Navigate to the backend directory:
```bash
cd backend
```
- Install dependencies:
```bash
npm install
```
- Create a `.env` file and add the following:
`env
PORT=3000
DB_CONNECTION="your mongodb url"
SALT_ROUNDS=6
JWT_PRIVATE="your jwt private key"
CLIENT_URL="your client url"
`
- Start the server:
```bash
npm run dev
```
#### 6. **Chart Visualization**
3. Frontend Setup
- **Frontend Visualization**:
- Use **Chart.js** or **D3.js** to create charts that display poll results.
- As Socket.io sends updates, the poll display page refreshes the chart data dynamically, showing live vote changes.
- Navigate to the frontend directory:
```bash
cd frontend
```
- Install dependencies:
```bash
npm install
```
- Update `.env` file with the backend URL (e.g., `http://localhost:3000`).
- Start the development server:
```bash
npm start
```
#### 7. **Bookmarking & Poll History**
4. Access the Application
- Open a browser and go to `http://localhost:5173`.
- **Bookmarking a Poll**:
- When a user bookmarks a poll, the poll ID is added to their bookmarks in the `Users` collection.
- The bookmarks page fetches all polls that match the bookmarked poll IDs and displays them.
---
- **Poll History**:
- Each vote by a user is recorded, with the poll ID added to their `history` field in the `Users` collection.
- This allows users to view past polls they participated in.
### To Switch Between Local and Deployed Environments
### System Diagram
- Update backend `.env` with:
```env
BACKEND_URL=http://localhost:3000
```
- Update frontend Axios base URL to:
```javascript
axios.defaults.baseURL = "http://localhost:3000/api/v1";
```
- Update the Socket.io URL in the voting page:
```javascript
const socket = io("http://localhost:3000");
```
A basic diagram for this system would include:
---
1. **Frontend (React)**: Login/Register Page, Dashboard, Poll Creation Page, Poll Display Page with charts, Bookmark and History pages.
2. **Backend (Express)**: API endpoints for user actions (vote, create poll, bookmark), real-time voting handled by Socket.io.
3. **Database (MongoDB)**: Stores collections for users, polls, votes, and bookmarks.
4. **Socket.io/WebSocket**: Handles real-time updates from the server to the clients as votes come in.
## Usage
- Navigate to the `frontend` directory and run `npm run dev` to start the development server.
- Navigate to the `backend` directory and run `npm run dev` to start the server.
- Open a browser and go to `http://localhost:5173` to access the application.
## Future Improvements
- Add a search feature to the poll page.
- Add like feature to the poll.
- sorting polls using created date and popularity.
- Updated user.
- User avatar.
- Multiple question poll.

View File

@@ -31,8 +31,9 @@ export async function signinController(req, res) {
const { token, userData } = await signinService(email, password);
res.cookie("livepoll-access-token", token, {
httpOnly: true,
secure: true,
sameSite: "None",
secure: false,
sameSite: "Lax",
expires: new Date(Date.now() + 24 * 60 * 60 * 1000 * 365), // 1 year
path: "/"
}).status(200).json({
success : true,

View File

@@ -1,4 +1,4 @@
import { voteMessageTestService } from "../services/vote.service.js";
import { getPollVoteService, voteMessageTestService } from "../services/vote.service.js";
export async function voteTestController(req, res) {
try{
@@ -23,3 +23,30 @@ export async function voteTestController(req, res) {
}
}
}
export async function getPollVoteController(req, res) {
try {
const pollId = req.params.pollId;
const userId = req.user._id;
const vote = await getPollVoteService(pollId, userId);
res.status(200).json({
success: true,
message: "Poll data fetched successfully",
data: vote,
});
}
catch (err) {
console.log(err);
if (err.statusCode) {
res.status(err.statusCode).json({
success: false,
message: err.message,
});
} else {
res.status(500).json({
success: false,
message: err.message,
});
}
}
}

View File

@@ -20,7 +20,6 @@ app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));
const io = new Server(httpServer, {
cors: {
origin: CLIENT_URL,
methods: ["GET", "POST", "PUT", "DELETE"],
credentials: true
}
})

View File

@@ -1,6 +1,6 @@
import express from "express";
import { verifyToken } from "../../middlwares/verifyToken.js";
import { voteTestController } from "../../controllers/vote.controller.js";
import { getPollVoteController, voteTestController } from "../../controllers/vote.controller.js";
const voteRouter = express.Router();
/**
@@ -14,6 +14,6 @@ const voteRouter = express.Router();
* description: Success
*/
voteRouter.get("/test", voteTestController)
// voteRouter.get("/voted/:pollId", verifyToken, getVotedDataController);
voteRouter.get("/voted/:pollId", verifyToken, getPollVoteController);
export default voteRouter;

View File

@@ -1,3 +1,6 @@
import mongoose from "mongoose";
import { findVoteByPollIdAndUserId } from "../repositories/vote.repo.js";
export function voteMessageTestService(){
try{
return "Vote route is working✔";
@@ -6,3 +9,14 @@ export function voteMessageTestService(){
throw err;
}
}
export function getPollVoteService(pollId, userId) {
try {
const pollIdObjet = new mongoose.Types.ObjectId(pollId);
const vote = findVoteByPollIdAndUserId(pollIdObjet, userId);
return vote;
}
catch (err) {
throw err;
}
}

View File

@@ -4,31 +4,43 @@ export const handlePollSocket = (io) => {
io.on("connection", (socket) => {
console.log(`User connected: ${socket.id}`);
socket.on("joinPoll", (pollId) => {
// Handle user joining a poll room
const handleJoinPoll = (pollId) => {
if (!pollId) {
socket.emit("error", { message: "Poll ID is required to join a poll room" });
return;
}
socket.join(pollId);
console.log(`User ${socket.id} joined poll room: ${pollId}`);
});
};
socket.on("disconnect", () => {
console.log(`User disconnected: ${socket.id}`);
});
// Handle user voting in a poll
const handleVote = async (data) => {
if (!data.pollId || !data.success) {
socket.emit("error", { message: "Invalid vote data" });
return;
}
socket.on("vote", async (data) => {
if (data.success) {
console.log("Vote received:", data);
try {
const pollData = await getPollDataService(data.pollId);
io.to(data.pollId).emit("pollDataUpdated", { data: pollData });
} catch (error) {
console.error("Error fetching poll data:", error);
socket.emit("error", { message: "Failed to fetch poll data" });
}
} else {
console.log("Vote failed:", data);
socket.emit("error", { message: "Vote was unsuccessful" });
}
});
};
// Handle user disconnection
const handleDisconnect = () => {
console.log(`User disconnected: ${socket.id}`);
};
// Event listeners
socket.on("joinPoll", handleJoinPoll);
socket.on("vote", handleVote);
socket.on("disconnect", handleDisconnect);
});
};

View File

@@ -1 +1 @@
VITE_BACKEND_URL="https://livepoll-anjx.onrender.com"
VITE_BACKEND_URL="http://localhost:3000"

View File

@@ -2,7 +2,7 @@
<html lang="en" data-theme="night">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="shortcut icon" href="./public/favicon-livepoll.ico" type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LivePoll</title>
</head>

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -17,15 +17,15 @@ function Header() {
<ul className="menu menu-horizontal px-1 gap-1">
{user.username ? (
<li>
<Link to={"/dashboard"}>Dashboard</Link>
<Link to={"/dashboard"}>仪表盘</Link>
</li>
) : (
<li>
<Link to={"/login"}>Login</Link>
<Link to={"/login"}>登陆</Link>
</li>
)}
<li>
<Link to={"/poll"}>Polls</Link>
<Link to={"/poll"}>投票列表</Link>
</li>
</ul>
</div>

View File

@@ -16,24 +16,24 @@ function ProfileImage({userData}) {
<div tabIndex={0} role="button" className="btn btn-ghost btn-circle avatar">
<div className="w-10 rounded-full">
<img
alt="Tailwind CSS Navbar component"
alt="用户头像"
src="https://img.daisyui.com/images/stock/photo-1534528741775-53994a69daeb.webp" />
</div>
</div>
<ul
tabIndex={0}
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow">
<li className='font-bold'><a >{userData?.username || "User"}</a></li>
<li className='font-bold'><a >{userData?.username || "用户"}</a></li>
<li>
<Link to={'/dashboard'} className="justify-between" >
Profile
<span className="badge">New</span>
个人资料
<span className="badge"></span>
</Link>
<Link to={"/bookmark"} className="justify-between" >
Bookmarks
收藏
</Link>
</li>
<li><a onClick={handleLogout}>Logout</a></li>
<li><a onClick={handleLogout}>退出登录</a></li>
</ul>
</div>
</div>

View File

@@ -1,8 +1,8 @@
import axios from "axios";
const axiosInstance = axios.create({
// baseURL: `http://localhost:3000/api/v1`,
baseURL: `https://livepoll-anjx.onrender.com/api/v1`,
baseURL: `http://110.42.109.143:3000/api/v1`,
//baseURL: `https://livepoll-anjx.onrender.com/api/v1`,
withCredentials: true,
headers: {
"Content-Type": "application/json",

View File

@@ -18,7 +18,7 @@ function Bookmark() {
getUserBookmarks,
{
cacheTime: 1000 * 60 * 5, // 5 minutes
staleTime: 1000 * 60 * 10, // 10 minutes
staleTime: 0, // 10 minutes
}
);
@@ -40,7 +40,7 @@ function Bookmark() {
return (
<div className="p-6 bg-base-200 h-screen">
<h1 className="md:text-4xl text-xl font-bold text-white mb-6">
Bookmarked Polls
收藏的投票
</h1>
{isError && (
<div className="h-60 w-full">
@@ -54,9 +54,9 @@ function Bookmark() {
<thead>
<tr>
<th>#</th>
<th>Title</th>
<th>Description</th>
<th>Actions</th>
<th>标题</th>
<th>描述</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@@ -74,13 +74,13 @@ function Bookmark() {
onClick={() => handleViewPollClick(bookmark._id)}
className="btn btn-sm btn-primary text-sm md:text-base mb-2 md:mb-1"
>
View Poll
查看投票
</button>
<button
onClick={() => handleRemoveBookmark(bookmark._id)}
className="btn btn-sm btn-error text-sm md:text-base ml-2"
>
Remove
移除
</button>
</td>
</tr>

View File

@@ -30,7 +30,7 @@ function CreatePollForm() {
const mutation = useMutation(createPollService, {
onSuccess: (data) => {
const message = data?.message || "Poll created successfully";
const message = data?.message || "投票创建成功";
toast.success(message);
handleClearPoll();
navigate(`/view/${data?.data?._id}`);
@@ -39,7 +39,7 @@ function CreatePollForm() {
console.log(error);
const errorMessage =
error.response?.data?.errors?.[0]?.message ||
"An unexpected error occurred";
"发生意外错误";
toast.error(errorMessage);
},
});
@@ -47,7 +47,7 @@ function CreatePollForm() {
const handlePollSubmit = (e) => {
e.preventDefault();
if (title.trim() == "" || description.trim() == "" || options.length == 0) {
toast.error("All fields are required");
toast.error("所有字段都是必填的");
return;
}
mutation.mutate({ title, description, options });
@@ -56,19 +56,19 @@ function CreatePollForm() {
return (
<div className="min-h-screen bg-base-200">
<div className="w-full p-6 text-white">
<h1 className="text-2xl font-bold mb-6 text-center">Create New Poll</h1>
<h1 className="text-2xl font-bold mb-6 text-center">创建新投票</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
{/* Poll Title */}
<div className="mb-4">
<label className="block text-lg font-medium mb-2">
Poll Title
投票标题
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter poll title"
placeholder="输入投票标题"
className="input input-bordered w-full"
/>
</div>
@@ -76,12 +76,12 @@ function CreatePollForm() {
{/* Poll Description */}
<div className="mb-4">
<label className="block text-lg font-medium mb-2">
Description
描述
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Describe the purpose of the poll"
placeholder="描述投票的目的"
className="textarea textarea-bordered w-full"
rows="3"
></textarea>
@@ -91,21 +91,21 @@ function CreatePollForm() {
<div>
{/* Poll Options */}
<div className="mb-4">
<label className="block text-lg font-medium mb-2">Options</label>
<label className="block text-lg font-medium mb-2">选项</label>
{options.map((option, index) => (
<div key={index} className="flex items-center mb-2">
<input
type="text"
value={option}
placeholder={`Option ${index + 1}`}
placeholder={`选项 ${index + 1}`}
className="input input-bordered w-full cursor-not-allowed"
readOnly
/>
{options.length > 2 && (
<button
className="btn btn-error btn-circle btn-xs ml-2"
title="Remove option"
title="移除选项"
onClick={() =>
setOptions(options.filter((_, i) => i !== index))
}
@@ -122,7 +122,7 @@ function CreatePollForm() {
type="text"
value={optionInput}
onChange={(e) => setOptionInput(e.target.value)}
placeholder="Enter new option"
placeholder="输入新选项"
className="input input-bordered w-full"
/>
</div>
@@ -131,7 +131,7 @@ function CreatePollForm() {
title="Add another option"
onClick={handleAddOption}
>
<FaPlus className="mr-2" /> Add Option
<FaPlus className="mr-2" /> 添加选项
</button>
</div>
</div>
@@ -142,21 +142,21 @@ function CreatePollForm() {
className="btn btn-ghost w-full mt-4 md:w-1/2"
onClick={() => {
const sure = window.confirm(
"Are you sure you want to clear the poll?"
"确定要清除投票内容吗?"
);
if (sure) {
handleClearPoll();
}
}}
>
Clear Poll
清除投票
</button>
{/* Submit Button */}
<button
className="btn btn-success w-full mt-4 md:w-1/2"
onClick={handlePollSubmit}
>
Create Poll
创建投票
</button>
</div>
</div>

View File

@@ -60,12 +60,12 @@ function Dashboard() {
<p className="mt-2 text-center text-gray-400">
{user?.email || "Email"}
</p>
<button className="btn btn-primary mt-4 w-full">Edit Profile</button>
<button className="btn btn-primary mt-4 w-full">编辑资料</button>
<button
className="btn btn-error btn-outline mt-4 w-full"
onClick={handleLogout}
>
Logout
退出登录
</button>
</aside>
@@ -74,10 +74,10 @@ function Dashboard() {
{/* Dashboard Header */}
<div className="mb-6">
<h1 className="text-2xl md:text-4xl font-bold text-white">
Poll Dashboard
投票仪表板
</h1>
<p className="md:text-lg text-gray-300">
Manage your polls, view results, and edit as needed.
管理您的投票查看结果并根据需要进行编辑
</p>
</div>
@@ -87,7 +87,7 @@ function Dashboard() {
className="btn btn-primary"
onClick={() => navigator("/create")}
>
Add New Poll <FaPlus />
添加新投票 <FaPlus />
</button>
</div>
@@ -100,10 +100,10 @@ function Dashboard() {
<thead>
<tr>
<th>#</th>
<th>Title</th>
<th>Description</th>
<th>Published</th>
<th>Actions</th>
<th>标题</th>
<th>描述</th>
<th>已发布</th>
<th>操作</th>
</tr>
</thead>

View File

@@ -3,37 +3,33 @@ import { useNavigate } from 'react-router-dom';
import useStore from '../store/useStore';
function Home() {
const navigator = useNavigate();
return (
<div className="flex bg-base-200 min-h-screen flex-col items-center text-white p-8">
<h1 className="text-4xl mt-2 md:text-5xl font-bold mb-6 text-center flex flex-col gap-2 md:block">Welcome to <span className='bg-slate-800 px-4 rounded-md relative'>LivePoll</span></h1>
<h1 className="text-4xl mt-2 md:text-5xl font-bold mb-6 text-center flex flex-col gap-2 md:block">
欢迎来到 <span className='bg-slate-800 px-4 rounded-md relative'>LivePoll</span>
</h1>
<p className="text-lg text-center max-w-2xl mb-8">
LivePoll is your platform for real-time, interactive polling. Create polls, participate in
active discussions, and get instant feedback with live updates and visualizations. Discover
what people think on topics that matter to you and have your voice heard!
LivePoll 是您进行实时互动投票的平台创建投票参与活跃讨论并通过实时更新和可视化获得即时反馈探索人们对您关心话题的看法让您的声音被听到
</p>
<div className="flex flex-col lg:flex-row gap-6 mb-8">
<div className="bg-base-300 p-6 rounded-lg shadow-md w-full lg:w-96 text-center">
<h2 className="text-3xl font-semibold mb-4">Create Polls</h2>
<h2 className="text-3xl font-semibold mb-4">创建投票</h2>
<p className="text-gray-400">
Create custom polls on any topic and share them with others. Add options, set
permissions, and see the responses roll in real-time.
创建关于任何主题的自定义投票并与他人分享添加选项设置权限并实时查看响应结果
</p>
</div>
<div className="bg-base-300 p-6 rounded-lg shadow-md w-full lg:w-96 text-center">
<h2 className="text-3xl font-semibold mb-4">Vote & Participate</h2>
<h2 className="text-3xl font-semibold mb-4">投票与参与</h2>
<p className="text-gray-400">
Browse a variety of public polls or join private ones shared with you. Cast your vote
and see the real-time results as others participate.
浏览各种公开投票或加入与您分享的私人投票投出您的一票并在他人参与时查看实时结果
</p>
</div>
<div className="bg-base-300 p-6 rounded-lg shadow-md w-full lg:w-96 text-center">
<h2 className="text-3xl font-semibold mb-4">Bookmark & Track</h2>
<h2 className="text-3xl font-semibold mb-4">收藏与追踪</h2>
<p className="text-gray-400">
Bookmark polls to save them for later, view your past participation, and stay updated
on topics you care about.
收藏投票以便稍后查看查看您过去的参与记录并随时了解您关心的话题动态
</p>
</div>
</div>
@@ -41,7 +37,7 @@ function Home() {
className="btn btn-primary px-6 py-3 font-semibold text-lg"
onClick={() => navigator("/dashboard")}
>
Make a Poll
创建投票
</button>
</div>
)

View File

@@ -41,75 +41,75 @@ const LoginPage = () => {
return (
<div className="flex justify-center bg-base-200 h-screen p-4">
<div className="w-full max-w-md rounded-lg p-8">
<h2 className="text-2xl font-semibold text-center text-white mb-6">Login</h2>
<h2 className="text-2xl font-semibold text-center text-white mb-6">登录</h2>
<form className="space-y-4">
{/* Email Input */}
{/* 邮箱输入 */}
<div className="form-control w-full">
<label className="label">
<span className="label-text text-gray-200">Email</span>
<span className="label-text text-gray-200">邮箱</span>
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
placeholder="请输入您的邮箱"
className="input input-bordered w-full bg-gray-700 text-white focus:outline-none focus:ring focus:ring-primary"
required
/>
</div>
{/* Password Input */}
{/* 密码输入 */}
<div className="form-control w-full">
<label className="label">
<span className="label-text text-gray-200">Password</span>
<span className="label-text text-gray-200">密码</span>
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
placeholder="请输入您的密码"
className="input input-bordered w-full bg-gray-700 text-white focus:outline-none focus:ring focus:ring-primary"
required
/>
</div>
{/* Error Message */}
{/* 错误信息 */}
{mutation.isError && <InlineTextError mutation={mutation} />}
{/* Success Message */}
{/* 成功信息 */}
{mutation.isSuccess && (
<p className="text-green-500 text-sm md:text-base">
🎉 {mutation.data.message || "Login is successfull"}
🎉 {mutation.data.message || "登录成功"}
</p>
)}
{/* Forgot Password Link */}
{/* 忘记密码链接 */}
<div className="text-right">
<a href="#" className="text-sm text-primary hover:underline">Forgot password?</a>
<a href="#" className="text-sm text-primary hover:underline">忘记密码</a>
</div>
{/* Submit Button */}
{/* 提交按钮 */}
<div>
<button
onClick={handleLogin}
type="submit"
className="btn btn-primary w-full text-white"
>
{mutation.isLoading ? <SpinnerLoader/> : "Login"}
{mutation.isLoading ? <SpinnerLoader/> : "登录"}
</button>
</div>
</form>
{/* Divider */}
<div className="divider text-gray-400">OR</div>
{/* 分隔符 */}
<div className="divider text-gray-400"></div>
{/* Sign Up Link */}
{/* 注册链接 */}
<p className="text-center text-gray-300">
Dont have an account?{' '}
<Link to="/register" href="#" className="text-primary hover:underline">Sign up</Link>
还没有账户{' '}
<Link to="/register" href="#" className="text-primary hover:underline">立即注册</Link>
</p>
</div>
</div>

View File

@@ -19,7 +19,7 @@ function Polls() {
return (
<div className="container mx-auto p-4 bg-base-200">
<h1 className="text-2xl font-bold text-center mb-6">Polls</h1>
<h1 className="text-2xl font-bold text-center mb-6">投票列表</h1>
{isSuccess && (
<div className="flex flex-wrap justify-center gap-6">
{data?.data?.polls?.map((poll) => (
@@ -42,14 +42,14 @@ function Polls() {
disabled={page <= 1}
onClick={() => setPage(page - 1)}
>
Prev
上一页
</button>
<button
className="btn btn-primary btn-circle"
disabled={data?.data?.totalPages === page}
onClick={() => setPage(page + 1)}
>
Next
下一页
</button>
</div>
</div>

View File

@@ -38,20 +38,20 @@ function Register() {
<div className=" flex justify-center h-screen bg-base-200 p-4">
<div className="w-full max-w-md rounded-lg ">
<h2 className="text-2xl font-semibold text-center text-white mb-6">
Sign Up
注册
</h2>
<form className="space-y-4">
{/* Username Input */}
<div className="form-control w-full">
<label className="label">
<span className="label-text text-gray-200">Username</span>
<span className="label-text text-gray-200">用户名</span>
</label>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
type="text"
placeholder="Enter your username"
placeholder="请输入您的用户名"
className="input input-bordered w-full bg-gray-700 text-white focus:outline-none focus:ring focus:ring-primary"
required
/>
@@ -60,13 +60,13 @@ function Register() {
{/* Email Input */}
<div className="form-control w-full">
<label className="label">
<span className="label-text text-gray-200">Email</span>
<span className="label-text text-gray-200">邮箱</span>
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
placeholder="请输入您的邮箱"
className="input input-bordered w-full bg-gray-700 text-white focus:outline-none focus:ring focus:ring-primary"
required
/>
@@ -75,13 +75,13 @@ function Register() {
{/* Password Input */}
<div className="form-control w-full">
<label className="label">
<span className="label-text text-gray-200">Password</span>
<span className="label-text text-gray-200">密码</span>
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
placeholder="请输入您的密码"
className="input input-bordered w-full bg-gray-700 text-white focus:outline-none focus:ring focus:ring-primary"
required
/>
@@ -91,7 +91,7 @@ function Register() {
{mutation.isError && <InlineTextError mutation={mutation} />}
{mutation.isSuccess && (
<p className="text-green-500 text-sm md:text-base">
🎉 {mutation.data.message || "Process is successfull"}
🎉 {mutation.data.message || "注册成功"}
</p>
)}
@@ -102,19 +102,19 @@ function Register() {
type="submit"
className="btn btn-primary w-full text-white mt-4"
>
{mutation.isLoading ? <SpinnerLoader /> : "Sign Up"}
{mutation.isLoading ? <SpinnerLoader /> : "注册"}
</button>
</div>
</form>
{/* Divider */}
<div className="divider text-gray-400">OR</div>
<div className="divider text-gray-400"></div>
{/* Login Link */}
<p className="text-center text-gray-300">
Already have an account?{" "}
已有账户{" "}
<Link to="/login" className="text-primary hover:underline">
Login
登录
</Link>
</p>
</div>

View File

@@ -16,6 +16,7 @@ import { toast } from "react-toastify";
import { makeChartDataObjFromPollData } from "../utils/util";
import useBookmark from "../hooks/useBookmark";
import { io } from "socket.io-client";
import { getPollSelectedOptionData } from "../services/getPollSelectedOptionData";
ChartJS.register(BarElement, CategoryScale, LinearScale);
@@ -27,7 +28,8 @@ function VotingPage() {
const [socket, setSocket] = useState(null);
useEffect(() => {
const s = io("http://localhost:3000");
// const s = io("https://livepoll-anjx.onrender.com");
const s = io("http://110.42.109.143:3000");
setSocket(s);
s.on("connect", () => {
@@ -41,6 +43,8 @@ function VotingPage() {
}, [pollId]);
const {
data,
isLoading,
@@ -48,12 +52,20 @@ function VotingPage() {
refetch,
} = useQuery(["poll", pollId], () => getPollData(pollId), {
cacheTime: 10 * 60 * 1000, // 10 minutes
staleTime: 20 * 60 * 1000, // 20 minutes
staleTime: 0,
onSuccess: (data) => {
setPoll(data);
},
});
useQuery(["selectedOption", pollId], () => getPollSelectedOptionData(pollId), {
cacheTime: 10 * 60 * 1000, // 10 minutes
staleTime: 20 * 60 * 1000,
onSuccess: (data) => {
setSelectedOption(data?.data?.optionId || null);
},
});
useEffect(() => {
if (socket) {
@@ -75,7 +87,7 @@ function VotingPage() {
const mutation = useMutation(createVoteService, {
onSuccess: (data) => {
toast.success("Vote submitted successfully");
toast.success("投票提交成功");
if (socket) {
socket.emit("vote", { pollId, success: data?.success });
}
@@ -83,13 +95,15 @@ function VotingPage() {
onError: (error) => {
console.error(error);
toast.error(
error?.response?.data?.message || "An unexpected error occurred"
error?.response?.data?.message || "发生意外错误"
);
},
});
const handleOptionSelect = (id) => {
if (!selectedOption){
setSelectedOption(id);
}
mutation.mutate({ pollId, optionId: id });
};
@@ -119,7 +133,7 @@ function VotingPage() {
className="rounded-full h-7 md:h-10 w-7 md:w-10"
/>
<h2 className="text-lg md:text-xl font-semibold">
{poll?.data?.creatorData?.username || "Unknown"}
{poll?.data?.creatorData?.username || "未知"}
</h2>
</div>
@@ -131,12 +145,12 @@ function VotingPage() {
{/* Poll Title */}
<h1 className="text-xl md:text-3xl font-bold text-center">
{poll?.data?.pollData?.title || "Loading.."}
{poll?.data?.pollData?.title || "加载中.."}
</h1>
{/* Poll Description */}
<p className="text-sm font-light md:text-base mb-6 text-center">
{poll?.data?.pollData?.description || "Loading.."}
{poll?.data?.pollData?.description || "加载中.."}
</p>
{/* Voting Options */}

View File

@@ -0,0 +1,6 @@
import axiosInstance from "../helper/axiosInstance";
export async function getPollSelectedOptionData(pollId) {
const response = await axiosInstance.get(`vote/voted/${pollId}`);
return response.data;
}

BIN
images/Home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
images/bookmark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
images/createPollPage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
images/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
images/imageGIF.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
images/pollsPage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
images/signup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
images/votingPage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB