- refactor state structure to include app, layout, navigation, owner, program, session, and user - add slices for managing program, session, owner, user, layout, navigation, and app states - create context provider for global state with automatic fetching and throttling - implement persistence using IndexedDB with localStorage fallback - add comprehensive README documentation for usage and API
412 lines
12 KiB
TypeScript
412 lines
12 KiB
TypeScript
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
|
|
import { Button, Card, Group, Stack, Switch, Text, TextInput, Title } from '@mantine/core';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
import {
|
|
GlobalStateStore,
|
|
GlobalStateStoreProvider,
|
|
useGlobalStateStore,
|
|
useGlobalStateStoreContext,
|
|
} from './';
|
|
|
|
// Basic State Display Component
|
|
const StateDisplay = () => {
|
|
const state = useGlobalStateStore();
|
|
|
|
return (
|
|
<Card>
|
|
<Stack gap="sm">
|
|
<Title order={3}>Current State</Title>
|
|
|
|
<div>
|
|
<Text fw={700}>Program:</Text>
|
|
<Text size="sm">Name: {state.program.name || '(empty)'}</Text>
|
|
<Text size="sm">Slug: {state.program.slug || '(empty)'}</Text>
|
|
</div>
|
|
|
|
<div>
|
|
<Text fw={700}>Session:</Text>
|
|
<Text size="sm">API URL: {state.session.apiURL || '(empty)'}</Text>
|
|
<Text size="sm">Connected: {state.session.connected ? 'Yes' : 'No'}</Text>
|
|
<Text size="sm">Auth Token: {state.session.authToken || '(empty)'}</Text>
|
|
</div>
|
|
|
|
<div>
|
|
<Text fw={700}>Owner:</Text>
|
|
<Text size="sm">Name: {state.owner.name || '(empty)'}</Text>
|
|
<Text size="sm">ID: {state.owner.id}</Text>
|
|
<Text size="sm">Theme: {state.owner.theme?.name || 'none'}</Text>
|
|
<Text size="sm">Dark Mode: {state.owner.theme?.darkMode ? 'Yes' : 'No'}</Text>
|
|
</div>
|
|
|
|
<div>
|
|
<Text fw={700}>User:</Text>
|
|
<Text size="sm">Username: {state.user.username || '(empty)'}</Text>
|
|
<Text size="sm">Email: {state.user.email || '(empty)'}</Text>
|
|
<Text size="sm">Theme: {state.user.theme?.name || 'none'}</Text>
|
|
<Text size="sm">Dark Mode: {state.user.theme?.darkMode ? 'Yes' : 'No'}</Text>
|
|
</div>
|
|
|
|
<div>
|
|
<Text fw={700}>Layout:</Text>
|
|
<Text size="sm">Left Bar: {state.layout.leftBar.open ? 'Open' : 'Closed'}</Text>
|
|
<Text size="sm">Right Bar: {state.layout.rightBar.open ? 'Open' : 'Closed'}</Text>
|
|
<Text size="sm">Top Bar: {state.layout.topBar.open ? 'Open' : 'Closed'}</Text>
|
|
<Text size="sm">Bottom Bar: {state.layout.bottomBar.open ? 'Open' : 'Closed'}</Text>
|
|
</div>
|
|
</Stack>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
// Interactive Controls Component
|
|
const InteractiveControls = () => {
|
|
const state = useGlobalStateStore();
|
|
const [programName, setProgramName] = useState('');
|
|
const [username, setUsername] = useState('');
|
|
const [email, setEmail] = useState('');
|
|
|
|
return (
|
|
<Card>
|
|
<Stack gap="md">
|
|
<Title order={3}>Controls</Title>
|
|
|
|
<div>
|
|
<Text fw={700} mb="xs">Program</Text>
|
|
<Group>
|
|
<TextInput
|
|
onChange={(e) => setProgramName(e.currentTarget.value)}
|
|
placeholder="Program name"
|
|
value={programName}
|
|
/>
|
|
<Button onClick={() => state.setProgram({ name: programName })}>
|
|
Set Program Name
|
|
</Button>
|
|
</Group>
|
|
</div>
|
|
|
|
<div>
|
|
<Text fw={700} mb="xs">User</Text>
|
|
<Stack gap="xs">
|
|
<Group>
|
|
<TextInput
|
|
onChange={(e) => setUsername(e.currentTarget.value)}
|
|
placeholder="Username"
|
|
value={username}
|
|
/>
|
|
<TextInput
|
|
onChange={(e) => setEmail(e.currentTarget.value)}
|
|
placeholder="Email"
|
|
value={email}
|
|
/>
|
|
<Button onClick={() => state.setUser({ email, username })}>
|
|
Set User Info
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</div>
|
|
|
|
<div>
|
|
<Text fw={700} mb="xs">Theme</Text>
|
|
<Group>
|
|
<Switch
|
|
checked={state.user.theme?.darkMode || false}
|
|
label="User Dark Mode"
|
|
onChange={(e) =>
|
|
state.setUser({
|
|
theme: { ...state.user.theme, darkMode: e.currentTarget.checked },
|
|
})
|
|
}
|
|
/>
|
|
<Switch
|
|
checked={state.owner.theme?.darkMode || false}
|
|
label="Owner Dark Mode"
|
|
onChange={(e) =>
|
|
state.setOwner({
|
|
theme: { ...state.owner.theme, darkMode: e.currentTarget.checked },
|
|
})
|
|
}
|
|
/>
|
|
</Group>
|
|
</div>
|
|
|
|
<div>
|
|
<Text fw={700} mb="xs">Layout</Text>
|
|
<Group>
|
|
<Switch
|
|
checked={state.layout.leftBar.open}
|
|
label="Left Bar"
|
|
onChange={(e) => state.setLeftBar({ open: e.currentTarget.checked })}
|
|
/>
|
|
<Switch
|
|
checked={state.layout.rightBar.open}
|
|
label="Right Bar"
|
|
onChange={(e) => state.setRightBar({ open: e.currentTarget.checked })}
|
|
/>
|
|
<Switch
|
|
checked={state.layout.topBar.open}
|
|
label="Top Bar"
|
|
onChange={(e) => state.setTopBar({ open: e.currentTarget.checked })}
|
|
/>
|
|
<Switch
|
|
checked={state.layout.bottomBar.open}
|
|
label="Bottom Bar"
|
|
onChange={(e) => state.setBottomBar({ open: e.currentTarget.checked })}
|
|
/>
|
|
</Group>
|
|
</div>
|
|
|
|
<div>
|
|
<Text fw={700} mb="xs">Actions</Text>
|
|
<Group>
|
|
<Button
|
|
color="red"
|
|
onClick={() => {
|
|
state.setProgram({ name: '', slug: '' });
|
|
state.setUser({ email: '', username: '' });
|
|
state.setOwner({ id: 0, name: '' });
|
|
}}
|
|
>
|
|
Reset State
|
|
</Button>
|
|
</Group>
|
|
</div>
|
|
</Stack>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
// Provider Context Example
|
|
const ProviderExample = () => {
|
|
const { refetch } = useGlobalStateStoreContext();
|
|
const state = useGlobalStateStore();
|
|
|
|
return (
|
|
<Card>
|
|
<Stack gap="md">
|
|
<Title order={3}>Provider Context</Title>
|
|
<Text>API URL: {state.session.apiURL}</Text>
|
|
<Text>Loading: {state.session.loading ? 'Yes' : 'No'}</Text>
|
|
<Text>Connected: {state.session.connected ? 'Yes' : 'No'}</Text>
|
|
<Button onClick={refetch}>Refetch Data</Button>
|
|
</Stack>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
// Main Story Component
|
|
const BasicStory = () => {
|
|
useEffect(() => {
|
|
// Set initial state for demo
|
|
GlobalStateStore.getState().setProgram({
|
|
description: 'A demonstration application',
|
|
name: 'Demo App',
|
|
slug: 'demo-app',
|
|
});
|
|
GlobalStateStore.getState().setOwner({
|
|
id: 1,
|
|
name: 'Demo Organization',
|
|
theme: { darkMode: false, name: 'light' },
|
|
});
|
|
GlobalStateStore.getState().setUser({
|
|
email: 'demo@example.com',
|
|
theme: { darkMode: false, name: 'light' },
|
|
username: 'demo-user',
|
|
});
|
|
}, []);
|
|
|
|
return (
|
|
<Stack gap="lg" h={"100%"}>
|
|
<StateDisplay />
|
|
<InteractiveControls />
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
// Provider Story Component
|
|
const ProviderStory = () => {
|
|
return (
|
|
<GlobalStateStoreProvider
|
|
apiURL="https://api.example.com"
|
|
fetchOnMount={false}
|
|
throttleMs={1000}
|
|
>
|
|
<Stack gap="lg" h={"100%"}>
|
|
<StateDisplay />
|
|
<ProviderExample />
|
|
<InteractiveControls />
|
|
</Stack>
|
|
</GlobalStateStoreProvider>
|
|
);
|
|
};
|
|
|
|
// Layout Controls Story
|
|
const LayoutStory = () => {
|
|
const state = useGlobalStateStore();
|
|
|
|
return (
|
|
<Stack gap="lg" h={"100%"}>
|
|
<Card>
|
|
<Title order={3}>Layout Controls</Title>
|
|
<Stack gap="md" mt="md">
|
|
<Group>
|
|
<Stack gap="xs" style={{ flex: 1 }}>
|
|
<Text fw={700}>Left Sidebar</Text>
|
|
<Switch
|
|
checked={state.layout.leftBar.open}
|
|
label="Open"
|
|
onChange={(e) => state.setLeftBar({ open: e.currentTarget.checked })}
|
|
/>
|
|
<Switch
|
|
checked={state.layout.leftBar.pinned || false}
|
|
label="Pinned"
|
|
onChange={(e) => state.setLeftBar({ pinned: e.currentTarget.checked })}
|
|
/>
|
|
<Switch
|
|
checked={state.layout.leftBar.collapsed || false}
|
|
label="Collapsed"
|
|
onChange={(e) => state.setLeftBar({ collapsed: e.currentTarget.checked })}
|
|
/>
|
|
<TextInput
|
|
label="Size"
|
|
onChange={(e) =>
|
|
state.setLeftBar({ size: parseInt(e.currentTarget.value) || 0 })
|
|
}
|
|
type="number"
|
|
value={state.layout.leftBar.size || 0}
|
|
/>
|
|
</Stack>
|
|
|
|
<Stack gap="xs" style={{ flex: 1 }}>
|
|
<Text fw={700}>Right Sidebar</Text>
|
|
<Switch
|
|
checked={state.layout.rightBar.open}
|
|
label="Open"
|
|
onChange={(e) => state.setRightBar({ open: e.currentTarget.checked })}
|
|
/>
|
|
<Switch
|
|
checked={state.layout.rightBar.pinned || false}
|
|
label="Pinned"
|
|
onChange={(e) => state.setRightBar({ pinned: e.currentTarget.checked })}
|
|
/>
|
|
</Stack>
|
|
</Group>
|
|
</Stack>
|
|
</Card>
|
|
<StateDisplay />
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
// Theme Story
|
|
const ThemeStory = () => {
|
|
const state = useGlobalStateStore();
|
|
|
|
useEffect(() => {
|
|
GlobalStateStore.getState().setOwner({
|
|
id: 1,
|
|
name: 'Acme Corp',
|
|
theme: { darkMode: false, name: 'corporate' },
|
|
});
|
|
}, []);
|
|
|
|
return (
|
|
<Stack gap="lg" h={"100%"}>
|
|
<Card>
|
|
<Title order={3}>Theme Settings</Title>
|
|
<Stack gap="md" mt="md">
|
|
<div>
|
|
<Text fw={700} mb="xs">Owner Theme (Organization Default)</Text>
|
|
<Group>
|
|
<TextInput
|
|
label="Theme Name"
|
|
onChange={(e) =>
|
|
state.setOwner({
|
|
theme: { ...state.owner.theme, name: e.currentTarget.value },
|
|
})
|
|
}
|
|
value={state.owner.theme?.name || ''}
|
|
/>
|
|
<Switch
|
|
checked={state.owner.theme?.darkMode || false}
|
|
label="Dark Mode"
|
|
onChange={(e) =>
|
|
state.setOwner({
|
|
theme: { ...state.owner.theme, darkMode: e.currentTarget.checked },
|
|
})
|
|
}
|
|
/>
|
|
</Group>
|
|
</div>
|
|
|
|
<div>
|
|
<Text fw={700} mb="xs">User Theme (Personal Override)</Text>
|
|
<Group>
|
|
<TextInput
|
|
label="Theme Name"
|
|
onChange={(e) =>
|
|
state.setUser({
|
|
theme: { ...state.user.theme, name: e.currentTarget.value },
|
|
})
|
|
}
|
|
value={state.user.theme?.name || ''}
|
|
/>
|
|
<Switch
|
|
checked={state.user.theme?.darkMode || false}
|
|
label="Dark Mode"
|
|
onChange={(e) =>
|
|
state.setUser({
|
|
theme: { ...state.user.theme, darkMode: e.currentTarget.checked },
|
|
})
|
|
}
|
|
/>
|
|
</Group>
|
|
</div>
|
|
|
|
<div>
|
|
<Text fw={700} mb="xs">Effective Theme</Text>
|
|
<Text>
|
|
Name: {state.user.theme?.name || state.owner.theme?.name || 'default'}
|
|
</Text>
|
|
<Text>
|
|
Dark Mode:{' '}
|
|
{(state.user.theme?.darkMode ?? state.owner.theme?.darkMode) ? 'Yes' : 'No'}
|
|
</Text>
|
|
</div>
|
|
</Stack>
|
|
</Card>
|
|
<StateDisplay />
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
const meta = {
|
|
component: BasicStory,
|
|
parameters: {
|
|
layout: 'fullscreen',
|
|
},
|
|
tags: ['autodocs'],
|
|
title: 'State/GlobalStateStore',
|
|
} satisfies Meta<typeof BasicStory>;
|
|
|
|
export default meta;
|
|
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
export const Basic: Story = {
|
|
render: () => <BasicStory />,
|
|
};
|
|
|
|
export const WithProvider: Story = {
|
|
render: () => <ProviderStory />,
|
|
};
|
|
|
|
export const LayoutControls: Story = {
|
|
render: () => <LayoutStory />,
|
|
};
|
|
|
|
export const ThemeControls: Story = {
|
|
render: () => <ThemeStory />,
|
|
};
|