From b1131c1f3981496901cd6fe38eb47668c4f91565 Mon Sep 17 00:00:00 2001 From: bufan Date: Tue, 16 Jan 2024 16:35:48 +0800 Subject: [PATCH 1/2] add email and node status --- packages/client/dashboard/src/api/index.ts | 2 +- packages/client/dashboard/src/api/user.ts | 59 ++++++ .../src/components/icons/CheckCircleIcon.tsx | 7 +- .../src/components/icons/CircleIcon.tsx | 13 ++ .../node/CreateCeramicNodeModal.tsx | 23 ++- .../src/components/node/NodeStatus.tsx | 187 ++++++++++++++++++ .../src/components/node/UserEmail.tsx | 164 +++++++++++++++ .../dashboard/src/container/CeramicNodes.tsx | 23 ++- .../dashboard/src/hooks/useUserAccount.ts | 77 ++++++++ packages/client/dashboard/src/types.d.ts | 8 +- 10 files changed, 545 insertions(+), 18 deletions(-) create mode 100644 packages/client/dashboard/src/api/user.ts create mode 100644 packages/client/dashboard/src/components/icons/CircleIcon.tsx create mode 100644 packages/client/dashboard/src/components/node/NodeStatus.tsx create mode 100644 packages/client/dashboard/src/components/node/UserEmail.tsx create mode 100644 packages/client/dashboard/src/hooks/useUserAccount.ts diff --git a/packages/client/dashboard/src/api/index.ts b/packages/client/dashboard/src/api/index.ts index d86e3e0e..5d938d3d 100644 --- a/packages/client/dashboard/src/api/index.ts +++ b/packages/client/dashboard/src/api/index.ts @@ -1,7 +1,7 @@ import axios from 'axios' import { UPLOAD_API_URL } from '../constants' -enum ApiRespCode { +export enum ApiRespCode { SUCCESS = 0, ERROR = 1, } diff --git a/packages/client/dashboard/src/api/user.ts b/packages/client/dashboard/src/api/user.ts new file mode 100644 index 00000000..da6912fa --- /dev/null +++ b/packages/client/dashboard/src/api/user.ts @@ -0,0 +1,59 @@ +import axios, { AxiosPromise } from 'axios' +import { ApiResp } from '.' +import { APP_API_URL } from '../constants' +import { AccountType } from '../types.d' + +export function getUserEmail( + didSession: string +): AxiosPromise> { + let host = APP_API_URL + return axios({ + url: host + `/users/email`, + method: 'GET', + headers: { + 'did-session': didSession, + }, + }) +} + +export function postUserEmail( + didSession: string, + email: string, +): AxiosPromise> { + if (!email) throw new Error('email id is required') + + let host = APP_API_URL + + return axios({ + url: host + '/users/email', + method: 'POST', + headers: { + 'did-session': didSession, + }, + data: { email }, + }) +} + +export function linkUserEmail( + didSession: string, + email: string, + code: string, +): AxiosPromise> { + if (!email) throw new Error('email is required') + if (!code) throw new Error('code is required') + + let host = APP_API_URL + + return axios({ + url: host + '/users/link', + method: 'POST', + headers: { + 'did-session': didSession, + }, + data: { + thirdpartyId: email, + code, + type: AccountType.EMAIL + }, + }) +} \ No newline at end of file diff --git a/packages/client/dashboard/src/components/icons/CheckCircleIcon.tsx b/packages/client/dashboard/src/components/icons/CheckCircleIcon.tsx index d87956d2..d585a088 100644 --- a/packages/client/dashboard/src/components/icons/CheckCircleIcon.tsx +++ b/packages/client/dashboard/src/components/icons/CheckCircleIcon.tsx @@ -1,4 +1,4 @@ -export default function CheckCircleIcon({ bgc = '#5BA85A' }: { bgc?: string }) { +export default function CheckCircleIcon({ bgc = '#00b171' }: { bgc?: string }) { return ( - + + + + ) +} diff --git a/packages/client/dashboard/src/components/node/CreateCeramicNodeModal.tsx b/packages/client/dashboard/src/components/node/CreateCeramicNodeModal.tsx index 276cfc34..9fbbab32 100644 --- a/packages/client/dashboard/src/components/node/CreateCeramicNodeModal.tsx +++ b/packages/client/dashboard/src/components/node/CreateCeramicNodeModal.tsx @@ -9,6 +9,8 @@ import { createCeramicNode } from '../../api/ceramicNode' import { CeramicDBType, CeramicNetwork } from '../../types.d' import EnumSelect from '../common/EnumSelect' import CloseIcon from '../icons/CloseIcon' +import UserEmail from './UserEmail' +import { EmailStatus } from '../../hooks/useUserAccount' export default function CreateCeramicNodeModal ({ fixedNetwork, @@ -60,8 +62,17 @@ export default function CreateCeramicNodeModal ({ } finally { setSubmitting(false) } - }, [submitting, session, signIn, nodeName, network, dbType, onSussess, closeModal]) - + }, [ + submitting, + session, + signIn, + nodeName, + network, + dbType, + onSussess, + closeModal + ]) + const [userEmailVerified, setUserEmailVerified] = useState(false) return (
@@ -100,6 +111,11 @@ export default function CreateCeramicNodeModal ({
Enable Historic Sync + + setUserEmailVerified(stutas === EmailStatus.VERIFIED) + } + />
@@ -108,7 +124,7 @@ export default function CreateCeramicNodeModal ({ )) || ( - )} @@ -116,6 +132,7 @@ export default function CreateCeramicNodeModal ({ ) } + const EditorBox = styled.div` display: flex; flex-direction: column; diff --git a/packages/client/dashboard/src/components/node/NodeStatus.tsx b/packages/client/dashboard/src/components/node/NodeStatus.tsx new file mode 100644 index 00000000..18309b77 --- /dev/null +++ b/packages/client/dashboard/src/components/node/NodeStatus.tsx @@ -0,0 +1,187 @@ +import styled from 'styled-components' +import { CeramicStatus } from '../../types.d' +import CircleIcon from '../icons/CircleIcon' +import CheckCircleIcon from '../icons/CheckCircleIcon' + +import { ProgressBar, Label } from 'react-aria-components' +import { useEffect, useState } from 'react' + +export default function NodeStatus ({ + status, +}: { + status: CeramicStatus +}) { + const [prepageingPercentage, setPrepageingPercentage] = useState(0) + const [startingPercentage, setStartingPercentage] = useState(0) + + useEffect(() => { + if (status !== CeramicStatus.PREPARING) return + if (prepageingPercentage < 100) { + console.log('set prepare timeout', prepageingPercentage) + setTimeout(() => { + setPrepageingPercentage(prepageingPercentage + 1) + }, 350) + } + }, [prepageingPercentage, status]) + + useEffect(() => { + if (status !== CeramicStatus.STARTING) return + if (startingPercentage < 100) { + console.log('set start timeout', startingPercentage) + setTimeout(() => { + setStartingPercentage(startingPercentage + 1) + }, 200) + } + }, [startingPercentage, status]) + + useEffect(() => { + switch (status) { + case CeramicStatus.PREPARING: + setPrepageingPercentage(0) + setStartingPercentage(0) + break + case CeramicStatus.STARTING: + setPrepageingPercentage(100) + setStartingPercentage(0) + break + case CeramicStatus.RUNNING: + setPrepageingPercentage(100) + setStartingPercentage(100) + break + default: + break + } + }, [status]) + + switch (status) { + case CeramicStatus.PREPARING: + return ( + +
+ + + + + +
+
+ + + +
+
+ ) + case CeramicStatus.STARTING: + return ( + +
+ + + + + +
+
+ + + +
+
+ ) + case CeramicStatus.RUNNING: + return ( + +
+ + + + + +
+
+ + + +
+
+ ) + default: + return null + } +} + +const Box = styled.div` + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; + > div { + width: 100%; + display: flex; + align-items: center; + gap: 10px; + justify-content: space-between; + } + .green { + color: #00b171; + } + .gray { + color: #718096; + } + label { + font-size: 12px; + font-weight: 500; + line-height: 1.2; + letter-spacing: 0.05em; + text-transform: uppercase; + } + .react-aria-ProgressBar { + display: grid; + grid-template-areas: "label value" + "bar bar"; + grid-template-columns: 1fr auto; + color: var(--text-color); + flex: 1; + .value { + grid-area: value; + } + + .bar { + grid-area: bar; + box-shadow: inset 0px 0px 0px 1px #718096; + forced-color-adjust: none; + height: 2px; + overflow: hidden; + will-change: transform; + } + + .fill { + background: #00b171; + height: 100%; + } + } +` +function StepNode ({ percentage = 0 }: { percentage?: number }) { + if (percentage === 0) { + return + } else if (percentage < 100) { + return + } else if (percentage >= 100) { + return + } + return null +} +function StepProgress ({ percentage = 0 }: { percentage?: number }) { + return ( + + {({ percentage, valueText }) => ( + <> + {/* {valueText} */} +
+
+
+ + )} + + ) +} diff --git a/packages/client/dashboard/src/components/node/UserEmail.tsx b/packages/client/dashboard/src/components/node/UserEmail.tsx new file mode 100644 index 00000000..6995a1ca --- /dev/null +++ b/packages/client/dashboard/src/components/node/UserEmail.tsx @@ -0,0 +1,164 @@ +import { useSession } from '@us3r-network/auth-with-rainbowkit' +import { useEffect, useState } from 'react' +import styled from 'styled-components' +import useUserAccount, { EmailStatus } from '../../hooks/useUserAccount' + +export default function Email ({ + emailStatusChange +}: { + emailStatusChange: (stutas: EmailStatus) => void +}) { + const session = useSession() + const { + email, + emailStatus, + setEmailStatus, + errorMsg, + sendEmail, + sendEmailCountDown, + verifyEmail + } = useUserAccount(session?.serialize()) + const [newEmail, setNewEmail] = useState(email) + const [code, setCode] = useState('') + const [sendingEmail, setSendingEmail] = useState(false) + + const [verifyingEmail, setVerifyingEmail] = useState(false) + useEffect(() => { + emailStatusChange(emailStatus) + }, [emailStatus, emailStatusChange]) + + const send = async () => { + setSendingEmail(true) + await sendEmail(newEmail) + setSendingEmail(false) + } + + switch (emailStatus) { + case EmailStatus.VERIFIED: + return ( + + * Email: +
+ + +
+
+ ) + case EmailStatus.NOT_VERIFIED: + return ( + + * Email: +
+ setNewEmail(e.target.value)} + /> + +
+

{errorMsg}

+
+ ) + case EmailStatus.VERIFYING: + return ( + + * Email: +
+ setNewEmail(e.target.value)} + /> + +
+
+ setCode(e.target.value)} + /> + +
+

