Compare commits
3 Commits
proposal-6
...
t.homas-pa
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a594269f5 | |||
| fc5f47cf25 | |||
| aa16ef0fa9 |
82
README.md
82
README.md
@@ -1,57 +1,37 @@
|
|||||||
# P2P Poll App
|
# P2P Poll App
|
||||||
|
There are lots of trust-issues:
|
||||||
|
The possiblity to generate lots of users that do a lot of things (at a rather low cost)
|
||||||
|
The possibility to put out wrong data, maby not even contradicting but additional to existing data.
|
||||||
|
The possibility to do all kinds of shenenigans like spam other users with some requests
|
||||||
|
|
||||||
A peer-to-peer polling application where users can create polls, add options, and vote in real-time without a central server.
|
Due to low programming knowledge, the starting point of this proposal was to mirror how normal groups of people solve issues of trust to then automate and possibly improve the process. There are already some systems out there like Trust flow or random walk.As far as i understand it, the Flexible Trust Web also already does something like this, also maby RWOT and GNUweb but i didn't read into them too much yet since i discovered them rather late.
|
||||||
|
|
||||||
## Features
|
If random new people should be able to use the system as equals to previous users, but the system never has real identities as an input, then there is no way to fully prevent the creation of new users to manipulate or sabotage the poll. But it can be assumed, that your friends are rather trustworthy and most likely also their friends and so on. And if someone makes huge ammounts or just one second account, they will probably only have the creator or maby some other people as friends, and even they might already be less socially connected than a normal user.
|
||||||
|
So the social distance to another user should be evaluated to see, whether you should count their vote.
|
||||||
|
This is evaluated for and by every user individually, based on the information they were sent. The ammount of contacts you won't count are displayed to you, such that you get a hint at how many people you are missing but also how many people are not counting you. This encourages people to try to prove others/vise versa and make social connections to officially tie the network closer together such that the voting system works and confirms itself. It would be great, if there was some chat attached to the poll. If people want to prove their (or others) trustworhiness within this system, they are then also encouraged to have productive discussions, probably about the matter of the poll.
|
||||||
|
Everyone in a poll with you is a "contact" of yours.
|
||||||
|
"users" can have "friends".
|
||||||
|
You can also manually mark users as suspicious or trustworthy or normal again.
|
||||||
|
The system for evaluating the trustworthyness of users is somehow a mix between the concepts "weighted path score" and "trust flow" with 5 steps.
|
||||||
|
That means for 5 steps starting with you, all friends and trusted people of people looked at in this step get some trust from the people we look at: 0.8 * The trust of the looked at person (if trusted) + 0.8 * The trust of the looked at person / friends the looked at person has (if friend). Then the trust of the person that received trust may maximally be 100. The Trust you have to yourself is 100.
|
||||||
|
You can also mark someone as trustworthy or untrustworthy. That is then also sent around to everyone if you want(should be the standard, but maby a user wants to just see how the trustworthyness will look like after the change).
|
||||||
|
If you receive such an information, you can make the following calculations immidiately and after every assesment of everyones trustworthyness:
|
||||||
|
If the accused is less trustworthy then the accusing person, decrease the accused trustworthyness to 0 and the accused friends and trustees trustworthyness by the trustworthyness of the accusing person.
|
||||||
|
If the trustworhyness of the accusing person is less than the trustworthyness of the accused, then reduce the trustworthyness of the accusing person to 0 and the accusing persons friends and trustees by the trustworthyness of the accused.
|
||||||
|
If you mark someone as trustworthy:
|
||||||
|
The Trust flowing to the trusted person from you will also be 0.8 of your trust.
|
||||||
|
Maby this should also be the effect of beeing "friends" since "trust" might be something you could more intuitively casually deal out after a short chat. If that change were to occur, then the effect would have to be switched around.
|
||||||
|
All contacts can maximally have the Trust 100.
|
||||||
|
|
||||||
- **Real-time P2P voting** using WebRTC via PeerJS
|
|
||||||
- **Dynamic option management** - add/remove options during polling
|
|
||||||
- **Duplicate vote prevention** - one vote per user
|
|
||||||
- **Automatic data synchronization** across all connected peers
|
|
||||||
- **Local storage persistence** for poll recovery
|
|
||||||
- **Responsive design** works on desktop and mobile
|
|
||||||
- **No server required** - uses PeerJS free signaling service
|
|
||||||
|
|
||||||
## How to Use
|
Future matters:
|
||||||
|
If there can be any discrepancy of sent information, depending on what sender you trust most, you will mark one of the senders as untrustworthy and neglect all future information from this user. Since everything can be signed and such, that shouldnˋt be an issue tho, but if it was, the ammount of "useless" messages to already informed people might have to increase to validate received data.
|
||||||
|
A system to showcase the social connections in a 2D - format would be neat.
|
||||||
|
(most likely something like this exists already)
|
||||||
|
Obviously the user would also have to see other context like the total of all votes (trusted or not)
|
||||||
|
|
||||||
1. **Open the app** in your browser (open `index.html`)
|
Anonymous polls:
|
||||||
2. **First user (Host)**: Leave the Peer ID field empty and click "Connect to Host"
|
A system of individually assigned trust poses a challenge for a system where you can decide not to trust some voters.
|
||||||
3. **Copy your Peer ID** using the "Copy Your Peer ID" button
|
If there is no other option some compromises might be makable, such as:
|
||||||
4. **Share your Peer ID** with other users (via chat, email, etc.)
|
-Your Friends can know what you voted for
|
||||||
5. **Other users**: Paste the host's Peer ID and click "Connect to Host"
|
-The Person initiating a poll just decides on the validity of participants according to an own judgement of trust at the moment of poll-creation
|
||||||
6. **Create a poll** with question and options
|
|
||||||
7. **Vote** by clicking on options (one vote per person)
|
|
||||||
8. **Watch results** update in real-time across all devices
|
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
|
|
||||||
- **P2P Library**: PeerJS (WebRTC-based)
|
|
||||||
- **Frontend**: Vanilla JavaScript with modern CSS
|
|
||||||
- **Data Sync**: Custom conflict resolution for concurrent operations
|
|
||||||
- **Storage**: localStorage for basic persistence
|
|
||||||
- **Network**: Full mesh topology where each peer connects to all others
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
├── index.html # Main application
|
|
||||||
├── css/
|
|
||||||
│ └── styles.css # Application styling
|
|
||||||
└── js/
|
|
||||||
├── app.js # Main application logic
|
|
||||||
├── peer-manager.js # P2P connection handling
|
|
||||||
├── poll-manager.js # Poll data and sync logic
|
|
||||||
└── ui-controller.js # UI interactions
|
|
||||||
```
|
|
||||||
|
|
||||||
## Browser Support
|
|
||||||
|
|
||||||
Requires modern browsers with WebRTC support:
|
|
||||||
- Chrome 23+
|
|
||||||
- Firefox 22+
|
|
||||||
- Safari 11+
|
|
||||||
- Edge 79+
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Simply open `index.html` in a browser - no build process required. For testing with multiple peers, open the app in multiple browser tabs or windows.
|
|
||||||
665
css/styles.css
665
css/styles.css
@@ -1,665 +0,0 @@
|
|||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 280px 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-section {
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-section h3 {
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-hint {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint-text {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #6b7280;
|
|
||||||
font-style: italic;
|
|
||||||
text-align: center;
|
|
||||||
padding: 8px;
|
|
||||||
background: #f9fafb;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.polls-list {
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-item {
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-item:hover {
|
|
||||||
border-color: #667eea;
|
|
||||||
background: #f9fafb;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-item.active {
|
|
||||||
border-color: #667eea;
|
|
||||||
background: #ede9fe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-item-title {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-item-meta {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-item::after {
|
|
||||||
content: ">";
|
|
||||||
position: absolute;
|
|
||||||
right: 12px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: #9ca3af;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-item:hover::after {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-polls {
|
|
||||||
color: #9ca3af;
|
|
||||||
font-style: italic;
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peers-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peers-status {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text.connected {
|
|
||||||
background: #dcfce7;
|
|
||||||
color: #16a34a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text.disconnected {
|
|
||||||
background: #fef2f2;
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text.connecting {
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #d97706;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peers-sidebar {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
max-height: 150px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peers-sidebar li {
|
|
||||||
padding: 6px 0;
|
|
||||||
border-bottom: 1px solid #f3f4f6;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peers-sidebar li:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #667eea;
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#peer-id {
|
|
||||||
background: #f0f0f0;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-family: monospace;
|
|
||||||
max-width: 150px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-connected {
|
|
||||||
color: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-disconnected {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-connecting {
|
|
||||||
color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-info {
|
|
||||||
background: #f0f9ff;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-left: 4px solid #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-info p {
|
|
||||||
margin: 5px 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #1e40af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"] {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
padding: 12px;
|
|
||||||
border: 2px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"]:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: #5a67d8;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
background: #9ca3af;
|
|
||||||
cursor: not-allowed;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.small-btn {
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.danger {
|
|
||||||
background: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.danger:hover {
|
|
||||||
background: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.poll-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#options-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-input {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-text {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-option-btn {
|
|
||||||
background: #ef4444;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-option-btn.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vote-hint {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vote-hint .hint-text {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #6b7280;
|
|
||||||
text-align: center;
|
|
||||||
padding: 10px;
|
|
||||||
background: #f0f9ff;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #bfdbfe;
|
|
||||||
}
|
|
||||||
|
|
||||||
.options-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option {
|
|
||||||
background: #f9fafb;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: all 0.3s;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option:hover {
|
|
||||||
border-color: #667eea;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option.voted {
|
|
||||||
border-color: #10b981;
|
|
||||||
background: #ecfdf5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option.voted::before {
|
|
||||||
content: "✓";
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
background: #10b981;
|
|
||||||
color: white;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option.change-vote {
|
|
||||||
border-color: #f59e0b;
|
|
||||||
background: #fffbeb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-option.change-vote:hover {
|
|
||||||
border-color: #d97706;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.option-text {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vote-count {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vote-bar {
|
|
||||||
background: #e5e7eb;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vote-fill {
|
|
||||||
background: #667eea;
|
|
||||||
height: 100%;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 4px solid #f3f3f3;
|
|
||||||
border-top: 4px solid #667eea;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification {
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
background: #333;
|
|
||||||
color: white;
|
|
||||||
padding: 15px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
z-index: 1001;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification.success {
|
|
||||||
background: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification.error {
|
|
||||||
background: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notification.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Styles */
|
|
||||||
.modal {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 1002;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 400px;
|
|
||||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
color: #333;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: #6b7280;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close:hover {
|
|
||||||
background: #f3f4f6;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
border: 2px solid #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: border-color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
padding: 20px;
|
|
||||||
border-top: 1px solid #e5e7eb;
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: #6b7280;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: #5a67d8;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.container {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 15px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connection-controls {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.poll-header {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
133
index.html
133
index.html
@@ -1,133 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>P2P Polling App</title>
|
|
||||||
<link rel="stylesheet" href="css/styles.css">
|
|
||||||
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header>
|
|
||||||
<h1>P2P Polling App</h1>
|
|
||||||
<div class="connection-status">
|
|
||||||
<span id="peer-id">Loading...</span>
|
|
||||||
<span id="connection-indicator" class="status-disconnected">●</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="main-content">
|
|
||||||
<!-- Sidebar -->
|
|
||||||
<aside class="sidebar">
|
|
||||||
<div class="sidebar-section">
|
|
||||||
<h3>Available Polls</h3>
|
|
||||||
<div class="sidebar-hint">
|
|
||||||
<p class="hint-text">Click a poll to view and vote</p>
|
|
||||||
</div>
|
|
||||||
<div id="polls-list" class="polls-list">
|
|
||||||
<p class="no-polls">No polls created yet</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-section">
|
|
||||||
<h3>Connected Peers</h3>
|
|
||||||
<div class="peers-info">
|
|
||||||
<div class="peers-status">
|
|
||||||
<span>Count: <span id="peer-count">0</span></span>
|
|
||||||
<span id="connection-status-text" class="status-text">Disconnected</span>
|
|
||||||
</div>
|
|
||||||
<ul id="peers" class="peers-sidebar"></ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<!-- Connection Setup -->
|
|
||||||
<section id="connection-section" class="section">
|
|
||||||
<h2>Connect to Peer</h2>
|
|
||||||
<div class="connection-info">
|
|
||||||
<p><strong>Host:</strong> Share your Peer ID below with others</p>
|
|
||||||
<p><strong>Joiner:</strong> Enter the host's Peer ID to connect</p>
|
|
||||||
</div>
|
|
||||||
<div class="connection-controls">
|
|
||||||
<input type="text" id="room-id" placeholder="Enter host's Peer ID to connect">
|
|
||||||
<button id="join-btn">Connect to Host / Create New Poll</button>
|
|
||||||
<button id="copy-id-btn">Copy Your Peer ID</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Poll Creation -->
|
|
||||||
<section id="poll-creation" class="section hidden">
|
|
||||||
<h2>Create Poll</h2>
|
|
||||||
<div class="poll-form">
|
|
||||||
<input type="text" id="poll-question" placeholder="Enter your poll question">
|
|
||||||
<div id="options-container">
|
|
||||||
<div class="option-input">
|
|
||||||
<input type="text" class="option-text" placeholder="Option 1">
|
|
||||||
<button class="remove-option-btn hidden">Remove</button>
|
|
||||||
</div>
|
|
||||||
<div class="option-input">
|
|
||||||
<input type="text" class="option-text" placeholder="Option 2">
|
|
||||||
<button class="remove-option-btn hidden">Remove</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button id="add-option-btn">+ Add Option</button>
|
|
||||||
<button id="create-poll-btn">Create Poll</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Active Poll -->
|
|
||||||
<section id="active-poll" class="section hidden">
|
|
||||||
<div class="poll-header">
|
|
||||||
<h2 id="poll-question-display"></h2>
|
|
||||||
<button id="add-poll-option-btn" class="small-btn">+ Add Option</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="vote-hint">
|
|
||||||
<p class="hint-text">Click any option to vote. Click your voted option again to remove your vote.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="poll-options" class="options-list"></div>
|
|
||||||
|
|
||||||
<div class="poll-actions">
|
|
||||||
<button id="reset-poll-btn">Reset Poll</button>
|
|
||||||
<button id="new-poll-btn" class="small-btn">New Poll</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading Overlay -->
|
|
||||||
<div id="loading-overlay" class="overlay hidden">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
<p>Connecting...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Notification Toast -->
|
|
||||||
<div id="notification" class="notification hidden"></div>
|
|
||||||
|
|
||||||
<!-- Add Option Modal -->
|
|
||||||
<div id="add-option-modal" class="modal hidden">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3>Add New Option</h3>
|
|
||||||
<button class="modal-close" id="close-modal-btn">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<input type="text" id="new-option-input" placeholder="Enter new option text">
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button id="cancel-modal-btn" class="btn-secondary">Cancel</button>
|
|
||||||
<button id="save-option-btn" class="btn-primary">Add Option</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="js/peer-manager.js"></script>
|
|
||||||
<script src="js/poll-manager.js"></script>
|
|
||||||
<script src="js/ui-controller.js"></script>
|
|
||||||
<script src="js/app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
210
js/app.js
210
js/app.js
@@ -1,210 +0,0 @@
|
|||||||
// Main application entry point
|
|
||||||
class P2PPollApp {
|
|
||||||
constructor() {
|
|
||||||
this.peerManager = null;
|
|
||||||
this.pollManager = null;
|
|
||||||
this.uiController = null;
|
|
||||||
this.isInitialized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize() {
|
|
||||||
try {
|
|
||||||
// Initialize peer manager
|
|
||||||
this.peerManager = new PeerManager();
|
|
||||||
this.pollManager = new PollManager(this.peerManager);
|
|
||||||
this.uiController = new UIController(this.peerManager, this.pollManager);
|
|
||||||
|
|
||||||
// Set up event handlers
|
|
||||||
this.setupEventHandlers();
|
|
||||||
|
|
||||||
// Initialize peer connection
|
|
||||||
await this.peerManager.initialize();
|
|
||||||
|
|
||||||
// Load saved data
|
|
||||||
this.pollManager.loadFromLocalStorage();
|
|
||||||
|
|
||||||
// If there are saved polls, update the sidebar
|
|
||||||
const savedPolls = this.pollManager.getAvailablePolls();
|
|
||||||
if (savedPolls.length > 0) {
|
|
||||||
this.uiController.updatePollsList(savedPolls);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
|
||||||
console.log('P2P Poll App initialized successfully');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize app:', error);
|
|
||||||
this.uiController?.showNotification('Failed to initialize app: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupEventHandlers() {
|
|
||||||
// Peer manager events
|
|
||||||
this.peerManager.onConnectionStatusChange = (status, peerId) => {
|
|
||||||
this.uiController.updatePeerId(peerId);
|
|
||||||
|
|
||||||
if (status === 'connected') {
|
|
||||||
this.uiController.showNotification('Connected to P2P network', 'success');
|
|
||||||
} else if (status === 'disconnected') {
|
|
||||||
this.uiController.showNotification('Disconnected from P2P network', 'error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.peerManager.onPeerConnected = (peerId) => {
|
|
||||||
this.uiController.showNotification(`Peer ${peerId} connected`, 'success');
|
|
||||||
|
|
||||||
// If we're the host and have polls, send them to the new peer
|
|
||||||
if (this.peerManager.isHost) {
|
|
||||||
const availablePolls = this.pollManager.getAvailablePolls();
|
|
||||||
if (availablePolls.length > 0) {
|
|
||||||
availablePolls.forEach(poll => {
|
|
||||||
this.peerManager.sendMessage(peerId, {
|
|
||||||
type: 'poll_update',
|
|
||||||
poll: poll,
|
|
||||||
senderId: this.peerManager.getPeerId()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also send current poll if there is one
|
|
||||||
const currentPoll = this.pollManager.getCurrentPoll();
|
|
||||||
if (currentPoll) {
|
|
||||||
this.peerManager.sendMessage(peerId, {
|
|
||||||
type: 'current_poll',
|
|
||||||
poll: currentPoll,
|
|
||||||
senderId: this.peerManager.getPeerId()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.peerManager.sendMessage(peerId, {
|
|
||||||
type: 'sync_request',
|
|
||||||
senderId: this.peerManager.getPeerId()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.peerManager.onPeerDisconnected = (peerId) => {
|
|
||||||
this.uiController.showNotification(`Peer ${peerId} disconnected`, 'info');
|
|
||||||
};
|
|
||||||
|
|
||||||
this.peerManager.onMessageReceived = (message, senderId) => {
|
|
||||||
this.handleMessage(message, senderId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Poll manager events
|
|
||||||
this.pollManager.onPollCreated = (poll) => {
|
|
||||||
this.uiController.renderPoll(poll);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pollManager.onPollUpdated = (poll) => {
|
|
||||||
this.uiController.renderPoll(poll);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pollManager.onPollSelected = (poll) => {
|
|
||||||
if (poll) {
|
|
||||||
this.uiController.renderPoll(poll);
|
|
||||||
this.uiController.showActivePoll();
|
|
||||||
} else {
|
|
||||||
this.uiController.showPollCreation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pollManager.onPollsListUpdated = (polls) => {
|
|
||||||
this.uiController.updatePollsList(polls);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMessage(message, senderId) {
|
|
||||||
console.log('Processing message:', message, 'from:', senderId);
|
|
||||||
|
|
||||||
switch (message.type) {
|
|
||||||
case 'poll_update':
|
|
||||||
this.pollManager.syncPoll(message.poll);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'current_poll':
|
|
||||||
this.pollManager.syncPoll(message.poll);
|
|
||||||
if (this.pollManager.getCurrentPoll() && this.pollManager.getCurrentPoll().id === message.poll.id) {
|
|
||||||
this.uiController.renderPoll(message.poll);
|
|
||||||
this.uiController.showActivePoll();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'vote':
|
|
||||||
this.pollManager.handleVoteMessage(message.optionId, message.voterId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'unvote':
|
|
||||||
this.pollManager.handleUnvoteMessage(message.voterId);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'poll_reset':
|
|
||||||
this.pollManager.handlePollReset();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'sync_request':
|
|
||||||
const currentPoll = this.pollManager.getCurrentPoll();
|
|
||||||
if (currentPoll) {
|
|
||||||
this.peerManager.sendMessage(senderId, {
|
|
||||||
type: 'current_poll',
|
|
||||||
poll: currentPoll,
|
|
||||||
senderId: this.peerManager.getPeerId()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const availablePolls = this.pollManager.getAvailablePolls();
|
|
||||||
availablePolls.forEach(poll => {
|
|
||||||
this.peerManager.sendMessage(senderId, {
|
|
||||||
type: 'poll_update',
|
|
||||||
poll: poll,
|
|
||||||
senderId: this.peerManager.getPeerId()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.warn('Unknown message type:', message.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
if (this.peerManager) {
|
|
||||||
this.peerManager.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
const app = new P2PPollApp();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await app.initialize();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('App initialization failed:', error);
|
|
||||||
|
|
||||||
// Show error message to user
|
|
||||||
const errorDiv = document.createElement('div');
|
|
||||||
errorDiv.className = 'notification error';
|
|
||||||
errorDiv.textContent = 'Failed to initialize app. Please refresh the page.';
|
|
||||||
errorDiv.style.position = 'fixed';
|
|
||||||
errorDiv.style.top = '20px';
|
|
||||||
errorDiv.style.right = '20px';
|
|
||||||
errorDiv.style.zIndex = '1001';
|
|
||||||
document.body.appendChild(errorDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup on page unload
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
app.destroy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle page visibility changes
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.hidden) {
|
|
||||||
console.log('Page hidden - connection may become unstable');
|
|
||||||
} else {
|
|
||||||
console.log('Page visible - checking connection status');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
class PeerManager {
|
|
||||||
constructor() {
|
|
||||||
this.peer = null;
|
|
||||||
this.connections = new Map();
|
|
||||||
this.roomId = null;
|
|
||||||
this.isHost = false;
|
|
||||||
this.onPeerConnected = null;
|
|
||||||
this.onPeerDisconnected = null;
|
|
||||||
this.onMessageReceived = null;
|
|
||||||
this.onConnectionStatusChange = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize() {
|
|
||||||
try {
|
|
||||||
this.peer = new Peer({
|
|
||||||
debug: 2,
|
|
||||||
config: {
|
|
||||||
iceServers: [
|
|
||||||
{ urls: 'stun:stun.l.google.com:19302' },
|
|
||||||
{ urls: 'stun:stun1.l.google.com:19302' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.peer.on('open', (id) => {
|
|
||||||
console.log('My peer ID is:', id);
|
|
||||||
this.updateConnectionStatus('connected');
|
|
||||||
this.updatePeersList();
|
|
||||||
if (this.onConnectionStatusChange) {
|
|
||||||
this.onConnectionStatusChange('connected', id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.peer.on('connection', (conn) => {
|
|
||||||
this.handleIncomingConnection(conn);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.peer.on('disconnected', () => {
|
|
||||||
this.updateConnectionStatus('disconnected');
|
|
||||||
this.updatePeersList();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.peer.on('error', (err) => {
|
|
||||||
console.error('Peer error:', err);
|
|
||||||
this.updateConnectionStatus('error');
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize peer:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createRoom() {
|
|
||||||
if (!this.peer) {
|
|
||||||
throw new Error('Peer not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.roomId = this.generateRoomId();
|
|
||||||
this.isHost = true;
|
|
||||||
|
|
||||||
console.log('Created room:', this.roomId);
|
|
||||||
return this.roomId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async joinRoom(roomId) {
|
|
||||||
if (!this.peer) {
|
|
||||||
throw new Error('Peer not initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.roomId = roomId;
|
|
||||||
this.isHost = false;
|
|
||||||
|
|
||||||
console.log('Attempting to join room:', roomId);
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
const conn = this.peer.connect(roomId);
|
|
||||||
await this.setupConnection(conn);
|
|
||||||
console.log('Successfully joined room');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to join room:', error);
|
|
||||||
throw new Error('Could not connect to host. Make sure the host is online and you have the correct peer ID.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleIncomingConnection(conn) {
|
|
||||||
this.setupConnection(conn);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setupConnection(conn) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
conn.on('open', () => {
|
|
||||||
console.log('Connected to:', conn.peer);
|
|
||||||
this.connections.set(conn.peer, conn);
|
|
||||||
this.updatePeersList();
|
|
||||||
|
|
||||||
if (this.isHost) {
|
|
||||||
if (this.onPeerConnected) {
|
|
||||||
this.onPeerConnected(conn.peer);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.onPeerConnected) {
|
|
||||||
this.onPeerConnected(conn.peer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(conn);
|
|
||||||
});
|
|
||||||
|
|
||||||
conn.on('data', (data) => {
|
|
||||||
this.handleMessage(data, conn.peer);
|
|
||||||
});
|
|
||||||
|
|
||||||
conn.on('close', () => {
|
|
||||||
console.log('Connection closed:', conn.peer);
|
|
||||||
this.connections.delete(conn.peer);
|
|
||||||
this.updatePeersList();
|
|
||||||
|
|
||||||
if (this.onPeerDisconnected) {
|
|
||||||
this.onPeerDisconnected(conn.peer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
conn.on('error', (err) => {
|
|
||||||
console.error('Connection error:', err);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMessage(data, senderId) {
|
|
||||||
console.log('Received message:', data, 'from:', senderId);
|
|
||||||
|
|
||||||
if (this.onMessageReceived) {
|
|
||||||
this.onMessageReceived(data, senderId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMessage(peerId, message) {
|
|
||||||
const conn = this.connections.get(peerId);
|
|
||||||
if (conn && conn.open) {
|
|
||||||
conn.send(message);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastMessage(message) {
|
|
||||||
let sentCount = 0;
|
|
||||||
this.connections.forEach((conn, peerId) => {
|
|
||||||
if (conn.open) {
|
|
||||||
conn.send(message);
|
|
||||||
sentCount++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return sentCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPeerId() {
|
|
||||||
return this.peer ? this.peer.id : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoomId() {
|
|
||||||
return this.roomId;
|
|
||||||
}
|
|
||||||
|
|
||||||
getConnectedPeers() {
|
|
||||||
return Array.from(this.connections.keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
getPeerCount() {
|
|
||||||
return this.connections.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnected() {
|
|
||||||
return this.peer && this.peer.disconnected !== true;
|
|
||||||
}
|
|
||||||
|
|
||||||
generateRoomId() {
|
|
||||||
return Math.random().toString(36).substr(2, 9).toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConnectionStatus(status) {
|
|
||||||
const indicator = document.getElementById('connection-indicator');
|
|
||||||
const statusText = document.getElementById('connection-status-text');
|
|
||||||
|
|
||||||
if (indicator) {
|
|
||||||
indicator.className = `status-${status}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (statusText) {
|
|
||||||
statusText.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
|
||||||
statusText.className = `status-text ${status}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePeersList() {
|
|
||||||
const peersList = document.getElementById('peers');
|
|
||||||
const peerCount = document.getElementById('peer-count');
|
|
||||||
|
|
||||||
if (peersList && peerCount) {
|
|
||||||
peerCount.textContent = this.getPeerCount();
|
|
||||||
peersList.innerHTML = '';
|
|
||||||
|
|
||||||
if (this.getPeerCount() === 0) {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.textContent = 'No connected peers';
|
|
||||||
li.style.fontStyle = 'italic';
|
|
||||||
li.style.color = '#9ca3af';
|
|
||||||
peersList.appendChild(li);
|
|
||||||
} else {
|
|
||||||
this.getConnectedPeers().forEach(peerId => {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.textContent = `${peerId} ${peerId === this.peer.id ? '(You)' : ''}`;
|
|
||||||
li.style.fontWeight = peerId === this.peer.id ? 'bold' : 'normal';
|
|
||||||
li.style.color = peerId === this.peer.id ? '#667eea' : '#6b7280';
|
|
||||||
peersList.appendChild(li);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
if (this.peer) {
|
|
||||||
this.connections.forEach(conn => conn.close());
|
|
||||||
this.peer.destroy();
|
|
||||||
this.peer = null;
|
|
||||||
this.connections.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,428 +0,0 @@
|
|||||||
class PollManager {
|
|
||||||
constructor(peerManager) {
|
|
||||||
this.peerManager = peerManager;
|
|
||||||
this.currentPoll = null;
|
|
||||||
this.availablePolls = new Map(); // pollId -> poll
|
|
||||||
this.myVotes = new Set();
|
|
||||||
this.onPollUpdated = null;
|
|
||||||
this.onPollCreated = null;
|
|
||||||
this.onPollSelected = null;
|
|
||||||
this.onPollsListUpdated = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
createPoll(question, options) {
|
|
||||||
const poll = {
|
|
||||||
id: this.generatePollId(),
|
|
||||||
question: question.trim(),
|
|
||||||
options: options.map((text, index) => ({
|
|
||||||
id: `opt-${index}`,
|
|
||||||
text: text.trim(),
|
|
||||||
votes: 0,
|
|
||||||
voters: []
|
|
||||||
})),
|
|
||||||
createdBy: this.peerManager.getPeerId(),
|
|
||||||
createdAt: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clear any existing votes when creating a new poll
|
|
||||||
this.myVotes.clear();
|
|
||||||
|
|
||||||
this.currentPoll = poll;
|
|
||||||
this.availablePolls.set(poll.id, poll);
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
|
|
||||||
if (this.onPollCreated) {
|
|
||||||
this.onPollCreated(poll);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.onPollsListUpdated) {
|
|
||||||
this.onPollsListUpdated(Array.from(this.availablePolls.values()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast new poll to all peers
|
|
||||||
this.broadcastPollUpdate();
|
|
||||||
|
|
||||||
return poll;
|
|
||||||
}
|
|
||||||
|
|
||||||
addOption(text) {
|
|
||||||
if (!this.currentPoll) {
|
|
||||||
throw new Error('No active poll');
|
|
||||||
}
|
|
||||||
|
|
||||||
const newOption = {
|
|
||||||
id: `opt-${Date.now()}`,
|
|
||||||
text: text.trim(),
|
|
||||||
votes: 0,
|
|
||||||
voters: []
|
|
||||||
};
|
|
||||||
|
|
||||||
this.currentPoll.options.push(newOption);
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
this.notifyPollUpdated();
|
|
||||||
this.broadcastPollUpdate();
|
|
||||||
|
|
||||||
return newOption;
|
|
||||||
}
|
|
||||||
|
|
||||||
vote(optionId) {
|
|
||||||
if (!this.currentPoll) {
|
|
||||||
throw new Error('No active poll');
|
|
||||||
}
|
|
||||||
|
|
||||||
const myPeerId = this.peerManager.getPeerId();
|
|
||||||
|
|
||||||
// Check if already voted for this option
|
|
||||||
if (this.myVotes.has(optionId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removePreviousVote(myPeerId);
|
|
||||||
|
|
||||||
// Add new vote
|
|
||||||
const option = this.currentPoll.options.find(opt => opt.id === optionId);
|
|
||||||
if (!option) {
|
|
||||||
throw new Error('Option not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
option.votes++;
|
|
||||||
option.voters.push(myPeerId);
|
|
||||||
this.myVotes.add(optionId);
|
|
||||||
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
this.notifyPollUpdated();
|
|
||||||
this.broadcastVote(optionId, myPeerId);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
removePreviousVote(peerId) {
|
|
||||||
if (!this.currentPoll) return;
|
|
||||||
|
|
||||||
this.currentPoll.options.forEach(option => {
|
|
||||||
const voterIndex = option.voters.indexOf(peerId);
|
|
||||||
if (voterIndex !== -1) {
|
|
||||||
option.votes--;
|
|
||||||
option.voters.splice(voterIndex, 1);
|
|
||||||
|
|
||||||
// Remove from my votes if it's my vote
|
|
||||||
if (peerId === this.peerManager.getPeerId()) {
|
|
||||||
this.myVotes.delete(option.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resetPoll() {
|
|
||||||
if (!this.currentPoll) {
|
|
||||||
throw new Error('No active poll');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentPoll.options.forEach(option => {
|
|
||||||
option.votes = 0;
|
|
||||||
option.voters = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
this.myVotes.clear();
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
this.notifyPollUpdated();
|
|
||||||
this.broadcastPollReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
syncPoll(pollData) {
|
|
||||||
if (!this.availablePolls.has(pollData.id)) {
|
|
||||||
this.availablePolls.set(pollData.id, pollData);
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
|
|
||||||
if (this.onPollsListUpdated) {
|
|
||||||
this.onPollsListUpdated(Array.from(this.availablePolls.values()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.currentPoll || this.currentPoll.id === pollData.id) {
|
|
||||||
this.currentPoll = pollData;
|
|
||||||
this.availablePolls.set(pollData.id, pollData);
|
|
||||||
|
|
||||||
this.validateMyVotes(pollData);
|
|
||||||
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
if (this.onPollCreated) {
|
|
||||||
this.onPollCreated(pollData);
|
|
||||||
}
|
|
||||||
} else if (this.currentPoll.id === pollData.id) {
|
|
||||||
this.mergePollData(pollData);
|
|
||||||
this.availablePolls.set(pollData.id, this.currentPoll);
|
|
||||||
|
|
||||||
this.validateMyVotes(this.currentPoll);
|
|
||||||
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
this.notifyPollUpdated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mergePollData(remotePoll) {
|
|
||||||
const localOptions = new Map(this.currentPoll.options.map(opt => [opt.id, opt]));
|
|
||||||
const remoteOptions = new Map(remotePoll.options.map(opt => [opt.id, opt]));
|
|
||||||
|
|
||||||
remoteOptions.forEach((remoteOpt, id) => {
|
|
||||||
if (!localOptions.has(id)) {
|
|
||||||
this.currentPoll.options.push(remoteOpt);
|
|
||||||
localOptions.set(id, remoteOpt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentPoll.options.forEach(localOpt => {
|
|
||||||
const remoteOpt = remoteOptions.get(localOpt.id);
|
|
||||||
if (remoteOpt) {
|
|
||||||
const allVoters = new Set([...localOpt.voters, ...remoteOpt.voters]);
|
|
||||||
localOpt.voters = Array.from(allVoters);
|
|
||||||
localOpt.votes = localOpt.voters.length;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleVoteMessage(optionId, voterId) {
|
|
||||||
if (!this.currentPoll) return;
|
|
||||||
|
|
||||||
this.removePreviousVote(voterId);
|
|
||||||
|
|
||||||
const option = this.currentPoll.options.find(opt => opt.id === optionId);
|
|
||||||
if (option && !option.voters.includes(voterId)) {
|
|
||||||
option.votes++;
|
|
||||||
option.voters.push(voterId);
|
|
||||||
|
|
||||||
// Update my votes if this is my vote
|
|
||||||
if (voterId === this.peerManager.getPeerId()) {
|
|
||||||
this.myVotes.add(optionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
this.notifyPollUpdated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUnvoteMessage(voterId) {
|
|
||||||
if (!this.currentPoll) return;
|
|
||||||
|
|
||||||
// Remove vote from this voter
|
|
||||||
this.removePreviousVote(voterId);
|
|
||||||
|
|
||||||
// Update my votes if this is my vote
|
|
||||||
if (voterId === this.peerManager.getPeerId()) {
|
|
||||||
this.myVotes.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
this.notifyPollUpdated();
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePollReset() {
|
|
||||||
if (!this.currentPoll) return;
|
|
||||||
|
|
||||||
this.currentPoll.options.forEach(option => {
|
|
||||||
option.votes = 0;
|
|
||||||
option.voters = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
this.myVotes.clear();
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
this.notifyPollUpdated();
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastPollUpdate() {
|
|
||||||
if (!this.currentPoll) return;
|
|
||||||
|
|
||||||
this.peerManager.broadcastMessage({
|
|
||||||
type: 'poll_update',
|
|
||||||
poll: this.currentPoll,
|
|
||||||
senderId: this.peerManager.getPeerId()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastVote(optionId, voterId) {
|
|
||||||
this.peerManager.broadcastMessage({
|
|
||||||
type: 'vote',
|
|
||||||
optionId: optionId,
|
|
||||||
voterId: voterId,
|
|
||||||
senderId: this.peerManager.getPeerId()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastPollReset() {
|
|
||||||
this.peerManager.broadcastMessage({
|
|
||||||
type: 'poll_reset',
|
|
||||||
senderId: this.peerManager.getPeerId()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
selectPoll(pollId) {
|
|
||||||
const poll = this.availablePolls.get(pollId);
|
|
||||||
if (poll) {
|
|
||||||
this.currentPoll = poll;
|
|
||||||
|
|
||||||
// Clear votes when switching to a different poll
|
|
||||||
// Only clear if this is a different poll than what we had voted in
|
|
||||||
const currentVotedOption = this.getMyVotedOption();
|
|
||||||
if (currentVotedOption && !poll.options.some(opt => opt.id === currentVotedOption)) {
|
|
||||||
this.myVotes.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
|
|
||||||
if (this.onPollSelected) {
|
|
||||||
this.onPollSelected(poll);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.onPollUpdated) {
|
|
||||||
this.onPollUpdated(poll);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
createNewPoll() {
|
|
||||||
this.currentPoll = null;
|
|
||||||
// Clear current poll inputs but keep available polls
|
|
||||||
if (this.onPollSelected) {
|
|
||||||
this.onPollSelected(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAvailablePolls() {
|
|
||||||
return Array.from(this.availablePolls.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
unvote() {
|
|
||||||
if (!this.currentPoll) {
|
|
||||||
throw new Error('No active poll');
|
|
||||||
}
|
|
||||||
|
|
||||||
const myPeerId = this.peerManager.getPeerId();
|
|
||||||
const myVotedOption = this.getMyVotedOption();
|
|
||||||
|
|
||||||
if (!myVotedOption) {
|
|
||||||
return false; // No vote to remove
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove vote from current option
|
|
||||||
this.removePreviousVote(myPeerId);
|
|
||||||
|
|
||||||
this.saveToLocalStorage();
|
|
||||||
this.notifyPollUpdated();
|
|
||||||
this.broadcastUnvote(myPeerId);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateMyVotes(poll) {
|
|
||||||
const myPeerId = this.peerManager.getPeerId();
|
|
||||||
const validVotes = new Set();
|
|
||||||
|
|
||||||
console.log('Validating votes for peer:', myPeerId);
|
|
||||||
console.log('Current myVotes:', Array.from(this.myVotes));
|
|
||||||
console.log('Poll voters:', poll.options.map(opt => ({ id: opt.id, voters: opt.voters })));
|
|
||||||
|
|
||||||
// Check each vote in myVotes to see if it's actually mine
|
|
||||||
this.myVotes.forEach(optionId => {
|
|
||||||
const option = poll.options.find(opt => opt.id === optionId);
|
|
||||||
if (option && option.voters.includes(myPeerId)) {
|
|
||||||
// This vote is actually mine
|
|
||||||
validVotes.add(optionId);
|
|
||||||
console.log(`Vote ${optionId} is valid for peer ${myPeerId}`);
|
|
||||||
} else {
|
|
||||||
console.log(`Vote ${optionId} is NOT valid for peer ${myPeerId}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Replace myVotes with only the valid votes
|
|
||||||
this.myVotes = validVotes;
|
|
||||||
console.log('Final valid votes:', Array.from(this.myVotes));
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentPoll() {
|
|
||||||
return this.currentPoll;
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastUnvote(peerId) {
|
|
||||||
this.peerManager.broadcastMessage({
|
|
||||||
type: 'unvote',
|
|
||||||
voterId: peerId,
|
|
||||||
senderId: this.peerManager.getPeerId()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
hasVoted(optionId) {
|
|
||||||
return this.myVotes.has(optionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getMyVotedOption() {
|
|
||||||
return Array.from(this.myVotes)[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
generatePollId() {
|
|
||||||
return `poll-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveToLocalStorage() {
|
|
||||||
// Save all available polls
|
|
||||||
const pollsData = Array.from(this.availablePolls.values());
|
|
||||||
localStorage.setItem('p2p-polls-list', JSON.stringify(pollsData));
|
|
||||||
|
|
||||||
// Save current poll separately
|
|
||||||
if (this.currentPoll) {
|
|
||||||
localStorage.setItem('p2p-poll-current', JSON.stringify(this.currentPoll));
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('p2p-poll-current');
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('p2p-poll-my-votes', JSON.stringify(Array.from(this.myVotes)));
|
|
||||||
}
|
|
||||||
|
|
||||||
loadFromLocalStorage() {
|
|
||||||
try {
|
|
||||||
const savedPollsList = localStorage.getItem('p2p-polls-list');
|
|
||||||
const savedVotes = localStorage.getItem('p2p-poll-my-votes');
|
|
||||||
|
|
||||||
if (savedPollsList) {
|
|
||||||
const polls = JSON.parse(savedPollsList);
|
|
||||||
this.availablePolls.clear();
|
|
||||||
polls.forEach(poll => {
|
|
||||||
this.availablePolls.set(poll.id, poll);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.onPollsListUpdated) {
|
|
||||||
this.onPollsListUpdated(polls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedVotes) {
|
|
||||||
this.myVotes = new Set(JSON.parse(savedVotes));
|
|
||||||
|
|
||||||
// Validate votes against current poll
|
|
||||||
const currentPoll = this.currentPoll || this.availablePolls.values().next().value;
|
|
||||||
if (currentPoll) {
|
|
||||||
this.validateMyVotes(currentPoll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load from localStorage:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearData() {
|
|
||||||
this.currentPoll = null;
|
|
||||||
this.availablePolls.clear();
|
|
||||||
this.myVotes.clear();
|
|
||||||
localStorage.removeItem('p2p-poll-current');
|
|
||||||
localStorage.removeItem('p2p-polls-list');
|
|
||||||
localStorage.removeItem('p2p-poll-my-votes');
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyPollUpdated() {
|
|
||||||
if (this.onPollUpdated) {
|
|
||||||
this.onPollUpdated(this.currentPoll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
class UIController {
|
|
||||||
constructor(peerManager, pollManager) {
|
|
||||||
this.peerManager = peerManager;
|
|
||||||
this.pollManager = pollManager;
|
|
||||||
this.initializeEventListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeEventListeners() {
|
|
||||||
// Connection controls
|
|
||||||
document.getElementById('join-btn').addEventListener('click', () => this.handleJoinRoom());
|
|
||||||
document.getElementById('copy-id-btn').addEventListener('click', () => this.copyPeerId());
|
|
||||||
|
|
||||||
// Poll creation
|
|
||||||
document.getElementById('create-poll-btn').addEventListener('click', () => this.handleCreatePoll());
|
|
||||||
document.getElementById('add-option-btn').addEventListener('click', () => this.addOptionInput());
|
|
||||||
|
|
||||||
// Active poll
|
|
||||||
document.getElementById('add-poll-option-btn').addEventListener('click', () => this.showAddOptionModal());
|
|
||||||
document.getElementById('reset-poll-btn').addEventListener('click', () => this.handleResetPoll());
|
|
||||||
document.getElementById('new-poll-btn').addEventListener('click', () => this.handleNewPoll());
|
|
||||||
|
|
||||||
// Modal controls
|
|
||||||
document.getElementById('close-modal-btn').addEventListener('click', () => this.hideAddOptionModal());
|
|
||||||
document.getElementById('cancel-modal-btn').addEventListener('click', () => this.hideAddOptionModal());
|
|
||||||
document.getElementById('save-option-btn').addEventListener('click', () => this.handleAddOptionFromModal());
|
|
||||||
|
|
||||||
// Enter key handlers
|
|
||||||
document.getElementById('room-id').addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') this.handleJoinRoom();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('poll-question').addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') this.handleCreatePoll();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('new-option-input').addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') this.handleAddOptionFromModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close modal on background click
|
|
||||||
document.getElementById('add-option-modal').addEventListener('click', (e) => {
|
|
||||||
if (e.target.id === 'add-option-modal') {
|
|
||||||
this.hideAddOptionModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleJoinRoom() {
|
|
||||||
const roomIdInput = document.getElementById('room-id');
|
|
||||||
const peerId = roomIdInput.value.trim();
|
|
||||||
|
|
||||||
this.showLoading(true);
|
|
||||||
|
|
||||||
if (peerId) {
|
|
||||||
// Connect to existing peer (host)
|
|
||||||
this.peerManager.joinRoom(peerId)
|
|
||||||
.then(() => {
|
|
||||||
this.showNotification('Connected to host successfully!', 'success');
|
|
||||||
this.showPollCreation();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.showNotification('Failed to connect: ' + error.message, 'error');
|
|
||||||
console.error('Connect error:', error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.showLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Act as host - just show poll creation
|
|
||||||
this.peerManager.createRoom();
|
|
||||||
this.showNotification('You are the host. Share your Peer ID with others to connect.', 'success');
|
|
||||||
this.showPollCreation();
|
|
||||||
this.showLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async copyPeerId() {
|
|
||||||
const peerId = this.peerManager.getPeerId();
|
|
||||||
if (peerId) {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(peerId);
|
|
||||||
this.showNotification('Peer ID copied to clipboard!', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
// Fallback for older browsers
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
textArea.value = peerId;
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
this.showNotification('Peer ID copied to clipboard!', 'success');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCreatePoll() {
|
|
||||||
const questionInput = document.getElementById('poll-question');
|
|
||||||
const optionInputs = document.querySelectorAll('.option-text');
|
|
||||||
const options = [];
|
|
||||||
|
|
||||||
console.log('Question input:', questionInput);
|
|
||||||
console.log('Found option inputs:', optionInputs.length);
|
|
||||||
|
|
||||||
// Check if question input exists
|
|
||||||
if (!questionInput) {
|
|
||||||
console.error('Question input not found!');
|
|
||||||
this.showNotification('Error: Question input not found', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const question = questionInput.value ? questionInput.value.trim() : '';
|
|
||||||
console.log('Question:', question);
|
|
||||||
|
|
||||||
optionInputs.forEach((input, index) => {
|
|
||||||
console.log(`Input ${index}:`, input, 'value:', input?.value);
|
|
||||||
if (input && typeof input.value !== 'undefined') {
|
|
||||||
const text = input.value.trim();
|
|
||||||
if (text) {
|
|
||||||
options.push(text);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(`Input ${index} is invalid or has no value property`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Final options:', options);
|
|
||||||
|
|
||||||
if (!question) {
|
|
||||||
this.showNotification('Please enter a poll question', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.length < 2) {
|
|
||||||
this.showNotification('Please enter at least 2 options', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.pollManager.createPoll(question, options);
|
|
||||||
this.showNotification('Poll created successfully!', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Create poll error:', error);
|
|
||||||
this.showNotification('Failed to create poll: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showAddOptionModal() {
|
|
||||||
const modal = document.getElementById('add-option-modal');
|
|
||||||
const input = document.getElementById('new-option-input');
|
|
||||||
modal.classList.remove('hidden');
|
|
||||||
input.value = '';
|
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
hideAddOptionModal() {
|
|
||||||
const modal = document.getElementById('add-option-modal');
|
|
||||||
const input = document.getElementById('new-option-input');
|
|
||||||
modal.classList.add('hidden');
|
|
||||||
input.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAddOptionFromModal() {
|
|
||||||
const input = document.getElementById('new-option-input');
|
|
||||||
const optionText = input.value.trim();
|
|
||||||
|
|
||||||
if (!optionText) {
|
|
||||||
this.showNotification('Please enter an option text', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.pollManager.addOption(optionText);
|
|
||||||
this.hideAddOptionModal();
|
|
||||||
this.showNotification('Option added successfully!', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
this.showNotification('Failed to add option: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAddPollOption() {
|
|
||||||
this.showAddOptionModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResetPoll() {
|
|
||||||
if (confirm('Are you sure you want to reset all votes?')) {
|
|
||||||
try {
|
|
||||||
this.pollManager.resetPoll();
|
|
||||||
this.showNotification('Poll reset successfully!', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
this.showNotification('Failed to reset poll: ' + error.message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewPoll() {
|
|
||||||
this.pollManager.createNewPoll();
|
|
||||||
this.showPollCreation();
|
|
||||||
this.clearPollForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
addOptionInput() {
|
|
||||||
const container = document.getElementById('options-container');
|
|
||||||
const optionCount = container.children.length;
|
|
||||||
|
|
||||||
const optionDiv = document.createElement('div');
|
|
||||||
optionDiv.className = 'option-input';
|
|
||||||
optionDiv.innerHTML = `
|
|
||||||
<input type="text" class="option-text" placeholder="Option ${optionCount + 1}">
|
|
||||||
<button class="remove-option-btn">Remove</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
container.appendChild(optionDiv);
|
|
||||||
this.updateOptionRemoveButtons();
|
|
||||||
|
|
||||||
// Add event listener to new input
|
|
||||||
const newInput = optionDiv.querySelector('.option-text');
|
|
||||||
newInput.addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') this.handleCreatePoll();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add event listener to remove button
|
|
||||||
const removeBtn = optionDiv.querySelector('.remove-option-btn');
|
|
||||||
removeBtn.addEventListener('click', () => {
|
|
||||||
optionDiv.remove();
|
|
||||||
this.updateOptionRemoveButtons();
|
|
||||||
this.updateOptionPlaceholders();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Focus on new input
|
|
||||||
newInput.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOptionRemoveButtons() {
|
|
||||||
const optionInputs = document.querySelectorAll('.option-input');
|
|
||||||
const removeButtons = document.querySelectorAll('.remove-option-btn');
|
|
||||||
|
|
||||||
removeButtons.forEach((btn, index) => {
|
|
||||||
if (optionInputs.length <= 2) {
|
|
||||||
btn.classList.add('hidden');
|
|
||||||
} else {
|
|
||||||
btn.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOptionPlaceholders() {
|
|
||||||
const optionInputs = document.querySelectorAll('.option-text');
|
|
||||||
optionInputs.forEach((input, index) => {
|
|
||||||
input.placeholder = `Option ${index + 1}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
clearPollForm() {
|
|
||||||
document.getElementById('poll-question').value = '';
|
|
||||||
const container = document.getElementById('options-container');
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="option-input">
|
|
||||||
<input type="text" class="option-text" placeholder="Option 1">
|
|
||||||
<button class="remove-option-btn hidden">Remove</button>
|
|
||||||
</div>
|
|
||||||
<div class="option-input">
|
|
||||||
<input type="text" class="option-text" placeholder="Option 2">
|
|
||||||
<button class="remove-option-btn hidden">Remove</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
this.updateOptionRemoveButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
showPollCreation() {
|
|
||||||
document.getElementById('connection-section').classList.add('hidden');
|
|
||||||
document.getElementById('poll-creation').classList.remove('hidden');
|
|
||||||
document.getElementById('active-poll').classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
showActivePoll() {
|
|
||||||
document.getElementById('connection-section').classList.add('hidden');
|
|
||||||
document.getElementById('poll-creation').classList.add('hidden');
|
|
||||||
document.getElementById('active-poll').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePeerId(peerId) {
|
|
||||||
const element = document.getElementById('peer-id');
|
|
||||||
if (element) {
|
|
||||||
element.textContent = peerId || 'Loading...';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPoll(poll) {
|
|
||||||
if (!poll) return;
|
|
||||||
|
|
||||||
this.showActivePoll();
|
|
||||||
|
|
||||||
// Update poll question
|
|
||||||
document.getElementById('poll-question-display').textContent = poll.question;
|
|
||||||
|
|
||||||
// Render options
|
|
||||||
const optionsContainer = document.getElementById('poll-options');
|
|
||||||
optionsContainer.innerHTML = '';
|
|
||||||
|
|
||||||
const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes, 0);
|
|
||||||
const myVotedOption = this.pollManager.getMyVotedOption();
|
|
||||||
|
|
||||||
poll.options.forEach(option => {
|
|
||||||
const optionDiv = document.createElement('div');
|
|
||||||
optionDiv.className = 'poll-option';
|
|
||||||
|
|
||||||
const hasVoted = this.pollManager.hasVoted(option.id);
|
|
||||||
const myVotedOption = this.pollManager.getMyVotedOption();
|
|
||||||
|
|
||||||
console.log(`Option ${option.id}: hasVoted=${hasVoted}, myVotedOption=${myVotedOption}`);
|
|
||||||
|
|
||||||
if (hasVoted) {
|
|
||||||
optionDiv.classList.add('voted');
|
|
||||||
} else if (myVotedOption) {
|
|
||||||
// User has voted but not for this option - this is a change vote option
|
|
||||||
optionDiv.classList.add('change-vote');
|
|
||||||
}
|
|
||||||
|
|
||||||
const percentage = totalVotes > 0 ? (option.votes / totalVotes * 100).toFixed(1) : 0;
|
|
||||||
|
|
||||||
optionDiv.innerHTML = `
|
|
||||||
<div class="option-header">
|
|
||||||
<span class="option-text">${option.text}</span>
|
|
||||||
<span class="vote-count">${option.votes} votes</span>
|
|
||||||
</div>
|
|
||||||
<div class="vote-bar">
|
|
||||||
<div class="vote-fill" style="width: ${percentage}%"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
optionDiv.addEventListener('click', () => {
|
|
||||||
const myVotedOption = this.pollManager.getMyVotedOption();
|
|
||||||
|
|
||||||
if (this.pollManager.hasVoted(option.id)) {
|
|
||||||
// Clicking your current vote - unvote
|
|
||||||
if (confirm('Remove your vote?')) {
|
|
||||||
const success = this.pollManager.unvote();
|
|
||||||
if (success) {
|
|
||||||
this.showNotification('Vote removed!', 'success');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Either first vote or changing vote
|
|
||||||
const success = this.pollManager.vote(option.id);
|
|
||||||
if (success) {
|
|
||||||
if (myVotedOption) {
|
|
||||||
this.showNotification('Vote changed successfully!', 'success');
|
|
||||||
} else {
|
|
||||||
this.showNotification('Vote recorded!', 'success');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
optionsContainer.appendChild(optionDiv);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showNotification(message, type = 'info') {
|
|
||||||
const notification = document.getElementById('notification');
|
|
||||||
notification.textContent = message;
|
|
||||||
notification.className = `notification ${type}`;
|
|
||||||
notification.classList.remove('hidden');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.classList.add('hidden');
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoading(show) {
|
|
||||||
const overlay = document.getElementById('loading-overlay');
|
|
||||||
if (show) {
|
|
||||||
overlay.classList.remove('hidden');
|
|
||||||
} else {
|
|
||||||
overlay.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePeersList() {
|
|
||||||
// Use the peer manager's updatePeersList method
|
|
||||||
this.peerManager.updatePeersList();
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePollsList(polls) {
|
|
||||||
const pollsList = document.getElementById('polls-list');
|
|
||||||
|
|
||||||
if (!pollsList) return;
|
|
||||||
|
|
||||||
if (polls.length === 0) {
|
|
||||||
pollsList.innerHTML = '<p class="no-polls">No polls created yet</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pollsList.innerHTML = '';
|
|
||||||
|
|
||||||
polls.forEach(poll => {
|
|
||||||
const pollDiv = document.createElement('div');
|
|
||||||
pollDiv.className = 'poll-item';
|
|
||||||
|
|
||||||
if (this.pollManager.getCurrentPoll() && this.pollManager.getCurrentPoll().id === poll.id) {
|
|
||||||
pollDiv.classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes, 0);
|
|
||||||
const createdDate = new Date(poll.createdAt).toLocaleString();
|
|
||||||
|
|
||||||
pollDiv.innerHTML = `
|
|
||||||
<div class="poll-item-title">${poll.question}</div>
|
|
||||||
<div class="poll-item-meta">${totalVotes} votes • ${createdDate}</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
pollDiv.addEventListener('click', () => {
|
|
||||||
this.pollManager.selectPoll(poll.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
pollsList.appendChild(pollDiv);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user