Skip to content

Commit

Permalink
refactor: no more global store on nextjs :v
Browse files Browse the repository at this point in the history
  • Loading branch information
matyson committed Mar 27, 2024
1 parent 00dc15c commit b301188
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 188 deletions.
7 changes: 5 additions & 2 deletions apps/pianno/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';

import { Toaster } from '@/components/ui/sonner';
import { StoreProvider } from '@/providers/store';
import { GeistSans } from 'geist/font';

import './globals.css';
Expand All @@ -18,8 +19,10 @@ export default function RootLayout({
return (
<html lang="en">
<body className={GeistSans.className}>
{children}
<Toaster richColors />
<StoreProvider>
{children}
<Toaster richColors />
</StoreProvider>
</body>
</html>
);
Expand Down
43 changes: 34 additions & 9 deletions apps/pianno/components/canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import {
useStoreActions,
useStoreImg,
useStoreLabel,
useStoreToggled,
useStoreViewport,
} from '@/hooks/use-store';
import { useWindowSize } from '@/hooks/use-window-size';
import { StoreContext } from '@/providers/store';
import { ColorMapFilter } from '@pixi/filter-color-map';
import {
Container,
Expand Down Expand Up @@ -153,18 +153,43 @@ const Canvas = () => {
</div>
{src !== '#' && <Sidebar />}
<Help />
<Stage
height={height}
options={{
backgroundAlpha: 0,
eventMode: 'static',
}}
width={width}
<StoreContextBridge
render={(children) => (
<Stage
height={height}
options={{
backgroundAlpha: 0,
eventMode: 'static',
}}
width={width}
>
{children}
</Stage>
)}
>
<CanvasWrapper setPos={setPos} />
</Stage>
</StoreContextBridge>
</div>
);
};

type ContextBridgeProps = {
children: React.ReactNode;
render: (children: React.ReactNode) => React.ReactNode;
};

const StoreContextBridge = ({ children, render }: ContextBridgeProps) => {
return (
<StoreContext.Consumer>
{(value) =>
render(
<StoreContext.Provider value={value}>
{children}
</StoreContext.Provider>,
)
}
</StoreContext.Consumer>
);
};

export default Canvas;
3 changes: 2 additions & 1 deletion apps/pianno/components/toolbar/clear-canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { useStoreActions, useTemporalStore } from '@/hooks/use-store';
import { useStoreActions } from '@/hooks/use-store';
import { useTemporalStore } from '@/providers/store';
import { Trash2Icon } from 'lucide-react';
import { FC } from 'react';

Expand Down
4 changes: 1 addition & 3 deletions apps/pianno/components/toolbar/open.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import {
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { useStoreImageMetadata, useTemporalStore } from '@/hooks/use-store';
import { useStoreActions } from '@/hooks/use-store';
import { useStoreActions, useStoreImageMetadata } from '@/hooks/use-store';
import { RAW_DATA_TYPES } from '@/lib/constants';
import { openImageSchema } from '@/lib/types';
import { fileOpen } from 'browser-fs-access';
Expand All @@ -38,7 +37,6 @@ interface OpenImageDialogProps {}
const OpenImageDialog: FC<OpenImageDialogProps> = ({}) => {
const [open, setOpen] = useState(false);
const [checked, setChecked] = useState(false);
const { clear } = useTemporalStore((state) => state);
const formRef = useRef<HTMLFormElement>(null);
const imgMetadata = useStoreImageMetadata();

Expand Down
3 changes: 2 additions & 1 deletion apps/pianno/components/toolbar/reset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { useStoreActions, useTemporalStore } from '@/hooks/use-store';
import { useStoreActions } from '@/hooks/use-store';
import { useWindowSize } from '@/hooks/use-window-size';
import { useTemporalStore } from '@/providers/store';
import { RotateCcwIcon } from 'lucide-react';
import { FC } from 'react';

Expand Down
2 changes: 1 addition & 1 deletion apps/pianno/components/toolbar/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
useStoreImageMetadata,
useStoreImg,
} from '@/hooks/use-store';
import { useTemporalStore } from '@/hooks/use-store';
import { useWindowSize } from '@/hooks/use-window-size';
import { useTemporalStore } from '@/providers/store';
import { type VariantProps } from 'class-variance-authority';
import {
AlertTriangleIcon,
Expand Down
172 changes: 1 addition & 171 deletions apps/pianno/hooks/use-store.tsx
Original file line number Diff line number Diff line change
@@ -1,175 +1,5 @@
'use client';

import type { Brush } from '@/lib/types';

import { annotationColorPallete } from '@/lib/constants';
import { type Viewport } from 'pixi-viewport';
import { type TemporalState, temporal } from 'zundo';
import { create, useStore as useZustandStore } from 'zustand';
import { persist } from 'zustand/middleware';
import { shallow } from 'zustand/shallow';

type Image = {
height: number;
src: string;
width: number;
};

type ImageMetadata = {
name: string;
size: number;
type: string;
};

type State = {
brush: Brush;
colors: typeof annotationColorPallete;
currentColor: string;
img: Image;
imgMetadata: ImageMetadata;
label: string; // dataUrl
toggled: boolean;
viewport: Viewport | null;
};

type Actions = {
recenterViewport: (width: number, height: number) => void;
reset: () => void;
setBrushMode: (mode: Brush['mode']) => void;
setBrushSize: (size: number) => void;
setColor: (color: string) => void;
setImage: (img: Image) => void;
setImageMetadata: (metadata: ImageMetadata) => void;
setLabel: (label: string) => void;
setNewColor: (
color: string,
colorLabel: keyof typeof annotationColorPallete,
) => void;
setViewport: (viewport: Viewport) => void;
softReset: () => void;
toggle: () => void;
};

type Store = State & {
actions: Actions;
};

const initialState: State = {
brush: {
eraserSize: 5,
maxSize: 10,
mode: 'pen',
penSize: 1,
},
colors: annotationColorPallete,
currentColor: annotationColorPallete.euclidean[0],
img: {
height: 0,
src: '#',
width: 0,
},
imgMetadata: {
name: '',
size: 0,
type: '',
},
label: 'data:image/png;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=', // 1x1 transparent png
toggled: true,
viewport: null,
};

const useStore = create<Store>()(
persist(
temporal(
(set, get) => ({
...initialState,
actions: {
recenterViewport: (width, height) => {
const viewport = get().viewport;
viewport?.moveCenter(width / 2, height / 2);
viewport?.fit(true, width, height);
},
reset: () => {
set(initialState);
// force reload to reset viewport
window.location.reload();
},
setBrushMode: (mode) => {
const brush = get().brush;
set({ brush: { ...brush, mode } });
},
setBrushSize: (brushSize) => {
const brush = get().brush;
if (brush.mode === 'eraser') {
set({ brush: { ...brush, eraserSize: brushSize } });
} else if (brush.mode === 'pen') {
set({ brush: { ...brush, penSize: brushSize } });
}
},
setColor: (color) => set({ currentColor: color }),
setImage: (img) => {
set({ img });
const { height, width } = img;
if (height && width) {
const maxSize = Math.floor(Math.max(height, width) * 0.04);
const eraserSize = Math.floor(maxSize / 2);
const brush = get().brush;
set({ brush: { ...brush, eraserSize, maxSize } });
}
},
setImageMetadata: (metadata) => set({ imgMetadata: metadata }),
setLabel: (label) => set({ label }),
setNewColor: (color, colorLabel) => {
const colors = get().colors;
// check if the color already exists in one of the color arrays
const colorExists = Object.values(colors).some((colorArr) =>
colorArr.includes(color),
);
// if it does, don't add it
if (colorExists) return;
const newColors = {
...colors,
[colorLabel]: [...colors[colorLabel], color],
};
set({ colors: newColors });
set({ currentColor: color });
},
setViewport: (viewport) => set({ viewport }),
softReset: () => {
// reset label and colors
set({
colors: initialState.colors,
currentColor: initialState.currentColor,
label: initialState.label,
});
},
toggle: () => set((state) => ({ toggled: !state.toggled })),
},
}),
{
equality: shallow,
partialize: (state) => ({
label: state.label,
}),
},
),
{
name: 'store',
partialize: (state) => ({
brush: state.brush,
colors: state.colors,
currentColor: state.currentColor,
imgMetadata: state.imgMetadata,
// img: state.img,
label: state.label,
}),
},
),
);

export const useTemporalStore = <T,>(
selector: (state: TemporalState<Partial<State>>) => T,
) => useZustandStore(useStore.temporal, selector);
import { useStore } from '@/providers/store';

export const useStoreViewport = () => useStore((state) => state.viewport);
export const useStoreColors = () => useStore((state) => state.colors);
Expand Down
40 changes: 40 additions & 0 deletions apps/pianno/providers/store.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client';

import { type State, type Store, createStore } from '@/store';
import { type ReactNode, createContext, useContext, useRef } from 'react';
import { type TemporalState } from 'zundo';
import { useStore as useZustandStore } from 'zustand';

export const StoreContext = createContext<ReturnType<
typeof createStore
> | null>(null);

export function StoreProvider({ children }: { children: ReactNode }) {
const storeRef = useRef<ReturnType<typeof createStore>>();
if (!storeRef.current) {
storeRef.current = createStore();
}
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
);
}

export const useStore = <T,>(selector: (state: Store) => T): T => {
const store = useContext(StoreContext);
if (!store) {
throw new Error('useStore must be used within a StoreProvider');
}
return useZustandStore(store, selector);
};

export const useTemporalStore = <T,>(
selector: (state: TemporalState<Partial<State>>) => T,
): T => {
const store = useContext(StoreContext);
if (!store) {
throw new Error('useTemporalStore must be used within a StoreProvider');
}
return useZustandStore(store.temporal, selector);
};
Loading

0 comments on commit b301188

Please sign in to comment.