diff --git a/src/components/journal/JournalPanel.tsx b/src/components/journal/JournalPanel.tsx index 8bb55c5..524fae4 100644 --- a/src/components/journal/JournalPanel.tsx +++ b/src/components/journal/JournalPanel.tsx @@ -27,6 +27,7 @@ export interface JournalPanelProps { export const JournalPanel: Component = (props) => { const stream = useJournalStream(); + const [showInvite, setShowInvite] = createSignal(false); // Player list (exclude gm and observer) const playerEntries = () => @@ -55,7 +56,7 @@ export const JournalPanel: Component = (props) => { {/* Player list tabs */} - 0}> +
{([name]) => ( @@ -64,6 +65,13 @@ export const JournalPanel: Component = (props) => { )} +
@@ -80,6 +88,11 @@ export const JournalPanel: Component = (props) => {
+ + {/* Invite modal */} + + setShowInvite(false)} /> + ); @@ -273,6 +286,22 @@ const ConnectDialog: Component = () => { ); const [connecting, setConnecting] = createSignal(false); const [error, setError] = createSignal(null); + const [autoJoined, setAutoJoined] = createSignal(false); + + // Auto-join when URL has both session and player params + onMount(() => { + const params = new URL(window.location.href).searchParams; + if ( + params.has("session") && + params.has("player") && + params.has("autojoin") + ) { + setPlayerName(params.get("player") || ""); + setRole("player"); + setAutoJoined(true); + handleConnect(); + } + }); const handleConnect = async () => { const name = playerName().trim() || stream.myName; @@ -299,49 +328,141 @@ const ConnectDialog: Component = () => { }; return ( -
-

- Enter your name and role to join the session. -
- - Connecting to {window.location.host} - -

- setPlayerName(e.currentTarget.value)} - onKeyDown={(e) => e.key === "Enter" && handleConnect()} - placeholder="Your name" - class="w-full border border-gray-300 rounded px-3 py-1.5 text-sm" - autofocus - /> - {/* Role selector */} -
- {(["gm", "player", "observer"] as const).map((r) => ( + + {connecting() ? "Joining session…" : "Joined!"} +

+ } + > +
+

+ Enter your name and role to join the session. +
+ + Connecting to {window.location.host} + +

+ setPlayerName(e.currentTarget.value)} + onKeyDown={(e) => e.key === "Enter" && handleConnect()} + placeholder="Your name" + class="w-full border border-gray-300 rounded px-3 py-1.5 text-sm" + autofocus + /> + {/* Role selector */} +
+ {(["gm", "player", "observer"] as const).map((r) => ( + + ))} +
+ + +

{error()}

+
+
+
+ ); +}; + +// --------------------------------------------------------------------------- +// Invite dialog — input player name, copy invite link +// --------------------------------------------------------------------------- + +interface InviteDialogProps { + onClose: () => void; +} + +const InviteDialog: Component = (props) => { + const stream = useJournalStream(); + const [playerName, setPlayerName] = createSignal(""); + const [copied, setCopied] = createSignal(false); + + const inviteLink = () => { + const url = new URL(window.location.href); + url.searchParams.set("session", stream.sessionId || "default"); + if (playerName().trim()) { + url.searchParams.set("player", playerName().trim()); + } + url.searchParams.set("autojoin", "1"); + return url.toString(); + }; + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(inviteLink()); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + const input = document.getElementById( + "invite-link-input", + ) as HTMLInputElement; + if (input) input.select(); + } + }; + + return ( +
+
+
+

Invite Player

+
+

+ Enter a player name and share the link. They will join as a player. +

+ setPlayerName(e.currentTarget.value)} + placeholder="Player name" + class="w-full border border-gray-300 rounded px-2 py-1 text-xs" + autofocus + /> +
+ + - ))} +
- - -

{error()}

-
); }; diff --git a/src/components/stores/journalStream.ts b/src/components/stores/journalStream.ts index eedd319..4152dd8 100644 --- a/src/components/stores/journalStream.ts +++ b/src/components/stores/journalStream.ts @@ -583,6 +583,9 @@ export function disconnectStream(): void { setState("connected", false); setState("connectionStatus", "disconnected"); setState("players", {}); + + // Strip autojoin param so the dialog shows normally on next connect + removeUrlParam("autojoin"); } // ---------------------------------------------------------------------------