feat: implement onboarding page

This commit is contained in:
Liyang Zhu
2022-05-29 19:26:48 +08:00
parent f1a2921231
commit c26000f3f5
11 changed files with 642 additions and 103 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg width="17" height="18" viewBox="0 0 17 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.60846 1.61513C2.1087 1.3432 1.5 1.70497 1.5 2.27392V15.7262C1.5 16.2952 2.1087 16.6569 2.60846 16.385L14.97 9.65887C15.4921 9.37481 15.4921 8.62534 14.97 8.34128L2.60846 1.61513ZM0 2.27392C0 0.567069 1.82609 -0.518242 3.32538 0.297548L15.687 7.0237C17.2531 7.87588 17.2531 10.1243 15.687 10.9764L3.32538 17.7026C1.82609 18.5184 0 17.4331 0 15.7262V2.27392Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 489 B

+91
View File
@@ -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 (
<StyledForm>
{options.map((item, index) => (
<div className="option" key={index}>
<input
type="radio"
checked={(value !== undefined ? value : innerValue) === index}
onChange={() => {
value === undefined && setInnerValue(index);
onChange !== null && onChange(index);
}}
id={`${id.current}-${index}`}
/>
<div className="box">
<label htmlFor={`${id.current}-${index}`}>{item}</label>
</div>
</div>
))}
</StyledForm>
);
}
+4 -5
View File
@@ -21,13 +21,14 @@ 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,
fileMessages
} = useSelector((store) => {
return { ui: store.ui, fileMessages: store.fileMessage };
});
@@ -84,6 +85,7 @@ const PageRoutes = () => {
}
/>
<Route path="/invite" element={<InvitePage />} />
<Route path="/onboarding" element={<OnboardingPage />} />
<Route
path="/"
element={
@@ -107,10 +109,7 @@ const PageRoutes = () => {
<Route path=":user_id" element={<ContactsPage />} />
</Route>
<Route path="favs" element={<FavoritesPage />}></Route>
<Route
path="files"
element={<ResourceManagement fileMessages={fileMessages} />}
></Route>
<Route path="files" element={<ResourceManagement fileMessages={fileMessages} />}></Route>
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
+89
View File
@@ -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 (
<StyledOnboardingPage>
<div className="horizontalBox">
<div className="verticalBox">
{step > 0 && step < 5 && (
<>
<StyledButton className="buttonBack ghost border_less" onClick={() => setStep(step - 1)}>
Back
</StyledButton>
<StyledButton
className="buttonNext"
onClick={() => {
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
</StyledButton>
</>
)}
{step === 0 && <FirstStep onButtonClick={() => setStep(step + 1)} />}
{step === 1 && <SpaceNameStep data={data} setData={setData} />}
{step === 2 && <AdminCredentialsStep data={data} setData={setData} />}
{step === 3 && <InviteRuleStep data={data} setData={setData} />}
{step === 4 && <InviteLinkStep />}
{step === 5 && (
<LastStep
data={data}
onButtonClick={() => {
// TODO: finish it
console.log("onboarding steps completed:", data);
}}
/>
)}
</div>
</div>
</StyledOnboardingPage>
);
}
@@ -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 (
<StyledAdminCredentialsStep>
<span className="primaryText">Now lets set up your admin account</span>
<span className="secondaryText">You are the 1st user and admin of your space!</span>
<StyledInput
className="input"
placeholder="Enter your email"
value={data.adminEmail}
onChange={(e) =>
setData({
...data,
adminEmail: e.target.value
})
}
/>
<StyledInput
className="input"
type="password"
placeholder="Enter your password"
value={data.adminPassword}
onChange={(e) =>
setData({
...data,
adminPassword: e.target.value
})
}
/>
<StyledInput
className="input"
type="password"
placeholder="Confirm your password"
value={data.adminPassword2}
onChange={(e) =>
setData({
...data,
adminPassword2: e.target.value
})
}
/>
</StyledAdminCredentialsStep>
);
}
+26
View File
@@ -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 (
<StyledFirstStep>
<span className="primaryText">Welcome to your Rustchat!</span>
<span className="secondaryText">
Everything in this space is owned by you. Lets set up your space!
</span>
<StyledButton className="startButton" onClick={onButtonClick}>
<img src={PlayIcon} alt="play icon" />
<span>Start</span>
</StyledButton>
</StyledFirstStep>
);
}
+70
View File
@@ -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 (
<StyledInviteLinkStep>
<span className="primaryText">Last step: invite others!</span>
<span className="secondaryText">Now lets invite others!</span>
<span className="tip">Send invitation link to your future community members:</span>
<div className="link">
<StyledInput className="large" readOnly placeholder="Generating" value={link} />
<StyledButton onClick={copyLink} className="ghost small border_less">
{linkCopied ? "Copied" : `Copy`}
</StyledButton>
</div>
</StyledInviteLinkStep>
);
}
+33
View File
@@ -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 (
<StyledInviteRuleStep>
<span className="primaryText">Last step: invite others!</span>
<span className="secondaryText">Firstly, who can sign up to this server?</span>
<StyledRadio
options={["Everyone", "Invitation link only"]}
value={data.inviteRule}
onChange={(v) =>
setData({
...data,
inviteRule: v
})
}
/>
</StyledInviteRuleStep>
);
}
+44
View File
@@ -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 (
<StyledLastStep>
<span className="primaryText">Welcome to {data.spaceName}</span>
<span className="secondaryText">Proudly presented by Rustchat</span>
<span className="tip">
More settings, including domain resolution, privileges, securities, and invites are available in{" "}
<span className="strong">Settings</span>
</span>
<StyledButton className="startButton" onClick={onButtonClick}>
<img src={PlayIcon} alt="play icon" />
<span>Start</span>
</StyledButton>
</StyledLastStep>
);
}
+34
View File
@@ -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 (
<StyledSpaceNameStep>
<span className="primaryText">Lets start with name:</span>
<span className="secondaryText">This space is called: </span>
<StyledInput
className="input"
placeholder="Enter space name"
value={data.spaceName}
onChange={(e) =>
setData({
...data,
spaceName: e.target.value
})
}
/>
</StyledSpaceNameStep>
);
}
+92
View File
@@ -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;