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: migrate routes to modals #406

Closed
wants to merge 18 commits into from
20 changes: 14 additions & 6 deletions packages/frontend/src/MobileApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import CONFIG from "./config";
import ToastContainer from "./containers/toasts/ToastContainer";
import "@alphaday/ui-kit/global.scss";
import "./customIonicStyles.scss";
import { ModalContainer } from "./mobile-containers/ModalContainer";
import {
EMobileRoutePaths,
EMobileTabRoutePaths,
mobileRoutes,
mobileModals,
} from "./routes";

const { IS_DEV, BOARDS } = CONFIG;
Expand All @@ -33,7 +35,9 @@ const boardRoutesHandler = (
BOARDS.BOARD_SLUG_MAP[
pathname as keyof typeof BOARDS.BOARD_SLUG_MAP
];
const newRoute = `/superfeed/search/${[...new Set(searchSlugs)].join(",")}`;
const newRoute = `/superfeed/search/${[...new Set(searchSlugs)].join(
","
)}`;

if (pathname !== newRoute) {
callback(newRoute);
Expand Down Expand Up @@ -160,15 +164,19 @@ const RouterChild = () => {
);
};

/**
* TODO: Move user-settings (and any other view that should be accessible from multiple tabs)
* to a modal.
* For the MVP it's fine to nest everything within /superfeed
*/
const MobileApp: React.FC = () => {
return (
<IonApp className="theme-dark">
<IonReactRouter>
{mobileModals.map((modal) => (
<ModalContainer
key={modal.id}
modalId={modal.id}
className="!max-w-full min-h-[100vh] rounded-none border-none"
>
<modal.component />
</ModalContainer>
))}
<RouterChild />
</IonReactRouter>
<ToastContainer
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/src/api/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ export * from "./useValueWatcher";
export * from "./useOnScreen";
export * from "./usePullToRefresh";
export * from "./useHistory";
export * from "../store/providers/controlled-modal-provider";
elcharitas marked this conversation as resolved.
Show resolved Hide resolved
22 changes: 7 additions & 15 deletions packages/frontend/src/api/hooks/useHistory.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,18 @@
import { useCallback } from "react";
import { useHistory as useRRDHistory } from "react-router-dom";
import { EMobileTabRoutePaths } from "src/routes";
import { useControlledModal } from "../store/providers/controlled-modal-provider";

export const useHistory = () => {
const history = useRRDHistory();
const { resetModal } = useControlledModal();

/**
* we shouldn't need this ideally, but adding a listener
* ensures route navigation to tabs route paths which is great
*/
elcharitas marked this conversation as resolved.
Show resolved Hide resolved
history.listen(() => {});
history.listen(() => {
// reset modal on route change
resetModal();
});

const backNavigation = useCallback(() => {
if (history.length > 0) {
history.goBack();
} else {
history.push(EMobileTabRoutePaths.Superfeed);
}
}, [history]);

return {
...history,
backNavigation,
};
return history;
};
26 changes: 12 additions & 14 deletions packages/frontend/src/api/store/providers/app-context-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FC } from "react";
import { ChatProvider } from "./chat-context";
import ControlledModalProvider from "./controlled-modal-provider";
import { DimensionsProvider } from "./dimensions-context";
import { OauthProvider } from "./oauth-provider";
import { PWAInstallProvider } from "./pwa-install-provider";
Expand All @@ -21,19 +22,16 @@ function Compose(props: Props) {
}, children);
}

const providers = [
PWAInstallProvider,
TutorialProvider,
DimensionsProvider,
ChatProvider,
WalletViewProvider,
OauthProvider,
ControlledModalProvider,
];

export const AppContextProvider: FC<{ children?: React.ReactNode }> = ({
children,
}) => (
<Compose
providers={[
PWAInstallProvider,
TutorialProvider,
DimensionsProvider,
ChatProvider,
WalletViewProvider,
OauthProvider,
]}
>
{children}
</Compose>
);
}) => <Compose providers={providers}>{children}</Compose>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from "react";
import useEventListener from "src/api/hooks/useEventListener";

