Add guided onboarding tour for first toolbox creation
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s
This commit is contained in:
parent
3007aa1389
commit
4f73e0068b
20 changed files with 470 additions and 20 deletions
|
|
@ -71,7 +71,7 @@ export function ToolboxCard({ toolbox, game, actions, reorder }) {
|
|||
);
|
||||
}
|
||||
|
||||
export function ToolboxIconPicker({ toolbox, onChange }) {
|
||||
export function ToolboxIconPicker({ toolbox, onChange, tutorialTarget = "" }) {
|
||||
const icon = normalizeToolboxIcon(toolbox.icon);
|
||||
|
||||
return (
|
||||
|
|
@ -86,6 +86,7 @@ export function ToolboxIconPicker({ toolbox, onChange }) {
|
|||
onClick={toggle}
|
||||
aria-label="Changer l'icône de la toolbox"
|
||||
aria-expanded={open}
|
||||
data-toolbox-tutorial-target={tutorialTarget || undefined}
|
||||
title="Changer l'icône"
|
||||
>
|
||||
<img src={icon} alt="" aria-hidden="true" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Rôle : affiche les pages toolbox, le panneau latéral et leurs contrôles.
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { GuidedTooltipTour } from "../../components/GuidedTooltipTour.jsx";
|
||||
import { Icon } from "../../components/Icon.jsx";
|
||||
import { ImportButton } from "../../components/ImportButton.jsx";
|
||||
import { StorageQuota } from "../../components/StorageQuota.jsx";
|
||||
|
|
@ -169,10 +170,18 @@ export function ToolboxPage(props) {
|
|||
return <ToolboxView {...props} toolbox={toolbox} toolboxGame={getToolboxGame(toolbox)} embedded={false} />;
|
||||
}
|
||||
|
||||
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, getModuleData, updateToolbox, updateModuleData, addImageFiles }) {
|
||||
function shouldShowTutorial(toolbox, pendingTutorialToolboxId) {
|
||||
return toolbox?.tutorial === true || pendingTutorialToolboxId === toolbox?.id;
|
||||
}
|
||||
|
||||
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, getModuleData, updateToolbox, updateModuleData, addImageFiles, pendingTutorialToolboxId = "", setPendingTutorialToolboxId }) {
|
||||
const toolboxId = toolbox.id;
|
||||
const toolboxTutorial = toolbox.tutorial;
|
||||
const moduleColumns = embedded || Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
||||
const toolboxGameCover = getGameCardCover(toolboxGame);
|
||||
const moduleText = siteContent.toolboxes.modules;
|
||||
const [showTutorial, setShowTutorial] = useState(() => shouldShowTutorial(toolbox, pendingTutorialToolboxId));
|
||||
const currentToolboxIdRef = useRef(toolboxId);
|
||||
const moduleContext = {
|
||||
getModuleData,
|
||||
moduleText,
|
||||
|
|
@ -202,6 +211,15 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
refresh: () => {}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentToolboxIdRef.current === toolboxId) {
|
||||
if (shouldShowTutorial(toolbox, pendingTutorialToolboxId)) setShowTutorial(true);
|
||||
return;
|
||||
}
|
||||
currentToolboxIdRef.current = toolboxId;
|
||||
setShowTutorial(shouldShowTutorial(toolbox, pendingTutorialToolboxId));
|
||||
}, [pendingTutorialToolboxId, toolbox, toolboxId, toolboxTutorial]);
|
||||
|
||||
function addModule(type) {
|
||||
if (!type) return;
|
||||
const module = { id: uid("mod"), type };
|
||||
|
|
@ -231,10 +249,10 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
<div>
|
||||
<p className="eyebrow">Toolbox</p>
|
||||
<div className="toolbox-title-row">
|
||||
{!embedded && !toolboxGame && <ToolboxIconPicker toolbox={toolbox} onChange={(icon) => updateToolbox({ ...toolbox, icon })} />}
|
||||
{!embedded && !toolboxGame && <ToolboxIconPicker toolbox={toolbox} onChange={(icon) => updateToolbox({ ...toolbox, icon })} tutorialTarget="icon" />}
|
||||
{!embedded && toolboxGameCover && <ToolboxGameIcon game={toolboxGame} />}
|
||||
{embedded ? <h1>{toolbox.name}</h1> : (
|
||||
<EditableTitle className="toolbox-title" value={toolbox.name} fallback="Nouvelle toolbox" onSave={(name) => name !== toolbox.name && updateToolbox({ ...toolbox, name })} />
|
||||
<EditableTitle className="toolbox-title" value={toolbox.name} fallback="Nouvelle toolbox" tutorialTarget="title" onSave={(name) => name !== toolbox.name && updateToolbox({ ...toolbox, name })} />
|
||||
)}
|
||||
</div>
|
||||
{toolboxGame && (
|
||||
|
|
@ -252,6 +270,25 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
<AddToolControls onAdd={addModule} />
|
||||
</div>
|
||||
</section>
|
||||
{!embedded && showTutorial && (
|
||||
<GuidedTooltipTour
|
||||
steps={siteContent.toolboxes.tutorial?.items || []}
|
||||
labels={{
|
||||
dismiss: siteContent.toolboxes.tutorial?.dismissLabel,
|
||||
next: siteContent.toolboxes.tutorial?.nextLabel,
|
||||
done: siteContent.toolboxes.tutorial?.doneLabel
|
||||
}}
|
||||
targetAttribute="data-toolbox-tutorial-target"
|
||||
onDismiss={() => {
|
||||
if (pendingTutorialToolboxId === toolbox.id) setPendingTutorialToolboxId?.("");
|
||||
setShowTutorial(false);
|
||||
}}
|
||||
onStarted={() => {
|
||||
if (pendingTutorialToolboxId === toolbox.id) setPendingTutorialToolboxId?.("");
|
||||
if (toolbox.tutorial === true) updateToolbox({ ...toolbox, tutorial: false });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!embedded && (
|
||||
<section className="modules-toolbar" aria-label="Options d'affichage des outils">
|
||||
<div className="layout-switch" role="group" aria-label="Mode d'affichage des outils">
|
||||
|
|
@ -359,7 +396,7 @@ export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, act
|
|||
);
|
||||
}
|
||||
|
||||
function EditableTitle({ value, fallback, onSave, className = "module-title" }) {
|
||||
function EditableTitle({ value, fallback, onSave, className = "module-title", tutorialTarget = "" }) {
|
||||
const inlineEdit = useInlineEdit({
|
||||
value,
|
||||
onCommit: (title) => onSave(title || fallback),
|
||||
|
|
@ -370,6 +407,7 @@ function EditableTitle({ value, fallback, onSave, className = "module-title" })
|
|||
<h1
|
||||
{...inlineEdit.getContentEditableProps({
|
||||
className,
|
||||
"data-toolbox-tutorial-target": tutorialTarget || undefined,
|
||||
title: "Cliquer pour renommer"
|
||||
})}
|
||||
>{value}</h1>
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ export function AddToolControls({ onAdd }) {
|
|||
className="primary tool-add-modal-button"
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
data-toolbox-tutorial-target="addTool"
|
||||
>
|
||||
Ajouter un outil
|
||||
</button>
|
||||
|
|
@ -311,7 +312,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${definition.icon || "notepad"}`} />
|
||||
</span>
|
||||
<EditableModuleTitle value={module.title || label} fallback={label} onSave={(title) => onRename(module.id, title)} />
|
||||
<EditableModuleTitle value={module.title || label} fallback={label} tutorialTarget={module.type === "notepad" ? "moduleTitle" : ""} onSave={(title) => onRename(module.id, title)} />
|
||||
</div>
|
||||
<div>
|
||||
{definition.scrollable && (
|
||||
|
|
@ -345,7 +346,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
<Icon name="add" />
|
||||
</button>
|
||||
)}
|
||||
<button className="module-delete-button danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} title="Retirer">
|
||||
<button className="module-delete-button danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} data-toolbox-tutorial-target={module.type === "links" ? "deleteTool" : undefined} title="Retirer">
|
||||
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -357,7 +358,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
);
|
||||
}
|
||||
|
||||
function EditableModuleTitle({ value, fallback, onSave }) {
|
||||
function EditableModuleTitle({ value, fallback, onSave, tutorialTarget = "" }) {
|
||||
const inlineEdit = useInlineEdit({
|
||||
value,
|
||||
onCommit: (title) => onSave(title || fallback),
|
||||
|
|
@ -368,6 +369,7 @@ function EditableModuleTitle({ value, fallback, onSave }) {
|
|||
<h2
|
||||
{...inlineEdit.getContentEditableProps({
|
||||
className: "module-title",
|
||||
"data-toolbox-tutorial-target": tutorialTarget || undefined,
|
||||
title: "Cliquer pour renommer"
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export function remapModuleOrderIds(moduleOrder, moduleIdMap) {
|
|||
export function normalizeToolbox(toolbox) {
|
||||
if (!toolbox || typeof toolbox !== "object") return null;
|
||||
const modules = (Array.isArray(toolbox.modules) ? toolbox.modules : []).map(normalizeToolboxModule).filter(Boolean);
|
||||
return {
|
||||
const normalized = {
|
||||
id: toolbox.id || uid("tbx"),
|
||||
name: String(toolbox.name || "Nouvelle toolbox").trim() || "Nouvelle toolbox",
|
||||
icon: normalizeToolboxIcon(toolbox.icon),
|
||||
|
|
@ -123,6 +123,8 @@ export function normalizeToolbox(toolbox) {
|
|||
moduleOrder: normalizeModuleOrder(toolbox.moduleOrder, modules),
|
||||
updatedAt: toolbox.updatedAt || new Date().toISOString()
|
||||
};
|
||||
if (toolbox.tutorial === true) normalized.tutorial = true;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function compactToolboxForStorage(toolbox) {
|
||||
|
|
@ -137,6 +139,7 @@ export function compactToolboxForStorage(toolbox) {
|
|||
};
|
||||
if (normalized.moduleColumns === 1) compact.moduleColumns = 1;
|
||||
if (normalized.icon !== DEFAULT_TOOLBOX_ICON) compact.icon = normalized.icon;
|
||||
if (normalized.tutorial === true) compact.tutorial = true;
|
||||
return compact;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,19 +20,36 @@ function downloadJson(payload, filename) {
|
|||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
export function useToolboxActions({ store, notify, setConfirmModal, setCreateModal, setLinkModalGameId, setDrawerGameId, setImage }) {
|
||||
function createToolbox(name, gameId = "") {
|
||||
function getLibraryUrl() {
|
||||
if (typeof window === "undefined") return "https://sokko-g.local/#/library";
|
||||
return `${window.location.origin}${window.location.pathname}#/library`;
|
||||
}
|
||||
|
||||
export function useToolboxActions({ store, notify, creationTemplate, setConfirmModal, setCreateModal, setLinkModalGameId, setDrawerGameId, setImage }) {
|
||||
function createToolbox(name, gameId = "", options = {}) {
|
||||
const now = new Date().toISOString();
|
||||
const notepadModuleId = uid("mod");
|
||||
const linksModuleId = uid("mod");
|
||||
const welcomeText = creationTemplate?.notepad?.text || "Profite bien de ta nouvelle toolbox.";
|
||||
const welcomeHtml = creationTemplate?.notepad?.html || `<p>${welcomeText}</p>`;
|
||||
const libraryLink = {
|
||||
id: uid("link"),
|
||||
title: creationTemplate?.links?.title || "Librairie des outils",
|
||||
url: getLibraryUrl()
|
||||
};
|
||||
const toolbox = {
|
||||
id: uid("tbx"),
|
||||
name: name.trim() || "Nouvelle toolbox",
|
||||
updatedAt: now,
|
||||
modules: [
|
||||
{ id: uid("mod"), type: "notepad", title: "Notes rapides" },
|
||||
{ id: uid("mod"), type: "checklist" }
|
||||
{ id: notepadModuleId, type: "notepad", title: creationTemplate?.notepad?.title || "Bienvenue" },
|
||||
{ id: linksModuleId, type: "links", title: creationTemplate?.links?.moduleTitle || "Liens utiles" }
|
||||
]
|
||||
};
|
||||
if (options.tutorial === true) toolbox.tutorial = true;
|
||||
if (!store.setToolboxes([toolbox, ...store.toolboxes])) return null;
|
||||
store.updateModuleData(toolbox.id, notepadModuleId, { html: welcomeHtml, text: welcomeText, updatedAt: now }, "notepad");
|
||||
store.updateModuleData(toolbox.id, linksModuleId, { links: [libraryLink] }, "links");
|
||||
if (gameId) store.setLinks({ ...store.links, [gameId]: toolbox.id });
|
||||
return toolbox;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue