Files
427e7578-d7bf-49c8-aee9-2dd…/src/app.ts
2026-05-06 23:18:08 +02:00

194 lines
5.9 KiB
TypeScript

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 = `
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect x="2" y="4" width="16" height="2.5" rx="1.25" fill="currentColor"/>
<rect x="2" y="8.75" width="11" height="2.5" rx="1.25" fill="currentColor" opacity="0.6"/>
<rect x="2" y="13.5" width="13" height="2.5" rx="1.25" fill="currentColor" opacity="0.35"/>
</svg>
<span>Polly</span>
`;
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();
};
}