diff --git a/public/manifest.json b/public/manifest.json index 5d88b27..b37d461 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -46,6 +46,6 @@ ], "content_security_policy": { - "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://api.qortal.org https://api2.qortal.org https://appnode.qortal.org https://apinode.qortalnodes.live https://apinode1.qortalnodes.live https://apinode2.qortalnodes.live https://apinode3.qortalnodes.live https://apinode4.qortalnodes.live https://ext-node.qortal.link wss://appnode.qortal.org wss://ext-node.qortal.link ws://127.0.0.1:12391 http://127.0.0.1:12391 https://ext-node.qortal.link; " + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://*:* http://*:* wss://*:* ws://*:*;" } } diff --git a/src/App.tsx b/src/App.tsx index 00cb3a0..53d88e8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -141,7 +141,7 @@ const defaultValues: MyContextInterface = { message: "", }, }; -export let isMobile = false; +export let isMobile = true; const isMobileDevice = () => { const userAgent = navigator.userAgent || navigator.vendor || window.opera; diff --git a/src/assets/svgs/ClearInput.svg b/src/assets/svgs/ClearInput.svg new file mode 100644 index 0000000..a4595df --- /dev/null +++ b/src/assets/svgs/ClearInput.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/svgs/LogoSelected.svg b/src/assets/svgs/LogoSelected.svg new file mode 100644 index 0000000..fec7e1e --- /dev/null +++ b/src/assets/svgs/LogoSelected.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svgs/Search.svg b/src/assets/svgs/Search.svg new file mode 100644 index 0000000..b6cb06b --- /dev/null +++ b/src/assets/svgs/Search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Apps/AppInfo.tsx b/src/components/Apps/AppInfo.tsx new file mode 100644 index 0000000..0e3be5a --- /dev/null +++ b/src/components/Apps/AppInfo.tsx @@ -0,0 +1,22 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + + AppsLibraryContainer, + +} from "./Apps-styles"; +import { Avatar, Box, ButtonBase, InputBase } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { getBaseApiReact } from "../../App"; +import LogoSelected from "../../assets/svgs/LogoSelected.svg"; + + +import { Spacer } from "../../common/Spacer"; + +export const AppInfo = ({ app }) => { + + return ( + + + + ); +}; diff --git a/src/components/Apps/Apps-styles.tsx b/src/components/Apps/Apps-styles.tsx new file mode 100644 index 0000000..1549596 --- /dev/null +++ b/src/components/Apps/Apps-styles.tsx @@ -0,0 +1,108 @@ +import { + AppBar, + Button, + Toolbar, + Typography, + Box, + TextField, + InputLabel, + } from "@mui/material"; + import { styled } from "@mui/system"; + + export const AppsParent = styled(Box)(({ theme }) => ({ + display: "flex", + width: "100%", + flexDirection: "column", + height: "100%", + alignItems: "center", + overflow: 'auto', + // For WebKit-based browsers (Chrome, Safari, etc.) + "::-webkit-scrollbar": { + width: "0px", // Set the width to 0 to hide the scrollbar + height: "0px", // Set the height to 0 for horizontal scrollbar + }, + + // For Firefox + scrollbarWidth: "none", // Hides the scrollbar in Firefox + + // Optional for better cross-browser consistency + "-ms-overflow-style": "none" // Hides scrollbar in IE and Edge + })); + export const AppsContainer = styled(Box)(({ theme }) => ({ + display: "flex", + width: "90%", + justifyContent: 'space-evenly', + gap: '24px', + flexWrap: 'wrap', + alignItems: 'flex-start', + + })); + export const AppsLibraryContainer = styled(Box)(({ theme }) => ({ + display: "flex", + width: "90%", + flexDirection: 'column', + justifyContent: 'flex-start', + alignItems: 'flex-start', + })); + export const AppsSearchContainer = styled(Box)(({ theme }) => ({ + display: "flex", + width: "90%", + justifyContent: 'space-between', + alignItems: 'center', + backgroundColor: '#434343', + borderRadius: '8px', + padding: '0px 10px', + height: '36px' + })); + export const AppsSearchLeft = styled(Box)(({ theme }) => ({ + display: "flex", + width: "90%", + justifyContent: 'flex-start', + alignItems: 'center', + gap: '10px' + })); + export const AppsSearchRight = styled(Box)(({ theme }) => ({ + display: "flex", + width: "90%", + justifyContent: 'flex-end', + alignItems: 'center', + })); + export const AppCircleContainer = styled(Box)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + gap: '5px', + alignItems: 'center', + width: '100%' + })); + export const Add = styled(Typography)(({ theme }) => ({ + fontSize: '36px', + fontWeight: 500, + lineHeight: '43.57px', + textAlign: 'left' + + })); + export const AppCircleLabel = styled(Typography)(({ theme }) => ({ + fontSize: '12px', + fontWeight: 500, + lineHeight: 1.2, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + width: '100%' + })); + export const AppLibrarySubTitle = styled(Typography)(({ theme }) => ({ + fontSize: '16px', + fontWeight: 500, + lineHeight: 1.2, + })); + export const AppCircle = styled(Box)(({ theme }) => ({ + display: "flex", + width: "60px", + flexDirection: "column", + height: "60px", + alignItems: 'center', + justifyContent: 'center', + borderRadius: '50%', + backgroundColor: "var(--apps-circle)", + border: '1px solid #FFFFFF' + })); \ No newline at end of file diff --git a/src/components/Apps/Apps.tsx b/src/components/Apps/Apps.tsx new file mode 100644 index 0000000..9ffe412 --- /dev/null +++ b/src/components/Apps/Apps.tsx @@ -0,0 +1,62 @@ +import React, { useEffect, useState } from 'react' +import { AppsHome } from './AppsHome' +import { Spacer } from '../../common/Spacer' +import { getBaseApiReact } from '../../App' +import { AppsLibrary } from './AppsLibrary' + +export const Apps = () => { + const [mode, setMode] = useState('home') + + const [availableQapps, setAvailableQapps] = useState([]) + const [downloadedQapps, setDownloadedQapps] = useState([]) + + const getQapps = React.useCallback( + async () => { + try { + let apps = [] + let websites = [] + // dispatch(setIsLoadingGlobal(true)) + const url = `${getBaseApiReact()}/arbitrary/resources/search?service=APP&mode=ALL&includestatus=true&limit=0&includemetadata=true`; + + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if(!response?.ok) return + const responseData = await response.json(); + const urlWebsites = `${getBaseApiReact()}/arbitrary/resources/search?service=WEBSITE&mode=ALL&includestatus=true&limit=0&includemetadata=true`; + + const responseWebsites = await fetch(urlWebsites, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + if(!responseWebsites?.ok) return + const responseDataWebsites = await responseWebsites.json(); + apps = responseData + websites = responseDataWebsites + const combine = [...apps, ...websites] + setAvailableQapps(combine) + setDownloadedQapps(combine.filter((qapp)=> qapp?.status?.status === 'READY')) + } catch (error) { + } finally { + // dispatch(setIsLoadingGlobal(false)) + } + }, + [] + ); + useEffect(()=> { + getQapps() + }, [getQapps]) + + return ( + <> + + {mode === 'home' && } + {mode === 'library' && } + + ) +} diff --git a/src/components/Apps/AppsHome.tsx b/src/components/Apps/AppsHome.tsx new file mode 100644 index 0000000..775d7e0 --- /dev/null +++ b/src/components/Apps/AppsHome.tsx @@ -0,0 +1,56 @@ +import React, { useMemo, useState } from 'react' +import { AppCircle, AppCircleContainer, AppCircleLabel, AppsContainer, AppsParent } from './Apps-styles' +import { Avatar, ButtonBase } from '@mui/material' +import { Add } from '@mui/icons-material' +import { getBaseApiReact } from '../../App' +import LogoSelected from "../../assets/svgs/LogoSelected.svg"; + +export const AppsHome = ({downloadedQapps, setMode}) => { + + + return ( + + + { + setMode('library') + }}> + + + + + + Add + + + {downloadedQapps?.map((qapp)=> { + return ( + + + + + center-icon + + + {qapp?.metadata?.title || qapp?.name} + + + ) + })} + + + ) +} diff --git a/src/components/Apps/AppsLibrary.tsx b/src/components/Apps/AppsLibrary.tsx new file mode 100644 index 0000000..d801bc0 --- /dev/null +++ b/src/components/Apps/AppsLibrary.tsx @@ -0,0 +1,163 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { + AppCircle, + AppCircleContainer, + AppCircleLabel, + AppLibrarySubTitle, + AppsContainer, + AppsLibraryContainer, + AppsParent, + AppsSearchContainer, + AppsSearchLeft, + AppsSearchRight, +} from "./Apps-styles"; +import { Avatar, Box, ButtonBase, InputBase } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { getBaseApiReact } from "../../App"; +import LogoSelected from "../../assets/svgs/LogoSelected.svg"; +import IconSearch from "../../assets/svgs/Search.svg"; +import IconClearInput from "../../assets/svgs/ClearInput.svg"; + +import { Spacer } from "../../common/Spacer"; +const officialAppList = [ + "q-tube", + "q-blog", + "q-share", + "q-support", + "q-mail", + "qombo", + "q-fund", + "q-shop", +]; + +export const AppsLibrary = ({ downloadedQapps, availableQapps }) => { + const [searchValue, setSearchValue] = useState('') + const officialApps = useMemo(() => { + return availableQapps.filter((app) => app.service === 'APP' && + officialAppList.includes(app?.name?.toLowerCase()) + ); + }, [availableQapps]); + + const [debouncedValue, setDebouncedValue] = useState(''); // Debounced value + + // Debounce logic + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(searchValue); // Update debounced value after delay + }, 500); // 500ms debounce time (adjustable) + + // Cleanup timeout if searchValue changes before the timeout completes + return () => { + clearTimeout(handler); + }; + }, [searchValue]); // Runs effect when searchValue changes + + // Example: Perform search or other actions based on debouncedValue + + const searchedList = useMemo(()=> { + if(!debouncedValue) return [] + return availableQapps.filter((app)=> app.name.toLowerCase().includes(debouncedValue.toLowerCase())) + }, [debouncedValue]) + console.log('officialApps', searchedList) + + + return ( + + + + + + + setSearchValue(e.target.value)} + sx={{ ml: 1, flex: 1 }} + placeholder="Search for apps" + inputProps={{ 'aria-label': 'Search for apps', fontSize: '16px', fontWeight: 400 }} + /> + + + {searchValue && ( + { + setSearchValue('') + }}> + + + )} + + + + + + {searchedList?.length > 0 ? ( + <> + {searchedList.map((app)=> { + + return ( + + ) + })} + + ) : ( + <> + Official Apps + + + {officialApps?.map((qapp) => { + return ( + + + + + center-icon + + + + {qapp?.metadata?.title || qapp?.name} + + + + ); + })} + + + Featured + + + Categories + + )} + + + + ); +}; diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 45d9c57..3e09a3a 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -88,6 +88,7 @@ import { ExitIcon } from "../../assets/Icons/ExitIcon"; import { HomeDesktop } from "./HomeDesktop"; import { DesktopFooter } from "../Desktop/DesktopFooter"; import { DesktopHeader } from "../Desktop/DesktopHeader"; +import { Apps } from "../Apps/Apps"; // let touchStartY = 0; // let disablePullToRefresh = false; @@ -2733,6 +2734,9 @@ export const Group = ({ setMobileViewMode={setMobileViewMode} /> )} + {isMobile && mobileViewMode === "apps" && ( + + )} { !isMobile && !selectedGroup && groupSection === "home" && ( @@ -2958,7 +2962,7 @@ export const Group = ({ /> - {isMobile && mobileViewMode === "home" && !mobileViewModeKeepOpen && ( + {(isMobile && mobileViewMode === "home" || isMobile && mobileViewMode === "apps") && !mobileViewModeKeepOpen && ( <>
+ { + if(mobileViewMode === 'home'){ + setMobileViewMode('apps') + + } else { + setMobileViewMode('home') + + } + }}> {/* Custom Center Icon */} - center-icon + center-icon +