diff --git a/src/app.ts b/src/app.ts
index b396169..bf00075 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,21 +1,19 @@
+import { getUserId } from "./identity";
import {
- User,
addOption,
toggleVote,
+ deleteOption,
setDeadline,
clearDeadline,
- getDeadline,
+ createViewModel,
} from "./state";
-import { v4 as uuidv4 } from 'uuid';
import { initSync } from "./sync";
-import { initUserSync } from "./userSync";
import { StatusBar } from "./components/StatusBar";
import { PollTitle } from "./components/PollTitle";
import { AddOption } from "./components/AddOption";
import { PollList } from "./components/PollList";
import { ShareSection } from "./components/ShareSection";
import { DeadlineTimer } from "./components/DeadlineTimer";
-import { generateUserKeyPair, exportPrivateKey, savePrivateKeyToFile, exportPublicKey, stringToCryptoKey } from "./crypto";
const ROOM_PARAM = "room";
@@ -41,9 +39,8 @@ function ensureRoomId(): string {
export function initApp(container: HTMLElement): () => void {
const roomId = ensureRoomId();
+ const userId = getUserId();
const sync = initSync(roomId);
- const userSync = initUserSync();
- let user : User | undefined = undefined;
const shareUrl = window.location.href;
@@ -51,12 +48,17 @@ export function initApp(container: HTMLElement): () => void {
const actions = {
addOption: (label: string) => {
- if (!user || isVotingClosed()) return;
- return addOption(sync.options, label, user.userid);
+ const vm = createViewModel(getViewModelParams());
+ if (vm.votingClosed) return;
+ return addOption(sync.options, label, userId);
},
toggleVote: (optionId: string) => {
- if (!user || isVotingClosed()) return;
- toggleVote(sync.votes, user.userid, optionId);
+ const vm = createViewModel(getViewModelParams());
+ if (vm.votingClosed) return;
+ toggleVote(sync.votes, userId, optionId);
+ },
+ deleteOption: (optionId: string) => {
+ deleteOption(sync.options, sync.votes, optionId);
},
startDeadline: (durationMs: number) => {
setDeadline(sync.deadlineMap, durationMs);
@@ -64,79 +66,20 @@ export function initApp(container: HTMLElement): () => void {
clearDeadline: () => {
clearDeadline(sync.deadlineMap);
},
- onLoginLogout: async (event: Event) => {
- if(user){
- user = undefined
- return false;
- } else {
- const target = event.target as HTMLInputElement;
- const file = target.files?.[0];
-
- if (file) {
- try {
- const content = await file.text();
- console.log("File loaded: ");
- if (file.name && content) {
- try {
- const uuid = file.name.replace(".pem", "");
- // Standardize the string for the importer
- const pkBase64 = content.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----/g, "").replace(/\s+/g, "");
-
- const key = await stringToCryptoKey(pkBase64, "private");
-
- user = {
- userid: uuid,
- private_key: key,
- public_key: undefined, // Note: You might need to import a pub key too!
- };
-
- console.log("Login successful for:", uuid);
- return true;
- } catch (err) {
- console.error("Crypto Import Error:", err);
- alert("The file content is not a valid Private Key.");
- }
- }
- } catch (e) {
- console.error("Failed to read file", e);
- }
- }
- return false;
- }
- },
- onCreateUser: async (event: Event) => {
- try {
- const keypair = await generateUserKeyPair();
-
- console.log('keypair:', keypair);
- const uuid = uuidv4();
- user = {
- userid: uuid,
- private_key: keypair.privateKey,
- public_key: keypair.publicKey,
- };
-
- const prvKeyString = await exportPrivateKey(keypair.privateKey);
-
- savePrivateKeyToFile(prvKeyString,uuid+".pem")
-
-
- const pubKeyString = await exportPublicKey(keypair.publicKey);
-
- userSync.users.set(user.userid,pubKeyString)
- return true;
- } catch (err) {
- user = undefined
- console.error("Failed to create new User!", err);
- }
- return false;
- }
};
- function isVotingClosed() {
- const deadline = getDeadline(sync.deadlineMap);
- const votingClosed = deadline !== null && Date.now() >= deadline;
- return votingClosed;
+ function getViewModelParams() {
+ return {
+ yTitle: sync.yTitle,
+ options: sync.options,
+ votes: sync.votes,
+ deadlineMap: sync.deadlineMap,
+ roomId,
+ shareUrl,
+ connectionStatus: sync.getConnectionStatus(),
+ peerCount: sync.getPeerCount(),
+ userId,
+ };
}
// --- Build UI ---
@@ -156,7 +99,7 @@ export function initApp(container: HTMLElement): () => void {
Polly
`;
- const statusBar = StatusBar(sync.provider,userSync.provider,actions.onLoginLogout,actions.onCreateUser);
+ const statusBar = StatusBar(sync.provider);
header.append(wordmark, statusBar);
// Main card
@@ -169,7 +112,10 @@ export function initApp(container: HTMLElement): () => void {
if (result && !result.ok) return result.error;
return null;
});
- const pollList = PollList(sync.options, sync.votes, user, isVotingClosed, actions.toggleVote);
+ const pollList = PollList(sync.options, sync.votes, userId, () => {
+ const vm = createViewModel(getViewModelParams());
+ return vm.votingClosed;
+ }, actions.toggleVote, actions.deleteOption);
const deadlineTimer = DeadlineTimer(
sync.deadlineMap,
actions.startDeadline,
diff --git a/src/components/DeadlineTimer.ts b/src/components/DeadlineTimer.ts
index 72e1072..9ef1cb9 100644
--- a/src/components/DeadlineTimer.ts
+++ b/src/components/DeadlineTimer.ts
@@ -1,6 +1,5 @@
import * as Y from "yjs";
import { getDeadline } from "../state";
-import { enforceAppendOnly } from "../yDocUtil";
const DEADLINE_DURATION_MS = 2 * 60 * 1000; // 2 minutes
@@ -80,7 +79,7 @@ export function DeadlineTimer(
onClearDeadline();
});
- deadlineMap.observe(enforceAppendOnly(deadlineMap,render));
+ deadlineMap.observe(() => render());
render();
return wrapper;
diff --git a/src/components/PollList.ts b/src/components/PollList.ts
index a4f909d..dfd4458 100644
--- a/src/components/PollList.ts
+++ b/src/components/PollList.ts
@@ -1,20 +1,15 @@
import * as Y from "yjs";
import type { OptionRecord } from "../state";
import { PollOption } from "./PollOption";
-import { enforceAppendOnly } from "../yDocUtil";
-import { User } from "../state";
export function PollList(
yOptions: Y.Map,
yVotes: Y.Map,
- user: User | undefined,
+ userId: string,
isVotingClosed: () => boolean,
onVote: (optionId: string) => void,
+ onDelete: (optionId: string) => void,
): HTMLElement {
-
- var currentOptions : { [x: string]: any; } | undefined = undefined
- var currentVotes : { [x: string]: any; } | undefined = undefined
-
const wrapper = document.createElement("div");
wrapper.className = "poll-list-wrapper";
@@ -46,32 +41,25 @@ export function PollList(
votes: number;
voted: boolean;
}> = [];
- if (currentOptions && currentVotes){
- // Tally votes per option
- const tally = new Map();
- for (const optionId of Object.values(currentVotes)) {
- tally.set(optionId, (tally.get(optionId) ?? 0) + 1);
- }
-
- let myVote = null;
- if (user) {
- myVote = currentVotes[user.userid]
- }
-
- Object.entries(currentOptions).forEach(([id,record]) => {
- console.log(`${record}: ${id}`)
- entries.push({
- id,
- name: record.label,
- votes: tally.get(id) ?? 0,
- voted: myVote === id,
- });
- });
-
- entries.sort((a, b) => b.votes - a.votes || a.name.localeCompare(b.name));
+ // Tally votes per option
+ const tally = new Map();
+ for (const optionId of yVotes.values()) {
+ tally.set(optionId, (tally.get(optionId) ?? 0) + 1);
}
+ const myVote = yVotes.get(userId) ?? null;
+
+ yOptions.forEach((record, id) => {
+ entries.push({
+ id,
+ name: record.label,
+ votes: tally.get(id) ?? 0,
+ voted: myVote === id,
+ });
+ });
+
+ entries.sort((a, b) => b.votes - a.votes || a.name.localeCompare(b.name));
return entries;
}
@@ -115,6 +103,7 @@ export function PollList(
totalVotes: total,
votingClosed,
onVote,
+ onDelete,
});
const currentEl = list.children[i] as HTMLElement | undefined;
@@ -129,10 +118,9 @@ export function PollList(
}
});
}
- yOptions.observe(enforceAppendOnly(yOptions,(update : { [x: string]: any; }) => {currentOptions = update}, render));
- yVotes.observe(enforceAppendOnly(yVotes,(update : { [x: string]: any; }) => {currentVotes = update},render));
- currentOptions=yOptions.toJSON()
- currentVotes=yVotes.toJSON()
+
+ yOptions.observeDeep(() => render());
+ yVotes.observe(() => render());
render();
return wrapper;
diff --git a/src/components/PollOption.ts b/src/components/PollOption.ts
index 70eff3b..c10ef22 100644
--- a/src/components/PollOption.ts
+++ b/src/components/PollOption.ts
@@ -8,10 +8,11 @@ export interface PollOptionProps {
totalVotes: number;
votingClosed: boolean;
onVote: (id: string) => void;
+ onDelete: (id: string) => void;
}
export function PollOption(props: PollOptionProps): HTMLElement {
- const { id, name, votes, voted, totalVotes, votingClosed, onVote } = props;
+ const { id, name, votes, voted, totalVotes, votingClosed, onVote, onDelete } = props;
const row = document.createElement("div");
row.className = `poll-option${voted ? " poll-option--voted" : ""}`;
@@ -29,11 +30,17 @@ export function PollOption(props: PollOptionProps): HTMLElement {
+
`;
row.querySelector(".poll-option__vote-btn")!.addEventListener("click", () => onVote(id));
+ row.querySelector(".poll-option__delete-btn")!.addEventListener("click", () => onDelete(id));
return row;
}
diff --git a/src/components/StatusBar.ts b/src/components/StatusBar.ts
index 52d9950..d981c0a 100644
--- a/src/components/StatusBar.ts
+++ b/src/components/StatusBar.ts
@@ -1,110 +1,30 @@
import type { WebrtcProvider } from "y-webrtc";
-export function StatusBar(provider: WebrtcProvider, user_provider: WebrtcProvider, onLoginLogout: (event: Event) => Promise, onCreateUser: (event: Event) => Promise): HTMLElement {
+export function StatusBar(provider: WebrtcProvider): HTMLElement {
const el = document.createElement("div");
el.className = "status-bar";
- const statusPanel=document.createElement("div");
- statusPanel.className = "status-bar";
+ const dot = document.createElement("span");
+ dot.className = "status-dot connecting";
+
+ const statusText = document.createElement("span");
+ statusText.className = "status-text";
+ statusText.textContent = "Connecting";
const divider = document.createElement("span");
divider.className = "status-divider";
divider.textContent = "\u00b7";
- function getProviderStatus(){
+ const peerText = document.createElement("span");
+ peerText.className = "status-peers";
- const dot = document.createElement("span");
- dot.className = "status-dot connecting";
-
- const statusText = document.createElement("span");
- statusText.className = "status-text";
- statusText.textContent = "Connecting";
-
- const peerText = document.createElement("span");
- peerText.className = "status-peers";
-
- return { dot: dot, statusText: statusText, peerText: peerText}
- }
-
- const providerStatusPanel=document.createElement("div");
- providerStatusPanel.className = "provider-status-container";
-
-
- const pollProviderText = document.createElement("span");
- pollProviderText.className = "status-text";
- pollProviderText.textContent = "Polls: ";
- const pollProviderElements = getProviderStatus()
- const pollProviderStatusPanel=document.createElement("div");
- pollProviderStatusPanel.className = "status-bar";
- pollProviderStatusPanel.append(pollProviderText,pollProviderElements.dot, pollProviderElements.statusText, divider, pollProviderElements.peerText);
-
-
- const userProviderText = document.createElement("span");
- userProviderText.className = "status-text";
- userProviderText.textContent = "Users: ";
- const userProviderElements = getProviderStatus()
- const userProviderStatusPanel=document.createElement("div");
- userProviderStatusPanel.className = "status-bar";
- userProviderStatusPanel.append(userProviderText,userProviderElements.dot, userProviderElements.statusText, divider, userProviderElements.peerText);
-
- providerStatusPanel.append(userProviderStatusPanel,pollProviderStatusPanel)
-
- const userButtons = document.createElement("div");
- userButtons.className = "status-bar";
-
- const loginLabel = document.createElement("label");
- loginLabel.setAttribute("title", "Select Key File");
- const loginSpan = document.createElement("span");
- loginSpan.className = "add-option-btn"
- loginSpan.textContent = "Login";
- const loginInput = document.createElement("input");
- loginInput.type = "file"
- loginInput.accept = ".pem"
- loginInput.hidden = true
-
-
- loginLabel.append(loginSpan,loginInput)
-
-
- const logoutButton = document.createElement("button");
- logoutButton.className = "add-option-btn";
- logoutButton.setAttribute("aria-label", "Logout");
- logoutButton.innerHTML="Logout"
- logoutButton.style.display = "none";
-
- const createUserButton = document.createElement("button");
- createUserButton.className = "add-option-btn";
- createUserButton.setAttribute("aria-label", "Create User");
- createUserButton.innerHTML="Create User"
-
- async function onLoginLogoutResult(event: Event, loginLogout: (event: Event) => Promise){
- if(await loginLogout(event)){
- console.log('created / logged in')
- loginLabel.style.display = "none";
- logoutButton.style.display = "block";
- createUserButton.style.display = "none";
- } else {
- console.log('logged out')
- loginLabel.style.display = "block";
- logoutButton.style.display = "none";
- createUserButton.hidden = false;
- createUserButton.style.display = "block";
- }
- }
-
- loginLabel.addEventListener("change", (e) => onLoginLogoutResult(e,onLoginLogout));
- logoutButton.addEventListener("click", (e) => onLoginLogoutResult(e,onLoginLogout));
- createUserButton.addEventListener("click", (e) => onLoginLogoutResult(e,onCreateUser));
-
- userButtons.append(loginLabel,logoutButton,createUserButton)
-
- el.append(providerStatusPanel, divider, userButtons);
+ el.append(dot, statusText, divider, peerText);
// --- Connection state ---
let syncTimeout: ReturnType | undefined = setTimeout(() => {
- pollProviderElements.statusText.textContent = "Ready";
- pollProviderElements.dot.className = "status-dot ready";
+ statusText.textContent = "Ready";
+ dot.className = "status-dot ready";
}, 3000);
provider.on("synced", ({ synced }: { synced: boolean }) => {
@@ -112,37 +32,18 @@ export function StatusBar(provider: WebrtcProvider, user_provider: WebrtcProvide
clearTimeout(syncTimeout);
syncTimeout = undefined;
}
- pollProviderElements.dot.className = `status-dot ${synced ? "connected" : "connecting"}`;
- pollProviderElements.statusText.textContent = synced ? "Connected" : "Connecting";
- });
-
-
- let syncTimeout2: ReturnType | undefined = setTimeout(() => {
- userProviderElements.statusText.textContent = "Ready";
- userProviderElements.dot.className = "status-dot ready";
- }, 3000);
-
- user_provider.on("synced", ({ synced }: { synced: boolean }) => {
- if (syncTimeout2) {
- clearTimeout(syncTimeout2);
- syncTimeout2 = undefined;
- }
- userProviderElements.dot.className = `status-dot ${synced ? "connected" : "connecting"}`;
- userProviderElements.statusText.textContent = synced ? "Connected" : "Connecting";
+ dot.className = `status-dot ${synced ? "connected" : "connecting"}`;
+ statusText.textContent = synced ? "Connected" : "Connecting";
});
// Online/offline awareness
const handleOffline = () => {
- pollProviderElements.dot.className = "status-dot connecting";
- pollProviderElements.statusText.textContent = "Offline";
- userProviderElements.dot.className = "status-dot connecting";
- userProviderElements.statusText.textContent = "Offline";
+ dot.className = "status-dot connecting";
+ statusText.textContent = "Offline";
};
const handleOnline = () => {
- pollProviderElements.dot.className = "status-dot connecting";
- pollProviderElements.statusText.textContent = "Reconnecting";
- userProviderElements.dot.className = "status-dot connecting";
- userProviderElements.statusText.textContent = "Reconnecting";
+ dot.className = "status-dot connecting";
+ statusText.textContent = "Reconnecting";
};
window.addEventListener("offline", handleOffline);
@@ -153,18 +54,10 @@ export function StatusBar(provider: WebrtcProvider, user_provider: WebrtcProvide
function updatePeerCount() {
const total = provider.awareness.getStates().size;
const others = total - 1;
- pollProviderElements.peerText.textContent =
+ peerText.textContent =
others === 0
? "Only you"
: `${others} other${others !== 1 ? "s" : ""}`;
-
-
- const total2 = user_provider.awareness.getStates().size;
- const others2 = total2 - 1;
- userProviderElements.peerText.textContent =
- others2 === 0
- ? "Only you"
- : `${others2} other${others2 !== 1 ? "s" : ""}`;
}
provider.awareness.on("change", updatePeerCount);
diff --git a/src/crypto.ts b/src/crypto.ts
deleted file mode 100644
index 91d9b0f..0000000
--- a/src/crypto.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-export const generateUserKeyPair = async () => {
- return await window.crypto.subtle.generateKey(
- {
- name: "RSASSA-PKCS1-v1_5",
- modulusLength: 2048,
- publicExponent: new Uint8Array([1, 0, 1]), // 65537
- hash: "SHA-256",
- },
- true, // extractable
- ["sign", "verify"]
- );
-};
-
-export const signData = async (data: any, privateKey: CryptoKey) => {
- const encoder = new TextEncoder();
- const encodedData = encoder.encode(JSON.stringify(data));
-
- const signature = await window.crypto.subtle.sign(
- "RSASSA-PKCS1-v1_5",
- privateKey,
- encodedData
- );
-
- // Convert to Base64 or Hex to store in Yjs easily
- return btoa(String.fromCharCode(...new Uint8Array(signature)));
-};
-
-
-// Helper to convert ArrayBuffer to Base64 string
-const bufferToBase64 = (buf: ArrayBuffer) =>
- window.btoa(String.fromCharCode(...new Uint8Array(buf)));
-
-export const exportPublicKey = async (key: CryptoKey) => {
- // Export Public Key
- const exportedPublic = await window.crypto.subtle.exportKey("spki", key);
- const publicKeyString = bufferToBase64(exportedPublic);
-
- return publicKeyString;
-};
-export const exportPrivateKey = async (key: CryptoKey) => {
- // Export Private Key
- const exportedPrivate = await window.crypto.subtle.exportKey("pkcs8", key);
- const privateKeyString = bufferToBase64(exportedPrivate);
-
- return privateKeyString;
-};
-
-/**
- * Converts a Base64 string back into a usable CryptoKey object
- * @param keyStr The Base64 string (without PEM headers)
- * @param type 'public' or 'private'
- */
-export const stringToCryptoKey = async (keyStr: string, type: 'public' | 'private'): Promise => {
- // 1. Convert Base64 string to a Uint8Array (binary)
- const binaryString = window.atob(keyStr);
- const bytes = new Uint8Array(binaryString.length);
- for (let i = 0; i < binaryString.length; i++) {
- bytes[i] = binaryString.charCodeAt(i);
- }
-
- // 2. Identify the format based on the key type
- // Public keys usually use 'spki', Private keys use 'pkcs8'
- const format = type === 'public' ? 'spki' : 'pkcs8';
- const usages: KeyUsage[] = type === 'public' ? ['verify'] : ['sign'];
-
- // 3. Import the key
- return await window.crypto.subtle.importKey(
- format,
- bytes.buffer,
- {
- name: "RSASSA-PKCS1-v1_5",
- hash: "SHA-256",
- },
- true, // extractable (set to false if you want to lock it in memory)
- usages
- );
-};
-
-export const savePrivateKeyToFile = (privateKeyStr: string, filename: string) => {
- // Optional: Wrap in PEM headers for standard formatting
- const pemHeader = "-----BEGIN PRIVATE KEY-----\n";
- const pemFooter = "\n-----END PRIVATE KEY-----";
- const fileContent = pemHeader + privateKeyStr + pemFooter;
-
- const blob = new Blob([fileContent], { type: "text/plain" });
- const url = URL.createObjectURL(blob);
-
- const link = document.createElement("a");
- link.href = url;
- link.download = filename;
-
- document.body.appendChild(link);
- link.click();
-
- // Cleanup
- document.body.removeChild(link);
- URL.revokeObjectURL(url);
-};
-
-export const loadPrivateKeyFromFile = async (file: File): Promise => {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
-
- reader.onload = (e) => {
- const content = e.target?.result as string;
-
- // Clean up the string by removing PEM headers and newlines
- const cleanKey = content
- .replace("-----BEGIN PRIVATE KEY-----", "")
- .replace("-----END PRIVATE KEY-----", "")
- .replace(/\s+/g, ""); // Removes all whitespace/newlines
-
- resolve(cleanKey);
- };
-
- reader.onerror = () => reject("Error reading file");
- reader.readAsText(file);
- });
-};
\ No newline at end of file
diff --git a/src/state.ts b/src/state.ts
index fc20f17..65fd560 100644
--- a/src/state.ts
+++ b/src/state.ts
@@ -4,12 +4,6 @@ import * as Y from "yjs";
export type ConnectionStatus = "connecting" | "connected" | "offline";
-export interface User{
- userid: string,
- private_key: CryptoKey,
- public_key: CryptoKey | undefined,
-}
-
export interface OptionRecord {
id: string;
label: string;
@@ -17,6 +11,25 @@ export interface OptionRecord {
createdBy: string;
}
+export interface PollOptionViewModel extends OptionRecord {
+ voteCount: number;
+ isVotedByMe: boolean;
+ percentage: number;
+}
+
+export interface PollViewModel {
+ title: string;
+ roomId: string;
+ shareUrl: string;
+ connectionStatus: ConnectionStatus;
+ peerCount: number;
+ options: PollOptionViewModel[];
+ totalVotes: number;
+ myVoteOptionId: string | null;
+ deadline: number | null;
+ votingClosed: boolean;
+}
+
// --- Helpers ---
export function createOptionId(): string {
@@ -91,6 +104,20 @@ export function toggleVote(
}
}
+export function deleteOption(
+ options: Y.Map,
+ votes: Y.Map,
+ optionId: string,
+): void {
+ options.delete(optionId);
+ // Clean up votes pointing to this option
+ for (const [userId, votedOptionId] of votes.entries()) {
+ if (votedOptionId === optionId) {
+ votes.delete(userId);
+ }
+ }
+}
+
// --- Deadline ---
export function setDeadline(
@@ -108,3 +135,73 @@ export function getDeadline(deadlineMap: Y.Map): number | null {
const val = deadlineMap.get("deadline");
return typeof val === "number" ? val : null;
}
+
+// --- ViewModel ---
+
+export function createViewModel(params: {
+ yTitle: Y.Text;
+ options: Y.Map;
+ votes: Y.Map;
+ deadlineMap: Y.Map;
+ roomId: string;
+ shareUrl: string;
+ connectionStatus: ConnectionStatus;
+ peerCount: number;
+ userId: string;
+}): PollViewModel {
+ const {
+ yTitle,
+ options,
+ votes,
+ deadlineMap,
+ roomId,
+ shareUrl,
+ connectionStatus,
+ peerCount,
+ userId,
+ } = params;
+
+ // Tally votes per option
+ const tally = new Map();
+ for (const optionId of votes.values()) {
+ tally.set(optionId, (tally.get(optionId) ?? 0) + 1);
+ }
+
+ let totalVotes = 0;
+ for (const count of tally.values()) {
+ totalVotes += count;
+ }
+
+ const myVoteOptionId = votes.get(userId) ?? null;
+ const deadline = getDeadline(deadlineMap);
+ const votingClosed = deadline !== null && Date.now() >= deadline;
+
+ // Sort by votes desc, then alphabetically
+ const sortedOptions = Array.from(options.values()).sort((a, b) => {
+ const aVotes = tally.get(a.id) ?? 0;
+ const bVotes = tally.get(b.id) ?? 0;
+ if (bVotes !== aVotes) return bVotes - aVotes;
+ return a.label.localeCompare(b.label);
+ });
+
+ return {
+ title: getPollTitle(yTitle),
+ roomId,
+ shareUrl,
+ connectionStatus,
+ peerCount,
+ myVoteOptionId,
+ totalVotes,
+ deadline,
+ votingClosed,
+ options: sortedOptions.map((option) => ({
+ ...option,
+ voteCount: tally.get(option.id) ?? 0,
+ isVotedByMe: myVoteOptionId === option.id,
+ percentage:
+ totalVotes > 0
+ ? Math.round(((tally.get(option.id) ?? 0) / totalVotes) * 100)
+ : 0,
+ })),
+ };
+}
diff --git a/src/styles.css b/src/styles.css
index e213662..965045c 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -86,14 +86,6 @@ button { cursor: pointer; }
font-size: 0.8rem;
color: var(--text-secondary);
}
-.provider-status-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.4rem;
- font-size: 0.8rem;
- color: var(--text-secondary);
-}
.status-dot {
width: 6px;
diff --git a/src/userSync.ts b/src/userSync.ts
deleted file mode 100644
index 5ba0f51..0000000
--- a/src/userSync.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { IndexeddbPersistence } from "y-indexeddb";
-import { WebrtcProvider } from "y-webrtc";
-import * as Y from "yjs";
-
-import type { ConnectionStatus, OptionRecord } from "./state";
-
-export interface UserSync {
- doc: Y.Doc;
- users: Y.Map;
- provider: WebrtcProvider;
- persistence: IndexeddbPersistence;
- getConnectionStatus: () => ConnectionStatus;
- getPeerCount: () => number;
- destroy: () => void;
-}
-
-export function initUserSync(): UserSync {
- const doc = new Y.Doc();
- const users = doc.getMap("users");
-
- let connectionStatus: ConnectionStatus = navigator.onLine
- ? "connecting"
- : "offline";
-
- const provider = new WebrtcProvider("users", doc,{
- signaling: ["ws://localhost:4444", "ws://lynxpi.ddns.net:4444"]
- });
- const persistence = new IndexeddbPersistence("users", doc);
-
- const syncConnectionStatus = (status: ConnectionStatus) => {
- connectionStatus = navigator.onLine ? status : "offline";
- };
-
- const handleOnline = () => {
- syncConnectionStatus(provider.connected ? "connected" : "connecting");
- };
- const handleOffline = () => {
- connectionStatus = "offline";
- };
-
- provider.on("status", (event: { connected: boolean }) => {
- syncConnectionStatus(event.connected ? "connected" : "connecting");
- });
-
- provider.on("synced", ({ synced }: { synced: boolean }) => {
- if (synced) syncConnectionStatus("connected");
- });
-
- window.addEventListener("online", handleOnline);
- window.addEventListener("offline", handleOffline);
-
- return {
- doc,
- users,
- provider,
- persistence,
- getConnectionStatus: () => connectionStatus,
- getPeerCount: () => {
- const total = provider.awareness.getStates().size;
- return Math.max(0, total - 1);
- },
- destroy: () => {
- window.removeEventListener("online", handleOnline);
- window.removeEventListener("offline", handleOffline);
- persistence.destroy();
- provider.destroy();
- doc.destroy();
- },
- };
-}
diff --git a/src/yDocUtil.ts b/src/yDocUtil.ts
deleted file mode 100644
index 66a687d..0000000
--- a/src/yDocUtil.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as Y from "yjs";
-
-/**
- * Enforces append-only logic on a Y.Map.
- * Reverts any 'update' or 'delete' actions detected in the observer.
- */
-export function enforceAppendOnly(yMap: Y.Map,update: (update : { [x: string]: any; }) => void, render: () => void) {
- return (event: Y.YMapEvent, transaction: Y.Transaction) => {
- var isOperationIllegal = false
- event.keys.forEach((change, key) => {
- const { action, oldValue } = change;
-
- if (action === 'update' || action === 'delete') {
- isOperationIllegal = true
- console.log("Illegal Operation: "+action)
- }
- });
- if(!isOperationIllegal) {
- console.log("Updating Map!")
- update(yMap.toJSON())
- render();
- }
- };
-}
\ No newline at end of file