diff --git a/package.json b/package.json index 41f7da9aa..882a117e3 100644 --- a/package.json +++ b/package.json @@ -65,5 +65,9 @@ "url": "https://github.com/AlphadayHQ/alphaday/issues" }, "homepage": "https://github.com/AlphadayHQ/alphaday#readme", - "packageManager": "yarn@1.22.19" + "packageManager": "yarn@1.22.19", + "resolutions": { + "@types/react": "18.2.25", + "@types/react-dom": "18.2.6" + } } \ No newline at end of file diff --git a/packages/frontend/capacitor.config.ts b/packages/frontend/capacitor.config.ts deleted file mode 100644 index 898fd9e7f..000000000 --- a/packages/frontend/capacitor.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CapacitorConfig } from "@capacitor/cli"; - -const config: CapacitorConfig = { - appId: "com.alphaday", - appName: "alphaday", - webDir: "dist", - server: { - androidScheme: "http", - iosScheme: "http", - }, - backgroundColor: "#121212", -}; - -export default config; diff --git a/packages/frontend/ionic.config.json b/packages/frontend/ionic.config.json deleted file mode 100644 index 354ad3563..000000000 --- a/packages/frontend/ionic.config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "alphaday", - "integrations": { - "capacitor": {} - }, - "type": "react-vite" -} diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 1a6329cc8..306beb4f9 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -8,27 +8,15 @@ "dev": "pwa-assets-generator && VITE_COMMIT=$(git rev-parse --short HEAD) VITE_COMMIT_TS=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d %H:%M:%S') vite", "dev:ssl": "USE_SSL=true yarn dev --host --port 443", "build": "pwa-assets-generator && VITE_COMMIT=$(git rev-parse --short HEAD) VITE_COMMIT_TS=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d %H:%M:%S') vite build", - "build:android:dev": "ionic capacitor build android -- --mode development", - "build:android:prod": "node env-check.cjs && ionic capacitor build android --prod", - "platform:add": "ionic capacitor add", "generate-pwa-assets": "pwa-assets-generator", "preview": "vite preview", "test.e2e": "cypress run", "test.unit": "vitest", "lint": "eslint --max-warnings=0 --ignore-path .gitignore . --ext ts --ext tsx", - "resources": "cordova-res ios --skip-config --copy && cordova-res android --skip-config --copy", "typecheck": "tsc" }, "dependencies": { "@alphaday/ui-kit": "0.0.0", - "@capacitor/android": "5.4.1", - "@capacitor/app": "5.0.7", - "@capacitor/core": "5.7.4", - "@capacitor/haptics": "5.0.7", - "@capacitor/keyboard": "5.0.8", - "@capacitor/status-bar": "5.0.7", - "@ionic/react": "7.8.1", - "@ionic/react-router": "7.8.1", "@react-oauth/google": "0.12.1", "@reduxjs/toolkit": "1.8.0", "@sentry/react": "7.109.0", @@ -43,7 +31,6 @@ "html2canvas": "1.4.1", "i18next": "23.16.5", "i18next-browser-languagedetector": "8.2.0", - "ionicons": "7.3.0", "md5": "2.3.0", "moment": "2.30.1", "moment-with-locales-es6": "1.0.1", @@ -74,8 +61,6 @@ "web3-eth-accounts": "1.9.0" }, "devDependencies": { - "@capacitor/cli": "5.4.1", - "@ionic/cli": "7.2.0", "@testing-library/react": "15.0.7", "@testing-library/react-hooks": "8.0.1", "@testing-library/user-event": "14.4.3", @@ -89,7 +74,6 @@ "@vite-pwa/assets-generator": "0.2.4", "@vitejs/plugin-basic-ssl": "1.2.0", "@vitejs/plugin-legacy": "5.3.2", - "cordova-res": "0.15.4", "cypress": "12.17.4", "dotenv": "16.4.5", "happy-dom": "20.0.11", diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 4aa9cc907..0fdd9ae6c 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -1,9 +1,7 @@ import { Suspense, memo, useMemo } from "react"; import { ErrorModal } from "@alphaday/ui-kit"; -import { IonApp, IonRouterOutlet } from "@ionic/react"; -import { IonReactRouter } from "@ionic/react-router"; import { Web3Modal } from "@web3modal/react"; -import { Redirect, Route } from "react-router-dom"; +import { BrowserRouter, Route } from "react-router-dom"; import * as userStore from "src/api/store/slices/user"; import ToastContainer from "src/containers/toasts/ToastContainer"; import { @@ -11,6 +9,7 @@ import { useResolvedView, useViewRoute, useGaTracker, + useIsMobile, } from "./api/hooks"; import { useGetRemoteStatusQuery } from "./api/services"; import { useAppDispatch } from "./api/store/hooks"; @@ -20,7 +19,7 @@ import { getRtkErrorCode } from "./api/utils/errorHandling"; import { Logger } from "./api/utils/logging"; import CONFIG from "./config/config"; import PreloaderPage from "./pages/preloader"; -import { EDesktopRoutePaths, desktopRoutes, errorRoutes } from "./routes"; +import { appRoutes, errorRoutes } from "./routes"; import "@alphaday/ui-kit/global.scss"; const landingPage = CONFIG.SEO.DOMAIN; @@ -30,6 +29,7 @@ const goToLandingPage = () => { }; const AppRoutes = () => { + const isMobile = useIsMobile(); useGaTracker(); const dispatch = useAppDispatch(); @@ -39,14 +39,24 @@ const AppRoutes = () => { }); const resolvedView = useResolvedView(); - const { pathContainsHashOrSlug, isRoot, isSuperfeed } = useViewRoute(); + const { + pathContainsHashOrSlug, + isRoot, + isBoardsLibrary, + isWidgetsLibrary, + } = useViewRoute(); const errorCode = useMemo(() => { /** * At this moment, we do not support any other routes than the root and the hash/slug routes * If the path does not contain a hash or slug, we show the 404 error page */ - if (!pathContainsHashOrSlug && !isRoot && !isSuperfeed) { + if ( + !pathContainsHashOrSlug && + !isRoot && + !isBoardsLibrary && + !isWidgetsLibrary + ) { return 404; } const errorInfo = error ?? resolvedView.error; @@ -54,7 +64,8 @@ const AppRoutes = () => { }, [ pathContainsHashOrSlug, isRoot, - isSuperfeed, + isBoardsLibrary, + isWidgetsLibrary, error, resolvedView.error, ]); @@ -63,7 +74,7 @@ const AppRoutes = () => { if (error || errorCode) { return errorRoutes; } - return desktopRoutes; + return appRoutes; }, [error, errorCode]); /** @@ -76,16 +87,8 @@ const AppRoutes = () => { location.reload(); } - if (isSuperfeed) { - return ( - - ); + if (!isMobile && (isBoardsLibrary || isWidgetsLibrary)) { + window.location.href = "/"; } return ( @@ -117,24 +120,22 @@ const App: React.FC = () => { if (!isCookieEnabled()) { return ( - +
- +
); } return ( - - - - - - +
+ + + { duration={CONFIG.UI.TOAST_DURATION} className="fontGroup-supportBold" /> - +
); }; diff --git a/packages/frontend/src/MobileApp.tsx b/packages/frontend/src/MobileApp.tsx deleted file mode 100644 index 3e07780e6..000000000 --- a/packages/frontend/src/MobileApp.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { memo } from "react"; -import { - IonApp, - IonRouterOutlet, - IonTabBar, - IonTabButton, - IonTabs, -} from "@ionic/react"; -import { IonReactRouter } from "@ionic/react-router"; -import { Redirect, Route, useHistory, useLocation } from "react-router-dom"; -import { ReactComponent as MarketsSVG } from "src/assets/svg/markets.svg"; -import { ReactComponent as PortfolioSVG } from "src/assets/svg/portfolio.svg"; -import { ReactComponent as SuperfeedSVG } from "src/assets/svg/superfeed.svg"; -import { useAuth, useViewRoute } from "./api/hooks"; -import CONFIG from "./config"; -import ToastContainer from "./containers/toasts/ToastContainer"; -import "@alphaday/ui-kit/global.scss"; -import "./customIonicStyles.scss"; -import { - EMobileRoutePaths, - EMobileTabRoutePaths, - mobileRoutes, -} from "./routes"; - -const { IS_DEV, BOARDS } = CONFIG; - -const boardRoutesHandler = ( - pathname: string, - callback: (path: string) => void -) => { - if (pathname in BOARDS.BOARD_SLUG_MAP) { - const searchSlugs = - BOARDS.BOARD_SLUG_MAP[ - pathname as keyof typeof BOARDS.BOARD_SLUG_MAP - ]; - const newRoute = `/superfeed/search/${[...new Set(searchSlugs)].join(",")}`; - - if (pathname !== newRoute) { - callback(newRoute); - } - } else { - callback(EMobileRoutePaths.Base); - } -}; - -const CustomNavTab: React.FC<{ - label: string; - Icon: React.FC>; - disabled?: boolean; -}> = ({ label, Icon, disabled }) => ( -
- - - - - {disabled ? `${label} (soon)` : label} - - {/* {disabled &&
(soon)
} */} -
-); - -const RouterChild = () => { - const { pathname } = useLocation(); - const history = useHistory(); - const { pathContainsHashOrSlug, routeInfo, isRoot } = useViewRoute(); - - const { isAuthenticated } = useAuth(); - const isTabBarHidden = !!mobileRoutes.find( - (route) => route.path === pathname && route?.hideTabBar - ); - - if (pathContainsHashOrSlug && routeInfo?.value) { - const navigate = (str: string) => history.push(str); - boardRoutesHandler(routeInfo.value, navigate); - - return null; - } - - if (isRoot) { - return ( - - ); - } - - return ( - - - {mobileRoutes.map((route) => { - return ( - - route.authWalled && !isAuthenticated ? ( - - ) : ( - - ) - } - /> - ); - })} - ( - - )} - exact - /> - - - - - - {IS_DEV && ( - - - - )} - - - - - - ); -}; - -/** - * 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 ( - - - - - - - ); -}; - -export default memo(MobileApp); diff --git a/packages/frontend/src/api/hooks/useHistory.ts b/packages/frontend/src/api/hooks/useHistory.ts index 480a41ff6..86ee2ee08 100644 --- a/packages/frontend/src/api/hooks/useHistory.ts +++ b/packages/frontend/src/api/hooks/useHistory.ts @@ -1,6 +1,6 @@ import { useCallback } from "react"; import { useHistory as useRRDHistory } from "react-router-dom"; -import { EMobileTabRoutePaths } from "src/routes"; +import { ERoutePaths } from "src/routes"; export const useHistory = () => { const history = useRRDHistory(); @@ -15,7 +15,7 @@ export const useHistory = () => { if (history.length > 0) { history.goBack(); } else { - history.push(EMobileTabRoutePaths.Superfeed); + history.push(ERoutePaths.Base); } }, [history]); diff --git a/packages/frontend/src/api/hooks/usePreferredLanguage.ts b/packages/frontend/src/api/hooks/usePreferredLanguage.ts index 3554b01ef..3e7ecc1f1 100644 --- a/packages/frontend/src/api/hooks/usePreferredLanguage.ts +++ b/packages/frontend/src/api/hooks/usePreferredLanguage.ts @@ -1,16 +1,7 @@ import { useEffect, useRef } from "react"; import i18next from "i18next"; import moment from "moment-with-locales-es6"; -import { initReactI18next } from "react-i18next"; import { useDispatch } from "react-redux"; -import { - translationEN, - translationES, - translationFR, - translationJA, - translationTR, - translationZH, -} from "../../locales/translation"; import { alphadayApi } from "../services"; import { setSelectedLanguageCode } from "../store"; import { useAppSelector } from "../store/hooks"; @@ -18,49 +9,6 @@ import { ELanguageCode } from "../types/language"; import { Logger } from "../utils/logging"; import { useAllowedTranslations } from "./useAllowedTranslations"; -const resources: Record = { - en: { - translation: translationEN, - }, - ja: { - translation: translationJA, - }, - es: { - translation: translationES, - }, - fr: { - translation: translationFR, - }, - tr: { - translation: translationTR, - }, - zh: { - translation: translationZH, - }, -}; - -const i18nInit = (selectedLangCode: ELanguageCode) => { - i18next - .use(initReactI18next) - .init({ - debug: true, - resources, - fallbackLng: selectedLangCode, - detection: { - order: ["navigator", "htmlTag", "path", "subdomain"], - }, - interpolation: { - escapeValue: false, - }, - }) - .then(() => { - Logger.info("usePreferredLanguage: i18n initialized successfully"); - }) - .catch((e) => { - Logger.error("usePreferredLanguage: could not initialize i18n", e); - }); -}; - export const usePreferredLanguage = () => { const dispatch = useDispatch(); const { isLoading, languages } = useAllowedTranslations(); @@ -79,9 +27,19 @@ export const usePreferredLanguage = () => { if (lang && lang in languages && selectedLangCode !== lang) { dispatch(setSelectedLanguageCode({ code: lang as ELanguageCode })); prevLangCodeRef.current = lang as ELanguageCode; - i18nInit(lang as ELanguageCode); - } else { - i18nInit(selectedLangCode); + i18next.changeLanguage(lang).catch((e) => { + Logger.error( + "usePreferredLanguage: could not change language", + e + ); + }); + } else if (i18next.language !== selectedLangCode) { + i18next.changeLanguage(selectedLangCode).catch((e) => { + Logger.error( + "usePreferredLanguage: could not change language", + e + ); + }); } // this should only run once // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/frontend/src/api/hooks/useViewRoute.ts b/packages/frontend/src/api/hooks/useViewRoute.ts index 9dd9131e3..2c9a68e97 100644 --- a/packages/frontend/src/api/hooks/useViewRoute.ts +++ b/packages/frontend/src/api/hooks/useViewRoute.ts @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { useLocation } from "react-router-dom"; import { isHash } from "src/api/utils/helpers"; import CONFIG from "src/config/config"; -import { EMobileRoutePaths } from "src/routes"; +import { ERoutePaths } from "src/routes"; import { FULLSIZE_ROUTES_ARR } from "src/types"; enum ERouteType { @@ -43,7 +43,11 @@ interface IViewRouteInfo { /** * true if the current path is superfeed path */ - isSuperfeed: boolean; + isBoardsLibrary: boolean; + /** + * true if the current path is widgets path + */ + isWidgetsLibrary: boolean; } export const useViewRoute = (): IViewRouteInfo => { @@ -122,7 +126,8 @@ export const useViewRoute = (): IViewRouteInfo => { isFullSize: fullSizeWidgetPath !== undefined, isViewHash, isRoot: location.pathname === "/", - isSuperfeed: location.pathname.includes(EMobileRoutePaths.Superfeed), + isBoardsLibrary: location.pathname.includes(ERoutePaths.BoardsLibrary), + isWidgetsLibrary: location.pathname.includes(ERoutePaths.Widgets), }; }; diff --git a/packages/frontend/src/api/utils/layoutUtils.ts b/packages/frontend/src/api/utils/layoutUtils.ts index 92be440cb..312b3ac00 100644 --- a/packages/frontend/src/api/utils/layoutUtils.ts +++ b/packages/frontend/src/api/utils/layoutUtils.ts @@ -308,3 +308,50 @@ export const calculateTwoColWidgetsHeight = ( return totalHeight; }; + +/** + * Computes the layout state from a selected view based on the window width. + * This function determines which column layout to use (single, two, three, or four column) + * and returns the corresponding widget arrangement. + * + * @param selectedView - The current selected view containing widgets + * @param windowWidth - The current window width in pixels + * @returns The layout state as a 2D array of widgets, or undefined if the view or layout is invalid + * + * @example + * const layoutState = getLayoutStateFromView(selectedView, 1920); + * // Returns four-column layout for wide screens + */ +export const getLayoutStateFromView = ( + selectedView: TCachedView | undefined, + windowWidth: number +): TUserViewWidget[][] | undefined => { + if (!selectedView) return undefined; + + const twoColWidgetSlugs = getTwoColWidgetTemplateSlugs(); + const layoutGrid = computeLayoutGrid( + selectedView.data.widgets.filter( + (w) => !twoColWidgetSlugs.includes(w.widget.template.slug) + ) + ); + + if (!layoutGrid) return undefined; + + const colType = getColType(windowWidth); + + if (colType === EColumnType.SingleCol && layoutGrid.singleCol) { + return layoutGrid.singleCol; + } + if (colType === EColumnType.TwoCol && layoutGrid.twoCol) { + return layoutGrid.twoCol; + } + if (colType === EColumnType.ThreeCol && layoutGrid.threeCol) { + return layoutGrid.threeCol; + } + + if (layoutGrid.fourCol) { + return layoutGrid.fourCol; + } + + return undefined; +}; diff --git a/packages/frontend/src/components/countdown/CountdownModule.tsx b/packages/frontend/src/components/countdown/CountdownModule.tsx index 7735abff5..69c8c6043 100644 --- a/packages/frontend/src/components/countdown/CountdownModule.tsx +++ b/packages/frontend/src/components/countdown/CountdownModule.tsx @@ -37,7 +37,7 @@ const CountdownModule: FC = ({ }, []); return ( -
+
= ({ layout, addExtraColumn }) => { return ( -
+
{layout.map((columnLayout) => ( = ({ return (
= ({ return (
= ({ if (!items) { return ( -
+