{errorMsg}

+
+ ) + } +} +const Box = styled.div` + display: flex; + flex-direction: column; + gap: 20px; + div { + /* height: calc(100v - 88px); */ + box-sizing: border-box; + display: flex; + gap: 20px; + } + + input { + width: auto; + background: #1a1e23; + outline: none; + border: 1px solid #39424c; + border-radius: 12px; + height: 48px; + padding: 0px 16px; + color: #ffffff; + font-weight: 400; + font-size: 16px; + line-height: 24px; + flex: 1 1 auto; + } + + button { + background: #1a1e23; + border: 1px solid #39424c; + border-radius: 12px; + padding: 12px 24px; + font-weight: 500; + font-size: 16px; + line-height: 24px; + color: #718096; + width: 160px; + flex: 0 0 150px; + &.submit { + color: #14171a; + background: #ffffff; + } + + > img { + height: 18px; + } + } +` diff --git a/packages/client/dashboard/src/container/CeramicNodes.tsx b/packages/client/dashboard/src/container/CeramicNodes.tsx index 5e9e6c00..92bae353 100644 --- a/packages/client/dashboard/src/container/CeramicNodes.tsx +++ b/packages/client/dashboard/src/container/CeramicNodes.tsx @@ -21,6 +21,7 @@ import NodeTerminal from '../components/node/Terminal' import { useAppCtx } from '../context/AppCtx' import { useCeramicNodeCtx } from '../context/CeramicNodeCtx' import { CeramicDto, CeramicNetwork, CeramicStatus, Network } from '../types.d' +import NodeStatus from '../components/node/NodeStatus' export default function CeramicNodes () { const { currCeramicNode, @@ -163,15 +164,18 @@ function CeramicNodeInfo ({ node }: { node: CeramicDto }) { return ( -
-
{node.name}
-
{node.network}
+
+
+
{node.name}
+
{node.network}
+
+ {node.status === CeramicStatus.PREPARING ? (
- - Preparing your deployment... (It may take a few minutes) + {/* */} + {/* Preparing your deployment... (It may take a few minutes) */}
) : node.status === CeramicStatus.RUNNING ? (
@@ -269,7 +273,6 @@ const ListBox = styled.div` text-align: center; } ` - const NodesListBox = styled.div` display: flex; flex-direction: column; @@ -337,7 +340,6 @@ const NodesListBox = styled.div` } } ` - const NodeInfoContainer = styled.div` height: 100%; display: flex; @@ -347,6 +349,7 @@ const NodeInfoContainer = styled.div` flex-grow: 1; ` const NodeInfoBox = styled.div` + width: 100%; padding: 20px; border-radius: 20px; border: 1px solid #39424c; @@ -356,6 +359,12 @@ const NodeInfoBox = styled.div` gap: 20px; flex-shrink: 0; flex-grow: 0; + .title-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + } .title { display: flex; flex-direction: row; diff --git a/packages/client/dashboard/src/hooks/useUserAccount.ts b/packages/client/dashboard/src/hooks/useUserAccount.ts new file mode 100644 index 00000000..ce0b1ff5 --- /dev/null +++ b/packages/client/dashboard/src/hooks/useUserAccount.ts @@ -0,0 +1,77 @@ +import { useCallback, useEffect, useState } from 'react' +import { getUserEmail, linkUserEmail, postUserEmail } from '../api/user' +import { ApiRespCode } from '../api'; + +export enum EmailStatus { + NOT_VERIFIED, + VERIFIED, + VERIFYING, +} +const COUNT_DOWN = 60 +export default function useUserAccount(didSession: string | undefined) { + const [email, setEmail] = useState('') + const [emailStatus, setEmailStatus] = useState(EmailStatus.NOT_VERIFIED) + const [errorMsg, setErrorMsg] = useState(undefined) + + const [sendEmailCountDown, setSendEmailCountDown] = useState(0) + + useEffect(() => { + if (sendEmailCountDown > 0) { + setTimeout(() => { + setSendEmailCountDown(sendEmailCountDown - 1) + }, 1000) + } + }, [sendEmailCountDown]) + + const getEmail = useCallback(async () => { + if (!didSession) return + setErrorMsg('') + const resp = await getUserEmail(didSession) + if (resp.data?.code === ApiRespCode.SUCCESS && resp.data?.data?.email) { + setEmail(resp.data.data.email) + setEmailStatus(EmailStatus.VERIFIED) + } + if (resp?.data?.code === ApiRespCode.ERROR) { + setErrorMsg(resp?.data?.msg) + } + }, [didSession]) + + const sendEmail = useCallback(async (email: string) => { + if (!didSession) return + setErrorMsg('') + const resp = await postUserEmail(didSession, email) + if (resp.data?.code === ApiRespCode.SUCCESS) { + setEmailStatus(EmailStatus.VERIFYING) + setSendEmailCountDown(COUNT_DOWN) + } + if (resp?.data?.code === ApiRespCode.ERROR) { + setErrorMsg(resp?.data?.msg) + } + return resp; + }, [didSession]) + + const verifyEmail = useCallback(async (email: string, code: string) => { + if (!didSession) return + setErrorMsg('') + const resp = await linkUserEmail(didSession, email, code) + if (resp.data?.code === ApiRespCode.SUCCESS) { + getEmail() + } else if (resp?.data?.code === ApiRespCode.ERROR) { + setErrorMsg(resp?.data?.msg) + } + }, [didSession, getEmail]) + + useEffect(() => { + if (didSession) getEmail(); + }, [didSession, getEmail]) + + return { + email, + emailStatus, + errorMsg, + sendEmailCountDown, + setEmailStatus, + sendEmail, + verifyEmail, + } +} diff --git a/packages/client/dashboard/src/types.d.ts b/packages/client/dashboard/src/types.d.ts index 0b7a97a1..a25da7dd 100644 --- a/packages/client/dashboard/src/types.d.ts +++ b/packages/client/dashboard/src/types.d.ts @@ -44,13 +44,17 @@ export type ClientDApp = { createdAt?: number lastModifiedAt?: number } - +/************************ Account ******************************/ +export enum AccountType { + EMAIL = 'email', + ALL = 'all', +} /************************ Node ******************************/ export type CeramicDto = { id: number; name: string; network: CeramicNetwork; - status: string; + status: CeramicStatus; privateKey: string; apiKey: string; namespace: string; From 73b291d1fce22072bba1b55a7643dab523be8a81 Mon Sep 17 00:00:00 2001 From: bufan Date: Tue, 16 Jan 2024 18:08:35 +0800 Subject: [PATCH 2/2] send email button style --- packages/client/dashboard/src/components/node/UserEmail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/dashboard/src/components/node/UserEmail.tsx b/packages/client/dashboard/src/components/node/UserEmail.tsx index 6995a1ca..cc7a54af 100644 --- a/packages/client/dashboard/src/components/node/UserEmail.tsx +++ b/packages/client/dashboard/src/components/node/UserEmail.tsx @@ -88,7 +88,7 @@ export default function Email ({ onClick={send} > {sendEmailCountDown - ? `Send Email (${sendEmailCountDown})` + ? `Send in ${sendEmailCountDown}s` : `Send Email`}