123 lines
3.4 KiB
TypeScript
123 lines
3.4 KiB
TypeScript
import { getUserId } from "./identity";
|
|
import {
|
|
addOption,
|
|
toggleVote,
|
|
setDeadline,
|
|
clearDeadline,
|
|
getDeadline,
|
|
} from "./state";
|
|
import { initSync } from "./sync";
|
|
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";
|
|
|
|
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 userId = getUserId();
|
|
const sync = initSync(roomId);
|
|
|
|
const shareUrl = window.location.href;
|
|
|
|
// --- Actions ---
|
|
|
|
const actions = {
|
|
addOption: (label: string) => {
|
|
if (isVotingClosed()) return;
|
|
return addOption(sync.options, label, userId);
|
|
},
|
|
toggleVote: (optionId: string) => {
|
|
if (isVotingClosed()) return;
|
|
toggleVote(sync.votes, userId, optionId);
|
|
},
|
|
startDeadline: (durationMs: number) => {
|
|
setDeadline(sync.deadlineMap, durationMs);
|
|
},
|
|
clearDeadline: () => {
|
|
clearDeadline(sync.deadlineMap);
|
|
},
|
|
};
|
|
|
|
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);
|
|
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, userId, 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();
|
|
};
|
|
}
|