interface Prop {
activeModal: string | null;
setActiveModal: (modalId: string) => void;
closeModal: () => void;
resetModal: () => void;
}

export const ControlledModalContext = createContext<Prop>({
activeModal: null,
setActiveModal: () => {},
closeModal: () => {},
resetModal: () => {},
});

export const useControlledModal = () => {
const context = useContext(ControlledModalContext);
if (context === undefined) {
throw new Error(
"controlled-modal-context:useControlledModal: not used within a Provider"
);
}
return context;
};

const MODAL_HISTORY = new Set<string>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any benefits of putting this outside and not using a useState

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useState would be an overkill and wiould cause rerenders.


const ControlledModalProvider: React.FC<{ children?: React.ReactNode }> = ({
children,
}) => {
const [activeModal, setActiveModal] = useState<string | null>(null);

const closeModal = useCallback(() => {
if (activeModal) {
MODAL_HISTORY.delete(activeModal);
// set active modal to last modal in history
const lastModal = Array.from(MODAL_HISTORY).pop();
setActiveModal(lastModal || null);
}
}, [activeModal]);

const setActiveModalWithHistory = (modalId: string) => {
if (!MODAL_HISTORY.has(modalId)) {
setActiveModal(modalId);
MODAL_HISTORY.add(modalId);
}
};

const resetModal = () => {
elcharitas marked this conversation as resolved.
Show resolved Hide resolved
setActiveModal(null);
MODAL_HISTORY.clear();
};

useEventListener("popstate", (e) => {
/**
* If there is an active modal,
* close it when the user swipes back or presses the back button
*/
if (activeModal) {
e.preventDefault();
history.forward();
closeModal();
}
});

return (
<ControlledModalContext.Provider
value={useMemo(
() => ({
activeModal,
setActiveModal: setActiveModalWithHistory,
closeModal,
resetModal,
}),
[activeModal, closeModal]
)}
>
{children}
</ControlledModalContext.Provider>
);
};

export default ControlledModalProvider;
6 changes: 3 additions & 3 deletions packages/frontend/src/layout/PagedMobileLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FC, ReactNode } from "react";
import { Pager, ScrollBar } from "@alphaday/ui-kit";
import { useHistory } from "src/api/hooks";
import { useControlledModal } from "src/api/hooks";

