Skip to content

Commit

Permalink
feat(ui): admin ui debug menu (#1066)
Browse files Browse the repository at this point in the history
This PR adds a `Admin UI` menu for controlling and debugging various UI
elements. Each button in the menu is a toggle so each element can be
turned on and off.

This will allow us to work on various UI elements, and debug them more
easily.

This menu will only be accessible when running the app in `development`
mode.

This menu can also be used for turning Feature Flags on and off during
development to test different states of the app.

![CleanShot 2024-11-14 at 18 12
33](https://github.com/user-attachments/assets/1a50ab1e-0401-41fb-8a2b-19245570ad24)


https://www.loom.com/share/f6854a529806458ba062ced1c407c87a?sid=19d95cae-b7f0-4e7a-a005-11322cea9a7f

---------

Co-authored-by: Brian Pearce <[email protected]>
  • Loading branch information
peps and brianp authored Nov 18, 2024
1 parent 7aeccb4 commit 3d15a0d
Show file tree
Hide file tree
Showing 11 changed files with 266 additions and 44 deletions.
50 changes: 50 additions & 0 deletions src/components/AdminUI/AdminUI.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable i18next/no-literal-string */
import { MenuWrapper, MenuContent, ToggleButton } from './styles';
import { useFloating, offset, shift, flip, useClick, useInteractions, useDismiss } from '@floating-ui/react';
import { useState } from 'react';
import { ThemeGroup } from './groups/ThemeGroup';
import { DialogsGroup } from './groups/DialogsGroup';
import { GreenModalsGroup } from './groups/GreenModalsGroup';
import { ToastsGroup } from './groups/ToastsGroup';
import { OtherUIGroup } from './groups/OtherUIGroup';
import { AnimatePresence } from 'framer-motion';

export default function AdminUI() {
const [isOpen, setIsOpen] = useState(false);

const { refs, floatingStyles, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
middleware: [offset(10), flip(), shift()],
placement: 'bottom-end',
});

const click = useClick(context);
const dismiss = useDismiss(context);
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss]);

return (
<>
<ToggleButton ref={refs.setReference} {...getReferenceProps()} $isOpen={isOpen}>
Admin UI
</ToggleButton>
<AnimatePresence>
{isOpen && (
<MenuWrapper ref={refs.setFloating} style={floatingStyles} {...getFloatingProps()}>
<MenuContent
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
>
<ThemeGroup />
<DialogsGroup />
<GreenModalsGroup />
<ToastsGroup />
<OtherUIGroup />
</MenuContent>
</MenuWrapper>
)}
</AnimatePresence>
</>
);
}
36 changes: 36 additions & 0 deletions src/components/AdminUI/groups/DialogsGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable i18next/no-literal-string */
import { Button, ButtonGroup, CategoryLabel } from '../styles';
import { useUIStore } from '@app/store/useUIStore';
import { useAppStateStore } from '@app/store/appStateStore';

export function DialogsGroup() {
const { setCriticalError, criticalError } = useAppStateStore();
const { setDialogToShow, dialogToShow, showExternalDependenciesDialog, setShowExternalDependenciesDialog } =
useUIStore();

return (
<>
<CategoryLabel>Dialogs</CategoryLabel>
<ButtonGroup>
<Button
onClick={() => setCriticalError(criticalError ? undefined : 'This is a critical error')}
$isActive={!!criticalError}
>
Critical Error
</Button>
<Button
onClick={() => setDialogToShow(dialogToShow === 'autoUpdate' ? undefined : 'autoUpdate')}
$isActive={dialogToShow === 'autoUpdate'}
>
Auto Update Dialog
</Button>
<Button
onClick={() => setShowExternalDependenciesDialog(!showExternalDependenciesDialog)}
$isActive={showExternalDependenciesDialog}
>
External Dependencies
</Button>
</ButtonGroup>
</>
);
}
28 changes: 28 additions & 0 deletions src/components/AdminUI/groups/GreenModalsGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable i18next/no-literal-string */
import { Button, ButtonGroup, CategoryLabel } from '../styles';
import { usePaperWalletStore } from '@app/store/usePaperWalletStore';
import { useStagedSecurityStore } from '@app/store/useStagedSecurityStore';
import { useShareRewardStore } from '@app/store/useShareRewardStore';

