Created
October 30, 2024 19:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use client'; | |
export type Flag = keyof BetaFlagsValueProps; | |
export type Value = boolean | undefined; | |
export type BetaFlagsContextProps = { | |
toggleBetaFlag: (flag: Flag) => void; | |
setBetaFlag: (flag: Flag, value: boolean) => void; | |
magicToggle: () => void; | |
getBetaFlag: (flag: Flag) => boolean; | |
betaFlags: BetaFlagsValueProps; | |
}; | |
export type BetaFlagsValueProps = { | |
v2: Value; | |
}; | |
import { createContext, useContext } from 'react'; | |
export const BetaFlagsContext = createContext({} as BetaFlagsContextProps); | |
export const useBetaFlagsContext = () => { | |
const context = useContext(BetaFlagsContext); | |
if (!context) | |
throw new Error( | |
'useBetaFlagsContext must be use inside BetaFlagsContext.Provider' | |
); | |
return context; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use client'; | |
import { useEffect, useState } from 'react'; | |
import { | |
BetaFlagsContext, | |
BetaFlagsValueProps, | |
Flag, | |
} from './BetaFlagsContext'; | |
import { Switcher } from './Switcher'; | |
export type BetaFlagsProviderProps = { | |
children: React.ReactNode; | |
defaultValues?: BetaFlagsValueProps; | |
}; | |
declare global { | |
interface Window { | |
secretSauce?: { | |
toggleBetaFlag?: (flag: Flag) => void; | |
magicToggle?: () => void; | |
}; | |
} | |
} | |
const isProduction = process.env.NODE_ENV === 'production'; | |
const defaults: BetaFlagsValueProps = isProduction | |
? ['v2'].reduce( | |
(prev, current) => ({ ...prev, [current]: false }), | |
{} as BetaFlagsValueProps | |
) | |
: { | |
v2: process.env.NEXT_PUBLIC_V2 === 'enabled', | |
}; | |
export function BetaFlagsProvider({ children }: BetaFlagsProviderProps) { | |
const [betaFlags, setBetaFlags] = useState(defaults); | |
const [switcherOn, setSwitcherOn] = useState(!isProduction); | |
const toggleBetaFlag = (flag: Flag) => { | |
setBetaFlags(current => ({ ...current, [flag]: !current[flag] })); | |
}; | |
const setBetaFlag = (flag: Flag, value: boolean) => { | |
setBetaFlags(current => ({ ...current, [flag]: value })); | |
}; | |
const getBetaFlag = (flag: Flag) => { | |
return !!betaFlags[flag]; | |
}; | |
const magicToggle = () => setSwitcherOn(current => !current); | |
useEffect(() => { | |
if (typeof window !== 'undefined') { | |
window.secretSauce = { | |
toggleBetaFlag, | |
magicToggle, | |
}; | |
if (!isProduction) { | |
setSwitcherOn(true); | |
} | |
} | |
}, []); | |
return ( | |
<BetaFlagsContext.Provider | |
value={{ | |
toggleBetaFlag, | |
getBetaFlag, | |
betaFlags, | |
magicToggle, | |
setBetaFlag, | |
}} | |
> | |
{switcherOn && <Switcher />} | |
{children} | |
</BetaFlagsContext.Provider> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Close, DragIndicator } from '@mui/icons-material'; | |
import { | |
Box, | |
Card, | |
CardContent, | |
Collapse, | |
Fab, | |
IconButton, | |
Stack, | |
Zoom, | |
} from '@mui/material'; | |
import { MouseEventHandler, useRef } from 'react'; | |
type Props = { | |
children: React.ReactNode; | |
icon: React.ReactNode; | |
isOpen?: boolean; | |
onOpen?: () => void; | |
onClose?: () => void; | |
}; | |
export const ExpandableFab = ({ | |
children, | |
icon, | |
isOpen, | |
onOpen, | |
onClose, | |
}: Props) => { | |
const pointer = useRef({ x: 0, y: 0 }); | |
const onMouseUp: ( | |
val: 'open' | 'close' | |
) => MouseEventHandler<HTMLButtonElement> = val => e => { | |
const { x, y } = pointer.current; | |
if (Math.abs(e.clientX - x) < 3 && Math.abs(e.clientY - y) < 3) { | |
switch (val) { | |
case 'open': | |
onOpen?.(); | |
break; | |
case 'close': | |
onClose?.(); | |
break; | |
} | |
pointer.current = { x: 0, y: 0 }; | |
} | |
}; | |
const onMouseDown: MouseEventHandler<HTMLButtonElement> = e => { | |
pointer.current = { x: e.clientX, y: e.clientY }; | |
}; | |
return ( | |
<> | |
<Zoom in={!isOpen}> | |
<Box | |
position={'absolute'} | |
sx={{ pointerEvents: isOpen ? 'none' : 'all' }} | |
> | |
<Fab onMouseDown={onMouseDown} onMouseUp={onMouseUp('open')}> | |
{icon} | |
</Fab> | |
</Box> | |
</Zoom> | |
<Collapse in={isOpen}> | |
<Card> | |
<Stack pt={1} px={1} justifyContent="space-between" direction="row"> | |
<IconButton | |
size="small" | |
onMouseDown={onMouseDown} | |
onMouseUp={onMouseUp('close')} | |
> | |
<Close /> | |
</IconButton> | |
<IconButton sx={{ cursor: 'move' }} disableRipple> | |
<DragIndicator /> | |
</IconButton> | |
</Stack> | |
<CardContent>{children}</CardContent> | |
</Card> | |
</Collapse> | |
</> | |
); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Box, useTheme } from '@mui/material'; | |
import { useEffect, useRef, useState } from 'react'; | |
import Draggable from 'react-draggable'; | |
type Props = { children: React.ReactNode }; | |
export const PersistedDraggableWindow = ({ children }: Props) => { | |
const theme = useTheme(); | |
const [initPosition, setInitPosition] = useState<{ x: string; y: string }>(); | |
useEffect(() => { | |
if (typeof localStorage !== 'undefined') { | |
const switcherPos = localStorage.getItem('switcherPos'); | |
const { x, y } = switcherPos | |
? JSON.parse(switcherPos) | |
: { y: '20px', x: 'calc(100vw - 360px)' }; | |
setInitPosition({ x, y }); | |
} | |
}, []); | |
const nodeRef = useRef(null); | |
if (!initPosition) { | |
return null; | |
} | |
return ( | |
<Draggable | |
nodeRef={nodeRef} | |
cancel={'[class*="MuiDialogContent-root"]'} | |
onStop={(e, data) => { | |
if (typeof localStorage !== 'undefined') { | |
localStorage.setItem( | |
'switcherPos', | |
JSON.stringify({ | |
x: data.node.getBoundingClientRect().x, | |
y: data.node.getBoundingClientRect().y, | |
}) | |
); | |
} | |
}} | |
> | |
<Box | |
position="fixed" | |
zIndex={theme.zIndex.appBar + 10} | |
top={initPosition.y} | |
left={initPosition.x} | |
ref={nodeRef} | |
> | |
{children} | |
</Box> | |
</Draggable> | |
); | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ToggleOnIcon from '@mui/icons-material/ToggleOn'; | |
import { Chip, FormControlLabel, Stack, Switch } from '@mui/material'; | |
import { useState } from 'react'; | |
import { Flag, useBetaFlagsContext } from './BetaFlagsContext'; | |
import { ExpandableFab } from './ExpandableFab'; | |
import { PersistedDraggableWindow } from './PersistedDraggableWindow'; | |
export const Switcher = () => { | |
const { betaFlags, setBetaFlag } = useBetaFlagsContext(); | |
const [isOpen, setIsOpen] = useState<boolean | undefined>(undefined); | |
return ( | |
<PersistedDraggableWindow> | |
<ExpandableFab | |
isOpen={isOpen} | |
onOpen={() => setIsOpen(true)} | |
onClose={() => setIsOpen(false)} | |
icon={<ToggleOnIcon />} | |
> | |
<Stack> | |
{Object.entries(betaFlags).map(([flag, value]) => ( | |
<FormControlLabel | |
key={flag} | |
control={ | |
<Switch | |
onChange={e => { | |
setBetaFlag(flag as Flag, e.target.checked); | |
}} | |
checked={!!value} | |
/> | |
} | |
label={<Chip label={flag} />} | |
/> | |
))} | |
</Stack> | |
</ExpandableFab> | |
</PersistedDraggableWindow> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment