Skip to content

Commit

Permalink
refactor(state-playground): add state for measure percentage of resiz…
Browse files Browse the repository at this point in the history
…able and store them
  • Loading branch information
jerensl committed Jul 19, 2024
1 parent 0a603ff commit 0281914
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 125 deletions.
122 changes: 3 additions & 119 deletions modelina-website/src/components/contexts/PlaygroundLayoutContext.tsx
Original file line number Diff line number Diff line change
@@ -1,133 +1,17 @@
'use client';

import { useMeasure } from '@uidotdev/usehooks';
import type { Draft, Immutable } from 'immer';
import { enableMapSet, produce } from 'immer';
import { createContext, useContext, useEffect, useReducer } from 'react';
import { IoOptionsOutline } from 'react-icons/io5';
import { LuFileInput, LuFileOutput } from 'react-icons/lu';
import { VscListSelection } from 'react-icons/vsc';

interface ISidebarItem {
name: string;
title: string;
isOpen: boolean;
devices: 'mobile' | 'tablet' | 'desktop' | 'all' ;
icon: React.ReactNode;
tooltip: string;
}

const sidebarItems: Map<string, ISidebarItem> = new Map([
[
'input-editor', {
name: 'input-editor',
title: 'Input Editor',
isOpen: true,
devices: 'mobile',
icon: <LuFileInput className='size-5' />,
tooltip: 'Show Input Editor'
}
],
[
'output-editor', {
name: 'output-editor',
title: 'Output Editor',
isOpen: false,
devices: 'mobile',
icon: <LuFileOutput className='size-5' />,
tooltip: 'Show Output Editor'
}
],
[
'general-options', {
name: 'general-options',
title: 'Options',
isOpen: true,
devices: 'all',
icon: <IoOptionsOutline className='size-5' />,
tooltip: 'Show or hide all the options'
}
],
[
'output-options', {
name: 'output-options',
title: 'Output',
isOpen: false,
devices: 'all',
icon: <VscListSelection className='size-5' />,
tooltip: 'Show or hide the list of output models'
}
]
]);

type IDeviceType = 'mobile' | 'tablet' | 'notebook' | 'desktop';
import type { Dispatch, State } from '@/store/useLayoutStore';
import { initialState, playgroundLayoutReducer } from '@/store/useLayoutStore';

type ActionType = { type: 'open-option', name: string } | { type: 'update-device', payload: IDeviceType } | { type: '' };
type Dispatch = (action: ActionType) => void;
type State = Immutable<{ open: string, device: IDeviceType, sidebarItems: typeof sidebarItems }>;
type PlaygroundtProviderProps = { children: React.ReactNode };

const initialState: State = { open: 'general-options', device: 'desktop', sidebarItems };

const PlaygroundtLayoutContext = createContext<
{ state: State; dispatch: Dispatch } | undefined
>(undefined);