export function GreenModalsGroup() {
const { showModal: showPaperWallet, setShowModal: setShowPaperWallet } = usePaperWalletStore();
const { showModal: showStagedSecurity, setShowModal: setShowStagedSecurity } = useStagedSecurityStore();
const { showModal: showShareReward, setShowModal: setShowShareReward } = useShareRewardStore();

return (
<>
<CategoryLabel>Green Modals</CategoryLabel>
<ButtonGroup>
<Button onClick={() => setShowPaperWallet(!showPaperWallet)} $isActive={showPaperWallet}>
Paper Wallet
</Button>
<Button onClick={() => setShowStagedSecurity(!showStagedSecurity)} $isActive={showStagedSecurity}>
Staged Security
</Button>
<Button onClick={() => setShowShareReward(!showShareReward)} $isActive={showShareReward}>
Share Reward
</Button>
</ButtonGroup>
</>
);
}
18 changes: 18 additions & 0 deletions src/components/AdminUI/groups/OtherUIGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-disable i18next/no-literal-string */
import { Button, ButtonGroup, CategoryLabel } from '../styles';
import { useAppStateStore } from '@app/store/appStateStore';

export function OtherUIGroup() {
const { isSettingUp, setIsSettingUp } = useAppStateStore();

return (
<>
<CategoryLabel>Other UI</CategoryLabel>
<ButtonGroup>
<Button onClick={() => setIsSettingUp(!isSettingUp)} $isActive={isSettingUp}>
Startup Screen
</Button>
</ButtonGroup>
</>
);
}
21 changes: 21 additions & 0 deletions src/components/AdminUI/groups/ThemeGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* eslint-disable i18next/no-literal-string */
import { Button, ButtonGroup, CategoryLabel } from '../styles';
import { useUIStore } from '@app/store/useUIStore';

