forked from quic-issues/427e7578-d7bf-49c8-aee9-2dd999e25316
- Add complete P2P polling application with React + TypeScript frontend - Implement Node.js backend with Yjs WebSocket and WebRTC signaling - Support dynamic poll creation, answer management, and voting - Add CRDT-based state synchronization using Yjs for conflict-free merging - Implement user tracking and vote prevention (one vote per user per option) - Create modern UI with Tailwind CSS and visual feedback - Add comprehensive documentation and setup instructions Features: - Users can create polls with custom questions - Anyone can add answer options to any poll - Real-time voting with instant cross-client synchronization - Smart vote tracking with visual feedback for voted options - User attribution showing who created polls and options - Connection status indicators for WebSocket and P2P connections Technical: - Hybrid P2P architecture (WebSocket + WebRTC) - CRDT-based state management with Yjs
134 lines
3.2 KiB
TypeScript
134 lines
3.2 KiB
TypeScript
import * as Y from 'yjs';
|
|
import { WebsocketProvider } from 'y-websocket';
|
|
import { WebrtcProvider } from 'y-webrtc';
|
|
import { Poll, PollOption } from '../types/poll.types';
|
|
|
|
const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost:3000';
|
|
const ROOM_NAME = 'default-poll';
|
|
|
|
export const ydoc = new Y.Doc();
|
|
|
|
export const yPolls = ydoc.getArray<Poll>('polls');
|
|
|
|
export let wsProvider: WebsocketProvider | null = null;
|
|
export let webrtcProvider: WebrtcProvider | null = null;
|
|
|
|
export function initializeProviders() {
|
|
wsProvider = new WebsocketProvider(
|
|
WS_URL + '/yjs',
|
|
ROOM_NAME,
|
|
ydoc,
|
|
{ connect: true }
|
|
);
|
|
|
|
webrtcProvider = new WebrtcProvider(
|
|
ROOM_NAME,
|
|
ydoc,
|
|
{
|
|
signaling: [WS_URL.replace('ws://', 'wss://').replace('http://', 'https://') + '/signal'],
|
|
password: null,
|
|
awareness: wsProvider.awareness,
|
|
maxConns: 20,
|
|
filterBcConns: true,
|
|
peerOpts: {}
|
|
}
|
|
);
|
|
|
|
wsProvider.on('status', (event: { status: string }) => {
|
|
console.log('WebSocket status:', event.status);
|
|
});
|
|
|
|
webrtcProvider.on('synced', (synced: boolean) => {
|
|
console.log('WebRTC synced:', synced);
|
|
});
|
|
|
|
return { wsProvider, webrtcProvider };
|
|
}
|
|
|
|
export function destroyProviders() {
|
|
wsProvider?.destroy();
|
|
webrtcProvider?.destroy();
|
|
}
|
|
|
|
export function createPoll(question: string, createdBy: string): string {
|
|
const pollId = Math.random().toString(36).substr(2, 9);
|
|
const poll: Poll = {
|
|
id: pollId,
|
|
question,
|
|
createdBy,
|
|
timestamp: Date.now(),
|
|
options: []
|
|
};
|
|
|
|
yPolls.push([poll]);
|
|
return pollId;
|
|
}
|
|
|
|
export function addOption(pollId: string, text: string, createdBy: string): void {
|
|
const polls = yPolls.toArray();
|
|
const pollIndex = polls.findIndex(p => p.id === pollId);
|
|
|
|
if (pollIndex !== -1) {
|
|
const poll = polls[pollIndex];
|
|
const option: PollOption = {
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
text,
|
|
votes: 0,
|
|
votedBy: [],
|
|
createdBy,
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
const updatedPoll = {
|
|
...poll,
|
|
options: [...poll.options, option]
|
|
};
|
|
|
|
yPolls.delete(pollIndex, 1);
|
|
yPolls.insert(pollIndex, [updatedPoll]);
|
|
}
|
|
}
|
|
|
|
export function voteForOption(pollId: string, optionId: string, userId: string): void {
|
|
const polls = yPolls.toArray();
|
|
const pollIndex = polls.findIndex(p => p.id === pollId);
|
|
|
|
if (pollIndex !== -1) {
|
|
const poll = polls[pollIndex];
|
|
const optionIndex = poll.options.findIndex(opt => opt.id === optionId);
|
|
|
|
if (optionIndex !== -1) {
|
|
const option = poll.options[optionIndex];
|
|
|
|
if (option.votedBy.includes(userId)) {
|
|
return;
|
|
}
|
|
|
|
const updatedOption = {
|
|
...option,
|
|
votes: option.votes + 1,
|
|
votedBy: [...option.votedBy, userId]
|
|
};
|
|
|
|
const updatedOptions = [...poll.options];
|
|
updatedOptions[optionIndex] = updatedOption;
|
|
|
|
const updatedPoll = {
|
|
...poll,
|
|
options: updatedOptions
|
|
};
|
|
|
|
yPolls.delete(pollIndex, 1);
|
|
yPolls.insert(pollIndex, [updatedPoll]);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function getPolls(): Poll[] {
|
|
return yPolls.toArray();
|
|
}
|
|
|
|
export function getPoll(pollId: string): Poll | undefined {
|
|
return yPolls.toArray().find(p => p.id === pollId);
|
|
}
|