const PagedMobileLayout: FC<{
title: string;
Expand All @@ -9,14 +9,14 @@ const PagedMobileLayout: FC<{
handleClose?: () => void;
handleBack?: () => void;
}> = ({ children, title, onScroll, handleBack, handleClose }) => {
const { backNavigation } = useHistory();
const { closeModal } = useControlledModal();
return (
<ScrollBar className="h-screen flex flex-col" onScroll={onScroll}>
<Pager
title={title}
handleBack={() => {
handleBack?.();
backNavigation();
closeModal();
}}
handleClose={handleClose}
/>
Expand Down
13 changes: 8 additions & 5 deletions packages/frontend/src/mobile-components/navigation/NavHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { FC } from "react";
import { Link } from "react-router-dom";
import { useControlledModal } from "src/api/hooks";
import { ReactComponent as SearchSVG } from "src/assets/svg/search.svg";
import { ReactComponent as UserSVG } from "src/assets/svg/user.svg";
import { EMobileRoutePaths } from "src/routes";
import { EMobileModalIds } from "src/routes";

interface IProps {
avatar: string | undefined;
onSearchHandleClick?: () => void;
}

export const NavHeader: FC<IProps> = ({ avatar, onSearchHandleClick }) => {
const { setActiveModal } = useControlledModal();
return (
<div className="w-full flex justify-between pt-2 px-5">
<Link
to={EMobileRoutePaths.UserSettings}
<span
role="button"
tabIndex={0}
className="relative flex rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800"
onClick={() => {
setActiveModal(EMobileModalIds.UserSettings);
}}
>
<span className="absolute -inset-1.5" />
<span className="sr-only">Open user menu</span>
Expand All @@ -31,7 +34,7 @@ export const NavHeader: FC<IProps> = ({ avatar, onSearchHandleClick }) => {
<UserSVG className="fill-primary h-7 w-7" />
</div>
)}
</Link>
</span>
<button
type="button"
className="bg-backgroundVariant300 self-center rounded-lg p-2"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FC } from "react";
import { FormInput, Pager, ScrollBar } from "@alphaday/ui-kit";
import moment from "moment";
import { useHistory } from "src/api/hooks";
import { useControlledModal } from "src/api/hooks";
import { TCoin, THolding } from "src/api/types";

interface IAddHolding {
Expand All @@ -17,7 +17,7 @@ const AddHolding: FC<IAddHolding> = ({
holding,
setHolding,
}) => {
const history = useHistory();
const { closeModal } = useControlledModal();

const defaultHolding: THolding = {
coin: selectedCoin,
Expand Down Expand Up @@ -54,7 +54,7 @@ const AddHolding: FC<IAddHolding> = ({
{selectedCoin.name}
</span>
}
handleClose={history.backNavigation}
handleClose={closeModal}
handleBack={() => setSelectedCoin(undefined)}
/>
<div className="flex flex-col items-center mt-4 mx-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { FC, FormEvent } from "react";
import { Input, Pager, ScrollBar, Spinner } from "@alphaday/ui-kit";
import { useHistory } from "src/api/hooks";
import { useControlledModal } from "src/api/hooks";
import { TCoin } from "src/api/types";
import { ReactComponent as ChevronSVG } from "src/assets/icons/chevron-right.svg";
import { ReactComponent as SearchSVG } from "src/assets/svg/search.svg";
Expand All @@ -22,11 +22,11 @@ const SelectHoldingCoin: FC<ISelectHoldingCoin> = ({
coins,
setSelectedCoin,
}) => {
const history = useHistory();
const { closeModal } = useControlledModal();

return (
<ScrollBar onScroll={onScroll}>
<Pager title="Add Manually" handleClose={history.backNavigation} />
<Pager title="Add Manually" handleClose={closeModal} />
<p className="mx-4 fontGroup-highlight">
Search or select the desired crypto coin that you have in your
portfolio.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { OutlineButton, twMerge } from "@alphaday/ui-kit";
import { ReactComponent as CopySVG } from "src/assets/icons/copy.svg";
import { ReactComponent as HandSVG } from "src/assets/icons/hand.svg";
import { ReactComponent as WalletSVG } from "src/assets/icons/wallet.svg";
import { EMobileRoutePaths } from "src/routes";
import { EMobileModalIds } from "src/routes";

const WalletConnectionOptions: FC<{
isAuthenticated: boolean;
Expand All @@ -16,23 +16,21 @@ const WalletConnectionOptions: FC<{
title="Add Wallet"
subtext="Add your wallet manually to get started"
icon={<WalletSVG className="w-[24px] mr-1" />}
onClick={() => onClick(EMobileRoutePaths.PortfolioAddWallet)}
onClick={() => onClick(EMobileModalIds.PortfolioAddWallet)}
isAuthenticated={isAuthenticated}
/>
<OutlineButton
title="Connect Wallet"
subtext="Connect your wallet to get started"
icon={<CopySVG className="w-[22px] mr-1" />}
onClick={() =>
onClick(EMobileRoutePaths.PortfolioConnectWallet)
}
onClick={() => onClick(EMobileModalIds.PortfolioConnectWallet)}
isAuthenticated={isAuthenticated}
/>
<OutlineButton
title="Add Holdings Manually"
subtext="Add your holdings manually"
icon={<HandSVG className="w-[20px] mr-1" />}
onClick={() => onClick(EMobileRoutePaths.PortfolioAddHolding)}
onClick={() => onClick(EMobileModalIds.PortfolioAddHolding)}
isAuthenticated={isAuthenticated}
/>
</div>
Expand Down
Loading
Loading