export function ThemeGroup() {
const { setTheme, theme } = useUIStore();

return (
<>
<CategoryLabel>Theme</CategoryLabel>
<ButtonGroup>
<Button onClick={() => setTheme('light')} $isActive={theme === 'light'}>
Light Theme
</Button>
<Button onClick={() => setTheme('dark')} $isActive={theme === 'dark'}>
Dark Theme
</Button>
</ButtonGroup>
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,8 @@
/* eslint-disable i18next/no-literal-string */
import { Button, ButtonGroup, CategoryLabel } from '../styles';
import { addToast } from '@app/components/ToastStack/useToastStore';

import { addToast } from './useToastStore';
import { styled } from 'styled-components';

const TestingWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 5px;
position: fixed;
top: 10px;
right: 10px;
pointer-events: all;
z-index: 999;
`;

const Button = styled.button`
padding: 8px 16px;
border-radius: 8px;
background: #342945;
color: white;
border: none;
cursor: pointer;
font-size: 12px;
&:hover {
background: #221f3e;
}
`;

export const ToastTesting = () => {
export function ToastsGroup() {
const showBasicToast = () => {
addToast({
title: 'Changes saved',
Expand Down Expand Up @@ -63,11 +35,14 @@ export const ToastTesting = () => {
};

return (
<TestingWrapper>
<Button onClick={showBasicToast}>Basic Toast</Button>
<Button onClick={showErrorToast}>Error Toast</Button>
<Button onClick={showWarningToast}>Warning Toast</Button>
<Button onClick={showSuccessToast}>Success Toast</Button>
</TestingWrapper>
<>
<CategoryLabel>Toasts</CategoryLabel>
<ButtonGroup>
<Button onClick={showBasicToast}>Basic Toast</Button>
<Button onClick={showErrorToast}>Error Toast</Button>
<Button onClick={showWarningToast}>Warning Toast</Button>
<Button onClick={showSuccessToast}>Success Toast</Button>
</ButtonGroup>
</>
);
};
}
93 changes: 93 additions & 0 deletions src/components/AdminUI/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { m } from 'framer-motion';
import styled, { css } from 'styled-components';

export const ToggleButton = styled('button')<{ $isOpen?: boolean }>`
background: #444;
color: white;
padding: 8px 20px;
border-radius: 4px;
cursor: pointer;
position: fixed;
top: 20px;
right: 30px;
z-index: 99999;
pointer-events: all;
font-size: 12px;
opacity: 0;
transition: opacity 0.2s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
&:hover {
background: #555;
opacity: 1;
}
${({ $isOpen }) =>
$isOpen &&
css`
background: #555;
opacity: 1;
`}
`;

export const MenuWrapper = styled(m.div)`
position: fixed;
top: 20px;
right: 30px;
z-index: 99999;
max-width: 350px;
`;

export const MenuContent = styled(m.div)`
background: rgba(0, 0, 0, 0.8);
padding: 20px;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 10px;
min-width: 200px;
max-height: calc(100vh - 100px);
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
`;

export const Button = styled('button')<{ $isActive?: boolean }>`
background: ${({ $isActive }) => ($isActive ? '#666' : '#444')};
color: white;
border: 1px solid #666;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
pointer-events: all;
font-size: 11px;
transition: background-color 0.2s ease;
&:hover {
background: ${({ $isActive }) => ($isActive ? '#666' : '#555')};
}
`;

export const CategoryLabel = styled('div')`
color: #999;
font-size: 11px;
text-transform: uppercase;
padding-bottom: 2px;
border-bottom: 1px solid #666;
`;

export const ButtonGroup = styled('div')`
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
padding-bottom: 10px;
&:last-child {
padding-bottom: 0;
}
`;
5 changes: 1 addition & 4 deletions src/components/ToastStack/ToastStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import { Toast } from './Toast/Toast';
import { useToastStore } from './useToastStore';
import { useAppStateStore } from '@app/store/appStateStore';
import { Inside, Wrapper } from './styles';
import { ToastTesting } from './ToastTesting';

export const ToastStack = () => {
const { toasts, showToastTesting } = useToastStore();
const { toasts } = useToastStore();
const { isSettingUp } = useAppStateStore();
const [isHovered, setIsHovered] = useState(false);

Expand Down Expand Up @@ -41,8 +40,6 @@ export const ToastStack = () => {
</AnimatePresence>
</Inside>
</Wrapper>

{showToastTesting && <ToastTesting />}
</>
);
};
2 changes: 0 additions & 2 deletions src/components/ToastStack/useToastStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ interface Toast {
}

interface ToastStore {
showToastTesting: boolean;
toasts: Toast[];
addToast: (toast: Toast) => void;
removeToast: (id: number | string) => void;
}

export const useToastStore = create<ToastStore>()(
devtools((set) => ({
showToastTesting: false,
toasts: [],
addToast: (toast) =>
set((state) => {
Expand Down
4 changes: 4 additions & 0 deletions src/containers/floating/FloatingElements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import { ExternalDependenciesDialog } from './ExternalDependenciesDialog/Externa
import CriticalErrorDialog from './CriticalErrorDialog/CriticalErrorDialog.tsx';
import PaperWalletModal from './PaperWalletModal/PaperWalletModal.tsx';
import ShareRewardModal from './ShareRewardModal/ShareRewardModal';
import AdminUI from '@app/components/AdminUI/AdminUI.tsx';
import { ToastStack } from '@app/components/ToastStack/ToastStack.tsx';

const environment = import.meta.env.MODE;

export default function FloatingElements() {
return (
<FloatingTree>
Expand All @@ -20,6 +23,7 @@ export default function FloatingElements() {
<PaperWalletModal />
<ShareRewardModal />
<ToastStack />
{environment === 'development' && <AdminUI />}
</FloatingTree>
);
}
2 changes: 2 additions & 0 deletions src/store/appStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface AppState {
isSettingsOpen: boolean;
setIsSettingsOpen: (value: boolean) => void;
isSettingUp: boolean;
setIsSettingUp: (value: boolean) => void;
setSettingUpFinished: () => Promise<void>;
externalDependencies: ExternalDependency[];
fetchExternalDependencies: () => Promise<void>;
Expand Down Expand Up @@ -57,6 +58,7 @@ export const useAppStateStore = create<AppState>()((set, getState) => ({
isSettingsOpen: false,
setIsSettingsOpen: (value: boolean) => set({ isSettingsOpen: value }),
isSettingUp: true,
setIsSettingUp: (value: boolean) => set({ isSettingUp: value }),
setSettingUpFinished: async () => {
set({ isSettingUp: false });
setAnimationState('showVisual');
Expand Down

0 comments on commit 3d15a0d

Please sign in to comment.