diff --git a/src/assets/icons/play.svg b/src/assets/icons/play.svg new file mode 100644 index 00000000..0bd11821 --- /dev/null +++ b/src/assets/icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/component/styled/Radio.js b/src/common/component/styled/Radio.js new file mode 100644 index 00000000..6e0cfd06 --- /dev/null +++ b/src/common/component/styled/Radio.js @@ -0,0 +1,91 @@ +import styled from "styled-components"; +import { v4 as UUIDv4 } from "uuid"; +import { useRef, useState } from "react"; + +const StyledForm = styled.form` + > .option { + &:not(:last-child) { + margin-bottom: 8px; + } + + > input[type="radio"] { + display: none; + + & + .box { + width: 512px; + background: #ffffff; + border: 1px solid #d0d5dd; + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); + border-radius: 8px; + transition: all ease-in-out 250ms; + + & > label { + display: flex; + flex-direction: row; + align-items: center; + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: #667085; + cursor: pointer; + user-select: none; + transition: all ease-in-out 250ms; + + &:before { + content: ""; + display: inline-block; + width: 14px; + height: 14px; + border-radius: 8px; + background: #ffffff; + box-shadow: inset 0 0 0 4px #ffffff; + border: 1px solid #d0d5dd; + margin: 14px 8px 14px 14px; + transition: all ease-in-out 500ms; + } + } + } + + &:checked + .box { + background: #22ccee; + border: 1px solid #d0d5dd; + + & > label { + color: #ffffff; + + &:before { + background: #ffffff; + box-shadow: inset 0 0 0 4px #22ccee; + border: 1px solid #ffffff; + } + } + } + } + } +`; + +export default function Radio({ options, value = undefined, onChange = undefined }) { + const [innerValue, setInnerValue] = useState(0); + const id = useRef(UUIDv4()); + + return ( + + {options.map((item, index) => ( +
+ { + value === undefined && setInnerValue(index); + onChange !== null && onChange(index); + }} + id={`${id.current}-${index}`} + /> +
+ +
+
+ ))} +
+ ); +} diff --git a/src/routes/index.js b/src/routes/index.js index d98fbe32..58d40f9e 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -21,115 +21,114 @@ import store from "../app/store"; import InvitePage from "./invite"; import SettingPage from "./setting"; import SettingChannelPage from "./settingChannel"; +import OnboardingPage from "./onboarding"; import toast from "react-hot-toast"; import ResourceManagement from "./resources"; const PageRoutes = () => { - const { - ui: { online }, - fileMessages, - } = useSelector((store) => { - return { ui: store.ui, fileMessages: store.fileMessage }; - }); - // 掉线检测 - useEffect(() => { - let toastId = 0; - if (!online) { - toast.error("Network Offline!", { duration: Infinity }); - } else { - toast.dismiss(toastId); - } - }, [online]); + const { + ui: { online }, + fileMessages + } = useSelector((store) => { + return { ui: store.ui, fileMessages: store.fileMessage }; + }); + // 掉线检测 + useEffect(() => { + let toastId = 0; + if (!online) { + toast.error("Network Offline!", { duration: Infinity }); + } else { + toast.dismiss(toastId); + } + }, [online]); - return ( - - - } /> - - - - } - /> - - - - } - /> - - - - } - > - } /> - - } /> - } /> - - - - - - } - /> - } /> - - - - } - > - - } /> - } /> - - } /> - - } /> - } /> - } /> - - - } /> - } /> - - }> - } - > - - } /> - - - ); + return ( + + + } /> + + + + } + /> + + + + } + /> + + + + } + > + } /> + + } /> + } /> + + + + + + } + /> + } /> + } /> + + + + } + > + + } /> + } /> + + } /> + + } /> + } /> + } /> + + + } /> + } /> + + }> + }> + + } /> + + + ); }; // const local_key = "AUTH_DATA"; export default function ReduxRoutes() { - // const [authData, setAuthData] = useState( - // JSON.parse(localStorage.getItem(local_key)) - // ); - // const updateAuthData = (data) => { - // localStorage.setItem(local_key, JSON.stringify(data)); - // setAuthData(data); - // }; - return ( - - - - - ); + // const [authData, setAuthData] = useState( + // JSON.parse(localStorage.getItem(local_key)) + // ); + // const updateAuthData = (data) => { + // localStorage.setItem(local_key, JSON.stringify(data)); + // setAuthData(data); + // }; + return ( + + + + + ); } diff --git a/src/routes/onboarding/index.js b/src/routes/onboarding/index.js new file mode 100644 index 00000000..7223f3e9 --- /dev/null +++ b/src/routes/onboarding/index.js @@ -0,0 +1,89 @@ +import { useState } from "react"; +import toast from "react-hot-toast"; +import StyledButton from "../../common/component/styled/Button"; +import FirstStep from "./steps/first"; +import SpaceNameStep from "./steps/spaceName"; +import AdminCredentialsStep from "./steps/adminCredentials"; +import InviteRuleStep from "./steps/inviteRule"; +import InviteLinkStep from "./steps/inviteLink"; +import LastStep from "./steps/last"; +import StyledOnboardingPage from "./styled"; + +export default function OnboardingPage() { + const [step, setStep] = useState(0); + const [data, setData] = useState({ + spaceName: "", + adminEmail: "", + adminPassword: "", + adminPassword2: "", + inviteRule: null + }); + + return ( + +
+
+ {step > 0 && step < 5 && ( + <> + setStep(step - 1)}> + Back + + { + if (step === 1) { + // Verification for space name + if (data.spaceName === "") { + toast.error("Please enter space name!"); + return; + } + setStep(step + 1); + } else if (step === 2) { + // Verification for admin credentials + if (data.adminEmail === "") { + toast.error("Please enter admin email!"); + return; + } else if (data.adminPassword === "") { + toast.error("Please enter admin password!"); + return; + } else if (data.adminPassword !== data.adminPassword2) { + toast.error("Two passwords do not match!"); + return; + } + setStep(step + 1); + } else if (step === 3) { + // Verification for invitation rule + if (data.inviteRule === null) { + toast.error("Please choose one option!"); + return; + } + setStep(step + 1); + } else if (step === 4) { + // Verification for invitation link + setStep(step + 1); + } + }} + > + Next + + + )} + {step === 0 && setStep(step + 1)} />} + {step === 1 && } + {step === 2 && } + {step === 3 && } + {step === 4 && } + {step === 5 && ( + { + // TODO: finish it + console.log("onboarding steps completed:", data); + }} + /> + )} +
+
+
+ ); +} diff --git a/src/routes/onboarding/steps/adminCredentials.js b/src/routes/onboarding/steps/adminCredentials.js new file mode 100644 index 00000000..4caafef0 --- /dev/null +++ b/src/routes/onboarding/steps/adminCredentials.js @@ -0,0 +1,58 @@ +import styled from "styled-components"; +import StyledInput from "../../../common/component/styled/Input"; + +const StyledAdminCredentialsStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .input:not(:last-child) { + margin-bottom: 20px; + } +`; + +export default function AdminCredentialsStep({ data, setData }) { + return ( + + Now let’s set up your admin account + You are the 1st user and admin of your space! + + setData({ + ...data, + adminEmail: e.target.value + }) + } + /> + + setData({ + ...data, + adminPassword: e.target.value + }) + } + /> + + setData({ + ...data, + adminPassword2: e.target.value + }) + } + /> + + ); +} diff --git a/src/routes/onboarding/steps/first.js b/src/routes/onboarding/steps/first.js new file mode 100644 index 00000000..21b96cf2 --- /dev/null +++ b/src/routes/onboarding/steps/first.js @@ -0,0 +1,26 @@ +import styled from "styled-components"; +import StyledButton from "../../../common/component/styled/Button"; +import PlayIcon from "../../../assets/icons/play.svg?url"; + +const StyledFirstStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +export default function FirstStep({ onButtonClick }) { + return ( + + Welcome to your Rustchat! + + Everything in this space is owned by you. Let’s set up your space! + + + play icon + Start + + + ); +} diff --git a/src/routes/onboarding/steps/inviteLink.js b/src/routes/onboarding/steps/inviteLink.js new file mode 100644 index 00000000..57b751de --- /dev/null +++ b/src/routes/onboarding/steps/inviteLink.js @@ -0,0 +1,70 @@ +import styled from "styled-components"; +import StyledInput from "../../../common/component/styled/Input"; +import StyledButton from "../../../common/component/styled/Button"; +import useInviteLink from "../../../common/hook/useInviteLink"; + +const StyledInviteLinkStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .secondaryText { + margin-bottom: 40px; + } + + > .tip { + font-weight: 600; + font-size: 14px; + line-height: 20px; + color: #475467; + margin-bottom: 8px; + } + + > .link { + position: relative; + background: #ffffff; + border: 1px solid #f4f4f5; + box-shadow: 0 1px 2px rgba(31, 41, 55, 0.08); + border-radius: 4px; + width: 374px; + display: flex; + + > input { + border: none; + box-shadow: none; + padding: 11px 0 11px 8px; + font-weight: 400; + font-size: 14px; + line-height: 20px; + color: #78787c; + } + + > button { + padding: 0 8px; + font-weight: 500; + font-size: 14px; + line-height: 20px; + color: #22ccee; + } + } +`; + +export default function InviteLinkStep() { + const { link, linkCopied, copyLink } = useInviteLink(); + + return ( + + Last step: invite others! + Now let’s invite others! + Send invitation link to your future community members: +
+ + + {linkCopied ? "Copied" : `Copy`} + +
+
+ ); +} diff --git a/src/routes/onboarding/steps/inviteRule.js b/src/routes/onboarding/steps/inviteRule.js new file mode 100644 index 00000000..a7e9b9d0 --- /dev/null +++ b/src/routes/onboarding/steps/inviteRule.js @@ -0,0 +1,33 @@ +import styled from "styled-components"; +import StyledRadio from "../../../common/component/styled/Radio"; + +const StyledInviteRuleStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .input:not(:last-child) { + margin-bottom: 20px; + } +`; + +export default function InviteRuleStep({ data, setData }) { + return ( + + Last step: invite others! + Firstly, who can sign up to this server? + + setData({ + ...data, + inviteRule: v + }) + } + /> + + ); +} diff --git a/src/routes/onboarding/steps/last.js b/src/routes/onboarding/steps/last.js new file mode 100644 index 00000000..5be297cc --- /dev/null +++ b/src/routes/onboarding/steps/last.js @@ -0,0 +1,44 @@ +import styled from "styled-components"; +import StyledButton from "../../../common/component/styled/Button"; +import PlayIcon from "../../../assets/icons/play.svg?url"; + +const StyledLastStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .secondaryText { + margin-bottom: 48px; + } + + > .tip { + width: 588px; + font-size: 20px; + line-height: 24px; + text-align: center; + margin-bottom: 96px; + + > .strong { + font-weight: 700; + } + } +`; + +export default function LastStep({ data, onButtonClick }) { + return ( + + Welcome to {data.spaceName} + Proudly presented by Rustchat + + More settings, including domain resolution, privileges, securities, and invites are available in{" "} + Settings + + + play icon + Start + + + ); +} diff --git a/src/routes/onboarding/steps/spaceName.js b/src/routes/onboarding/steps/spaceName.js new file mode 100644 index 00000000..170ddc73 --- /dev/null +++ b/src/routes/onboarding/steps/spaceName.js @@ -0,0 +1,34 @@ +import styled from "styled-components"; +import StyledInput from "../../../common/component/styled/Input"; + +const StyledSpaceNameStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .secondaryText { + color: #667085; + } +`; + +export default function SpaceNameStep({ data, setData }) { + return ( + + Let’s start with name: + This space is called: + + setData({ + ...data, + spaceName: e.target.value + }) + } + /> + + ); +} diff --git a/src/routes/onboarding/styled.js b/src/routes/onboarding/styled.js new file mode 100644 index 00000000..7328f390 --- /dev/null +++ b/src/routes/onboarding/styled.js @@ -0,0 +1,92 @@ +import styled from "styled-components"; + +const StyledOnboardingPage = styled.div` + height: 100vh; + overflow-y: auto; + + > .horizontalBox { + width: calc(100vw - 40px); + max-width: 860px; + margin: 0 auto; + padding: max(50vh - 340px, 50px) 0; + + > .verticalBox { + position: relative; + height: 680px; + border: 1px solid #f2f4f7; + border-radius: 12px; + box-shadow: 0 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06); + + > .buttonBack, + > .buttonNext { + position: absolute; + bottom: 32px; + } + + > .buttonBack { + left: 30px; + color: #98a2b3; + } + + > .buttonNext { + right: 30px; + } + } + } + + // shared with child components + .primaryText, + .secondaryText { + text-align: center; + } + + .primaryText { + font-weight: 700; + font-size: 24px; + line-height: 30px; + margin-bottom: 8px; + } + + .secondaryText { + font-size: 14px; + line-height: 20px; + margin-bottom: 24px; + } + + .startButton { + width: 128px; + display: flex; + flex-direction: column; + align-items: center; + padding: 15px 0 12px; + + > img { + margin-bottom: 7px; + } + + > span { + font-weight: 500; + font-size: 14px; + line-height: 20px; + } + } + + .input { + width: 360px; + height: 44px; + border: none; + box-shadow: none; + } + + input.input { + font-weight: 400; + font-size: 16px; + line-height: 24px; + padding: 10px 14px; + border: 1px solid #d0d5dd; + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); + border-radius: 8px; + } +`; + +export default StyledOnboardingPage;