feat: implement session creation flow
Refactor `CreateSessionDialog` to be controlled by its parent instead of using an internal `open` prop. Lift the state up to `JournalPanel` and pass the creation callback through `JournalHeader` to `SessionDropdown`.
This commit is contained in:
parent
376dde44b5
commit
c13f66153a
|
|
@ -1,11 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* CreateSessionDialog — modal for naming a new session
|
* CreateSessionDialog — modal for naming a new session
|
||||||
*/
|
*/
|
||||||
import { Component, createSignal, Show } from "solid-js";
|
import { Component, createSignal, onMount } from "solid-js";
|
||||||
import { createSession } from "../stores/journalStream";
|
import { createSession } from "../stores/journalStream";
|
||||||
|
|
||||||
interface CreateSessionDialogProps {
|
interface CreateSessionDialogProps {
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,56 +22,56 @@ export const CreateSessionDialog: Component<CreateSessionDialogProps> = (
|
||||||
props.onClose();
|
props.onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMount(() => inputRef?.focus());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={props.open}>
|
<div
|
||||||
|
class="absolute inset-0 z-50 flex items-center justify-center bg-black/20"
|
||||||
|
onClick={props.onClose}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 z-50 flex items-center justify-center bg-black/20"
|
class="bg-white rounded-lg border border-gray-200 shadow-xl w-[280px] p-4 space-y-3"
|
||||||
onClick={props.onClose}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div
|
<div class="flex items-center justify-between">
|
||||||
class="bg-white rounded-lg border border-gray-200 shadow-xl w-[280px] p-4 space-y-3"
|
<h3 class="text-sm font-semibold text-gray-800">新建会话</h3>
|
||||||
onClick={(e) => e.stopPropagation()}
|
<button
|
||||||
>
|
onClick={props.onClose}
|
||||||
<div class="flex items-center justify-between">
|
class="text-gray-400 hover:text-gray-600 text-sm"
|
||||||
<h3 class="text-sm font-semibold text-gray-800">新建会话</h3>
|
>
|
||||||
<button
|
✕
|
||||||
onClick={props.onClose}
|
</button>
|
||||||
class="text-gray-400 hover:text-gray-600 text-sm"
|
</div>
|
||||||
>
|
<p class="text-xs text-gray-500">输入新会话的名称。</p>
|
||||||
✕
|
<input
|
||||||
</button>
|
ref={inputRef!}
|
||||||
</div>
|
type="text"
|
||||||
<p class="text-xs text-gray-500">输入新会话的名称。</p>
|
value={name()}
|
||||||
<input
|
onInput={(e) => setName(e.currentTarget.value)}
|
||||||
ref={inputRef!}
|
onKeyDown={(e) => {
|
||||||
type="text"
|
if (e.key === "Enter") handleCreate();
|
||||||
value={name()}
|
if (e.key === "Escape") props.onClose();
|
||||||
onInput={(e) => setName(e.currentTarget.value)}
|
}}
|
||||||
onKeyDown={(e) => {
|
placeholder="会话名称"
|
||||||
if (e.key === "Enter") handleCreate();
|
class="w-full border border-gray-300 rounded px-2 py-1.5 text-sm"
|
||||||
if (e.key === "Escape") props.onClose();
|
autofocus
|
||||||
}}
|
/>
|
||||||
placeholder="会话名称"
|
<div class="flex justify-end gap-1">
|
||||||
class="w-full border border-gray-300 rounded px-2 py-1.5 text-sm"
|
<button
|
||||||
autofocus
|
onClick={props.onClose}
|
||||||
/>
|
class="px-3 py-1 text-xs text-gray-600 hover:text-gray-800"
|
||||||
<div class="flex justify-end gap-1">
|
>
|
||||||
<button
|
取消
|
||||||
onClick={props.onClose}
|
</button>
|
||||||
class="px-3 py-1 text-xs text-gray-600 hover:text-gray-800"
|
<button
|
||||||
>
|
onClick={handleCreate}
|
||||||
取消
|
disabled={!name().trim()}
|
||||||
</button>
|
class="bg-blue-600 text-white rounded px-3 py-1 text-xs font-medium hover:bg-blue-700 disabled:opacity-50"
|
||||||
<button
|
>
|
||||||
onClick={handleCreate}
|
创建
|
||||||
disabled={!name().trim()}
|
</button>
|
||||||
class="bg-blue-600 text-white rounded px-3 py-1 text-xs font-medium hover:bg-blue-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
创建
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { SessionDropdown } from "./SessionDropdown";
|
||||||
|
|
||||||
interface JournalHeaderProps {
|
interface JournalHeaderProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onCreateSession: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JournalHeader: Component<JournalHeaderProps> = (props) => {
|
export const JournalHeader: Component<JournalHeaderProps> = (props) => {
|
||||||
|
|
@ -51,7 +52,7 @@ export const JournalHeader: Component<JournalHeaderProps> = (props) => {
|
||||||
</p>
|
</p>
|
||||||
{/* Clickable subtitle — session dropdown (GM only) */}
|
{/* Clickable subtitle — session dropdown (GM only) */}
|
||||||
<Show when={stream.myRole === "gm"}>
|
<Show when={stream.myRole === "gm"}>
|
||||||
<SessionDropdown />
|
<SessionDropdown onCreate={props.onCreateSession} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { JournalContext } from "./JournalContext";
|
||||||
import { JournalHeader } from "./JournalHeader";
|
import { JournalHeader } from "./JournalHeader";
|
||||||
import { ConnectDialog } from "./ConnectDialog";
|
import { ConnectDialog } from "./ConnectDialog";
|
||||||
import { InviteDialog } from "./InviteDialog";
|
import { InviteDialog } from "./InviteDialog";
|
||||||
|
import { CreateSessionDialog } from "./CreateSessionDialog";
|
||||||
|
|
||||||
export interface JournalPanelProps {
|
export interface JournalPanelProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -22,6 +23,7 @@ export interface JournalPanelProps {
|
||||||
export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
||||||
const stream = useJournalStream();
|
const stream = useJournalStream();
|
||||||
const [showInvite, setShowInvite] = createSignal(false);
|
const [showInvite, setShowInvite] = createSignal(false);
|
||||||
|
const [showCreateSession, setShowCreateSession] = createSignal(false);
|
||||||
|
|
||||||
// Player list (exclude gm and observer)
|
// Player list (exclude gm and observer)
|
||||||
const playerEntries = () =>
|
const playerEntries = () =>
|
||||||
|
|
@ -48,7 +50,10 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
||||||
>
|
>
|
||||||
<JournalContext.Provider value={{ onClose: props.onClose }}>
|
<JournalContext.Provider value={{ onClose: props.onClose }}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<JournalHeader onClose={props.onClose} />
|
<JournalHeader
|
||||||
|
onClose={props.onClose}
|
||||||
|
onCreateSession={() => setShowCreateSession(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Player list tabs */}
|
{/* Player list tabs */}
|
||||||
<Show when={stream.connected}>
|
<Show when={stream.connected}>
|
||||||
|
|
@ -88,6 +93,11 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
||||||
<Show when={showInvite()}>
|
<Show when={showInvite()}>
|
||||||
<InviteDialog onClose={() => setShowInvite(false)} />
|
<InviteDialog onClose={() => setShowInvite(false)} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
{/* Create session modal */}
|
||||||
|
<Show when={showCreateSession()}>
|
||||||
|
<CreateSessionDialog onClose={() => setShowCreateSession(false)} />
|
||||||
|
</Show>
|
||||||
</JournalContext.Provider>
|
</JournalContext.Provider>
|
||||||
</aside>
|
</aside>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,15 @@ import {
|
||||||
hydrateFromServer,
|
hydrateFromServer,
|
||||||
useJournalStream,
|
useJournalStream,
|
||||||
} from "../stores/journalStream";
|
} from "../stores/journalStream";
|
||||||
import { CreateSessionDialog } from "./CreateSessionDialog";
|
|
||||||
|
|
||||||
export const SessionDropdown: Component = () => {
|
interface SessionDropdownProps {
|
||||||
|
onCreate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SessionDropdown: Component<SessionDropdownProps> = (props) => {
|
||||||
const stream = useJournalStream();
|
const stream = useJournalStream();
|
||||||
const manifest = sessions;
|
const manifest = sessions;
|
||||||
const [dropdownOpen, setDropdownOpen] = createSignal(false);
|
const [dropdownOpen, setDropdownOpen] = createSignal(false);
|
||||||
const [showCreateDialog, setShowCreateDialog] = createSignal(false);
|
|
||||||
let dropdownRef!: HTMLDivElement;
|
let dropdownRef!: HTMLDivElement;
|
||||||
|
|
||||||
const handleSessionSelect = (id: string) => {
|
const handleSessionSelect = (id: string) => {
|
||||||
|
|
@ -75,7 +77,8 @@ export const SessionDropdown: Component = () => {
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setShowCreateDialog(true);
|
setDropdownOpen(false);
|
||||||
|
props.onCreate();
|
||||||
}}
|
}}
|
||||||
class="w-full text-left px-3 py-1 text-xs text-blue-600 hover:bg-gray-100"
|
class="w-full text-left px-3 py-1 text-xs text-blue-600 hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
|
|
@ -83,10 +86,6 @@ export const SessionDropdown: Component = () => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<CreateSessionDialog
|
|
||||||
open={showCreateDialog()}
|
|
||||||
onClose={() => setShowCreateDialog(false)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue