use new register component

This commit is contained in:
SrGooglo 2024-02-19 18:59:20 +00:00
parent 843186ca33
commit 3e6de98d10
15 changed files with 935 additions and 152 deletions

View File

@ -388,7 +388,7 @@ class ComtyApp extends React.Component {
"app.no_session": async () => {
const location = window.location.pathname
if (location !== "/" && location !== "/login" && location !== "/register") {
if (location !== "/" && location !== "/auth" && location !== "/register") {
antd.notification.info({
message: "You are not logged in, to use some features you will need to log in.",
btn: <antd.Button type="primary" onClick={() => app.goAuth()}>Login</antd.Button>,

View File

@ -0,0 +1,215 @@
import React from "react"
import * as antd from "antd"
import { Icons, createIconRender } from "components/Icons"
import classnames from "classnames"
import AuthModel from "models/auth"
import "./index.less"
import UsernameStep from "./steps/username"
import PasswordStep from "./steps/password"
import EmailStep from "./steps/email"
import TOSStep from "./steps/tos"
const steps = [
UsernameStep,
PasswordStep,
EmailStep,
TOSStep,
]
const RegisterForm = (props) => {
const [finishing, setFinishing] = React.useState(false)
const [finishError, setFinishError] = React.useState(false)
const [finishSuccess, setFinishSuccess] = React.useState(false)
const [stepsValues, setStepsValues] = React.useState({})
const [step, setStep] = React.useState(0)
const currentStepData = steps[step - 1]
async function finish() {
setFinishError(null)
setFinishSuccess(false)
setFinishing(true)
const result = await AuthModel.register({
username: stepsValues.username,
password: stepsValues.password,
email: stepsValues.email,
tos: stepsValues.tos,
}).catch((err) => {
setFinishSuccess(false)
setFinishing(false)
setFinishError(err)
})
if (result) {
setFinishing(false)
setFinishSuccess(true)
}
}
function nextStep(to) {
setStep((prev) => {
if (!to) {
to = prev + 1
}
if (to === steps.length + 1) {
finish()
return prev
}
return to
})
}
function prevStep() {
setStep((prev) => {
return prev - 1
})
}
const updateStepValue = (value) => setStepsValues((prev) => {
return {
...prev,
[currentStepData.key]: value
}
})
function canNextStep() {
if (!currentStepData) {
return true
}
if (!currentStepData.required) {
return true
}
const currentStepValue = stepsValues[currentStepData.key]
if (currentStepData.required) {
if (!currentStepValue) {
return false
}
}
return true
}
return <div
className={classnames(
"register_form",
{
["welcome_step"]: step === 0 && !finishing
}
)}
>
<div className="register_form_header-text">
{
!finishSuccess && !finishing && step === 0 && <>
<h1>👋 Hi! Nice to meet you</h1>
<p>Tell us some basic information to get started creating your account.</p>
</>
}
{
!finishSuccess && !finishing && step > 0 && <>
<h1>
{
currentStepData?.icon && createIconRender(currentStepData.icon)
}
{currentStepData?.title}
</h1>
<p>
{
typeof currentStepData?.description === "function" ?
currentStepData?.description() : currentStepData.description
}
</p>
</>
}
</div>
{
!finishSuccess && !finishing && step > 0 && React.createElement(currentStepData.content, {
onPressEnter: nextStep,
currentValue: stepsValues[currentStepData.key],
updateValue: updateStepValue,
})
}
{
finishing && <div className="register_form_creating">
<Icons.LoadingOutlined />
<h1>
Creating your account
</h1>
</div>
}
{
finishSuccess && <div className="register_form_success">
<Icons.CheckCircleOutlined />
<h1>
Welcome abord!
</h1>
<p>
One last step, we need you to login with your new account.
</p>
<antd.Button
type="primary"
onClick={() => props.changeStage(0)}
>
Go to login
</antd.Button>
</div>
}
{
finishError && <antd.Alert
type="error"
message={finishError.message}
/>
}
{
!finishSuccess && !finishing && <div className="register_form_actions">
{
step === 0 &&
<antd.Button
onClick={() => props.changeStage(0)}
>
Cancel
</antd.Button>
}
{
step > 0 &&
<antd.Button
onClick={() => prevStep()}
>
Back
</antd.Button>
}
<antd.Button
type="primary"
onClick={() => nextStep()}
disabled={!canNextStep()}
>
{
step === steps.length ? "Finish" : "Next"
}
</antd.Button>
</div>
}
</div>
}
export default RegisterForm

View File

@ -0,0 +1,77 @@
.register_form {
position: relative;
display: flex;
flex-direction: column;
gap: 20px;
height: 100%;
transition: all 250ms ease-in-out;
&.welcome_step {
height: 150px;
}
.register_form_header-text {
display: flex;
flex-direction: column;
}
.register_form_step_content {
display: flex;
flex-direction: column;
gap: 10px;
}
.register_form_creating,
.register_form_success {
display: flex;
flex-direction: row;
height: 100%;
gap: 10px;
align-items: center;
justify-content: center;
h1 {
margin: 0;
}
svg {
width: fit-content;
font-size: 1.5rem;
color: var(--text-color);
margin: 0;
}
}
.register_form_success {
flex-direction: column;
align-items: flex-start;
}
.register_form_actions {
position: absolute;
display: inline-flex;
flex-direction: row;
justify-content: flex-end;
width: 100%;
gap: 10px;
bottom: 0;
right: 0;
padding: 20px;
}
}

View File

@ -0,0 +1,90 @@
import React from "react"
import * as antd from "antd"
import UserModel from "models/user"
const EmailStepComponent = (props) => {
const [email, setEmail] = React.useState(props.currentValue ?? "")
const [loading, setLoading] = React.useState(false)
const [validFormat, setValidFormat] = React.useState(null)
const [emailAvailable, setEmailAvailable] = React.useState(null)
const isValid = () => {
return email.length > 0 && validFormat && emailAvailable
}
const checkIfIsEmail = (email) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
const submit = () => {
if (!isValid()) return
props.onPressEnter()
}
const handleUpdate = (e) => {
setEmail(e.target.value)
}
React.useEffect(() => {
if (email.length === 0) {
setEmailAvailable(null)
setValidFormat(null)
return
}
props.updateValue(null)
setLoading(true)
setValidFormat(checkIfIsEmail(email))
// check if email is available
const timer = setTimeout(async () => {
if (!validFormat) return
const request = await UserModel.checkEmailAvailability(email).catch((error) => {
antd.message.error(`Cannot check email availability: ${error.message}`)
return false
})
if (request) {
setEmailAvailable(request.available)
if (!request.available) {
antd.message.error("Email is already in use")
props.updateValue(null)
} else {
props.updateValue(email)
}
}
setLoading(false)
}, 1000)
return () => clearTimeout(timer)
}, [email])
return <div className="register_form_step_content">
<antd.Input
defaultValue={props.currentValue}
placeholder="Email"
onPressEnter={submit}
onChange={handleUpdate}
status={email.length == 0 ? "default" : loading ? "default" : (isValid() ? "success" : "error")}
/>
</div>
}
export default {
key: "email",
title: "Step 3",
icon: "Mail",
description: "Enter a email for the account, it can be used to access to your account. \n Will not be shared with anyone else and not be used for marketing purposes.",
required: true,
content: EmailStepComponent,
}

View File

@ -0,0 +1,125 @@
import React from "react"
import * as antd from "antd"
import "./index.less"
export const PasswordStepComponent = (props) => {
const confirmRef = React.useRef(null)
const [password, setPassword] = React.useState(props.currentValue ?? "")
const [confirmedPassword, setConfirmedPassword] = React.useState(props.currentValue ?? "")
const [passwordStrength, setPasswordStrength] = React.useState(null)
const passwordMinimunStrength = 3
const passwordsMatch = password === confirmedPassword
const passwordError = !passwordsMatch && confirmedPassword.length > 0
const submit = () => {
if (!passwordError) {
props.onPressEnter()
}
}
const passwordStrengthCalculator = (password) => {
let strength = 0
if (password.length === 0 || password.length < 8) {
return strength
}
strength += 1
if (password.length >= 12) {
strength += 1
}
if (password.match(/[a-z]/)) {
strength += 1
}
if (password.match(/[A-Z]/)) {
strength += 1
}
if (password.match(/[0-9]/)) {
strength += 1
}
if (password.match(/[^a-zA-Z0-9]/)) {
strength += 1
}
return strength
}
React.useEffect(() => {
const calculatedStrength = passwordStrengthCalculator(password)
setPasswordStrength(calculatedStrength)
if (password !== confirmedPassword) {
props.updateValue(null)
}
if (calculatedStrength < passwordMinimunStrength) {
props.updateValue(null)
}
if (calculatedStrength >= passwordMinimunStrength && password === confirmedPassword) {
props.updateValue(password)
}
}, [password, confirmedPassword])
return <div className="register_form_step_content, passwords_fields">
<antd.Input.Password
className="password"
placeholder="Password"
autoCorrect="off"
autoCapitalize="none"
defaultValue={props.currentValue}
onPressEnter={() => {
confirmRef.current.focus()
}}
onChange={(e) => {
setPassword(e.target.value)
}}
status={passwordError ? "error" : "success"}
autoFocus
/>
<antd.Input.Password
className="password"
placeholder="Confirm Password"
ref={confirmRef}
autoCorrect="off"
autoCapitalize="none"
defaultValue={props.currentValue}
onPressEnter={submit}
status={passwordError ? "error" : "success"}
onChange={(e) => {
setConfirmedPassword(e.target.value)
}}
/>
<antd.Progress
percent={passwordStrength * 20}
status={passwordStrength < passwordMinimunStrength ? "exception" : "success"}
showInfo={false}
/>
<div className="passwordPolicy">
<p>Password must be at least 8 characters long.</p>
<p>Password must contain at least one number.</p>
</div>
</div>
}
export default {
key: "password",
title: "Step 2",
icon: "Key",
description: "Enter a password for the account. must comply with the password requirements policy.",
required: true,
content: PasswordStepComponent,
}

View File

@ -0,0 +1,6 @@
.passwords_fields {
display: flex;
flex-direction: column;
gap: 10px;
}

View File

@ -0,0 +1,80 @@
import React from "react"
import * as antd from "antd"
import MarkdownReader from "components/MarkdownReader"
import config from "config"
const FrameStyle = {
"width": "60vw",
"max-width": "60vw",
"height": "90vh",
"max-height": "90vh",
"overflow": "overlay",
"justify-content": "flex-start",
}
const LegalDocumentsDecorators = {
"terms": "Terms of Service",
"privacy": "Privacy Policy",
}
function composeConfirmationCheckboxLabel(documents) {
let labels = [
"I have read and accept"
]
documents.forEach(([key, value], index) => {
const isLast = index === documents.length - 1
labels.push(`the ${LegalDocumentsDecorators[key] ?? `document (${key})`} ${!isLast ? "and" : ""}`)
})
return labels.join(" ")
}
const TermsOfServiceStepComponent = (props) => {
const legalDocuments = Object.entries(config.legal)
return <div className="register_form_step_content">
{
Object.entries(config.legal).map(([key, value]) => {
if (!value) {
return null
}
return <antd.Button
key={key}
onClick={() => {
app.layout.modal.open(key, MarkdownReader, {
includeCloseButton: true,
frameContentStyle: FrameStyle,
props: {
url: value
}
})
}}
>
Read {LegalDocumentsDecorators[key] ?? `document (${key})`}
</antd.Button>
})
}
<antd.Checkbox
defaultChecked={props.currentValue}
onChange={(event) => {
props.updateValue(event.target.checked)
}}
>
{composeConfirmationCheckboxLabel(legalDocuments)}
</antd.Checkbox>
</div>
}
export default {
key: "tos",
title: "Step 3",
icon: "FileDone",
description: "Take your time to read these legal documents.",
required: true,
content: TermsOfServiceStepComponent,
}

View File

@ -0,0 +1,163 @@
import React from "react"
import * as antd from "antd"
import { Icons } from "components/Icons"
import UserModel from "models/user"
export const UsernameStepComponent = (props) => {
const [loading, setLoading] = React.useState(false)
const [username, setUsername] = React.useState(props.currentValue ?? "")
const [validLength, setValidLength] = React.useState(props.currentValue ? true : null)
const [validCharacters, setValidCharacters] = React.useState(props.currentValue ? true : null)
const [usernameAvailable, setUsernameAvailable] = React.useState(props.currentValue ? true : null)
const isValid = () => {
return username.length > 0 && validCharacters && usernameAvailable
}
const hasValidCharacters = (username) => {
return /^[a-z0-9_]+$/.test(username)
}
const submit = () => {
if (!isValid()) return
props.onPressEnter()
}
const handleUpdate = (e) => {
if (e.target.value === " ") {
return
}
e.target.value = e.target.value.toLowerCase()
setUsername(e.target.value)
}
const renderIndicator = (value, label) => {
if (loading) {
return <>
<Icons.LoadingOutlined
style={{
color: "var(--text-color)"
}}
/>
<p>{label}</p>
</>
}
if (value) {
return <>
<Icons.CheckCircleOutlined
style={{
color: "#99F7AB"
}}
/>
<p>{label}</p>
</>
}
return <>
<Icons.CloseCircleOutlined
style={{
color: "var(--text-color)"
}}
/>
<p>{label}</p>
</>
}
React.useEffect(() => {
if (username.length < 3) {
setUsernameAvailable(null)
setValidCharacters(null)
setValidLength(false)
setLoading(false)
return
} else {
setValidLength(true)
}
props.updateValue(null)
setLoading(true)
setValidCharacters(hasValidCharacters(username))
const timer = setTimeout(async () => {
if (!validCharacters) {
setLoading(false)
return
}
const request = await UserModel.checkUsernameAvailability(username).catch((error) => {
app.message.error(`Cannot check username availability: ${error.message}`)
console.error(error)
return false
})
if (request) {
setUsernameAvailable(request.available)
if (!request.available) {
props.updateValue(null)
} else {
props.updateValue(username)
}
}
setLoading(false)
}, 1000)
return () => clearTimeout(timer)
}, [username])
return <div className="register_form_step_content">
<antd.Input
autoCorrect="off"
autoCapitalize="none"
onPressEnter={submit}
placeholder="newuser"
value={username}
onChange={handleUpdate}
status={username.length == 0 ? "default" : loading ? "default" : (isValid() ? "success" : "error")}
maxLength={64}
/>
<div className="usernameValidity">
<div className="check">
{
renderIndicator(validLength, "At least 3 characters / Maximum 64 characters")
}
</div>
<div className="check">
{
renderIndicator(usernameAvailable, "Username available")
}
</div>
<div className="check">
{
renderIndicator(validCharacters, "Valid characters (letters, numbers, underscores)")
}
</div>
</div>
</div>
}
export default {
key: "username",
title: "Step 1",
icon: "User",
description: () => <div>
<p>Enter your username you gonna use for your account, its used to access to your account and give a easy name to identify you.</p>
<p>You can set a diferent public name for your account after registration.</p>
</div>,
required: true,
content: UsernameStepComponent,
}

View File

@ -0,0 +1,76 @@
import * as antd from "antd"
import config from "config"
import { Icons } from "components/Icons"
const MainSelector = (props) => {
const {
onClickLogin,
onClickRegister,
} = props
return <>
<div className="content_header">
<img src={app.isMobile ? config.logo.alt : config.logo.full} className="logo" />
</div>
{
app.userData && <div
className="actions"
style={{
marginBottom: "50px"
}}
>
<antd.Button
type="default"
size="large"
onClick={() => {
app.navigation.goMain()
}}
>
Continue as {app.userData.username}
</antd.Button>
</div>
}
<div className="actions">
<antd.Button
onClick={onClickLogin}
size="large"
icon={<Icons.LogIn />}
type="primary"
>
Continue with a Comty Account
</antd.Button>
<antd.Button
onClick={onClickLogin}
size="large"
icon={<Icons.LogIn />}
type="primary"
disabled
>
Continue with a RageStudio© ID
</antd.Button>
</div>
<h4>Or create a new account</h4>
<div className="actions">
<antd.Button
onClick={onClickRegister}
icon={<Icons.UserPlus />}
type="primary"
>
Create a Comty Account
</antd.Button>
<p>
<Icons.Info />
Registering a new account accepts the <a onClick={() => app.location.push("/terms")}>Terms and Conditions</a> and <a onClick={() => app.location.push("/privacy")}>Privacy policy</a> for the services provided by {config.author}
</p>
</div>
</>
}
export default MainSelector

View File

@ -0,0 +1,80 @@
import React from "react"
import useRandomFeaturedWallpaperUrl from "hooks/useRandomFeaturedWallpaperUrl"
import RegisterForm from "./forms/register"
import MainSelector from "./forms/selector"
import "./index.less"
const GradientSVG = () => {
return <svg height="100%" width="100%">
<defs>
<linearGradient id="0" x1="0" y1="0.5" x2="1" y2="0.5">
<stop offset="0%" stop-color="rgba(225, 0, 209, 0.1)" />
<stop offset="25%" stop-color="rgba(233, 0, 182, 0.08)" />
<stop offset="50%" stop-color="rgba(240, 0, 154, 0.05)" />
<stop offset="100%" stop-color="rgba(255, 0, 0, 0)" />
</linearGradient>
<radialGradient id="1" gradientTransform="translate(-0.81 -0.5) scale(2, 1.2)">
<stop offset="0%" stop-color="rgba(255, 96, 100, 0.2)" />
<stop offset="20%" stop-color="rgba(255, 96, 100, 0.16)" />
<stop offset="40%" stop-color="rgba(255, 96, 100, 0.12)" />
<stop offset="60%" stop-color="rgba(255, 96, 100, 0.08)" />
<stop offset="100%" stop-color="rgba(255, 96, 100, 0)" />
</radialGradient>
</defs>
<rect fill="url(#0)" height="100%" width="100%" />
<rect fill="url(#1)" height="100%" width="100%" />
</svg>
}
const stagesToComponents = {
0: MainSelector,
2: RegisterForm
}
const AuthPage = (props) => {
const [stage, setStage] = React.useState(0)
const randomWallpaperURL = useRandomFeaturedWallpaperUrl()
function changeStage(nextStage) {
setStage(nextStage)
}
const onClickLogin = () => {
app.controls.openLoginForm()
}
const onClickRegister = () => {
changeStage(2)
}
return <div className="loginPage">
<div className="background">
<GradientSVG />
</div>
<div className="wrapper">
<div
className="wrapper_background"
style={{
backgroundImage: randomWallpaperURL ? `url(${randomWallpaperURL.url})` : null,
animation: randomWallpaperURL ? "opacityIn 1s" : null
}}
/>
<div className="content">
{
React.createElement(stagesToComponents[stage] ?? stagesToComponents[0], {
onClickLogin,
onClickRegister,
changeStage,
})
}
</div>
</div>
</div>
}
export default AuthPage

View File

@ -39,22 +39,33 @@
}
.wrapper {
position: relative;
z-index: 55;
display: flex;
flex-direction: row;
position: relative;
width: 55vw;
max-width: 800px;
z-index: 55;
overflow: hidden;
height: 50vh;
max-height: 500px;
transition: all 250ms ease-in-out;
background-color: var(--background-color-accent);
outline: 1px solid var(--border-color);
border-radius: 12px;
min-height: 50vh;
outline: 1px solid var(--border-color);
border-radius: 12px;
.wrapper_background {
height: 100%;
width: 300px;
min-width: 250px;
width: 250px;
border-radius: 12px;
@ -70,7 +81,7 @@
align-items: center;
justify-content: center;
max-width: 500px;
width: 100%;
padding: 40px;

View File

@ -1,142 +0,0 @@
import React from "react"
import * as antd from "antd"
import config from "config"
import { Icons } from "components/Icons"
import { Footer } from "components"
import "./index.less"
const GradientSVG = () => {
return <svg height="100%" width="100%">
<defs>
<linearGradient id="0" x1="0" y1="0.5" x2="1" y2="0.5">
<stop offset="0%" stop-color="rgba(225, 0, 209, 0.1)" />
<stop offset="25%" stop-color="rgba(233, 0, 182, 0.08)" />
<stop offset="50%" stop-color="rgba(240, 0, 154, 0.05)" />
<stop offset="100%" stop-color="rgba(255, 0, 0, 0)" />
</linearGradient>
<radialGradient id="1" gradientTransform="translate(-0.81 -0.5) scale(2, 1.2)">
<stop offset="0%" stop-color="rgba(255, 96, 100, 0.2)" />
<stop offset="20%" stop-color="rgba(255, 96, 100, 0.16)" />
<stop offset="40%" stop-color="rgba(255, 96, 100, 0.12)" />
<stop offset="60%" stop-color="rgba(255, 96, 100, 0.08)" />
<stop offset="100%" stop-color="rgba(255, 96, 100, 0)" />
</radialGradient>
</defs>
<rect fill="url(#0)" height="100%" width="100%" />
<rect fill="url(#1)" height="100%" width="100%" />
</svg>
}
export default (props) => {
const [wallpaperData, setWallpaperData] = React.useState(null)
const setRandomWallpaper = async () => {
const { data: featuredWallpapers } = await app.cores.api.customRequest({
method: "GET",
url: "/featured_wallpapers"
}).catch((err) => {
console.error(err)
return []
})
// get random wallpaper from array
const randomWallpaper = featuredWallpapers[Math.floor(Math.random() * featuredWallpapers.length)]
setWallpaperData(randomWallpaper)
}
const onClickRegister = () => {
app.controls.openRegisterForm()
}
const onClickLogin = () => {
app.controls.openLoginForm()
}
React.useEffect(() => {
setRandomWallpaper()
}, [])
return <div className="loginPage">
<div className="background">
<GradientSVG />
</div>
<div className="wrapper">
<div
className="wrapper_background"
style={{
backgroundImage: wallpaperData ? `url(${wallpaperData.url})` : null,
animation: wallpaperData ? "opacityIn 1s" : null
}}
/>
<div className="content">
<div className="content_header">
<img src={app.isMobile ? config.logo.alt : config.logo.full} className="logo" />
</div>
{
app.userData && <div
className="actions"
style={{
marginBottom: "50px"
}}
>
<antd.Button
type="default"
size="large"
onClick={() => {
app.navigation.goMain()
}}
>
Continue as {app.userData.username}
</antd.Button>
</div>
}
<div className="actions">
<antd.Button
onClick={onClickLogin}
size="large"
icon={<Icons.LogIn />}
type="primary"
>
Continue with a Comty Account
</antd.Button>
<antd.Button
onClick={onClickLogin}
size="large"
icon={<Icons.LogIn />}
type="primary"
disabled
>
Continue with a RageStudio© ID
</antd.Button>
</div>
<h4>Or create a new account</h4>
<div className="actions">
<antd.Button
onClick={onClickRegister}
icon={<Icons.UserPlus />}
type="primary"
>
Create a Comty Account
</antd.Button>
<p>
<Icons.Info />
Registering a new account accepts the <a onClick={() => app.location.push("/terms")}>Terms and Conditions</a> and <a onClick={() => app.location.push("/privacy")}>Privacy policy</a> for the services provided by {config.author}
</p>
</div>
</div>
</div>
{/* <Footer /> */}
</div>
}

View File

@ -2,8 +2,10 @@ import React from "react"
import MarkdownReader from "components/MarkdownReader"
import config from "config"
export default () => {
const PrivacyReader = () => {
return <MarkdownReader
url={config.legal.privacy}
/>
}
export default PrivacyReader