import { getUserId } from "./identity"; import { User, addOption, toggleVote, setDeadline, clearDeadline, getDeadline, } 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"; function createRoomId(): string { if (typeof crypto.randomUUID === "function") { return `poll-${crypto.randomUUID().slice(0, 8)}`; } return `poll-${Math.random().toString(36).slice(2, 10)}`; } function ensureRoomId(): string { const url = new URL(window.location.href); let roomId = url.searchParams.get(ROOM_PARAM)?.trim(); if (!roomId) { roomId = createRoomId(); url.searchParams.set(ROOM_PARAM, roomId); window.history.replaceState({}, "", url); } return roomId; } export function initApp(container: HTMLElement): () => void { const roomId = ensureRoomId(); const sync = initSync(roomId); const userSync = initUserSync(); let user : User | undefined = undefined; const shareUrl = window.location.href; // --- Actions --- const actions = { addOption: (label: string) => { if (!user || isVotingClosed()) return; return addOption(sync.options, label, user.userid); }, toggleVote: (optionId: string) => { if (!user || isVotingClosed()) return; toggleVote(sync.votes, user.userid, optionId); }, startDeadline: (durationMs: number) => { setDeadline(sync.deadlineMap, durationMs); }, 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; } // --- Build UI --- // Header const header = document.createElement("header"); header.className = "app-header"; const wordmark = document.createElement("div"); wordmark.className = "app-wordmark"; wordmark.innerHTML = ` Polly `; const statusBar = StatusBar(sync.provider,userSync.provider,actions.onLoginLogout,actions.onCreateUser); header.append(wordmark, statusBar); // Main card const card = document.createElement("main"); card.className = "app-card"; const pollTitle = PollTitle(sync.doc, sync.yTitle); const addOptionComponent = AddOption((label: string) => { const result = actions.addOption(label); if (result && !result.ok) return result.error; return null; }); const pollList = PollList(sync.options, sync.votes, user, isVotingClosed, actions.toggleVote); const deadlineTimer = DeadlineTimer( sync.deadlineMap, actions.startDeadline, actions.clearDeadline, ); card.append(pollTitle, addOptionComponent, deadlineTimer, pollList); // Footer const footer = document.createElement("footer"); footer.className = "app-footer"; footer.appendChild(ShareSection(roomId)); container.append(header, card, footer); // --- Cleanup --- return () => { sync.destroy(); }; }