Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat sync store between tabs #40

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,29 @@ function Redirect({ to }: { to: string }) {
return null;
}

const useRehydrateOnTabFocus = () => {
const updateStore = () => {
useStore.persist.rehydrate();
};

useEffect(() => {
document.addEventListener('visibilitychange', updateStore);
window.addEventListener('focus', updateStore);
return () => {
document.removeEventListener('visibilitychange', updateStore);
window.removeEventListener('focus', updateStore);
};
}, []);
};

export default function App() {
useRehydrateOnTabFocus();

const hasRehydrated = useStore(
state => state.gameSave.hasRehydratedLocalData,
);

if (!hasRehydrated) {
if (!hasRehydrated || !useStore.persist.hasHydrated) {
console.log('Waiting for rehydration');
return (
<MantineProvider theme={theme} forceColorScheme="dark">
Expand Down
1 change: 0 additions & 1 deletion src/core/migrations/migratePersistedStoreFromRedux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export function migratePersistedStoreFromRedux() {
parsed.factories.present.factories?.map((f: any) => [f.id, f]) ?? [],
),
},
factoryView: parsed.factories.present.filter ?? {},
games: {
selected: migratedGameId,
games: {
Expand Down
4 changes: 2 additions & 2 deletions src/core/zustand-helpers/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ export function withActions<
}

export function createActions<
Actions extends Record<string, Action<RootState>>,
>(actions: Actions) {
State extends Record<string, any> = RootState
>(actions: Record<string, Action<State>>) {
return actions;
}

Expand Down
Empty file removed src/core/zustand-slices.ts
Empty file.
26 changes: 16 additions & 10 deletions src/core/zustand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,23 @@ const slices = withSlices(
gamesSlice,
gameSaveSlice,
factoriesSlice,
factoryViewSlice,
solversSlice,
chartsSlice,
);

const uiSlices = withSlices(factoryViewSlice, chartsSlice);

export type RootState = ReturnType<typeof slices>;
export type UiState = ReturnType<typeof uiSlices>;

const slicesWithActions = withActions(
slices,
gameFactoriesActions,
solverFactoriesActions,
gameRemoteActions,
factoryViewSortActions,
);

const uiSlicesWithActions = withActions(uiSlices, factoryViewSortActions);
3;
export const useStore = create(
devtools(
persist(slicesWithActions, {
Expand Down Expand Up @@ -92,18 +95,21 @@ export const useStore = create(
return removeMissingFactoriesInGames(state as any);
}

if (version === 3) {
logger.log('Migrating from version 3 to 4 [kanban]');
return migrateStoreWithPlan(storeMigrationV4, state as any, draft => {
draft.factoryView.viewMode = 'grid';
});
}

return state;
},
}),
),
);

export const useUiStore = create(
devtools(
persist(uiSlicesWithActions, {
name: 'zustand:persist:ui',
version: 1,
storage: createJSONStorage(() => sessionStorage),
}),
),
);

export const useShallowStore = <T>(selector: (state: RootState) => T) =>
useStore(useShallow(selector));
25 changes: 25 additions & 0 deletions src/factories/FactoriesListHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ReactNode } from 'react';
import { Group, Text } from '@mantine/core';

export const FactoriesListHeader = ({
factoriesShown,
factoriesTotal,
rightSide,
}: {
factoriesShown: number;
factoriesTotal: number;
rightSide: ReactNode;
}) => {
return (
<Group justify="space-between" align="center" h="36px">
<Text
size="xs"
fw="bold"
c={factoriesShown !== factoriesTotal ? 'orange' : 'dimmed'}
>
{factoriesShown} / {factoriesTotal} factories filtered
</Text>
{rightSide}
</Group>
);
};
40 changes: 40 additions & 0 deletions src/factories/FactoriesListSortBy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useUiStore } from '@/core/zustand';
import { Group, Select } from '@mantine/core';
import { IconArrowsDownUp } from '@tabler/icons-react';

const NO_ORDER = '__NO_ORDER';

export const FactoriesListSortBy = () => {
const { sortBy } = useUiStore(state => state.factoryView);

return (
<Select
size="xs"
variant="filled"
leftSection={
<IconArrowsDownUp
stroke={2}
size={14}
color="var(--mantine-color-white)"
/>
}
data={[
{
value: 'name',
label: 'Name',
},
{
value: NO_ORDER,
label: 'Unspecified',
},
]}
color={'white'}
value={sortBy ?? NO_ORDER}
onChange={value => {
useUiStore
.getState()
.sortFactoriesBy(value === NO_ORDER ? undefined : value);
}}
/>
);
};
80 changes: 67 additions & 13 deletions src/factories/FactoriesTab.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { Container, Divider, SimpleGrid, Space, Stack } from '@mantine/core';
import { useState } from 'react';
import {
Container,
Divider,
Group,
SimpleGrid,
Space,
Stack,
} from '@mantine/core';
import { useMemo, useState } from 'react';
import { useSession } from '@/auth/authSelectors';
// import { loadFromRemote } from '../auth/sync/loadFromRemote';
import { useNavigate } from 'react-router-dom';
import { useStore } from '@/core/zustand';
import { useStore, useUiStore } from '@/core/zustand';
import { useGameFactoriesIds } from '@/games/gamesSlice';
import { FactoryGridCard } from './list/FactoryGridCard';
import { FactoriesFiltersMenu } from './filters/FactoriesFiltersMenu';
import { useGameFactories } from '@/games/store/gameFactoriesSelectors';
import { FactoriesKanban } from '@/factories/list/FactoriesKanban';
import { FactoryRow } from '@/factories/list/FactoryRow';
import { FactoriesEmptyState } from '@/factories/list/FactoriesEmptyState';
import { sortBy } from 'lodash';
import { useIsFactoryVisible } from '@/factories/useIsFactoryVisible';
import { FactoriesListSortBy } from '@/factories/FactoriesListSortBy';
import { FactoriesListHeader } from '@/factories/FactoriesListHeader';
import { GameFactoriesExpandActionIcon } from '@/factories/components/expand/GameFactoriesExpandActionIcon';

export interface IFactoriesTabProps {}

Expand All @@ -19,10 +31,10 @@ export function FactoriesTab(_props: IFactoriesTabProps) {
const navigate = useNavigate();

const gameId = useStore(state => state.games.selected);
const viewMode = useStore(state => state.factoryView.viewMode ?? 'grid');
const viewMode = useUiStore(state => state.factoryView.viewMode ?? 'grid');
const viewSortBy = useUiStore(state => state.factoryView.sortBy);

const [loadingFactories, setLoadingFactories] = useState(false);

const hasFactories = useStore(
state =>
Object.keys(state.games.games).length > 0 &&
Expand All @@ -31,6 +43,16 @@ export function FactoriesTab(_props: IFactoriesTabProps) {
);
const factoriesIds = useGameFactoriesIds(gameId);
const factories = useGameFactories(gameId);
const isFactoryVisible = useIsFactoryVisible(true);

const factoryList = useMemo(
() =>
(typeof viewSortBy === 'string'
? sortBy(factories, [viewSortBy])
: factories
).filter(({ id }) => isFactoryVisible(id)),
[factories, viewSortBy, isFactoryVisible],
);

return (
<div>
Expand All @@ -40,22 +62,54 @@ export function FactoriesTab(_props: IFactoriesTabProps) {
{!hasFactories && <FactoriesEmptyState />}
{viewMode === 'spreadsheet' && (
<Stack gap="md">
{factoriesIds.map((factoryId, index) => (
<FactoryRow key={factoryId} id={factoryId} index={index} />
<FactoriesListHeader
factoriesShown={factoryList.length}
factoriesTotal={factories.length}
rightSide={
<Group>
<GameFactoriesExpandActionIcon />
<FactoriesListSortBy />
</Group>
}
/>
{factoryList.map(({ id }, index) => (
<FactoryRow key={id} id={id} index={index} />
))}
</Stack>
)}
{viewMode === 'grid' && (
<SimpleGrid spacing="lg" cols={3}>
{factoriesIds.map((factoryId, index) => (
<FactoryGridCard key={factoryId} id={factoryId} />
))}
</SimpleGrid>
<Stack gap="md">
<FactoriesListHeader
factoriesShown={factoryList.length}
factoriesTotal={factories.length}
rightSide={<FactoriesListSortBy />}
/>

<SimpleGrid spacing="lg" cols={3}>
{factoryList.map(({ id }, index) => (
<FactoryGridCard key={id} id={id} />
))}
</SimpleGrid>
</Stack>
)}
{!hasFactories && <Divider mb="lg" />}
</Container>

{viewMode === 'kanban' && <FactoriesKanban />}
{viewMode === 'kanban' && (
<Stack gap="md">
<Container size="lg" w="100%">
<FactoriesListHeader
factoriesShown={factoryList.length}
factoriesTotal={factories.length}
rightSide={<div></div>}
/>
</Container>{' '}
<FactoriesKanban
factories={factoryList}
disableCardDrag={factoryList.length !== factories.length}
/>
</Stack>
)}
<Space h={100} />
</div>
);
Expand Down
8 changes: 4 additions & 4 deletions src/factories/charts/store/chartsSlice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useStore } from '@/core/zustand';
import { useStore, useUiStore } from '@/core/zustand';
import { createSlice } from '@/core/zustand-helpers/slices';

export interface ChartsSlice {
Expand Down Expand Up @@ -30,16 +30,16 @@ export const chartsSlice = createSlice({
});

export function useChartsView() {
return useStore(state => state.charts.selected);
return useUiStore(state => state.charts.selected);
}

export function useChartsSettings() {
return useStore(state => state.charts.settings);
return useUiStore(state => state.charts.settings);
}

export function useChartSetting<K extends keyof ChartsSlice['settings']>(
key: K,
defaultValue?: ChartsSlice['settings'][K],
) {
return useStore(state => state.charts.settings[key] ?? defaultValue);
return useUiStore(state => state.charts.settings[key] ?? defaultValue);
}
6 changes: 4 additions & 2 deletions src/factories/components/expand/FactoryExpandActionIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ActionIcon, Tooltip } from '@mantine/core';
import {
IconArrowsDiagonal,
IconArrowsDiagonalMinimize2,
IconChevronDown,
IconChevronUp,
} from '@tabler/icons-react';
export interface IFactoryExpandActionIconProps {
isCollapsed: boolean;
Expand All @@ -19,9 +21,9 @@ export function FactoryExpandActionIcon(props: IFactoryExpandActionIconProps) {
onClick={() => useStore.getState().toggleGameFactoryExpanded(factoryId)}
>
{isCollapsed ? (
<IconArrowsDiagonal stroke={2} size={16} />
<IconChevronDown stroke={2} size={16} />
) : (
<IconArrowsDiagonalMinimize2 stroke={2} size={16} />
<IconChevronUp stroke={2} size={16} />
)}
</ActionIcon>
</Tooltip>
Expand Down
30 changes: 30 additions & 0 deletions src/factories/components/expand/GameFactoriesExpandActionIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useStore } from '@/core/zustand';
import { useGameFactoriesHasAnyCollapsed } from '@/games/gamesSlice';
import { Button } from '@mantine/core';
import { IconChevronsDown, IconChevronsUp } from '@tabler/icons-react';

export interface IGameFactoriesExpandActionIconProps {}

export function GameFactoriesExpandActionIcon(
props: IGameFactoriesExpandActionIconProps,
) {
const isCollapsed = useGameFactoriesHasAnyCollapsed();

return (
<Button
size="xs"
variant="subtle"
color="dimmed"
onClick={() => useStore.getState().toggleAllFactoriesExpanded()}
leftSection={
isCollapsed ? (
<IconChevronsDown stroke={2} size={16} />
) : (
<IconChevronsUp stroke={2} size={16} />
)
}
>
{isCollapsed ? 'Expand all' : 'Collapse all'}
</Button>
);
}
Loading
Loading