forked from quic-issues/427e7578-d7bf-49c8-aee9-2dd999e25316
feat: implement dynamic P2P polling app with real-time synchronization
- 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
This commit is contained in:
133
frontend/src/lib/yjs-setup.ts
Normal file
133
frontend/src/lib/yjs-setup.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user