diff --git a/apps/feeds/components/FeedLink.tsx b/apps/feeds/components/FeedLink.tsx index 12437b4a6..4855ad0e8 100644 --- a/apps/feeds/components/FeedLink.tsx +++ b/apps/feeds/components/FeedLink.tsx @@ -5,7 +5,7 @@ import Grid from "@mui/material/Grid" import Card from "@mui/material/Card" import CardActionArea from "@mui/material/CardActionArea" import CardContent from "@mui/material/CardContent" -import Image from 'next/image' +import Image from "next/image" import React, { useEffect, useState } from "react" import { FeedInfo } from "../constants/types" import { Lang, LangMap } from "../constants/langs" @@ -38,14 +38,14 @@ const FeedLink = ({ name, feed, locale }: Props) => { } } } - console.log(coverImg); + setCover(coverImg) }, []) return ( - {

-
- {cover && Feed Cover Image} +
+ {cover && ( + Feed Cover Image + )}
diff --git a/apps/forum/src/components/App.tsx b/apps/forum/src/components/App.tsx index efff15e16..914e1e859 100644 --- a/apps/forum/src/components/App.tsx +++ b/apps/forum/src/components/App.tsx @@ -12,6 +12,7 @@ import FeedBackBox from "./FeedBackBox"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import { API } from "@aws-amplify/api"; import { getUserAttr, getIdToken, LoadingSpinner } from "wasedatime-ui"; +import { storeDate } from "@app/utils/storeDate"; const App = () => { return ( @@ -67,6 +68,7 @@ const InnerApp = () => { useEffect(() => { fetchNotification(); + storeDate(); }, []); return ( diff --git a/apps/forum/src/utils/getDate.ts b/apps/forum/src/utils/getDate.ts new file mode 100644 index 000000000..c4ae2f6c6 --- /dev/null +++ b/apps/forum/src/utils/getDate.ts @@ -0,0 +1,15 @@ +export const getCurrentDateInJST = () => { + const date = new Date(); + const jstOffset = 9 * 60; // JST is UTC+9 + const localOffset = date.getTimezoneOffset(); + date.setMinutes(date.getMinutes() + localOffset + jstOffset); + + const YYYY = date.getFullYear(); + const MM = String(date.getMonth() + 1).padStart(2, "0"); // Months are 0-based + const DD = String(date.getDate()).padStart(2, "0"); + const HH = String(date.getHours()).padStart(2, "0"); + const mm = String(date.getMinutes()).padStart(2, "0"); + const SS = String(date.getSeconds()).padStart(2, "0"); + + return `${YYYY}${MM}${DD}${HH}${mm}${SS}`; +}; diff --git a/apps/forum/src/utils/storeDate.ts b/apps/forum/src/utils/storeDate.ts new file mode 100644 index 000000000..513e19fc5 --- /dev/null +++ b/apps/forum/src/utils/storeDate.ts @@ -0,0 +1,14 @@ +import { getCurrentDateInJST } from "./getDate"; + +export const storeDate = () => { + const storedDateInJST = localStorage.getItem("lastCheckedDateJST"); + const currentDateInJST = getCurrentDateInJST(); + + if (!storedDateInJST) { + // If there's no stored date, set the current date to local storage. + localStorage.setItem("lastCheckedDateJST", currentDateInJST); + } else if (storedDateInJST !== currentDateInJST) { + // If the stored date and the current date are different, update the stored date. + localStorage.setItem("lastCheckedDateJST", currentDateInJST); + } +}; diff --git a/apps/root/package.json b/apps/root/package.json index 181bf874b..63cda7caf 100644 --- a/apps/root/package.json +++ b/apps/root/package.json @@ -56,6 +56,7 @@ "vite-plugin-pwa": "0.12.0" }, "dependencies": { + "@aws-amplify/api": "4.0.42", "@aws-amplify/auth": "4.5.6", "@aws-amplify/core": "4.5.6", "@emotion/react": "11.9.0", diff --git a/apps/root/pnpm-lock.yaml b/apps/root/pnpm-lock.yaml index 549cf601f..0c9a2f492 100644 --- a/apps/root/pnpm-lock.yaml +++ b/apps/root/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@aws-amplify/api': + specifier: 4.0.42 + version: 4.0.42(react-native@0.68.2) '@aws-amplify/auth': specifier: 4.5.6 version: 4.5.6(react-native@0.68.2) @@ -262,6 +265,44 @@ packages: leven: 3.1.0 dev: true + /@aws-amplify/api-graphql@2.3.6(react-native@0.68.2): + resolution: {integrity: sha512-bAFApP7Yw2uLythEG4og0nDm8xVqqEMLKiT5AUpElKmlzU5t106gYissRLet+oHeiE2DMnIWCL9g/rOn93Z1NQ==} + dependencies: + '@aws-amplify/api-rest': 2.0.42(react-native@0.68.2) + '@aws-amplify/auth': 4.5.6(react-native@0.68.2) + '@aws-amplify/cache': 4.0.44(react-native@0.68.2) + '@aws-amplify/core': 4.5.6(react-native@0.68.2) + '@aws-amplify/pubsub': 4.4.3(react-native@0.68.2) + graphql: 15.8.0 + uuid: 3.4.0 + zen-observable-ts: 0.8.19 + transitivePeerDependencies: + - debug + - encoding + - react-native + dev: false + + /@aws-amplify/api-rest@2.0.42(react-native@0.68.2): + resolution: {integrity: sha512-37BUCnI1PM273jUwWceEZVi0frO0mHY8dY/9EZ2zUmA6Rbscbm5ZpyB8E+txtO+odbbBxnU5JHdc7JnWr7Fwpw==} + dependencies: + '@aws-amplify/core': 4.5.6(react-native@0.68.2) + axios: 0.21.4 + transitivePeerDependencies: + - debug + - react-native + dev: false + + /@aws-amplify/api@4.0.42(react-native@0.68.2): + resolution: {integrity: sha512-h+nm6Frbu4G2Ftc7s0SVdQEoLOE9tVlfRM6q0JrQ48Ilu1myQdQ3wormWUxdhlpkuTq+UOYcDTcIUeiD3CC2mA==} + dependencies: + '@aws-amplify/api-graphql': 2.3.6(react-native@0.68.2) + '@aws-amplify/api-rest': 2.0.42(react-native@0.68.2) + transitivePeerDependencies: + - debug + - encoding + - react-native + dev: false + /@aws-amplify/auth@4.5.6(react-native@0.68.2): resolution: {integrity: sha512-G0pxqKaVouuhVK6qH0EBHG5ziNVRT7AzKSj8jN2I7klHtFJHnY/F6NEEjr/e99IgbyzspjWX5xFRBAQecgF44A==} dependencies: @@ -297,6 +338,21 @@ packages: - react-native dev: false + /@aws-amplify/pubsub@4.4.3(react-native@0.68.2): + resolution: {integrity: sha512-bpjucdYHpnrz0fq+0PZ/UfaUR67z7RUm13iDL3GnX8TMCRTTaquOF7S2Sfn9zlWzwDOFzrne6JAl8lnwBmjuEw==} + dependencies: + '@aws-amplify/auth': 4.5.6(react-native@0.68.2) + '@aws-amplify/cache': 4.0.44(react-native@0.68.2) + '@aws-amplify/core': 4.5.6(react-native@0.68.2) + graphql: 15.8.0 + paho-mqtt: 1.1.0 + uuid: 3.4.0 + zen-observable-ts: 0.8.19 + transitivePeerDependencies: + - encoding + - react-native + dev: false + /@aws-crypto/ie11-detection@1.0.0: resolution: {integrity: sha512-kCKVhCF1oDxFYgQrxXmIrS5oaWulkvRcPz+QBDMsUr2crbF4VGgGT6+uQhSwJFdUAQ2A//Vq+uT83eJrkzFgXA==} dependencies: @@ -3564,6 +3620,14 @@ packages: engines: {node: '>= 0.4'} dev: true + /axios@0.21.4: + resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} + dependencies: + follow-redirects: 1.15.3 + transitivePeerDependencies: + - debug + dev: false + /babel-core@7.0.0-bridge.0(@babel/core@7.18.2): resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: @@ -5037,6 +5101,16 @@ packages: resolution: {integrity: sha512-1gIBiWJNR0tKUNv8gZuk7l9rVX06OuLzY9AoGio7y/JT4V1IZErEMEq2TJS+PFcw/y0RshZ1J/27VfK1UQzYVg==} engines: {node: '>=0.4.0'} + /follow-redirects@1.15.3: + resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -5210,6 +5284,11 @@ packages: /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + /graphql@15.8.0: + resolution: {integrity: sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==} + engines: {node: '>= 10.x'} + dev: false + /has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} dev: true @@ -6745,6 +6824,10 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + /paho-mqtt@1.1.0: + resolution: {integrity: sha512-KPbL9KAB0ASvhSDbOrZBaccXS+/s7/LIofbPyERww8hM5Ko71GUJQ6Nmg0BWqj8phAIT8zdf/Sd/RftHU9i2HA==} + dev: false + /param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: diff --git a/apps/root/src/components/aboutUs/MeetOurTeam/MeetOurTeam.tsx b/apps/root/src/components/aboutUs/MeetOurTeam/MeetOurTeam.tsx index 99ed0052f..81b916fc8 100644 --- a/apps/root/src/components/aboutUs/MeetOurTeam/MeetOurTeam.tsx +++ b/apps/root/src/components/aboutUs/MeetOurTeam/MeetOurTeam.tsx @@ -35,10 +35,6 @@ const MeetOurTeam = () => { const { t } = useTranslation() const [activeCardName, setActiveCardName] = useState("") - useEffect(() => { - console.log(activeCardName) - }, [activeCardName]) - return ( diff --git a/apps/root/src/components/block/IconTextGroup.tsx b/apps/root/src/components/block/IconTextGroup.tsx index 2c4e47ca0..009820588 100644 --- a/apps/root/src/components/block/IconTextGroup.tsx +++ b/apps/root/src/components/block/IconTextGroup.tsx @@ -19,6 +19,7 @@ interface IconTextGroupProps { iconPath?: string expanded?: boolean currentPath?: string + tooltip?: string } const NavItemBlock = styled.div<NavItemBlockProps>` @@ -62,6 +63,7 @@ export const IconTextGroup = ({ iconPath, expanded, currentPath, + tooltip, }: IconTextGroupProps) => { const [isHover, setIsHover] = useState(false) return ( @@ -72,17 +74,27 @@ export const IconTextGroup = ({ onMouseOver={() => setIsHover(true)} onMouseOut={() => setIsHover(false)} > - <div className="text-light-text2 group-hover:text-light-main dark:text-dark-text2 dark:group-hover:text-dark-text1"> - {isHover && hoverIcon ? hoverIcon : icon} + <div className="relative inline-flex items-center"> + {" "} + {/* This is the wrapper */} + <div className="text-light-text2 group-hover:text-light-main dark:text-dark-text2 dark:group-hover:text-dark-text1"> + {isHover && hoverIcon ? hoverIcon : icon} + </div> + {text && ( + <NavItemText + className="ml-2 text-light-text2 group-hover:text-light-main dark:text-dark-text2 dark:group-hover:text-dark-text1" // added ml-2 for spacing + expanded={expanded} + > + {text} + </NavItemText> + )} + {/* Tooltip rendering */} + {tooltip && expanded && ( + <span className="absolute top-1/2 left-full z-10 ml-2 w-auto -translate-y-1/2 transform whitespace-nowrap rounded bg-light-main px-3 py-2 text-xl text-dark-text1 dark:bg-dark-main"> + {tooltip} + </span> + )} </div> - {text && ( - <NavItemText - className="text-light-text2 group-hover:text-light-main dark:text-dark-text2 dark:group-hover:text-dark-text1" - expanded={expanded} - > - {text} - </NavItemText> - )} </NavItemBlock> ) } diff --git a/apps/root/src/components/frame/MobileNav.tsx b/apps/root/src/components/frame/MobileNav.tsx index 857c768c9..c4b714112 100644 --- a/apps/root/src/components/frame/MobileNav.tsx +++ b/apps/root/src/components/frame/MobileNav.tsx @@ -27,11 +27,18 @@ const MobileNav = ({ navItems, openSignInModal }: Props) => { className="flex-1 text-center" customOnClick={() => setCurrentPath(item.path)} > - <div className="text-light-text2 group-hover:text-light-main dark:text-dark-text2 dark:group-hover:text-dark-text1"> - {item.icon} - </div> - <div className="text-lg text-light-text2 group-hover:text-light-main dark:text-dark-text2 dark:group-hover:text-dark-text1"> - {item.name} + <div className="relative"> + <div className="text-light-text2 group-hover:text-light-main dark:text-dark-text2 dark:group-hover:text-dark-text1"> + {item.icon} + </div> + <div className="text-lg text-light-text2 group-hover:text-light-main dark:text-dark-text2 dark:group-hover:text-dark-text1"> + {item.name} + </div> + {item.tooltip && ( + <span className="absolute top-0 left-1/2 z-10 -translate-x-1/2 -translate-y-full transform whitespace-nowrap rounded bg-light-main px-3 py-2 text-lg text-dark-text1 dark:bg-dark-main"> + {item.tooltip} + </span> + )} </div> </LinkOutsideRouter> )) diff --git a/apps/root/src/components/frame/Nav.tsx b/apps/root/src/components/frame/Nav.tsx index e46190463..981982898 100644 --- a/apps/root/src/components/frame/Nav.tsx +++ b/apps/root/src/components/frame/Nav.tsx @@ -26,6 +26,13 @@ import { TimetableIconHovered, } from "@app/components/icons/TimetableIcon" import { ThemeContext, ThemeProvider } from "@app/utils/theme-context" +import { + getCurrentDateInJST, + getCurrentDateInUTC, + extractDate, +} from "@app/utils/getDate" +import { shouldCallApi } from "@app/utils/shouldCallApi" +import { fetchNotificaiton } from "@app/utils/fetchNotification" const Sidebar = lazy(() => import("@app/components/frame/Sidebar")) const MobileNav = lazy(() => import("@app/components/frame/MobileNav")) @@ -37,6 +44,7 @@ export interface NavItemsProps { path: string icon: ReactNode iconHovered?: ReactNode + tooltip?: string } const Nav = () => { @@ -50,6 +58,7 @@ const Nav = () => { const { t, i18n } = useTranslation() useEffect(() => { + // fetchNotificaiton("") window.onstorage = () => { i18n.changeLanguage(localStorage.getItem("wasedatime-lng")) } @@ -60,6 +69,52 @@ const Nav = () => { page_path: page_path, }) + const fetchNotificationAndUpdateState = async () => { + try { + const storedDateInJST = localStorage.getItem("lastCheckedDateJST") + const currentDateInJST = getCurrentDateInJST() + const storedDateOnly = extractDate(storedDateInJST || "") + const currentDateOnly = extractDate(currentDateInJST) + + if (!storedDateOnly) { + localStorage.setItem("lastCheckedDateJST", currentDateInJST) + } else if (shouldCallApi()) { + const newPostsCount = await fetchNotificaiton(storedDateInJST || "") + const updatedNavItems = navItems.map((item) => + item.path === "/forum" + ? { + ...item, + tooltip: + newPostsCount > 0 + ? `${newPostsCount} unread posts!` + : "Share something in WTF!", + } + : item + ) + setNavItems(updatedNavItems) + localStorage.setItem("lastCheckedDateJST", currentDateInJST) + localStorage.setItem( + "lastApiCallTimestamp", + new Date().getTime().toString() + ) + } else { + const updatedNavItems = navItems.map((item) => + item.path === "/forum" + ? { + ...item, + tooltip: "Check out WTF!", + } + : item + ) + setNavItems(updatedNavItems) + } + } catch (error) { + console.error("Error fetching notifications:", error) + } + } + + fetchNotificationAndUpdateState() + return history.listen(({ location, action }) => { if (action === "POP") { const new_page_path = location.pathname + location.search @@ -71,7 +126,9 @@ const Nav = () => { }) }, []) - const navItems: NavItemsProps[] = [ + // const currentDateInUTC = getCurrentDateInUTC() + + const [navItems, setNavItems] = useState<NavItemsProps[]>([ { name: t("navigation.timetable"), path: "/courses/timetable", @@ -89,6 +146,7 @@ const Nav = () => { path: "/forum", icon: <ForumIcon />, iconHovered: <ForumIconHovered />, + tooltip: "Check out the new posts!", }, // { // name: t("navigation.campus"), @@ -102,7 +160,41 @@ const Nav = () => { // icon: <FeedsIcon />, // iconHovered: <FeedsIconHovered />, // }, - ] + ]) + + // const navItems: NavItemsProps[] = [ + // { + // name: t("navigation.timetable"), + // path: "/courses/timetable", + // icon: <TimetableIcon />, + // iconHovered: <TimetableIconHovered />, + // }, + // { + // name: t("navigation.syllabus"), + // path: "/courses/syllabus", + // icon: <SyllabusIcon />, + // iconHovered: <SyllabusIconHovered />, + // }, + // { + // name: t("navigation.forum"), + // path: "/forum", + // icon: <ForumIcon />, + // iconHovered: <ForumIconHovered />, + // tooltip: "Check out the new posts!", + // }, + // { + // name: t("navigation.campus"), + // path: "/campus", + // icon: <CampusIcon />, + // iconHovered: <CampusIconHovered />, + // }, + // { + // name: t("navigation.feeds"), + // path: "/feeds", + // icon: <FeedsIcon />, + // iconHovered: <FeedsIconHovered />, + // }, + // ] return ( <Suspense fallback=""> diff --git a/apps/root/src/components/frame/Sidebar.tsx b/apps/root/src/components/frame/Sidebar.tsx index 91528b110..8a58f68b8 100644 --- a/apps/root/src/components/frame/Sidebar.tsx +++ b/apps/root/src/components/frame/Sidebar.tsx @@ -115,6 +115,7 @@ const Sidebar = ({ navItems, openSignInModal }: SidebarProps) => { iconPath={item.path} expanded={expanded} currentPath={currentPath} + tooltip={item.tooltip} /> </LinkOutsideRouter> ))} diff --git a/apps/root/src/utils/fetchNotification.ts b/apps/root/src/utils/fetchNotification.ts new file mode 100644 index 000000000..88a57a77a --- /dev/null +++ b/apps/root/src/utils/fetchNotification.ts @@ -0,0 +1,18 @@ +import { API } from "@aws-amplify/api" + +export const fetchNotificaiton = async (lastCheckDate: string) => { + const res = await API.get( + "wasedatime-dev", + `/forum/notify?lastChecked=${lastCheckDate}`, + { + headers: { + "Content-Type": "application/json", + }, + response: true, + } + ) + + const threadCount = res.data.data + + return threadCount +} diff --git a/apps/root/src/utils/getDate.ts b/apps/root/src/utils/getDate.ts new file mode 100644 index 000000000..0ae37cae6 --- /dev/null +++ b/apps/root/src/utils/getDate.ts @@ -0,0 +1,30 @@ +export const getCurrentDateInJST = () => { + const date = new Date() + const jstOffset = 9 * 60 // JST is UTC+9 + const localOffset = date.getTimezoneOffset() + date.setMinutes(date.getMinutes() + localOffset + jstOffset) + + const YYYY = date.getFullYear() + const MM = String(date.getMonth() + 1).padStart(2, "0") // Months are 0-based + const DD = String(date.getDate()).padStart(2, "0") + const HH = String(date.getHours()).padStart(2, "0") + const mm = String(date.getMinutes()).padStart(2, "0") + const SS = String(date.getSeconds()).padStart(2, "0") + + return `${YYYY}${MM}${DD}${HH}${mm}${SS}` +} + +export const extractDate = (fullDate: string) => { + return fullDate.substring(0, 8) // Extracts the first 8 characters (YYYYMMDD) +} + +export const getCurrentDateInUTC = () => { + const date = new Date() + const YYYY = date.getUTCFullYear() + const MM = String(date.getUTCMonth() + 1).padStart(2, "0") + const DD = String(date.getUTCDate()).padStart(2, "0") + const HH = String(date.getUTCHours()).padStart(2, "0") + const mm = String(date.getUTCMinutes()).padStart(2, "0") + const SS = String(date.getUTCSeconds()).padStart(2, "0") + return `${YYYY}${MM}${DD}${HH}${mm}${SS}` +} diff --git a/apps/root/src/utils/shouldCallApi.ts b/apps/root/src/utils/shouldCallApi.ts new file mode 100644 index 000000000..80ad15ffb --- /dev/null +++ b/apps/root/src/utils/shouldCallApi.ts @@ -0,0 +1,26 @@ +const LAST_API_CALL_TIMESTAMP = "lastApiCallTimestamp" +const ONE_HOUR_IN_MS = 60 * 60 * 1000 + +const convertToTimestamp = (datetime: string) => { + const year = parseInt(datetime.substring(0, 4), 10) + const month = parseInt(datetime.substring(4, 6), 10) - 1 // Months in JS are 0-indexed + const day = parseInt(datetime.substring(6, 8), 10) + const hour = parseInt(datetime.substring(8, 10), 10) + const minute = parseInt(datetime.substring(10, 12), 10) + const second = parseInt(datetime.substring(12, 14), 10) + + return new Date(year, month, day, hour, minute, second).getTime() +} + +export const shouldCallApi = () => { + const lastCallTimestamp = localStorage.getItem("lastApiCallTimestamp") + + if (!lastCallTimestamp) { + return true + } + + const timeSinceLastCall = + new Date().getTime() - parseInt(lastCallTimestamp, 10) + + return timeSinceLastCall > ONE_HOUR_IN_MS +} diff --git a/apps/root/src/wasedatime-root-config.ts b/apps/root/src/wasedatime-root-config.ts index 07b647f77..5358defc2 100644 --- a/apps/root/src/wasedatime-root-config.ts +++ b/apps/root/src/wasedatime-root-config.ts @@ -16,10 +16,24 @@ import Nav from "@app/components/frame/Nav" import { registerSW } from "virtual:pwa-register" +import { API } from "@aws-amplify/api" + if (import.meta.env.MODE !== "development" && "serviceWorker" in navigator) { registerSW() } +const apiConfig = { + API: { + endpoints: [ + { + name: "wasedatime-dev", + endpoint: import.meta.env.VITE_API_BASE_URL, + }, + ], + }, +} +API.configure(apiConfig) + const routes = constructRoutes(document.querySelector("#single-spa-layout")) const applications = constructApplications({ routes,