feat(globalStateStore): implement global state management with persistence
- 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
This commit is contained in:
411
src/GlobalStateStore/GlobalStateStore.stories.tsx
Normal file
411
src/GlobalStateStore/GlobalStateStore.stories.tsx
Normal file
@@ -0,0 +1,411 @@
|
||||
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 />,
|
||||
};
|
||||
Reference in New Issue
Block a user