Merge pull request #5 from Privoce/improve/onboarding
Improve/onboarding
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>Don’t have an account?</span>
|
||||
<a onClick={handleSignUp}>Sign up</a>
|
||||
</StyledSignUpLink>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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. Let’s 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>
|
||||
);
|
||||
}
|
||||
+43
-8
@@ -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 let’s 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+28
-9
@@ -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 let’s 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>
|
||||
);
|
||||
}
|
||||
+34
-13
@@ -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. Let’s set up your space!
|
||||
</span>
|
||||
<StyledButton className="startButton" onClick={nextStep}>
|
||||
<img src={PlayIcon} alt="play icon" />
|
||||
<span>Start</span>
|
||||
</StyledButton>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
+48
-20
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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 };
|
||||
@@ -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> */}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user