feat: add combined codebase
This commit is contained in:
139
src/app.ts
Normal file
139
src/app.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { getUserId } from "./identity";
|
||||
import {
|
||||
addOption,
|
||||
toggleVote,
|
||||
deleteOption,
|
||||
setDeadline,
|
||||
clearDeadline,
|
||||
createViewModel,
|
||||
} 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) => {
|
||||
const vm = createViewModel(getViewModelParams());
|
||||
if (vm.votingClosed) return;
|
||||
return addOption(sync.options, label, userId);
|
||||
},
|
||||
toggleVote: (optionId: string) => {
|
||||
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);
|
||||
},
|
||||
clearDeadline: () => {
|
||||
clearDeadline(sync.deadlineMap);
|
||||
},
|
||||
};
|
||||
|
||||
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 ---
|
||||
|
||||
// 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, () => {
|
||||
const vm = createViewModel(getViewModelParams());
|
||||
return vm.votingClosed;
|
||||
}, actions.toggleVote, actions.deleteOption);
|
||||
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();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user