Merge pull request #5 from Privoce/improve/onboarding

Improve/onboarding
This commit is contained in:
Tristan Yang
2022-06-13 21:16:51 +08:00
committed by GitHub
72 changed files with 4072 additions and 271 deletions
+65
View File
@@ -0,0 +1,65 @@
/* eslint-disable no-undef */
import { useState } from "react";
import styled from "styled-components";
import StyledModal from "../../common/component/styled/Modal";
import Modal from "../../common/component/Modal";
import { StyledSocialButton } from "./styled";
import solidSvg from "../../assets/icons/solid.svg?url";
import StyledButton from "../../common/component/styled/Button";
import SolidLoginButton from "./SolidLoginButton";
const StyledOicdLoginModal = styled(StyledModal)`
text-align: center;
padding: 32px 32px 16px;
> *:first-child {
margin-bottom: 32px;
}
> .button {
> .icon {
width: 24px;
height: 24px;
}
&Cancel {
color: #8f8f8f;
}
}
`;
export default function OidcLoginButton({ issuers }) {
const [modal, setModal] = useState(false);
if (!issuers) return null;
return (
<>
<StyledSocialButton
onClick={() => {
setModal(true);
}}
>
<img src={solidSvg} className="icon" alt="solid icon" />
Sign in with OIDC
</StyledSocialButton>
{modal && (
<Modal id="modal-modal">
<StyledOicdLoginModal title="Login with OIDC">
{issuers
.filter((issuer) => issuer.enable)
.map((issuer, index) => (
<SolidLoginButton issuer={issuer} key={index} />
))}
<StyledButton
className="border_less ghost buttonCancel"
onClick={() => {
setModal(false);
}}
>
Close
</StyledButton>
</StyledOicdLoginModal>
</Modal>
)}
</>
);
}
+35
View File
@@ -0,0 +1,35 @@
import styled from "styled-components";
import { useNavigate } from "react-router-dom";
const StyledSignUpLink = styled.p`
text-align: center;
margin: 24px 0 8px;
> span {
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: #667085;
margin-right: 4px;
}
> a {
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #22d3ee;
cursor: pointer;
}
`;
export default function MagicLinkLogin() {
const navigate = useNavigate();
const handleSignUp = () => {
navigate("/");
};
return (
<StyledSignUpLink>
<span>Dont have an account?</span>
<a onClick={handleSignUp}>Sign up</a>
</StyledSignUpLink>
);
}
+5 -6
View File
@@ -1,16 +1,15 @@
/* eslint-disable no-undef */
import { useEffect } from "react";
import { useGetOpenidMutation } from "../../app/services/auth";
import solidSvg from "../../assets/icons/solid.svg?url";
import { StyledSocialButton } from "./styled";
export default function SolidLoginButton({ issuers }) {
export default function SolidLoginButton({ issuer }) {
const [getOpenId, { data, isLoading, isSuccess }] = useGetOpenidMutation();
const handleSolidLogin = () => {
getOpenId({
// issuer: "solidweb.org",
issuer: issuers[0],
issuer,
redirect_uri: `${location.origin}/#/login`
});
};
@@ -21,11 +20,11 @@ export default function SolidLoginButton({ issuers }) {
location.href = url;
}
}, [data, isSuccess]);
console.log(issuer);
return (
<StyledSocialButton disabled={isLoading} onClick={handleSolidLogin}>
<img src={solidSvg} className="icon" alt="solid icon" />
{isLoading ? `Redirecting...` : `Sign in with Solid`}
{Boolean(issuer.favicon) && <img src={issuer.favicon} className="icon" alt="icon" />}
Login with {issuer.domain}
</StyledSocialButton>
);
}
+6 -3
View File
@@ -5,11 +5,12 @@ import BASE_URL from "../../app/config";
// import web3 from "web3";
import StyledWrapper from "./styled";
import MetamaskLoginButton from "./MetamaskLoginButton";
import SolidLoginButton from "./SolidLoginButton";
import OidcLoginButton from "./OidcLoginButton";
import Input from "../../common/component/styled/Input";
import Button from "../../common/component/styled/Button";
import GoogleLoginButton from "./GoogleLoginButton";
import MagicLinkLogin from "./MagicLinkLogin";
import SignUpLink from "./SignUpLink";
import { useLoginMutation } from "../../app/services/auth";
import { useGetLoginConfigQuery, useGetSMTPStatusQuery } from "../../app/services/server";
import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig";
@@ -125,7 +126,8 @@ export default function LoginPage() {
github: enableGithubLogin,
google: enableGoogleLogin,
metamask: enableMetamaskLogin,
oidc = []
oidc = [],
who_can_sign_up: whoCanSignUp
} = loginConfig;
const enableMagicLink = enableSMTP && magic_link;
const googleLogin = enableGoogleLogin && clientId;
@@ -168,7 +170,8 @@ export default function LoginPage() {
{googleLogin && <GoogleLoginButton login={login} clientId={clientId} />}
{enableGithubLogin && <GithubLoginButton config={githubAuthConfig} />}
{enableMetamaskLogin && <MetamaskLoginButton login={login} />}
{oidc.length > 0 && <SolidLoginButton issuers={oidc} />}
{oidc.length > 0 && <OidcLoginButton issuers={oidc} />}
{whoCanSignUp === "EveryOne" && <SignUpLink />}
</div>
</StyledWrapper>
);
+68 -20
View File
@@ -1,20 +1,50 @@
import { useState } from "react";
import { Navigate } from "react-router-dom";
import React from "react";
import { Helmet } from "react-helmet";
import WelcomeStep from "./steps/1-welcome";
import ServerNameStep from "./steps/2-serverName";
import AdminCredentialsStep from "./steps/3-adminCredentials";
import InviteRuleStep from "./steps/4-inviteRule";
import InviteLinkStep from "./steps/5-inviteLink";
import CompletedStep from "./steps/6-completed";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import WelcomePage from "./steps/welcomePage";
import ServerName from "./steps/serverName";
import AdminAccount from "./steps/adminAccount";
import WhoCanSignUp from "./steps/whoCanSignUp";
import InviteLink from "./steps/inviteLink";
import DonePage from "./steps/donePage";
import useServerSetup, { steps } from "./useServerSetup";
import StyledOnboardingPage from "./styled";
function Navigator({ step, setStep }) {
const index = steps.map((value) => value.name).indexOf(step);
const canJumpTo = steps.find((value) => value.name === step).canJumpTo || [];
return (
<div className="navigator">
{steps.map((stepToRender, indexToRender) => {
const clickable = canJumpTo.includes(stepToRender.name);
const nodeCls = `node ${indexToRender === index ? "emphasized" : ""} ${
indexToRender > index ? "disabled" : ""
} ${clickable ? "clickable" : ""}`;
const arrowCls = `arrow ${indexToRender >= index ? "disabled" : ""}`;
return (
<React.Fragment key={indexToRender}>
<span
className={nodeCls}
onClick={() => {
if (clickable) {
setStep(stepToRender.name);
}
}}
>
{stepToRender.label}
</span>
{indexToRender !== steps.length - 1 && <span className={arrowCls}></span>}
</React.Fragment>
);
})}
</div>
);
}
export default function OnboardingPage() {
const [step, setStep] = useState(0);
const [data, setData] = useState({
serverName: ""
});
const props = { setStep, data, setData };
const serverSetup = useServerSetup();
return (
<>
@@ -22,13 +52,31 @@ export default function OnboardingPage() {
<title>Rustchat Setup</title>
</Helmet>
<StyledOnboardingPage>
{step === 0 && <WelcomeStep {...props} />}
{step === 1 && <ServerNameStep {...props} />}
{step === 2 && <AdminCredentialsStep {...props} />}
{step === 3 && <InviteRuleStep {...props} />}
{step === 4 && <InviteLinkStep {...props} />}
{step === 5 && <CompletedStep {...props} />}
{step === 6 && <Navigate replace to="/" />}
<Navigator {...serverSetup} />
<Swiper
spaceBetween={50}
allowTouchMove={false}
onSwiper={(swiper) => serverSetup.setSwiper(swiper)}
>
<SwiperSlide>
<WelcomePage {...serverSetup} />
</SwiperSlide>
<SwiperSlide>
<ServerName {...serverSetup} />
</SwiperSlide>
<SwiperSlide>
<AdminAccount {...serverSetup} />
</SwiperSlide>
<SwiperSlide>
<WhoCanSignUp {...serverSetup} />
</SwiperSlide>
<SwiperSlide>
<InviteLink {...serverSetup} />
</SwiperSlide>
<SwiperSlide>
<DonePage {...serverSetup} />
</SwiperSlide>
</Swiper>
</StyledOnboardingPage>
</>
);
-26
View File
@@ -1,26 +0,0 @@
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 WelcomeStep({ setStep }) {
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={() => setStep((prev) => prev + 1)}>
<img src={PlayIcon} alt="play icon" />
<span>Start</span>
</StyledButton>
</StyledFirstStep>
);
}
@@ -1,44 +0,0 @@
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: 560px;
font-size: 20px;
line-height: 24px;
text-align: center;
margin-bottom: 96px;
> .strong {
font-weight: 700;
}
}
`;
export default function CompletedStep({ data, setStep }) {
return (
<StyledLastStep>
<span className="primaryText">Welcome to {data.serverName}</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={() => setStep((prev) => prev + 1)}>
<img src={PlayIcon} alt="play icon" />
<span>Enter</span>
</StyledButton>
</StyledLastStep>
);
}
@@ -12,23 +12,58 @@ import {
import { useLoginMutation } from "../../../app/services/auth";
import { updateInitialized } from "../../../app/slices/auth.data";
const StyledAdminCredentialsStep = styled.div`
const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .input:not(:nth-last-child(2)) {
margin-bottom: 20px;
> .primaryText {
text-align: center;
font-weight: 700;
font-size: 24px;
line-height: 30px;
margin-bottom: 8px;
}
> .secondaryText {
text-align: center;
font-size: 14px;
line-height: 20px;
margin-bottom: 24px;
}
> .input {
width: 360px;
height: 44px;
font-weight: 400;
font-size: 16px;
line-height: 24px;
padding: 10px 14px;
border: 1px solid #d0d5dd;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
> .inner {
padding: 0;
font-weight: 400;
font-size: 16px;
line-height: 24px;
}
&:not(:nth-last-child(2)) {
margin-bottom: 20px;
}
}
> .button {
width: 360px;
margin-top: 24px;
}
`;
export default function AdminCredentialsStep({ data, setStep }) {
export default function AdminAccount({ serverName, nextStep }) {
const dispatch = useDispatch();
const [createAdmin, { isLoading: isSigningUp, error: signUpError }] = useCreateAdminMutation();
@@ -60,7 +95,7 @@ export default function AdminCredentialsStep({ data, setStep }) {
// Set server name
updateServer({
...serverData,
name: data.serverName
name: serverName
});
}, 0);
}
@@ -69,12 +104,12 @@ export default function AdminCredentialsStep({ data, setStep }) {
// After updated server
useEffect(() => {
if (isUpdatedServer) {
setStep((prev) => prev + 1);
nextStep();
}
}, [isUpdatedServer]);
return (
<StyledAdminCredentialsStep>
<StyledWrapper>
<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
@@ -126,6 +161,6 @@ export default function AdminCredentialsStep({ data, setStep }) {
>
{!(isSigningUp || isLoggingIn || isUpdatingServer) ? "Sign Up" : "..."}
</StyledButton>
</StyledAdminCredentialsStep>
</StyledWrapper>
);
}
+77
View File
@@ -0,0 +1,77 @@
import styled from "styled-components";
import { useNavigate } from "react-router-dom";
import StyledButton from "../../../common/component/styled/Button";
import PlayIcon from "../../../assets/icons/play.svg?url";
const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .primaryText {
text-align: center;
font-weight: 700;
font-size: 24px;
line-height: 30px;
margin-bottom: 8px;
}
> .secondaryText {
text-align: center;
font-weight: 400;
font-size: 14px;
line-height: 20px;
margin-bottom: 48px;
}
> .tip {
width: 588px;
font-size: 20px;
line-height: 24px;
text-align: center;
margin-bottom: 48px;
> .strong {
font-weight: 700;
}
}
> .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;
}
}
`;
export default function DonePage({ serverName }) {
const navigate = useNavigate();
return (
<StyledWrapper>
<span className="primaryText">Welcome to {serverName}</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={() => navigate("/")}>
<img src={PlayIcon} alt="play icon" />
<span>Enter</span>
</StyledButton>
</StyledWrapper>
);
}
@@ -3,14 +3,25 @@ import StyledInput from "../../../common/component/styled/Input";
import StyledButton from "../../../common/component/styled/Button";
import useInviteLink from "../../../common/hook/useInviteLink";
const StyledInviteLinkStep = styled.div`
const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .primaryText {
text-align: center;
font-weight: 700;
font-size: 24px;
line-height: 30px;
margin-bottom: 8px;
}
> .secondaryText {
text-align: center;
font-size: 14px;
line-height: 20px;
margin-bottom: 40px;
}
@@ -47,15 +58,26 @@ const StyledInviteLinkStep = styled.div`
font-size: 14px;
line-height: 20px;
color: #22ccee;
transition: color 150ms ease-in-out;
&:hover {
color: #088ab2;
}
}
}
> .button {
width: 124px;
height: 44px;
margin-top: 24px;
}
`;
export default function InviteLinkStep({ setStep }) {
export default function InviteLink({ nextStep }) {
const { link, linkCopied, copyLink } = useInviteLink();
return (
<StyledInviteLinkStep>
<StyledWrapper>
<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>
@@ -65,12 +87,9 @@ export default function InviteLinkStep({ setStep }) {
{linkCopied ? "Copied" : `Copy`}
</StyledButton>
</div>
<StyledButton
className="button border_less ghost"
onClick={() => setStep((prev) => prev + 1)}
>
Skip
<StyledButton className="button" onClick={nextStep}>
Done
</StyledButton>
</StyledInviteLinkStep>
</StyledWrapper>
);
}
@@ -3,25 +3,51 @@ import toast from "react-hot-toast";
import StyledInput from "../../../common/component/styled/Input";
import StyledButton from "../../../common/component/styled/Button";
const StyledSpaceNameStep = styled.div`
const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .primaryText {
text-align: center;
font-weight: 700;
font-size: 24px;
line-height: 30px;
margin-bottom: 8px;
}
> .secondaryText {
width: 360px;
text-align: center;
font-size: 14px;
line-height: 20px;
margin-bottom: 24px;
color: #667085;
}
> .input {
width: 360px;
height: 44px;
font-weight: 400;
font-size: 16px;
line-height: 24px;
padding: 10px 14px;
border: 1px solid #d0d5dd;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
}
> .button {
width: 360px;
margin-top: 24px;
}
`;
export default function ServerNameStep({ setStep, data, setData }) {
export default function ServerName({ serverName, setServerName, nextStep }) {
return (
<StyledSpaceNameStep>
<StyledWrapper>
<span className="primaryText">Create a new server</span>
<span className="secondaryText">
Servers are shared environments where teams can work on projects and chat.
@@ -29,27 +55,22 @@ export default function ServerNameStep({ setStep, data, setData }) {
<StyledInput
className="input"
placeholder="Enter server name"
value={data.serverName}
onChange={(e) =>
setData({
...data,
serverName: e.target.value
})
}
value={serverName}
onChange={(e) => setServerName(e.target.value)}
/>
<StyledButton
className="button"
onClick={() => {
// Verification for space name
if (data.serverName === "") {
if (serverName === "") {
toast.error("Please enter server name!");
return;
}
setStep((prev) => prev + 1);
nextStep();
}}
>
Create Server
</StyledButton>
</StyledSpaceNameStep>
</StyledWrapper>
);
}
@@ -0,0 +1,59 @@
import styled from "styled-components";
import StyledButton from "../../../common/component/styled/Button";
import PlayIcon from "../../../assets/icons/play.svg?url";
const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .primaryText {
text-align: center;
font-weight: 700;
font-size: 24px;
line-height: 30px;
margin-bottom: 8px;
}
> .secondaryText {
text-align: center;
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;
}
}
`;
export default function WelcomePage({ nextStep }) {
return (
<StyledWrapper>
<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={nextStep}>
<img src={PlayIcon} alt="play icon" />
<span>Start</span>
</StyledButton>
</StyledWrapper>
);
}
@@ -1,63 +1,91 @@
import { useState, useEffect } from "react";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import styled from "styled-components";
import StyledRadio from "../../../common/component/styled/Radio";
import StyledButton from "../../../common/component/styled/Button";
import { useGetLoginConfigQuery, useUpdateLoginConfigMutation } from "../../../app/services/server";
const StyledInviteRuleStep = styled.div`
const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .input:not(:nth-last-child(2)) {
margin-bottom: 20px;
> .primaryText {
text-align: center;
font-weight: 700;
font-size: 24px;
line-height: 30px;
margin-bottom: 8px;
}
> .secondaryText {
text-align: center;
font-size: 14px;
line-height: 20px;
margin-bottom: 24px;
}
> form {
width: 512px;
}
> .button {
width: 124px;
height: 44px;
margin-top: 24px;
}
`;
export default function InviteRuleStep({ setStep }) {
export default function WhoCanSignUp({ nextStep }) {
const { data: loginConfig } = useGetLoginConfigQuery();
const [updateLoginConfig, { isSuccess, error }] = useUpdateLoginConfigMutation();
const [value, setValue] = useState(0);
const [value, setValue] = useState(undefined);
// Sync to `value` when `loginConfig` is fetched
useEffect(() => {
if (loginConfig) {
setValue(loginConfig.who_can_sign_up);
}
}, [loginConfig]);
// Display error
useEffect(() => {
if (error === undefined) return;
toast.error(`Failed to update invitation rule: ${error.data}`);
toast.error(`Failed to update sign up rule: ${error.data}`);
}, [error]);
// Increment `step` when updating has completed
useEffect(() => {
if (isSuccess) setStep((prev) => prev + 1);
if (isSuccess) nextStep();
}, [isSuccess]);
return (
<StyledInviteRuleStep>
<StyledWrapper>
<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"]}
values={["EveryOne", "InvitationOnly"]}
value={value}
onChange={async (v) => {
setValue(v);
onChange={setValue}
/>
<StyledButton
className="button"
disabled={!value}
onClick={() => {
if (loginConfig !== undefined) {
const whoCanSignUp = ["EveryOne", "InvitationOnly"][v];
await updateLoginConfig({
updateLoginConfig({
...loginConfig,
who_can_sign_up: whoCanSignUp
who_can_sign_up: value
});
}
}}
/>
<StyledButton
className="button border_less ghost"
onClick={() => setStep((prev) => prev + 1)}
>
Skip
Confirm
</StyledButton>
</StyledInviteRuleStep>
</StyledWrapper>
);
}
+32 -59
View File
@@ -4,71 +4,44 @@ const StyledOnboardingPage = styled.div`
height: 100vh;
overflow-y: auto;
// 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;
> .navigator {
position: absolute;
top: 20px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 15px 0 12px;
justify-content: center;
gap: 8px;
z-index: 10;
> img {
margin-bottom: 7px;
}
> span {
font-weight: 500;
> .node,
> .arrow {
font-weight: 400;
font-size: 14px;
line-height: 20px;
line-height: 28px;
color: #101828;
cursor: default;
transition: all 150ms ease-in-out;
&.disabled {
color: #d0d5dd;
}
&.emphasized {
font-weight: 600;
}
&.clickable {
cursor: pointer;
&:hover {
color: #717180;
}
}
}
}
.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;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
}
.button {
width: 360px;
&.ghost.border_less {
width: fit-content;
font-weight: 500;
font-size: 16px;
line-height: 24px;
color: #98a2b3;
margin-top: 14px;
}
> .swiper {
height: 100%;
}
`;
+68
View File
@@ -0,0 +1,68 @@
import { useCallback, useState } from "react";
// `name` for in-code usage, `label` for display
const steps = [
{
name: "welcomePage",
label: "Welcome Page"
},
{
name: "serverName",
label: "Set Name"
},
{
name: "adminAccount",
label: "Admin Account"
},
{
name: "whoCanSignUp",
label: "Who Can Sign Up"
},
{
name: "inviteLink",
label: "Invites",
canJumpTo: ["whoCanSignUp"]
},
{
name: "donePage",
label: "Done",
canJumpTo: ["whoCanSignUp", "inviteLink"]
}
];
export default function useServerSetup() {
const [swiper, setSwiper] = useState(null);
const [index, setIndex] = useState(0);
const step = steps[index].name;
const setStep = useCallback(
(name) => {
const newIndex = steps.map((step) => step.name).indexOf(name);
setIndex(newIndex);
if (swiper !== null) {
swiper.slideTo(newIndex);
}
},
[swiper]
);
const nextStep = useCallback(() => {
setIndex((prev) => prev + 1);
if (swiper !== null) {
swiper.slideNext(500);
}
}, [swiper]);
const [serverName, setServerName] = useState("");
return {
swiper,
setSwiper,
step,
setStep,
nextStep,
serverName,
setServerName
};
}
export { steps };
+90 -25
View File
@@ -4,7 +4,9 @@ import { useSelector } from "react-redux";
import {
useGetServerQuery,
useUpdateServerMutation,
useUpdateLogoMutation
useUpdateLogoMutation,
useUpdateLoginConfigMutation,
useGetLoginConfigQuery
} from "../../app/services/server";
import LogoUploader from "../../common/component/AvatarUploader";
import Input from "../../common/component/styled/Input";
@@ -12,6 +14,8 @@ import Label from "../../common/component/styled/Label";
import Textarea from "../../common/component/styled/Textarea";
import SaveTip from "../../common/component/SaveTip";
import toast from "react-hot-toast";
import StyledRadio from "../../common/component/styled/Radio";
const StyledWrapper = styled.div`
position: relative;
width: 512px;
@@ -19,24 +23,29 @@ const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 24px;
.logo {
display: flex;
gap: 16px;
.preview {
width: 96px;
height: 96px;
}
.upload {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
.tip {
font-weight: normal;
font-size: 14px;
line-height: 20px;
color: #374151;
}
.btn {
padding: 8px 14px;
font-weight: 500;
@@ -51,11 +60,13 @@ const StyledWrapper = styled.div`
}
}
}
.inputs {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
.input {
width: 100%;
display: flex;
@@ -63,6 +74,27 @@ const StyledWrapper = styled.div`
align-items: flex-start;
gap: 8px;
}
> .radioButton {
> .label {
margin-top: 64px;
font-weight: 500;
font-size: 14px;
line-height: 20px;
}
> .tip {
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: #667085;
}
> form {
margin-top: 16px;
width: 512px;
}
}
}
`;
export default function Overview() {
@@ -70,57 +102,73 @@ export default function Overview() {
return store.contacts.byId[store.authData.uid];
});
const [changed, setChanged] = useState(false);
const [values, setValues] = useState(null);
const { data, refetch } = useGetServerQuery();
const [updateServer, { isSuccess: updated }] = useUpdateServerMutation();
const [serverValues, setServerValues] = useState(null);
const [loginConfigValues, setLoginConfigValues] = useState(null);
const { data: server, refetch: refetchServer } = useGetServerQuery();
const { data: loginConfig, refetch: refetchLoginConfig } = useGetLoginConfigQuery();
const [updateServer, { isSuccess: serverUpdated }] = useUpdateServerMutation();
const [uploadLogo, { isSuccess: uploadSuccess }] = useUpdateLogoMutation();
const [updateLoginConfig, { isSuccess: loginConfigUpdated }] = useUpdateLoginConfigMutation();
const handleUpdate = () => {
const { name, description } = values;
const { name, description } = serverValues;
updateServer({ name, description });
updateLoginConfig({
...loginConfig,
who_can_sign_up: loginConfigValues.who_can_sign_up
});
};
const handleChange = (evt) => {
const newValue = evt.target.value;
const { type } = evt.target.dataset;
setValues((prev) => {
setServerValues((prev) => {
return { ...prev, [type]: newValue };
});
};
const handleReset = () => {
console.log("reset", data);
setValues(data);
console.log("reset", server, loginConfig);
setServerValues(server);
setLoginConfigValues(loginConfig);
};
useEffect(() => {
if (uploadSuccess) {
toast.success("update logo successfully");
refetch();
toast.success("Update logo successfully!");
refetchServer();
}
}, [uploadSuccess]);
useEffect(() => {
if (data) {
setValues(data);
if (server) {
setServerValues(server);
}
}, [data]);
}, [server]);
useEffect(() => {
if (data && values) {
const { name, description } = values;
const { name: oName, description: oDescription } = data;
if (oName !== name || oDescription !== description) {
if (loginConfig) {
setLoginConfigValues(loginConfig);
}
}, [loginConfig]);
useEffect(() => {
if (server && serverValues && loginConfig && loginConfigValues) {
const { name, description } = serverValues;
const { name: oName, description: oDescription } = server;
const { who_can_sign_up: whoCanSignUp } = loginConfigValues;
const { who_can_sign_up: oWhoCanSignUp } = loginConfig;
if (oName !== name || oDescription !== description || oWhoCanSignUp !== whoCanSignUp) {
setChanged(true);
} else {
setChanged(false);
}
}
}, [data, values]);
}, [server, serverValues, loginConfig, loginConfigValues]);
useEffect(() => {
if (updated) {
toast.success("Server updated!");
refetch();
if (serverUpdated && loginConfigUpdated) {
toast.success("Configuration updated!");
refetchServer();
refetchLoginConfig();
}
}, [updated]);
}, [serverUpdated, loginConfigUpdated]);
if (!values) return null;
const { name, description, logo } = values;
if (!serverValues || !loginConfigValues) return null;
const { name, description, logo } = serverValues;
const { who_can_sign_up: whoCanSignUp } = loginConfigValues;
const isAdmin = loginUser?.is_admin;
return (
<StyledWrapper>
@@ -169,6 +217,23 @@ export default function Overview() {
placeholder="Tell the world a bit about this server"
/>
</div>
{isAdmin && (
<div className="radioButton">
<p className="label">Default Sign Up</p>
<p className="tip">Who can sign up this server.</p>
<StyledRadio
options={["Everyone", "Invitation Link Only"]}
values={["EveryOne", "InvitationOnly"]}
value={whoCanSignUp}
onChange={(v) =>
setLoginConfigValues({
...loginConfig,
who_can_sign_up: v
})
}
/>
</div>
)}
</div>
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={handleReset} />}
{/* <button onClick={handleUpdate} className="btn">update</button> */}
+49 -8
View File
@@ -8,13 +8,16 @@ import Styled from "./styled";
// import IconPlus from "../../../../assets/icons/plus.circle.svg";
import IconMinus from "../../../../assets/icons/minus.circle.svg";
export default function IssuerList({ issuers = [] }) {
const [select, setSelect] = useState(undefined);
export default function IssuerList({ issuers = [], onChange }) {
const [select, setSelect] = useState({});
const [newDomain, setNewDomain] = useState("");
const handleNewDomain = (evt) => {
setNewDomain(evt.target.value);
};
const disableBtn = !(newDomain || !!select?.value);
const disableBtn =
(!newDomain && !select?.value) ||
!select?.title ||
issuers.some((issuer) => issuer.domain === newDomain);
return (
<Styled>
<ul className="issuers">
@@ -22,9 +25,14 @@ export default function IssuerList({ issuers = [] }) {
return (
<li key={domain} className="issuer">
<div className="left">
<IconMinus className="remove" />
<IconMinus
className="remove"
onClick={() => {
onChange(issuers.filter((issuer) => issuer.domain !== domain));
}}
/>
<div className="data">
<img src={favicon} alt="logo" className="icon" />
{Boolean(favicon) && <img src={favicon} alt="logo" className="icon" />}
<Input
readOnly
value={domain}
@@ -35,7 +43,17 @@ export default function IssuerList({ issuers = [] }) {
</div>
</div>
<div className="right">
<Toggle data-checked={enable} />
<Toggle
data-checked={enable}
onClick={() => {
onChange(
issuers.map((issuer) => ({
...issuer,
enable: issuer.domain === domain ? !enable : issuer.enable
}))
);
}}
/>
</div>
</li>
);
@@ -43,7 +61,14 @@ export default function IssuerList({ issuers = [] }) {
<li className="issuer add">
<div className="left">
<Select options={options} updateSelect={setSelect} />
<Select
options={options.map((option) => ({
...option,
selected: issuers.some((issuer) => issuer.domain === option.value)
}))}
current={select}
updateSelect={setSelect}
/>
<div className="data">
<Input
onChange={handleNewDomain}
@@ -56,7 +81,23 @@ export default function IssuerList({ issuers = [] }) {
</div>
</div>
<div className="right">
<Button disabled={disableBtn}>Add</Button>
<Button
disabled={disableBtn}
onClick={() => {
const { icon, value } = options.find((option) => option.value === select.value);
onChange(
issuers.concat({
enable: true,
favicon: icon || "",
domain: value || newDomain
})
);
setSelect({});
setNewDomain("");
}}
>
Add
</Button>
</div>
</li>
</ul>
File diff suppressed because one or more lines are too long
@@ -18,14 +18,13 @@ const Styled = styled.div`
flex: 1;
display: flex;
align-items: center;
gap: 16px;
justify-content: space-between;
.remove {
cursor: pointer;
}
.data {
display: flex;
align-items: flex-end;
align-items: center;
gap: 16px;
justify-content: space-between;
> .icon {
+9 -1
View File
@@ -182,7 +182,15 @@ export default function Logins() {
</div>
</div>
<div className="row">
<IssuerList issuers={oidc} />
<IssuerList
issuers={oidc}
onChange={(newOidc) => {
setValues((prev) => ({
...prev,
oidc: newOidc
}));
}}
/>
</div>
</div>
</div>