diff --git a/src/assets/Icons/q-trade-logo.webp b/src/assets/Icons/q-trade-logo.webp
new file mode 100644
index 0000000..77ba972
Binary files /dev/null and b/src/assets/Icons/q-trade-logo.webp differ
diff --git a/src/common/Spinners/BarSpinner/BarSpinner.tsx b/src/common/Spinners/BarSpinner/BarSpinner.tsx
new file mode 100644
index 0000000..a51a512
--- /dev/null
+++ b/src/common/Spinners/BarSpinner/BarSpinner.tsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import './barSpinner.css'
+export const BarSpinner = ({width = '20px', color}) => {
+ return (
+
+ )
+}
diff --git a/src/common/Spinners/BarSpinner/barSpinner.css b/src/common/Spinners/BarSpinner/barSpinner.css
new file mode 100644
index 0000000..529bc50
--- /dev/null
+++ b/src/common/Spinners/BarSpinner/barSpinner.css
@@ -0,0 +1,19 @@
+/* HTML: */
+.loader-bar {
+ width: 45px;
+ aspect-ratio: .75;
+ --c:no-repeat linear-gradient(currentColor 0 0);
+ background:
+ var(--c) 0% 100%,
+ var(--c) 50% 100%,
+ var(--c) 100% 100%;
+ background-size: 20% 65%;
+ animation: l8 1s infinite linear;
+ }
+ @keyframes l8 {
+ 16.67% {background-position: 0% 0% ,50% 100%,100% 100%}
+ 33.33% {background-position: 0% 0% ,50% 0% ,100% 100%}
+ 50% {background-position: 0% 0% ,50% 0% ,100% 0% }
+ 66.67% {background-position: 0% 100%,50% 0% ,100% 0% }
+ 83.33% {background-position: 0% 100%,50% 100%,100% 0% }
+ }
\ No newline at end of file
diff --git a/src/components/Explore/Explore.tsx b/src/components/Explore/Explore.tsx
new file mode 100644
index 0000000..7cc96a0
--- /dev/null
+++ b/src/components/Explore/Explore.tsx
@@ -0,0 +1,101 @@
+import { Box, ButtonBase, Typography } from "@mui/material";
+import React from "react";
+import ChatIcon from "@mui/icons-material/Chat";
+import qTradeLogo from "../../assets/Icons/q-trade-logo.webp";
+import AppsIcon from "@mui/icons-material/Apps";
+import { executeEvent } from "../../utils/events";
+export const Explore = ({setDesktopViewMode}) => {
+ return (
+
+ {
+ executeEvent("addTab", {
+ data: { service: "APP", name: "q-trade" },
+ });
+ executeEvent("open-apps-mode", {});
+ }}
+ >
+
+
+ Trade QORT
+
+
+ {
+ setDesktopViewMode('apps')
+
+ }}
+ >
+
+
+ See Apps
+
+
+ {
+ executeEvent("openGroupMessage", {
+ from: "0" ,
+ });
+ }}
+ >
+
+
+ General Chat
+
+
+
+ );
+};
diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx
index 48ef02c..08f75e9 100644
--- a/src/components/Group/Group.tsx
+++ b/src/components/Group/Group.tsx
@@ -1299,11 +1299,11 @@ export const Group = ({
if (isLoadingOpenSectionFromNotification.current) return;
const groupId = e.detail?.from;
-
const findGroup = groups?.find((group) => +group?.groupId === +groupId);
if (findGroup?.groupId === selectedGroup?.groupId) {
isLoadingOpenSectionFromNotification.current = false;
-
+ setChatMode("groups");
+ setDesktopViewMode('chat')
return;
}
if (findGroup) {
diff --git a/src/components/Group/GroupInvites.tsx b/src/components/Group/GroupInvites.tsx
index 0eda0f6..13e5850 100644
--- a/src/components/Group/GroupInvites.tsx
+++ b/src/components/Group/GroupInvites.tsx
@@ -10,16 +10,20 @@ import CommentIcon from "@mui/icons-material/Comment";
import InfoIcon from "@mui/icons-material/Info";
import GroupAddIcon from "@mui/icons-material/GroupAdd";
import { executeEvent } from "../../utils/events";
-import { Box, Typography } from "@mui/material";
+import { Box, ButtonBase, Collapse, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer";
import { getGroupNames } from "./UserListOfInvites";
import { CustomLoader } from "../../common/CustomLoader";
import { getBaseApiReact, isMobile } from "../../App";
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+import ExpandLessIcon from "@mui/icons-material/ExpandLess";
export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState(
[]
);
+ const [isExpanded, setIsExpanded] = React.useState(false);
+
const [loading, setLoading] = React.useState(true);
const getJoinRequests = async () => {
@@ -53,121 +57,129 @@ export const GroupInvites = ({ myAddress, setOpenAddGroup }) => {
alignItems: "center",
}}
>
- setIsExpanded((prev)=> !prev)}
>
- Group Invites:
+ Group Invites {groupsWithJoinRequests?.length > 0 && ` (${groupsWithJoinRequests?.length})`}
-
-
+ {isExpanded ? : (
+
+ )}
+
+
+
- {loading && groupsWithJoinRequests.length === 0 && (
-
+ {loading && groupsWithJoinRequests.length === 0 && (
+
+
+
+ )}
+ {!loading && groupsWithJoinRequests.length === 0 && (
+
+
+ Nothing to display
+
+
+ )}
+
-
-
- )}
- {!loading && groupsWithJoinRequests.length === 0 && (
-
-
- Nothing to display
-
-
- )}
-
- {groupsWithJoinRequests?.map((group) => {
- return (
- {
- setOpenAddGroup(true);
- setTimeout(() => {
- executeEvent("openGroupInvitesRequest", {});
- }, 300);
- }}
- disablePadding
- secondaryAction={
-
- {
+ return (
+ {
+ setOpenAddGroup(true);
+ setTimeout(() => {
+ executeEvent("openGroupInvitesRequest", {});
+ }, 300);
+ }}
+ disablePadding
+ secondaryAction={
+
+
+
+ }
+ >
+
+
-
- }
- >
-
-
-
-
- );
- })}
-
-
+
+
+ );
+ })}
+
+
+
);
};
diff --git a/src/components/Group/GroupJoinRequests.tsx b/src/components/Group/GroupJoinRequests.tsx
index 03c2258..76f958a 100644
--- a/src/components/Group/GroupJoinRequests.tsx
+++ b/src/components/Group/GroupJoinRequests.tsx
@@ -11,16 +11,20 @@ import InfoIcon from "@mui/icons-material/Info";
import { RequestQueueWithPromise } from "../../utils/queue/queue";
import GroupAddIcon from '@mui/icons-material/GroupAdd';
import { executeEvent } from "../../utils/events";
-import { Box, Typography } from "@mui/material";
+import { Box, ButtonBase, Collapse, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer";
import { CustomLoader } from "../../common/CustomLoader";
import { getBaseApi } from "../../background";
import { MyContext, getBaseApiReact, isMobile } from "../../App";
import { myGroupsWhereIAmAdminAtom } from "../../atoms/global";
import { useSetRecoilState } from "recoil";
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import ExpandLessIcon from '@mui/icons-material/ExpandLess';
export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2)
export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection, setMobileViewMode, setDesktopViewMode }) => {
+ const [isExpanded, setIsExpanded] = React.useState(false)
+
const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([])
const [loading, setLoading] = React.useState(true)
const {txList, setTxList} = React.useContext(MyContext)
@@ -109,26 +113,33 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get
flexDirection: "column",
alignItems: 'center'
}}>
- setIsExpanded((prev)=> !prev)}
>
- Join Requests:
+ Join Requests {filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length > 0 && ` (${filteredJoinRequests?.filter((group)=> group?.data?.length > 0)?.length})`}
-
-
-
+ {isExpanded ? : (
+
+ )}
+
+
+
);
};
diff --git a/src/components/Group/HomeDesktop.tsx b/src/components/Group/HomeDesktop.tsx
index e3b847d..26cf612 100644
--- a/src/components/Group/HomeDesktop.tsx
+++ b/src/components/Group/HomeDesktop.tsx
@@ -1,4 +1,4 @@
-import { Box, Button, Typography } from "@mui/material";
+import { Box, Button, Divider, Typography } from "@mui/material";
import React from "react";
import { Spacer } from "../../common/Spacer";
import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched";
@@ -7,7 +7,10 @@ import { GroupJoinRequests } from "./GroupJoinRequests";
import { GroupInvites } from "./GroupInvites";
import RefreshIcon from "@mui/icons-material/Refresh";
import { ListOfGroupPromotions } from "./ListOfGroupPromotions";
-
+import { QortPrice } from "../Home/QortPrice";
+import ExploreIcon from "@mui/icons-material/Explore";
+import { Explore } from "../Explore/Explore";
+import { NewUsersCTA } from "../Home/NewUsersCTA";
export const HomeDesktop = ({
refreshHomeDataFunc,
myAddress,
@@ -22,12 +25,12 @@ export const HomeDesktop = ({
setOpenAddGroup,
setMobileViewMode,
setDesktopViewMode,
- desktopViewMode
+ desktopViewMode,
}) => {
return (
-
- 15 ? "16px" : "20px",
- padding: '10px'
+ display: "flex",
+ width: "100%",
+ flexDirection: "column",
+ height: "100%",
+ alignItems: "flex-start",
+ maxWidth: "1036px",
}}
>
- Welcome
- {userInfo?.name ? (
- {`, ${userInfo?.name}`}
- ) : null}
-
-
- {!isLoadingGroups && (
- 15 ? "16px" : "20px",
+ padding: "10px",
}}
>
-
-
-
- {desktopViewMode === 'home' && (
- <>
- {`, ${userInfo?.name}`}
+ ) : null}
+
+
+ {!isLoadingGroups && (
+
+
+
+ item?.groupId !== "0").length !== 0
+ }
+ />
+
+
+ {desktopViewMode === "home" && (
+ <>
+ {/*
+ */}
+
+
+
+
+
+
+ >
+ )}
+
+
-
-
+ )}
+
+ {!isLoadingGroups && (
+ <>
+
+
+
+ {" "}
+
+ Explore
+ {" "}
-
-
+
+
+
+
- >
- )}
-
-
- )}
- {!isLoadingGroups && (
-
- )}
+
+ >
+
+ )}
-
{/*
*/}
-
+
);
diff --git a/src/components/Group/ListOfGroupPromotions.tsx b/src/components/Group/ListOfGroupPromotions.tsx
index 2f474d3..a983f56 100644
--- a/src/components/Group/ListOfGroupPromotions.tsx
+++ b/src/components/Group/ListOfGroupPromotions.tsx
@@ -9,6 +9,8 @@ import {
Avatar,
Box,
Button,
+ ButtonBase,
+ Collapse,
Dialog,
DialogActions,
DialogContent,
@@ -28,8 +30,8 @@ import {
import { getNameInfo } from "./Group";
import { getBaseApi, getFee } from "../../background";
import { LoadingButton } from "@mui/lab";
-import LockIcon from '@mui/icons-material/Lock';
-import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred';
+import LockIcon from "@mui/icons-material/Lock";
+import NoEncryptionGmailerrorredIcon from "@mui/icons-material/NoEncryptionGmailerrorred";
import {
MyContext,
getArbitraryEndpointReact,
@@ -40,7 +42,11 @@ import { Spacer } from "../../common/Spacer";
import { CustomLoader } from "../../common/CustomLoader";
import { RequestQueueWithPromise } from "../../utils/queue/queue";
import { useRecoilState } from "recoil";
-import { myGroupsWhereIAmAdminAtom, promotionTimeIntervalAtom, promotionsAtom } from "../../atoms/global";
+import {
+ myGroupsWhereIAmAdminAtom,
+ promotionTimeIntervalAtom,
+ promotionsAtom,
+} from "../../atoms/global";
import { Label } from "./AddGroup";
import ShortUniqueId from "short-unique-id";
import { CustomizedSnackbars } from "../Snackbar/Snackbar";
@@ -48,7 +54,8 @@ import { getGroupNames } from "./UserListOfInvites";
import { WrapperUserAction } from "../WrapperUserAction";
import { useVirtualizer } from "@tanstack/react-virtual";
import ErrorBoundary from "../../common/ErrorBoundary";
-
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+import ExpandLessIcon from "@mui/icons-material/ExpandLess";
export const requestQueuePromos = new RequestQueueWithPromise(20);
export function utf8ToBase64(inputString: string): string {
@@ -65,8 +72,6 @@ export function utf8ToBase64(inputString: string): string {
const uid = new ShortUniqueId({ length: 8 });
-
-
export function getGroupId(str) {
const match = str.match(/group-(\d+)-/);
return match ? match[1] : null;
@@ -82,12 +87,12 @@ export const ListOfGroupPromotions = () => {
const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState(
myGroupsWhereIAmAdminAtom
);
- const [promotions, setPromotions] = useRecoilState(
- promotionsAtom
- );
+ const [promotions, setPromotions] = useRecoilState(promotionsAtom);
const [promotionTimeInterval, setPromotionTimeInterval] = useRecoilState(
promotionTimeIntervalAtom
);
+ const [isExpanded, setIsExpanded] = React.useState(false);
+
const [openSnack, setOpenSnack] = useState(false);
const [infoSnack, setInfoSnack] = useState(null);
const [fee, setFee] = useState(null);
@@ -96,16 +101,16 @@ export const ListOfGroupPromotions = () => {
const { show, setTxList } = useContext(MyContext);
const listRef = useRef();
- const rowVirtualizer = useVirtualizer({
- count: promotions.length,
- getItemKey: React.useCallback(
- (index) => promotions[index]?.identifier,
- [promotions]
- ),
- getScrollElement: () => listRef.current,
- estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed
- overscan: 10, // Number of items to render outside the visible area to improve smoothness
- });
+ const rowVirtualizer = useVirtualizer({
+ count: promotions.length,
+ getItemKey: React.useCallback(
+ (index) => promotions[index]?.identifier,
+ [promotions]
+ ),
+ getScrollElement: () => listRef.current,
+ estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed
+ overscan: 10, // Number of items to render outside the visible area to improve smoothness
+ });
useEffect(() => {
try {
@@ -117,7 +122,7 @@ export const ListOfGroupPromotions = () => {
}, []);
const getPromotions = useCallback(async () => {
try {
- setPromotionTimeInterval(Date.now())
+ setPromotionTimeInterval(Date.now());
const identifier = `group-promotions-ui24-`;
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`;
const response = await fetch(url, {
@@ -168,7 +173,9 @@ export const ListOfGroupPromotions = () => {
});
await Promise.all(getPromos);
- const groupWithInfo = await getGroupNames(data.sort((a, b) => b.created - a.created));
+ const groupWithInfo = await getGroupNames(
+ data.sort((a, b) => b.created - a.created)
+ );
setPromotions(groupWithInfo);
} catch (error) {
console.error(error);
@@ -177,22 +184,23 @@ export const ListOfGroupPromotions = () => {
useEffect(() => {
const now = Date.now();
-
+
const timeSinceLastFetch = now - promotionTimeInterval;
- const initialDelay = timeSinceLastFetch >= THIRTY_MINUTES
- ? 0
- : THIRTY_MINUTES - timeSinceLastFetch;
+ const initialDelay =
+ timeSinceLastFetch >= THIRTY_MINUTES
+ ? 0
+ : THIRTY_MINUTES - timeSinceLastFetch;
const initialTimeout = setTimeout(() => {
getPromotions();
-
+
// Start a 30-minute interval
const interval = setInterval(() => {
getPromotions();
}, THIRTY_MINUTES);
-
+
return () => clearInterval(interval);
}, initialDelay);
-
+
return () => clearTimeout(initialTimeout);
}, [getPromotions, promotionTimeInterval]);
@@ -328,100 +336,143 @@ export const ListOfGroupPromotions = () => {
}
};
-
-
return (
-
-
+ setIsExpanded((prev) => !prev)}
>
- Group Promotions
+ Group promotions {promotions.length > 0 && ` (${promotions.length})`}
-
-
-
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
-
- {loading && promotions.length === 0 && (
+
+ <>
-
-
- )}
- {!loading && promotions.length === 0 && (
-
-
- Nothing to display
-
+
+
+
+
- )}
+
+ {loading && promotions.length === 0 && (
+
+
+
+ )}
+ {!loading && promotions.length === 0 && (
+
+
+ Nothing to display
+
+
+ )}
{
const index = virtualRow.index;
const promotion = promotions[index];
return (
-
{
gap: "5px",
}}
>
-
- Error loading content: Invalid Data
-
- }
- >
-
- {
- if (reason === "backdropClick") {
- // Prevent closing on backdrop click
- return;
- }
- handlePopoverClose(); // Close only on other events like Esc key press
- }}
- anchorOrigin={{
- vertical: "top",
- horizontal: "center",
- }}
- transformOrigin={{
- vertical: "bottom",
- horizontal: "center",
- }}
- style={{ marginTop: "8px" }}
- >
-
-
- Group name: {` ${promotion?.groupName}`}
-
-
- Number of members: {` ${promotion?.memberCount}`}
-
- {promotion?.description && (
-
- {promotion?.description}
-
- )}
- {promotion?.isOpen === false && (
-
- *This is a closed/private group, so you will need to wait
- until an admin accepts your request
-
- )}
-
-
-
- Close
-
-
- handleJoinGroup(promotion, promotion?.isOpen)
- }
- >
- Join
-
-
-
-
+
+ Error loading content: Invalid Data
+
+ }
+ >
+
+ {
+ if (reason === "backdropClick") {
+ // Prevent closing on backdrop click
+ return;
+ }
+ handlePopoverClose(); // Close only on other events like Esc key press
+ }}
+ anchorOrigin={{
+ vertical: "top",
+ horizontal: "center",
+ }}
+ transformOrigin={{
+ vertical: "bottom",
+ horizontal: "center",
+ }}
+ style={{ marginTop: "8px" }}
+ >
+
+
+ Group name: {` ${promotion?.groupName}`}
+
+
+ Number of members:{" "}
+ {` ${promotion?.memberCount}`}
+
+ {promotion?.description && (
+
+ {promotion?.description}
+
+ )}
+ {promotion?.isOpen === false && (
+
+ *This is a closed/private group, so you
+ will need to wait until an admin accepts
+ your request
+
+ )}
+
+
+
+ Close
+
+
+ handleJoinGroup(
+ promotion,
+ promotion?.isOpen
+ )
+ }
+ >
+ Join
+
+
+
+
-
-
-
- {promotion?.name?.charAt(0)}
-
-
- {promotion?.name}
-
-
-
- {promotion?.groupName}
-
-
-
-
- {promotion?.isOpen === false && (
-
- )}
- {promotion?.isOpen === true && (
-
- )}
-
- {promotion?.isOpen ? 'Public group' : 'Private group' }
-
-
-
-
- {promotion?.data}
-
-
-
-
-
-
-
+
+
+
+ {promotion?.name?.charAt(0)}
+
+
+ {promotion?.name}
+
+
+
+ {promotion?.groupName}
+
+
+
+
+ {promotion?.isOpen === false && (
+
+ )}
+ {promotion?.isOpen === true && (
+
+ )}
+
+ {promotion?.isOpen
+ ? "Public group"
+ : "Private group"}
+
+
+
+
+ {promotion?.data}
+
+
+
+
+
+
+
-
);
})}
-
-
+
+ >
+
{isShowModal && (
diff --git a/src/components/Group/QMailMessages.tsx b/src/components/Group/QMailMessages.tsx
index ccce59b..ee62a4c 100644
--- a/src/components/Group/QMailMessages.tsx
+++ b/src/components/Group/QMailMessages.tsx
@@ -1,11 +1,11 @@
-import React, { useCallback, useEffect, useState } from 'react'
+import React, { useCallback, useEffect, useMemo, useState } from 'react'
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import moment from 'moment'
-import { Box, Typography } from "@mui/material";
+import { Box, ButtonBase, Collapse, Typography } from "@mui/material";
import { Spacer } from "../../common/Spacer";
import { getBaseApiReact, isMobile } from "../../App";
import { MessagingIcon } from '../../assets/Icons/MessagingIcon';
@@ -15,6 +15,10 @@ import { executeEvent } from '../../utils/events';
import { CustomLoader } from '../../common/CustomLoader';
import { useRecoilState } from 'recoil';
import { mailsAtom, qMailLastEnteredTimestampAtom } from '../../atoms/global';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import ExpandLessIcon from '@mui/icons-material/ExpandLess';
+import MarkEmailUnreadIcon from '@mui/icons-material/MarkEmailUnread';
+import { last } from 'slate';
export const isLessThanOneWeekOld = (timestamp) => {
// Current time in milliseconds
const now = Date.now();
@@ -41,6 +45,7 @@ export function formatEmailDate(timestamp: number) {
}
}
export const QMailMessages = ({userName, userAddress}) => {
+ const [isExpanded, setIsExpanded] = useState(false)
const [mails, setMails] = useRecoilState(mailsAtom)
const [lastEnteredTimestamp, setLastEnteredTimestamp] = useRecoilState(qMailLastEnteredTimestampAtom)
const [loading, setLoading] = useState(true)
@@ -99,7 +104,16 @@ export const QMailMessages = ({userName, userAddress}) => {
}, [getMails, userName, userAddress]);
-
+ const anyUnread = useMemo(()=> {
+ let unread = false
+
+ mails.forEach((mail)=> {
+ if(lastEnteredTimestamp && isLessThanOneWeekOld(mail?.created)){
+ unread = true
+ }
+ })
+ return unread
+ }, [mails, lastEnteredTimestamp])
return (
{
}}
>
- setIsExpanded((prev)=> !prev)}
>
Latest Q-Mails
-
-
-
+
+ {isExpanded ? : (
+
+ )}
+
+
{
+
)
}
diff --git a/src/components/Group/ThingsToDoInitial.tsx b/src/components/Group/ThingsToDoInitial.tsx
index 6825947..0a02f36 100644
--- a/src/components/Group/ThingsToDoInitial.tsx
+++ b/src/components/Group/ThingsToDoInitial.tsx
@@ -64,6 +64,7 @@ if(hasDoneNameAndBalanceAndIsLoaded){
);
}
+if(!isLoaded) return null
return (
@@ -96,7 +97,6 @@ if(hasDoneNameAndBalanceAndIsLoaded){
{
+ if (balance === undefined || +balance > 0) return null;
+ return (
+
+
+
+
+
+ Are you a new user?
+
+
+
+ Please message us on Telegram or Discord if you need 4 QORT to start
+ chatting without any limitations
+
+
+
+ {
+ if (window?.electronAPI?.openExternal) {
+ window.electronAPI.openExternal(
+ "https://link.qortal.dev/telegram-invite"
+ );
+ } else {
+ window.open(
+ "https://link.qortal.dev/telegram-invite",
+ "_blank"
+ );
+ }
+ }}
+ >
+ Telegram
+
+ {
+ if (window?.electronAPI?.openExternal) {
+ window.electronAPI.openExternal(
+ "https://link.qortal.dev/discord-invite"
+ );
+ } else {
+ window.open("https://link.qortal.dev/discord-invite", "_blank");
+ }
+ }}
+ >
+ Discord
+
+
+
+
+ );
+};
diff --git a/src/components/Home/QortPrice.tsx b/src/components/Home/QortPrice.tsx
new file mode 100644
index 0000000..3d54a04
--- /dev/null
+++ b/src/components/Home/QortPrice.tsx
@@ -0,0 +1,209 @@
+import React, { useCallback, useEffect, useState } from 'react'
+import { getBaseApiReact } from '../../App';
+import { Box, Tooltip, Typography } from '@mui/material';
+import { BarSpinner } from '../../common/Spinners/BarSpinner/BarSpinner';
+
+function getAverageLtcPerQort(trades) {
+ let totalQort = 0;
+ let totalLtc = 0;
+
+ trades.forEach((trade) => {
+ const qort = parseFloat(trade.qortAmount);
+ const ltc = parseFloat(trade.foreignAmount);
+
+ totalQort += qort;
+ totalLtc += ltc;
+ });
+
+ // Avoid division by zero
+ if (totalQort === 0) return 0;
+
+ // Weighted average price
+ return parseFloat((totalLtc / totalQort).toFixed(8));
+ }
+
+ function getTwoWeeksAgoTimestamp() {
+ const now = new Date();
+ now.setDate(now.getDate() - 14); // Subtract 14 days
+ return now.getTime(); // Get timestamp in milliseconds
+ }
+
+ function formatWithCommasAndDecimals(number) {
+
+ return Number(number).toLocaleString();
+ }
+
+
+export const QortPrice = () => {
+ const [ltcPerQort, setLtcPerQort] = useState(null)
+ const [supply, setSupply] = useState(null)
+ const [lastBlock, setLastBlock] = useState(null)
+ const [loading, setLoading] = useState(true)
+
+ const getPrice = useCallback(async () => {
+ try {
+ setLoading(true)
+
+ const response = await fetch(`${getBaseApiReact()}/crosschain/trades?foreignBlockchain=LITECOIN&minimumTimestamp=${getTwoWeeksAgoTimestamp()}&limit=20&reverse=true`);
+ const data = await response.json();
+
+
+ setLtcPerQort(getAverageLtcPerQort(data));
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setLoading(false)
+
+ }
+ }, [])
+
+ const getLastBlock = useCallback(async () => {
+ try {
+ setLoading(true)
+
+ const response = await fetch(`${getBaseApiReact()}/blocks/last`);
+ const data = await response.json();
+
+ setLastBlock(data);
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setLoading(false)
+
+ }
+ }, [])
+
+ const getSupplyInCirculation = useCallback(async () => {
+ try {
+ setLoading(true)
+
+ const response = await fetch(`${getBaseApiReact()}/stats/supply/circulating`);
+ const data = await response.text();
+ formatWithCommasAndDecimals(data)
+ setSupply(formatWithCommasAndDecimals(data));
+ } catch (error) {
+ console.error(error);
+ } finally {
+ setLoading(false)
+
+ }
+ }, [])
+
+
+
+
+ useEffect(() => {
+
+ getPrice();
+ getSupplyInCirculation()
+ getLastBlock()
+ const interval = setInterval(() => {
+ getPrice();
+ getSupplyInCirculation()
+ getLastBlock()
+ }, 900000);
+
+ return () => clearInterval(interval);
+
+ }, [getPrice]);
+
+ console.log('supply', supply)
+
+ return (
+
+ Based on the latest 20 trades}
+ placement="bottom"
+ arrow
+ sx={{ fontSize: "24" }}
+ slotProps={{
+ tooltip: {
+ sx: {
+ color: "#ffffff",
+ backgroundColor: "#444444",
+ },
+ },
+ arrow: {
+ sx: {
+ color: "#444444",
+ },
+ },
+ }}
+ >
+
+ Price
+ {!ltcPerQort ? (
+
+ ): (
+ {ltcPerQort} LTC/QORT
+ )}
+
+
+
+
+
+ Supply
+ {!supply ? (
+
+ ): (
+ {supply} QORT
+ )}
+
+
+
+ Last height
+ {!lastBlock?.height ? (
+
+ ): (
+ {lastBlock?.height}
+
+ )}
+
+
+
+ )
+}