const playgroundLayoutReducer = produce<State, [ActionType]>((draft: Draft<State>, action) => {
enableMapSet();

switch (action.type) {
case 'update-device': {
// eslint-disable-next-line no-param-reassign
draft.device = action.payload;
break;
}
case 'open-option': {
const findOpts = draft.sidebarItems.get(action.name);

const alwaysOpen = ['input-editor', 'output-editor', 'general-options'];
const isMobile = draft.device === 'mobile';

if (!findOpts) {
break;
}

if (alwaysOpen.includes(action.name) && isMobile) {
if (draft.open === action.name) {
// eslint-disable-next-line no-param-reassign
draft.open = findOpts.name ?? draft.open;
break;
}
}

draft.sidebarItems.set(action.name, {
...findOpts,
isOpen: !findOpts.isOpen
});

if (draft.open !== findOpts.name) {
const findOne = draft.sidebarItems.get(draft.open);

if (findOne) {
draft.sidebarItems.set(draft.open, {
...findOne,
isOpen: false
}
);
}
}

// eslint-disable-next-line no-param-reassign
draft.open = findOpts.name ?? draft.open;

break;
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
});

/**
* This component to consume Plaground Layout State.
*
Expand Down Expand Up @@ -170,7 +54,7 @@ function PlaygroundLayoutProvider({ children }: PlaygroundtProviderProps) {
}, [width]);

return (
<PlaygroundtLayoutContext.Provider value={value}>
<PlaygroundtLayoutContext.Provider value={value}>
<div ref={ref}>
{children}
</div>
Expand Down
27 changes: 23 additions & 4 deletions modelina-website/src/components/playground/Resizable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useMeasure } from '@uidotdev/usehooks';
import { motion, useMotionValue, useTransform } from 'framer-motion';
import { memo, type ReactNode, useEffect } from 'react';

import { usePlaygroundLayout } from '../contexts/PlaygroundLayoutContext';

interface ResizableComponentProps {
leftComponent?: ReactNode;
rightComponent?: ReactNode;
Expand All @@ -15,20 +17,37 @@ interface ResizableComponentProps {
* @property {React.ReactElement} rightComponent The right element which will be stretch.
*/
function Resizable({ leftComponent, rightComponent }: ResizableComponentProps) {
const { state, dispatch } = usePlaygroundLayout();

const [ref, { width: containerWidth }] = useMeasure();
const DefaultWidth = 640;

const dragableX = useMotionValue(DefaultWidth);
const width = useTransform(dragableX, (value) => `${value + 0.5 * 4}px`);
const width = useTransform(dragableX, (value) => {
if (containerWidth !== null) {
const visibleView = value / containerWidth;

dispatch({ type: 'resizable-size', total: visibleView });
}

return `${value + 0.5 * 4}px`;
});

useEffect(() => {
if (containerWidth !== null && containerWidth >= 640) {
dragableX.set(Math.round(containerWidth / 2));
if (containerWidth !== null) {
dragableX.set(Math.round(containerWidth * state.editorSize));
}
}, [containerWidth]);

if (state.device === 'mobile') {
return <section className='grid size-full'>
{leftComponent}
{rightComponent}
</section>;
}

return (
<section ref={ref} className='grid size-full bg-code-editor-dark md:grid-cols-[auto_auto]'>
<section ref={ref} className='grid size-full bg-code-editor-dark md:grid-cols-[auto_auto]'>
<motion.article
style={{ width }}
>
Expand Down
4 changes: 2 additions & 2 deletions modelina-website/src/components/playground/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ export const Sidebar: React.FunctionComponent = () => {
<button
title={item.title}
onClick={() => handleClick({ name: item.name })}
className={clsx('border-box flex p-2 text-sm focus:outline-none', {
className={clsx('border-box flex p-2 text-sm focus:outline-none disabled:opacity-25', {
'md:hidden': item.devices === 'mobile'
})}
disabled={item.name === 'output-options' && state.open === 'general-options' && state.device === 'mobile'}
type='button'
>
<div
className={item.isOpen ? 'rounded bg-gray-900 p-2 text-white' : 'p-2 text-gray-700 hover:text-white'}
className={item.isOpen ? 'rounded bg-slate-200/25 p-2 text-white' : 'p-2 text-gray-700 hover:text-white'}
>
{item.icon}
</div>
Expand Down
153 changes: 153 additions & 0 deletions modelina-website/src/store/useLayoutStore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type { Draft, Immutable } from 'immer';
import { enableMapSet, produce } from 'immer';
import { IoOptionsOutline } from 'react-icons/io5';
import { LuFileInput, LuFileOutput } from 'react-icons/lu';
import { VscListSelection } from 'react-icons/vsc';

interface ISidebarItem {
name: string;
title: string;
isOpen: boolean;
devices: 'mobile' | 'tablet' | 'desktop' | 'all' ;
icon: React.ReactNode;
tooltip: string;
}

const sidebarItems: Map<string, ISidebarItem> = new Map([
[
'input-editor', {
name: 'input-editor',
title: 'Input Editor',
isOpen: false,
devices: 'mobile',
icon: <LuFileInput className='size-5' />,
tooltip: 'Show Input Editor'
}
],
[
'output-editor', {
name: 'output-editor',
title: 'Output Editor',
isOpen: false,
devices: 'mobile',
icon: <LuFileOutput className='size-5' />,
tooltip: 'Show Output Editor'
}
],
[
'general-options', {
name: 'general-options',
title: 'Options',
isOpen: true,
devices: 'all',
icon: <IoOptionsOutline className='size-5' />,
tooltip: 'Show or hide all the options'
}
],
[
'output-options', {
name: 'output-options',
title: 'Output',
isOpen: false,
devices: 'all',
icon: <VscListSelection className='size-5' />,
tooltip: 'Show or hide the list of output models'
}
]
]);

type IDeviceType = 'mobile' | 'tablet' | 'notebook' | 'desktop';

export type ActionType = { type: 'open-option', name: string } | { type: 'update-device', payload: IDeviceType } | { type: 'resizable-size', total: number } | { type: '' };
export type Dispatch = (action: ActionType) => void;
export type State = Immutable<{ open: string, editorSize: number, device: IDeviceType, sidebarItems: typeof sidebarItems }>;

export const initialState: State = { open: 'general-options', device: 'desktop', editorSize: 0.5, sidebarItems };

export const playgroundLayoutReducer = produce<State, [ActionType]>((draft: Draft<State>, action) => {
enableMapSet();

switch (action.type) {
case 'update-device': {
// eslint-disable-next-line no-param-reassign
draft.device = action.payload;
break;
}
case 'resizable-size': {
// eslint-disable-next-line no-param-reassign
draft.editorSize = action.total;
break;
}
case 'open-option': {
const findOpts = draft.sidebarItems.get(action.name);

const alwaysOpen = ['input-editor', 'output-editor', 'general-options'];
const isMobile = draft.device === 'mobile';

if (!findOpts) {
break;
}

if (isMobile && alwaysOpen.includes(action.name)) {
if (draft.open === action.name) {
// eslint-disable-next-line no-param-reassign
draft.open = findOpts.name ?? draft.open;
break;
}
}

draft.sidebarItems.set(action.name, {
...findOpts,
isOpen: !findOpts.isOpen
});

if (isMobile && action.name === 'output-options' && !findOpts.isOpen) {
// eslint-disable-next-line no-param-reassign
draft.open = findOpts.name ?? draft.open;
break;
}

if (isMobile && action.name === 'output-options' && findOpts.isOpen) {
draft.sidebarItems.forEach((value, key) => {
if (value.isOpen) {
// eslint-disable-next-line no-param-reassign
draft.open = key;
}
});
break;
}

if (draft.open !== findOpts.name) {
const findOne = draft.sidebarItems.get(draft.open);

if (findOne) {
draft.sidebarItems.set(draft.open, {
...findOne,
isOpen: false
}
);
}

if (isMobile && draft.open === 'output-options') {
draft.sidebarItems.forEach((value, key) => {
if (value.isOpen && key !== action.name) {
draft.sidebarItems.set(key, {
...value,
isOpen: false
}
);
}
});
}
}

// eslint-disable-next-line no-param-reassign
draft.open = findOpts.name ?? draft.open;

break;
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
});

0 comments on commit 0281914

Please sign in to comment.