From 81af217e101062747a1a614496762b4c7a2e6630 Mon Sep 17 00:00:00 2001 From: Vitali Karpuk Date: Tue, 16 Jul 2024 12:11:40 +0300 Subject: [PATCH] added timeline modal and validate input --- ui/assets/success.svg | 26 +- .../atoms/alertMessage/img/CloseIcon.svg | 4 + .../atoms/alertMessage/img/InfoIcon.svg | 5 + .../atoms/alertMessage/img/ScamAlert.svg | 10 + .../atoms/alertMessage/img/error.svg | 4 + .../atoms/alertMessage/img/success.svg | 4 + .../atoms/alertMessage/img/warning.svg | 5 + .../atoms/alertMessage/index.module.css | 120 +++++++++ ui/components/atoms/alertMessage/index.tsx | 75 ++++++ .../atoms/copyIcon/copyIcon.module.css | 6 +- ui/components/atoms/copyIcon/iconElement.tsx | 29 +++ ui/components/atoms/copyIcon/index.tsx | 19 +- ui/components/atoms/loader/loader.tsx | 4 + ui/components/atoms/status/status.tsx | 4 + ui/components/atoms/timeline/index.module.css | 125 ++++++++++ ui/components/atoms/timeline/index.ts | 1 + ui/components/atoms/timeline/timeline.tsx | 231 ++++++++++++++++++ ui/components/molecules/modals/index.ts | 1 + .../modals/modalPurchase/modalPurchase.tsx | 1 - .../molecules/modals/modals.types.ts | 4 +- .../molecules/modals/modals.variant.ts | 2 + .../modals/pendingModal/index.module.css | 11 + .../molecules/modals/pendingModal/index.ts | 1 + .../modals/pendingModal/pendingInfo.tsx | 54 ++++ .../molecules/nameCard/index.module.css | 6 +- ui/components/molecules/nameCard/nameCard.tsx | 9 +- .../molecules/staticEllipse/staticEllipse.tsx | 7 +- .../components/names/allContent.tsx | 81 ++++-- .../organisms/accountConent/constants.ts | 5 +- .../buttonTemplate/buttonTemplate.tsx | 5 +- .../templates/buttonTemplate/index.module.css | 1 + .../organisms/table/view/nameCards.tsx | 7 +- .../sections/homeSection/homeSection.tsx | 31 ++- .../sections/homeSection/index.module.css | 5 + ui/helpers/timeHelper.ts | 67 +++++ ui/hooks/index.ts | 3 +- ui/hooks/useZkcloudworkerWS.ts | 61 +++++ ui/package-lock.json | 27 ++ ui/package.json | 1 + ui/store/index.tsx | 21 +- 40 files changed, 1029 insertions(+), 54 deletions(-) create mode 100644 ui/components/atoms/alertMessage/img/CloseIcon.svg create mode 100644 ui/components/atoms/alertMessage/img/InfoIcon.svg create mode 100644 ui/components/atoms/alertMessage/img/ScamAlert.svg create mode 100644 ui/components/atoms/alertMessage/img/error.svg create mode 100644 ui/components/atoms/alertMessage/img/success.svg create mode 100644 ui/components/atoms/alertMessage/img/warning.svg create mode 100644 ui/components/atoms/alertMessage/index.module.css create mode 100644 ui/components/atoms/alertMessage/index.tsx create mode 100644 ui/components/atoms/copyIcon/iconElement.tsx create mode 100644 ui/components/atoms/timeline/index.module.css create mode 100644 ui/components/atoms/timeline/index.ts create mode 100644 ui/components/atoms/timeline/timeline.tsx create mode 100644 ui/components/molecules/modals/pendingModal/index.module.css create mode 100644 ui/components/molecules/modals/pendingModal/index.ts create mode 100644 ui/components/molecules/modals/pendingModal/pendingInfo.tsx create mode 100644 ui/hooks/useZkcloudworkerWS.ts diff --git a/ui/assets/success.svg b/ui/assets/success.svg index 6bbd9dc..1828f7c 100644 --- a/ui/assets/success.svg +++ b/ui/assets/success.svg @@ -1,6 +1,20 @@ - - - - \ No newline at end of file + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/components/atoms/alertMessage/img/CloseIcon.svg b/ui/components/atoms/alertMessage/img/CloseIcon.svg new file mode 100644 index 0000000..bb62469 --- /dev/null +++ b/ui/components/atoms/alertMessage/img/CloseIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/components/atoms/alertMessage/img/InfoIcon.svg b/ui/components/atoms/alertMessage/img/InfoIcon.svg new file mode 100644 index 0000000..9186329 --- /dev/null +++ b/ui/components/atoms/alertMessage/img/InfoIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/components/atoms/alertMessage/img/ScamAlert.svg b/ui/components/atoms/alertMessage/img/ScamAlert.svg new file mode 100644 index 0000000..ed3fb51 --- /dev/null +++ b/ui/components/atoms/alertMessage/img/ScamAlert.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ui/components/atoms/alertMessage/img/error.svg b/ui/components/atoms/alertMessage/img/error.svg new file mode 100644 index 0000000..6632b77 --- /dev/null +++ b/ui/components/atoms/alertMessage/img/error.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/components/atoms/alertMessage/img/success.svg b/ui/components/atoms/alertMessage/img/success.svg new file mode 100644 index 0000000..879e99f --- /dev/null +++ b/ui/components/atoms/alertMessage/img/success.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/components/atoms/alertMessage/img/warning.svg b/ui/components/atoms/alertMessage/img/warning.svg new file mode 100644 index 0000000..aa9542f --- /dev/null +++ b/ui/components/atoms/alertMessage/img/warning.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/components/atoms/alertMessage/index.module.css b/ui/components/atoms/alertMessage/index.module.css new file mode 100644 index 0000000..e7c6515 --- /dev/null +++ b/ui/components/atoms/alertMessage/index.module.css @@ -0,0 +1,120 @@ +.alertMessage { + display: flex; + align-items: flex-start; + justify-content: flex-start; + padding: 10px 16px; + flex-direction: column; + border-radius: 7px; + line-height: 22px; + background-color: #EDEFF5; + position: relative; +} + +.alertMessage .messageWrapper { + width: 100%; + display: flex; + align-items: center; +} + +@media (max-width: 768px) { + .alertMessage .messageWrapper { + flex-direction: column; + align-items: start; + } +} + +.alertMessage .messageWrapper .messageInfo { + max-width: 100%; + width: 100%; + display: flex; + align-items: flex-start; +} + +.alertMessage .messageWrapper .messageInfo .icon { + min-width: 16px; + min-height: 16px; + margin-right: 6px; + margin-top: 3px; + display: flex; + align-items: center; +} + +.alertMessage .messageWrapper .readmoreBtn { + margin-left: 6px; + display: flex; + align-items: center; + padding-right: 5px; + user-select: none; +} + +.alertMessage .messageWrapper .readmoreBtn:hover { + cursor: pointer; +} + +@media (max-width: 768px) { + .alertMessage .messageWrapper .readmoreBtn { + margin-left: 22px; + } +} + +.alertMessage .messageWrapper .readmoreBtn .buttonTextUnderlined { + text-decoration: underline; + margin-left: 6px; + width: max-content; +} + +.alertMessage .closeIcon { + fill: #7f7f7f; + width: 24px; + height: 24px; + cursor: pointer; + transition: all ease 100ms; + position: absolute; + top: 21px; + transform: translate(0, -50%); + right: 12px; +} + +.alertMessage .closeIcon:hover { + fill: #464646; +} + +.alertMessage .text { + word-break: break-word; +} + +.alertMessage .breakAllText { + word-break: break-all; +} + +.success { + background: #F3FAF7; + color: #68734B; +} + +.warning { + background: #FAF6F3; + color: #73604B; +} + +.error { + background: #FAF3F3; + color: #7C5B56; +} + +.info { + background: #F9FAF3; + color: #73714B; +} + +.normal { + background: #F7F8FA; + color: rgba(0, 0, 0, 0.8); +} + +.scam { + background: rgba(250, 242, 242, 1); + color: rgba(124, 91, 86, 1); +} + + diff --git a/ui/components/atoms/alertMessage/index.tsx b/ui/components/atoms/alertMessage/index.tsx new file mode 100644 index 0000000..152035d --- /dev/null +++ b/ui/components/atoms/alertMessage/index.tsx @@ -0,0 +1,75 @@ +import React, { useState, ReactNode } from "react"; +import classNames from "classnames"; + +import InfoIcon from "./img/InfoIcon.svg"; +import ErrorIcon from "./img/error.svg"; +import WarningIcon from "./img/warning.svg"; +import SucccessIcon from "./img/success.svg"; +import CloseIcon from "./img/CloseIcon.svg"; +import ScamAlert from "./img/ScamAlert.svg"; + +import styles from "./index.module.css"; +import Image from "next/image"; + +interface AlertMessageProps { + text?: string | ReactNode; + type: "info" | "success" | "error"; + className?: string; + classNameWrapper?: string; + closeHandler?: () => void; + needCloseBtn?: boolean; + noIcon?: boolean; +} + +const AlertMessage = ({ + text, + type, + className, + needCloseBtn, + noIcon = false, + classNameWrapper, + closeHandler = () => {}, +}: AlertMessageProps) => { + const [show, setShow] = useState(true); + const infoIcon = { + success: , + warning: , + info: , + error: , + normal: , + scam: , + }; + + const handleCloseClick = () => { + closeHandler(); + setShow(false); + }; + + return show ? ( +
+
+
+ {!noIcon && infoIcon[type] && ( +
{infoIcon[type]}
+ )} + + {text} + +
+ {needCloseBtn && ( + + )} +
+
+ ) : null; +}; + +export default AlertMessage; diff --git a/ui/components/atoms/copyIcon/copyIcon.module.css b/ui/components/atoms/copyIcon/copyIcon.module.css index b060abe..cf6b74a 100644 --- a/ui/components/atoms/copyIcon/copyIcon.module.css +++ b/ui/components/atoms/copyIcon/copyIcon.module.css @@ -11,12 +11,16 @@ } .wrapper .copyIcon { - fill: #a9b5ff; + fill: #949499; position: absolute; left: calc(50% - 6px); margin: 0; } +.wrapper .activeIcon { + fill: #a9b5ff; +} + .wrapper .pressed { position: absolute; left: calc(50% - 8px); diff --git a/ui/components/atoms/copyIcon/iconElement.tsx b/ui/components/atoms/copyIcon/iconElement.tsx new file mode 100644 index 0000000..8551b7f --- /dev/null +++ b/ui/components/atoms/copyIcon/iconElement.tsx @@ -0,0 +1,29 @@ +import classNames from "classnames"; +import style from "./copyIcon.module.css"; + +const CopyIconSVG = ({ + isActive, + className, +}: { + isActive?: boolean; + className?: string; +}) => { + return ( + <> + + + + + ); +}; + +export default CopyIconSVG; diff --git a/ui/components/atoms/copyIcon/index.tsx b/ui/components/atoms/copyIcon/index.tsx index 63d0faa..2a1fa30 100644 --- a/ui/components/atoms/copyIcon/index.tsx +++ b/ui/components/atoms/copyIcon/index.tsx @@ -5,20 +5,27 @@ import iconPressed from "./img/CopyPressed.svg"; import styles from "./copyIcon.module.css"; import classNames from "classnames"; import Image from "next/image"; +import CopyIconSVG from "./iconElement"; type CopyIconProps = { onClick?: () => void; className?: string; - value: string + value: string; + isActive?: boolean; }; -const CopyIcon = ({ onClick, className, value }: CopyIconProps): JSX.Element => { +const CopyIcon = ({ + onClick, + className, + value, + isActive, +}: CopyIconProps): JSX.Element => { const [pressed, setPressed] = useState(false); const [showPressed, setShowPressed] = useState(false); const copyHandler = (e) => { e.stopPropagation(); onClick?.(); - navigator.clipboard.writeText(value); + navigator.clipboard.writeText(value); if (!pressed) { setPressed(true); setTimeout(() => { @@ -55,13 +62,13 @@ const CopyIcon = ({ onClick, className, value }: CopyIconProps): JSX.Element => className={classNames(styles.wrapper, className)} onClick={copyHandler} > - + {showPressed && ( { const r = radius; const g = gap; @@ -81,6 +83,7 @@ const Loader = ({ height={circleSize?.height ? circleSize?.height + "px" : "20px"} viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" + className={className} > void; }): JSX.Element => { + return (
div:last-child { + margin: 0; +} + +@media (min-width: 577px) { + .wrapper { + border-radius: 9px; + } +} + +.dots { + position: absolute; + top: 85px; + left: 32px; + height: calc(100% - 130px); + border: 1px dashed #d7e0e5; + z-index: 0; +} + +.title { + font-size: 18px; + font-weight: 700; + color: rgba(0, 0, 0, 0.8); + margin-bottom: 26px; +} + +.item { + position: relative; + margin-bottom: 36px; + z-index: 1; +} + +.itemHeader { + display: flex; + align-items: flex-start; +} + +.time { + flex-grow: 1; + font-size: 14px; + font-weight: 500; + color: rgba(0, 0, 0, 0.8); +} + +.iconStatus { + margin-right: 12px; +} + +.textStatus { + margin-left: 5px; + padding: 1px 6px; + border-radius: 30px; + white-space: nowrap; + font-size: 12px; + font-weight: 500; +} + +.active { + color: #7dd3a1; + background-color: rgba(125, 211, 161, 0.2); +} + +.offchainIcon svg { + fill: #7dd3a1; +} + +.inactiveOffchainIcon svg { + fill: #d7e0e5; +} + +.remainingTime { + padding: 30px 0 0 30px; +} + +.chartWrapper { + margin-bottom: 24px; +} + +.chartWrapper p { + font-weight: 600; + font-size: 14px; + color: rgba(0, 0, 0, 0.8); + margin-bottom: 24px; +} + +.votingStart { + margin-bottom: 24px; +} + +.defaultStatusIcon { + display: block; + width: 18px; + height: 18px; + background-color: #e7f0f6; + border-radius: 50%; + margin-top: 36px; +} + +.timeItem { + display: flex; + flex-direction: column; + margin-left: 12px; + flex-grow: 1; + color: rgba(0, 0, 0, 0.8); + margin-right: 12px; +} + +.infoTimeItem { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.description span { + color: #7191fc; +} + diff --git a/ui/components/atoms/timeline/index.ts b/ui/components/atoms/timeline/index.ts new file mode 100644 index 0000000..2d206af --- /dev/null +++ b/ui/components/atoms/timeline/index.ts @@ -0,0 +1 @@ +export { default } from "./timeline"; diff --git a/ui/components/atoms/timeline/timeline.tsx b/ui/components/atoms/timeline/timeline.tsx new file mode 100644 index 0000000..287cc34 --- /dev/null +++ b/ui/components/atoms/timeline/timeline.tsx @@ -0,0 +1,231 @@ +import classNames from "classnames"; +import React, { FC, useRef } from "react"; +// import StatusFlagBig from "../statusFlagBig"; + +import styles from "./index.module.css"; +import pendingIcon from "@/assets/pending.svg"; +import successIcon from "@/assets/success.svg"; + +import { dayMonthYearTimeFormat } from "@/helpers/timeHelper"; +import Image from "next/image"; +import { StaticEllipse } from "@/components/molecules/staticEllipse"; +import { DOMAIN_STATUS } from "@/comman/types"; +import { chain } from "@/comman/constants"; + +const getPendingComponent = (message) => ( + + {message} + +); + +const getDescription = (value) => { + const text = value?.match(/[^\d]+/); + const number = value?.match(/\d+/); + return ( +
+ {text} {number} +
+ ); +}; + +export type Timelines = { statusTime: number; status: string; hash?: string }[]; + +type TimelineProps = { + className?: string; + isSendToCloudWorker: boolean; + zkTxId: string | null; + domainStatus: DOMAIN_STATUS; + startTimestamp: number; + timelines: Timelines; + transaction?: string; +}; + +const Timeline: FC = ({ + timelines, + className, + isSendToCloudWorker, + zkTxId, + domainStatus = DOMAIN_STATUS.ACTIVE, + startTimestamp, + transaction, +}) => { + const refTest = useRef(null); + + const newData = [ + { + ...(isSendToCloudWorker + ? { hash: transaction, status: "Paid", statusTime: null } + : { + pendingComponent: getPendingComponent("Sending payment.."), + status: "", + statusTime: null, + }), + }, + ...timelines, + ] + .reduce((acc, item, index, array) => { + const isLastItem = array.length - 1 === index; + + const status = item.status?.toLowerCase(); + const statusTime = item.statusTime; + + if (timelines.length < 1 && isSendToCloudWorker) { + return [ + ...acc, + item, + { + ...(DOMAIN_STATUS.ACTIVE === domainStatus + ? { + statusTime: startTimestamp, + status: "Finalization", + description: getDescription("Finalization"), + } + : { pendingComponent: getPendingComponent("Finalization..") }), + }, + ]; + } + + if (status === "received") { + return [ + ...acc, + // { statusTime, status: 'Received' }, + + isLastItem && { + pendingComponent: getPendingComponent("Reserving domain.."), + }, + ]; + } + if (status === "pending") { + return [ + ...acc, + { statusTime, status: "Received" }, + isLastItem && { + pendingComponent: getPendingComponent("Creating block.."), + }, + ]; + } + if (status.includes("included into block")) { + return [ + ...acc, + { + hash: item?.hash, + statusTime, + status: "Block Created", + description: getDescription(status), + }, + isLastItem && { + pendingComponent: getPendingComponent("Validating block.."), + }, + ]; + } + if (status.includes("included into validated block")) { + return [ + ...acc, + { + statusTime, + status: "Block Validated", + description: getDescription(status), + hash: item?.hash, + }, + isLastItem && { + pendingComponent: getPendingComponent("Proving Block.."), + }, + ]; + } + if (status.includes("included into proved block")) { + return [ + ...acc, + { + statusTime, + status: "Block Proved", + description: getDescription(status), + hash: item?.hash, + }, + isLastItem && DOMAIN_STATUS.ACTIVE !== domainStatus + ? { + pendingComponent: getPendingComponent("Finalization.."), + } + : { + statusTime, + status: "Finalization", + description: getDescription("Finalization"), + }, + ]; + } + + return [...acc, item]; + }, []) + .filter(Boolean); + + const renderTimeItem = ({ + statusTime, + status, + hash, + description, + isFirst, + }) => { + const availableHash = isFirst ? transaction : hash; + return ( + <> + +
+ + {dayMonthYearTimeFormat(statusTime)} + +
+ {availableHash && ( + <> + + + )} + {description && description} +
+
+ + {status} + + + ); + }; + + return ( +
+
+

Timeline

+ {newData.map( + ( + { status, statusTime, hash, pendingComponent, description }, + index + ) => { + return ( +
+
+ {pendingComponent + ? pendingComponent + : renderTimeItem({ + statusTime, + status, + hash, + description, + isFirst: index === 0, + })} +
+
+ ); + } + )} +
+ ); +}; + +export default Timeline; diff --git a/ui/components/molecules/modals/index.ts b/ui/components/molecules/modals/index.ts index 022b892..356ff13 100644 --- a/ui/components/molecules/modals/index.ts +++ b/ui/components/molecules/modals/index.ts @@ -4,3 +4,4 @@ export * from "./modalPurchase"; export * from "./transactionAppliedModal"; export * from "./transactionFailedModal"; export * from "./uploadModal"; +export * from "./pendingModal" diff --git a/ui/components/molecules/modals/modalPurchase/modalPurchase.tsx b/ui/components/molecules/modals/modalPurchase/modalPurchase.tsx index 8a0f2f4..77d1a61 100644 --- a/ui/components/molecules/modals/modalPurchase/modalPurchase.tsx +++ b/ui/components/molecules/modals/modalPurchase/modalPurchase.tsx @@ -19,7 +19,6 @@ import { TABS_VARIANT, Tabs } from "../../tabs"; import { useRouter } from "next/navigation"; import { Routs } from "@/comman/types"; import { useWallet } from "@/hooks"; -import { WalletService } from "@/services/walletService"; const ModalPurchase = ({ name }: ModalPurchaseProps): JSX.Element => { const [selectedPeriod, setSelectedPeriod] = useState(1); diff --git a/ui/components/molecules/modals/modals.types.ts b/ui/components/molecules/modals/modals.types.ts index 28b08f0..664c8b4 100644 --- a/ui/components/molecules/modals/modals.types.ts +++ b/ui/components/molecules/modals/modals.types.ts @@ -1,6 +1,6 @@ import { AccountDomainDetailsResponse } from "@/app/actions/types"; -import { IUseWallet } from "@/hooks"; import { WalletConnectPopUpCoreProps } from "../connectWalletButton/walletConnectPopUp/core"; +import { PendingModalProps } from "./pendingModal/pendingInfo"; export enum Modals { transactionApplied = "transactionApplied", @@ -10,6 +10,7 @@ export enum Modals { confirmation = "confirmation", upload = "upload", walletConnect = "walletConnect", + pending = 'pending' } export type ConfirmationModalProps = { @@ -68,4 +69,5 @@ export type ModalData = { confirmation: ConfirmationModalProps; upload: UploadModalProps; walletConnect: WalletConnectPopUpCoreProps; + pending: PendingModalProps; }; diff --git a/ui/components/molecules/modals/modals.variant.ts b/ui/components/molecules/modals/modals.variant.ts index 79e40a7..5c2eca3 100644 --- a/ui/components/molecules/modals/modals.variant.ts +++ b/ui/components/molecules/modals/modals.variant.ts @@ -7,6 +7,7 @@ import { UploadModal, TransactionAppliedModal, TransactionFailedModal, + PendingModal } from "."; import WalletConnectPopUpCore from "../connectWalletButton/walletConnectPopUp/core"; @@ -18,4 +19,5 @@ export const modalVariants: Record> = { [Modals.confirmation]: ConfirmationModal, [Modals.upload]: UploadModal, [Modals.walletConnect]: WalletConnectPopUpCore, + [Modals.pending]: PendingModal, }; diff --git a/ui/components/molecules/modals/pendingModal/index.module.css b/ui/components/molecules/modals/pendingModal/index.module.css new file mode 100644 index 0000000..37a55ac --- /dev/null +++ b/ui/components/molecules/modals/pendingModal/index.module.css @@ -0,0 +1,11 @@ +.wrapper { + max-width: 700px; + width: 100%; + min-height: 452px; + min-width: 414px; + background-color: #f8fafd; +} + +.loader { + position: fixed; +} diff --git a/ui/components/molecules/modals/pendingModal/index.ts b/ui/components/molecules/modals/pendingModal/index.ts new file mode 100644 index 0000000..1711ce6 --- /dev/null +++ b/ui/components/molecules/modals/pendingModal/index.ts @@ -0,0 +1 @@ +export { default as PendingModal } from "./pendingInfo"; diff --git a/ui/components/molecules/modals/pendingModal/pendingInfo.tsx b/ui/components/molecules/modals/pendingModal/pendingInfo.tsx new file mode 100644 index 0000000..749b741 --- /dev/null +++ b/ui/components/molecules/modals/pendingModal/pendingInfo.tsx @@ -0,0 +1,54 @@ +import { FC, useEffect } from "react"; +import { useZkcloudworkerWS } from "@/hooks"; +import Timeline, { Timelines } from "@/components/atoms/timeline/timeline"; + +import styles from "./index.module.css"; +import { DOMAIN_STATUS } from "@/comman/types"; +import { Loader, LoaderVariant } from "@/components/atoms/loader"; +import { useStoreContext } from "@/store"; + +export type PendingModalProps = { + isSendToCloudWorker: boolean; + zkTxId: string | null; + domainStatus: DOMAIN_STATUS; + startTimestamp: number | null; +}; +const PendingModal: FC = (props) => { + const { isSendToCloudWorker, zkTxId, domainStatus } = props; + const { + state: { additionData }, + } = useStoreContext(); + + const { startNats, statuses, loading } = useZkcloudworkerWS(); + + const availableZkTxId = zkTxId || additionData?.zkTxId; + const availableDomainStatus = zkTxId + ? domainStatus + : additionData?.domainStatus; + + const transaction = additionData?.transaction; + + useEffect(() => { + if (availableZkTxId) { + startNats(availableZkTxId); + } + }, [availableZkTxId]); + + if (loading && zkTxId) { + return ; + } + return ( +
+ +
+ ); +}; + +export default PendingModal; diff --git a/ui/components/molecules/nameCard/index.module.css b/ui/components/molecules/nameCard/index.module.css index 66aba00..0b34583 100644 --- a/ui/components/molecules/nameCard/index.module.css +++ b/ui/components/molecules/nameCard/index.module.css @@ -7,7 +7,6 @@ padding: 12px; border-radius: 10px; border: 1px solid transparent; - cursor: pointer; transition: all 0.3s; overflow: hidden; } @@ -52,3 +51,8 @@ font-size: 13px; margin: 2px 0 0 0; } + +.pendingStatus { + cursor: pointer; + margin-top: 8px; +} diff --git a/ui/components/molecules/nameCard/nameCard.tsx b/ui/components/molecules/nameCard/nameCard.tsx index 795db83..f4fb999 100644 --- a/ui/components/molecules/nameCard/nameCard.tsx +++ b/ui/components/molecules/nameCard/nameCard.tsx @@ -20,6 +20,7 @@ type NameCardProps = { id: string; domainStatus: DOMAIN_STATUS; endTimestamp: number; + handlePendingStatus?: () => void; }; const NameCard = ({ @@ -28,8 +29,10 @@ const NameCard = ({ id, domainStatus, endTimestamp, + handlePendingStatus, }: NameCardProps): JSX.Element => { const base64Data = encode("../../../assets/blur.jpg"); + return (
@@ -51,7 +54,11 @@ const NameCard = ({ )} {domainStatus === DOMAIN_STATUS.PENDING ? ( - + ) : (
); } @@ -121,6 +125,7 @@ const StaticEllipse = ({ )}
{children}
+ {isCopy && }
); }; diff --git a/ui/components/organisms/accountConent/components/names/allContent.tsx b/ui/components/organisms/accountConent/components/names/allContent.tsx index add36bd..7373f48 100644 --- a/ui/components/organisms/accountConent/components/names/allContent.tsx +++ b/ui/components/organisms/accountConent/components/names/allContent.tsx @@ -7,6 +7,8 @@ import { useParams } from "next/navigation"; import { getAccountDomains } from "@/app/actions/actions"; import { useStoreContext } from "@/store"; import { addMinaText } from "@/helpers/name.helper"; +import { Modals } from "@/components/molecules/modals/modals.types"; + const initPage = 0; const initSize = 50; @@ -22,47 +24,82 @@ const AllContent = ({ const [size, setSize] = useState(initSize); const [page, setPage] = useState(initPage); const [accountDomains, setAccountDomains] = useState(null); - + const [selectedDomainId, setSelectedDomainId] = useState(null); const params = useParams(); + const { + actions: { setAdditionData }, + } = useStoreContext(); const { state: { walletData: { accountId }, }, + actions: { openModal }, } = useStoreContext(); const mapDomainImgByIPFS = (domain) => { - const imgHash = - domain?.ipfsImg && - JSON.parse(domain?.ipfsImg)?.linkedObject?.storage?.slice(2); return { ...domain, domainName: addMinaText(domain?.domainName), + ...(domain?.domainStatus === DOMAIN_STATUS.PENDING + ? { + handlePendingStatus: () => { + setSelectedDomainId(domain.id); + openModal(Modals.pending, { + isSendToCloudWorker: domain.isSendToCloudWorker, + zkTxId: domain.zkTxId, + domainStatus: domain.domainStatus, + startTimestamp: domain.startTimestamp, + }); + }, + } + : []), domainImg: - (imgHash && `https://gateway.pinata.cloud/ipfs/${imgHash}`) || null, + (domain?.domainImg && + domain?.domainImg + + "?pinataGatewayToken=gFuDmY7m1Pa5XzZ3bL1TjPPvO4Ojz6tL-VGIdweN1fUa5oSFZXce3y9mL8y1nSSU") || + null, }; }; + + const getData = async (id: string): Promise => { + try { + const response = await getAccountDomains({ + accountAddress: id, + page: page, + size: size, + sortBy: SORT_BY.RESERVATION_TIMESTAMP, + orderBy: ORDER_BY.DESC, + domainStatus: domainStatus, + }); + const accountDomains = response?.content.map(mapDomainImgByIPFS); + setAccountDomains({ ...response, content: accountDomains }); + } catch (error) { + } finally { + setLoading(false); + } + }; useEffect(() => { if (params?.id || accountId) { - (async () => { - try { - setLoading(true); - const response = await getAccountDomains({ - accountAddress: (params?.id as string) || accountId, - page: page, - size: size, - sortBy: SORT_BY.RESERVATION_TIMESTAMP, - orderBy: ORDER_BY.DESC, - domainStatus: domainStatus, - }); - const accountDomains = response?.content.map(mapDomainImgByIPFS); - setAccountDomains({ ...response, content: accountDomains }); - } catch (error) {} - setLoading(false); - })(); - return; + setLoading(true); + const interval = setInterval(() => { + getData((params?.id as string) || accountId); + }, 5000); + + return () => { + clearInterval(interval); + }; } }, [domainStatus, params?.id, accountId, size, page]); + useEffect(() => { + if (selectedDomainId) { + const domain = accountDomains?.content?.find( + (item) => item.id === selectedDomainId + ); + domain?.zkTxId && setAdditionData(domain); + } + }, [accountDomains]); + const onSize = (size) => { setPage(initPage); setSize(size); diff --git a/ui/components/organisms/accountConent/constants.ts b/ui/components/organisms/accountConent/constants.ts index e1aaeef..c26a33e 100644 --- a/ui/components/organisms/accountConent/constants.ts +++ b/ui/components/organisms/accountConent/constants.ts @@ -17,8 +17,8 @@ export const ScoringConfig: TableConfig[] = [ lg: 12, }, style: { - width: '300px' - } + width: "300px", + }, }, { colName: "button", @@ -29,6 +29,7 @@ export const ScoringConfig: TableConfig[] = [ url: "id", parentPage: "name", status: "domainStatus", + onClick: "handlePendingStatus", }, }, { diff --git a/ui/components/organisms/table/templates/buttonTemplate/buttonTemplate.tsx b/ui/components/organisms/table/templates/buttonTemplate/buttonTemplate.tsx index bce1e57..07c5006 100644 --- a/ui/components/organisms/table/templates/buttonTemplate/buttonTemplate.tsx +++ b/ui/components/organisms/table/templates/buttonTemplate/buttonTemplate.tsx @@ -1,3 +1,4 @@ +"use client"; import Link from "next/link"; import { Button } from "../../../../atoms/button"; import { Variant } from "../../../../atoms/button/types"; @@ -32,7 +33,9 @@ const ButtonTemplate = ({ data, config }: ButtonTemplateProps) => { }; if (status === DOMAIN_STATUS.PENDING) { - return ; + return ( + + ); } if (url) { diff --git a/ui/components/organisms/table/templates/buttonTemplate/index.module.css b/ui/components/organisms/table/templates/buttonTemplate/index.module.css index 5059e30..9582f2c 100644 --- a/ui/components/organisms/table/templates/buttonTemplate/index.module.css +++ b/ui/components/organisms/table/templates/buttonTemplate/index.module.css @@ -1,3 +1,4 @@ .status { width: fit-content; + cursor: pointer; } \ No newline at end of file diff --git a/ui/components/organisms/table/view/nameCards.tsx b/ui/components/organisms/table/view/nameCards.tsx index a4e1b1e..c0d3d54 100644 --- a/ui/components/organisms/table/view/nameCards.tsx +++ b/ui/components/organisms/table/view/nameCards.tsx @@ -10,6 +10,7 @@ type NameCardsProps = { id: string; domainStatus: DOMAIN_STATUS; endTimestamp: number; + handlePendingStatus?: () => void; }[]; isLoading: boolean; }; @@ -20,7 +21,10 @@ const NameCards = ({ data, isLoading }: NameCardsProps): JSX.Element => { return (
{data.map( - ({ domainName, domainImg, id, domainStatus, endTimestamp }, index) => { + ( + { domainName, domainImg, id, domainStatus, endTimestamp, handlePendingStatus }, + index + ) => { return ( { id={id} domainStatus={domainStatus} endTimestamp={endTimestamp} + handlePendingStatus={handlePendingStatus} /> ); } diff --git a/ui/components/sections/homeSection/homeSection.tsx b/ui/components/sections/homeSection/homeSection.tsx index 5ec068c..3346660 100644 --- a/ui/components/sections/homeSection/homeSection.tsx +++ b/ui/components/sections/homeSection/homeSection.tsx @@ -3,13 +3,14 @@ import classNames from "classnames"; import { manropeSemiBold, wixMadeforDisplayExtraBold } from "@/app/fonts"; import { Input } from "@/components/atoms/input"; import { ResultItem } from "@/components/atoms/resultItem"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { InputVariant } from "@/components/atoms/input/types"; import style from "./index.module.css"; import { DOMAIN_STATUS } from "@/comman/types"; import { debounceAsync } from "@/helpers/debounce.helper"; import { checkReservedName } from "@/app/actions/actions"; +import AlertMessage from "@/components/atoms/alertMessage"; const HomeSection = () => { const [statusName, setStatusName] = useState<{ @@ -18,6 +19,7 @@ const HomeSection = () => { status: DOMAIN_STATUS; }>(null); const [value, setValue] = useState(""); + const [isCorrectInput, setIsCorrectInput] = useState(false); const handleInput = async (asyncValue: string): Promise => { const currentValue = typeof asyncValue === "string" ? asyncValue : value; @@ -37,12 +39,17 @@ const HomeSection = () => { }; const handleChange = async (value: string) => { - const cleanInput = value.toLocaleLowerCase().trim().replace(/[^a-z0-9- ]/g, ""); - setValue(cleanInput); + const regex = /^[a-z0-9-]*$/; + const isValid = + regex.test(value) && + value === value.trim() && + value === value.toLocaleLowerCase(); + setIsCorrectInput(isValid); + setValue(value); try { - if (!cleanInput) return; - const result = await debouncedServerFetch(cleanInput); + if (!value || !isValid) return; + const result = await debouncedServerFetch(value); if (result === "skipped") { return; @@ -51,7 +58,7 @@ const HomeSection = () => { if (result) { setStatusName({ id: result.id, - name: `${cleanInput}`, + name: `${value}`, status: result.status, }); } @@ -79,13 +86,23 @@ const HomeSection = () => { maxLength={25} enableClear /> - {statusName?.name && value && ( + {isCorrectInput && statusName?.name && value && ( )} + {!isCorrectInput && value && ( + + )}
); diff --git a/ui/components/sections/homeSection/index.module.css b/ui/components/sections/homeSection/index.module.css index d078454..754a40f 100644 --- a/ui/components/sections/homeSection/index.module.css +++ b/ui/components/sections/homeSection/index.module.css @@ -35,3 +35,8 @@ .resultItem { margin-top: 12px; } + +.alertMessage { + width: 100%; + margin-top: 12px; +} diff --git a/ui/helpers/timeHelper.ts b/ui/helpers/timeHelper.ts index f29551f..c1656a2 100644 --- a/ui/helpers/timeHelper.ts +++ b/ui/helpers/timeHelper.ts @@ -127,3 +127,70 @@ export const getTimeDifference = (timelocalstorage: number): string => { return; } }; + +export const exactTimeFuture = (timestamp) => { + const now = new Date() + const duration = require('dayjs/plugin/duration') + dayjs.extend(duration) + const x = dayjs(now) + const y = dayjs(timestamp) +//@ts-ignore + const differenceBetweenXandY = duration in dayjs && dayjs?.duration(y.diff(x))['$d'] + const titleYear = differenceBetweenXandY['years'] > 1 ? 'years' : 'year' + const titleMonth = differenceBetweenXandY['months'] > 1 ? 'months' : 'month' + const totalNumberDays = differenceBetweenXandY['days'] + const numberWeeks = Math.floor(totalNumberDays / 7) + const numberDays = totalNumberDays - numberWeeks * 7 + const titleDay = numberDays > 1 ? 'days' : 'day' + + const years = + differenceBetweenXandY['years'] > 0 && + `${differenceBetweenXandY['years']} ${titleYear}}` + const months = + differenceBetweenXandY['months'] > 0 && + `${differenceBetweenXandY['months']} ${titleMonth}` + const days = totalNumberDays > 0 && `${totalNumberDays} ${titleDay}` + const hours = + differenceBetweenXandY['hours'] > 0 && `${differenceBetweenXandY['hours']}h` + const minutes = + differenceBetweenXandY['minutes'] > 0 && + `${differenceBetweenXandY['minutes']}m` + + return { + years, + months, + days, + hours, + minutes, + } +} + +export const timeAgo = (timestamp) => { + if (!Number(timestamp)) return '-' + const now = new Date() + const duration = require('dayjs/plugin/duration') + dayjs.extend(duration) + const x = dayjs(now) + const y = dayjs(timestamp) +//@ts-ignore + const differenceBetweenXandY = dayjs.duration(x.diff(y))['$d'] + + const totalNumberDays = differenceBetweenXandY['days'] + if (differenceBetweenXandY['years'] > 0) { + return `${differenceBetweenXandY['years']} y ago` + } + if (differenceBetweenXandY['months'] > 0) { + return `${differenceBetweenXandY['months']} mon ago` + } + if (totalNumberDays > 0) { + return `${totalNumberDays} d ago` + } + if (differenceBetweenXandY['hours'] > 0) { + return `${differenceBetweenXandY['hours']} h ago` + } + if (differenceBetweenXandY['minutes'] > 0) { + return `${differenceBetweenXandY['minutes']} m ago` + } + + return '' +} \ No newline at end of file diff --git a/ui/hooks/index.ts b/ui/hooks/index.ts index 61a3643..8c86bec 100644 --- a/ui/hooks/index.ts +++ b/ui/hooks/index.ts @@ -5,5 +5,4 @@ export * from "./useLocalStorage"; export * from "./useMedia"; export * from "./useTable"; export * from "./useWallet"; - - +export * from "./useZkcloudworkerWS"; diff --git a/ui/hooks/useZkcloudworkerWS.ts b/ui/hooks/useZkcloudworkerWS.ts new file mode 100644 index 0000000..606bb38 --- /dev/null +++ b/ui/hooks/useZkcloudworkerWS.ts @@ -0,0 +1,61 @@ +import { useState } from "react"; +import { connect, NatsConnection, KV } from "nats.ws"; + +type Statuses = { + hash: string; + statusTime: number; + status: string; +}[]; +const useZkcloudworkerWS = (): { + startNats: (hash: string) => Promise; + statuses: Statuses; + loading: boolean; +} => { + const server = "wss://cloud.zkcloudworker.com:4223"; + //NEXT_PUBLIC_NATS_SERVER="wss://cloud.zkcloudworker.com:4223" + const [tx, setTx] = useState([]); + const [nc, setNc] = useState(undefined); + const [statuses, setStatuses] = useState([]); + const [loading, setLoading] = useState(true); + + async function watch(kv: KV, keys: string[]) { + const iter = await kv.watch({ key: keys }); + setLoading(false); + + for await (const e of iter) { + const item = JSON.parse(e.string()); + // console.log(`${e.key} @ ${e.revision} -> `, item); + if (item.transaction) { + setTx(JSON.parse(item.transaction)); + setStatuses((prevStatuses) => [ + ...prevStatuses, + { status: item.status, statusTime: item.statusTime, hash: item.hash }, + ]); + } + } + } + + const startNats = async (hash): Promise => { + if (nc === undefined) { + const nc = await connect({ + servers: server, + }); + + setNc(nc); + const js = nc.jetstream(); + const kv = await js.views.kv("profiles"); + watch(kv, [`zkcloudworker.rolluptx.${hash}`]); + } + }; + + return { + startNats, + statuses: statuses.filter( + (item, index, self) => + index === self.findIndex((t) => t.status === item.status) + ), + loading, + }; +}; + +export { useZkcloudworkerWS }; diff --git a/ui/package-lock.json b/ui/package-lock.json index 80409d9..5af6008 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -16,6 +16,7 @@ "js-base64": "^3.7.7", "lodash": "^4.17.21", "lodash.debounce": "^4.0.8", + "nats.ws": "^1.29.0", "next": "14.1.0", "next-plugin-svgr": "^1.1.10", "react": "^18", @@ -5552,6 +5553,14 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nats.ws": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/nats.ws/-/nats.ws-1.29.0.tgz", + "integrity": "sha512-n3+2M/vZp8odDxn4NbIDE4pVBE12dQqxk3ckzXJvbgIjfquUHvrvF3lrMqhdjeGjYdAvTZpaEEZTJG3v5HS3xg==", + "optionalDependencies": { + "nkeys.js": "1.1.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5612,6 +5621,18 @@ "file-loader": "^6.2.0" } }, + "node_modules/nkeys.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nkeys.js/-/nkeys.js-1.1.0.tgz", + "integrity": "sha512-tB/a0shZL5UZWSwsoeyqfTszONTt4k2YS0tuQioMOD180+MbombYVgzDUYHlx+gejYK6rgf08n/2Df99WY0Sxg==", + "optional": true, + "dependencies": { + "tweetnacl": "1.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -6820,6 +6841,12 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/ui/package.json b/ui/package.json index af92231..8c672e3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -18,6 +18,7 @@ "js-base64": "^3.7.7", "lodash": "^4.17.21", "lodash.debounce": "^4.0.8", + "nats.ws": "^1.29.0", "next": "14.1.0", "next-plugin-svgr": "^1.1.10", "react": "^18", diff --git a/ui/store/index.tsx b/ui/store/index.tsx index d9b714f..038d6c8 100755 --- a/ui/store/index.tsx +++ b/ui/store/index.tsx @@ -82,6 +82,11 @@ export type SET_WALLET_DATA = { payload?: WalletData; }; +export type SET_ADDITION_DATA = { + type: "SET_ADDITION_DATA"; + payload?: any; +}; + type StoreActions = | OPEN_MODAL | CLOSE_MODAL @@ -90,7 +95,8 @@ type StoreActions = | ADD_PERIOD | INIT_STOR_FROM_LOCAL_STORAGE | CLEAR_BAG - | SET_WALLET_DATA; + | SET_WALLET_DATA + | SET_ADDITION_DATA; type initStore = (value: IState) => void; type OpenModal = (modal: T, data?: ModalData[T]) => void; @@ -100,6 +106,7 @@ type addPeriod = (payload: { id: string; value: number; key: string }) => void; type clearBag = () => void; type setWalletData = (value?: WalletData) => void; type deleteFromBag = (payload: { id: string; key: string }) => void; +type setAdditionData = (payload: any) => void; type IStore = { state: IState; @@ -112,6 +119,7 @@ type IStore = { initStore: initStore; clearBag: clearBag; setWalletData: setWalletData; + setAdditionData: setAdditionData; }; }; @@ -122,6 +130,7 @@ export interface IState { }[]; walletData?: WalletData; bag?: Bag; + additionData?: any; } export const initWalletData: WalletData = { @@ -239,6 +248,11 @@ export const reducer = (state: IState, action: StoreActions): IState => { ...action.payload, }, }; + case "SET_ADDITION_DATA": + return { + ...state, + additionData: action.payload, + }; default: return state; @@ -259,6 +273,7 @@ export const StoreContext: React.Context = React.createContext({ clearBag: noop, setWalletData: noop, newAddToBag: noop, + setAdditionData: noop, }, }); @@ -316,6 +331,9 @@ const Store = ({ } }; + const setAdditionData = (payload) => + dispatch({ type: "SET_ADDITION_DATA", payload: payload }); + useEffect(() => { initFunction(); }, []); @@ -333,6 +351,7 @@ const Store = ({ initStore, clearBag, setWalletData, + setAdditionData, }, }} >