From 3e6de98d10cffdd4e01fdfb4a70b4790df68e0eb Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Mon, 19 Feb 2024 18:59:20 +0000 Subject: [PATCH] use new register component --- packages/app/src/App.jsx | 2 +- .../src/pages/auth/forms/register/index.jsx | 215 ++++++++++++++++++ .../src/pages/auth/forms/register/index.less | 77 +++++++ .../auth/forms/register/steps/email/index.jsx | 90 ++++++++ .../forms/register/steps/password/index.jsx | 125 ++++++++++ .../forms/register/steps/password/index.less | 6 + .../auth/forms/register/steps/tos/index.jsx | 80 +++++++ .../forms/register/steps/username/index.jsx | 163 +++++++++++++ .../src/pages/auth/forms/selector/index.jsx | 76 +++++++ packages/app/src/pages/auth/index.jsx | 80 +++++++ .../app/src/pages/{login => auth}/index.less | 25 +- .../pages/{login => auth}/index.mobile.jsx | 0 .../pages/{login => auth}/index.mobile.less | 0 packages/app/src/pages/login/index.jsx | 142 ------------ packages/app/src/pages/privacy/index.jsx | 6 +- 15 files changed, 935 insertions(+), 152 deletions(-) create mode 100644 packages/app/src/pages/auth/forms/register/index.jsx create mode 100644 packages/app/src/pages/auth/forms/register/index.less create mode 100644 packages/app/src/pages/auth/forms/register/steps/email/index.jsx create mode 100644 packages/app/src/pages/auth/forms/register/steps/password/index.jsx create mode 100644 packages/app/src/pages/auth/forms/register/steps/password/index.less create mode 100644 packages/app/src/pages/auth/forms/register/steps/tos/index.jsx create mode 100644 packages/app/src/pages/auth/forms/register/steps/username/index.jsx create mode 100644 packages/app/src/pages/auth/forms/selector/index.jsx create mode 100755 packages/app/src/pages/auth/index.jsx rename packages/app/src/pages/{login => auth}/index.less (90%) rename packages/app/src/pages/{login => auth}/index.mobile.jsx (100%) rename packages/app/src/pages/{login => auth}/index.mobile.less (100%) delete mode 100755 packages/app/src/pages/login/index.jsx diff --git a/packages/app/src/App.jsx b/packages/app/src/App.jsx index 20049579..8ac2ab0c 100755 --- a/packages/app/src/App.jsx +++ b/packages/app/src/App.jsx @@ -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: app.goAuth()}>Login, diff --git a/packages/app/src/pages/auth/forms/register/index.jsx b/packages/app/src/pages/auth/forms/register/index.jsx new file mode 100644 index 00000000..83851b67 --- /dev/null +++ b/packages/app/src/pages/auth/forms/register/index.jsx @@ -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
+
+ { + !finishSuccess && !finishing && step === 0 && <> +

đź‘‹ Hi! Nice to meet you

+

Tell us some basic information to get started creating your account.

+ + } + + { + !finishSuccess && !finishing && step > 0 && <> +

+ { + currentStepData?.icon && createIconRender(currentStepData.icon) + } + + {currentStepData?.title} +

+

+ { + typeof currentStepData?.description === "function" ? + currentStepData?.description() : currentStepData.description + } +

+ + } +
+ + { + !finishSuccess && !finishing && step > 0 && React.createElement(currentStepData.content, { + onPressEnter: nextStep, + currentValue: stepsValues[currentStepData.key], + updateValue: updateStepValue, + }) + } + + { + finishing &&
+ +

+ Creating your account +

+
+ } + + { + finishSuccess &&
+ +

+ Welcome abord! +

+

+ One last step, we need you to login with your new account. +

+ + props.changeStage(0)} + > + Go to login + +
+ } + + { + finishError && + } + + { + !finishSuccess && !finishing &&
+ { + step === 0 && + props.changeStage(0)} + > + Cancel + + } + { + step > 0 && + prevStep()} + > + Back + + } + + nextStep()} + disabled={!canNextStep()} + > + { + step === steps.length ? "Finish" : "Next" + } + +
+ } +
+} + +export default RegisterForm \ No newline at end of file diff --git a/packages/app/src/pages/auth/forms/register/index.less b/packages/app/src/pages/auth/forms/register/index.less new file mode 100644 index 00000000..fc80e948 --- /dev/null +++ b/packages/app/src/pages/auth/forms/register/index.less @@ -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; + } +} \ No newline at end of file diff --git a/packages/app/src/pages/auth/forms/register/steps/email/index.jsx b/packages/app/src/pages/auth/forms/register/steps/email/index.jsx new file mode 100644 index 00000000..a9506f95 --- /dev/null +++ b/packages/app/src/pages/auth/forms/register/steps/email/index.jsx @@ -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
+ +
+} + +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, +} \ No newline at end of file diff --git a/packages/app/src/pages/auth/forms/register/steps/password/index.jsx b/packages/app/src/pages/auth/forms/register/steps/password/index.jsx new file mode 100644 index 00000000..a06fcd9c --- /dev/null +++ b/packages/app/src/pages/auth/forms/register/steps/password/index.jsx @@ -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
+ { + confirmRef.current.focus() + }} + onChange={(e) => { + setPassword(e.target.value) + }} + status={passwordError ? "error" : "success"} + autoFocus + /> + + { + setConfirmedPassword(e.target.value) + }} + /> + + + +
+

Password must be at least 8 characters long.

+

Password must contain at least one number.

+
+
+} + + +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, +} diff --git a/packages/app/src/pages/auth/forms/register/steps/password/index.less b/packages/app/src/pages/auth/forms/register/steps/password/index.less new file mode 100644 index 00000000..5c418d7e --- /dev/null +++ b/packages/app/src/pages/auth/forms/register/steps/password/index.less @@ -0,0 +1,6 @@ +.passwords_fields { + display: flex; + flex-direction: column; + + gap: 10px; +} \ No newline at end of file diff --git a/packages/app/src/pages/auth/forms/register/steps/tos/index.jsx b/packages/app/src/pages/auth/forms/register/steps/tos/index.jsx new file mode 100644 index 00000000..f3398260 --- /dev/null +++ b/packages/app/src/pages/auth/forms/register/steps/tos/index.jsx @@ -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
+ { + Object.entries(config.legal).map(([key, value]) => { + if (!value) { + return null + } + + return { + app.layout.modal.open(key, MarkdownReader, { + includeCloseButton: true, + frameContentStyle: FrameStyle, + props: { + url: value + } + }) + }} + > + Read {LegalDocumentsDecorators[key] ?? `document (${key})`} + + }) + } + + { + props.updateValue(event.target.checked) + }} + > + {composeConfirmationCheckboxLabel(legalDocuments)} + +
+} + +export default { + key: "tos", + title: "Step 3", + icon: "FileDone", + description: "Take your time to read these legal documents.", + required: true, + content: TermsOfServiceStepComponent, +} diff --git a/packages/app/src/pages/auth/forms/register/steps/username/index.jsx b/packages/app/src/pages/auth/forms/register/steps/username/index.jsx new file mode 100644 index 00000000..013eba8d --- /dev/null +++ b/packages/app/src/pages/auth/forms/register/steps/username/index.jsx @@ -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 <> + +

{label}

+ + } + + if (value) { + return <> + +

{label}

+ + } + + return <> + +

{label}

+ + } + + 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
+ + +
+
+ { + renderIndicator(validLength, "At least 3 characters / Maximum 64 characters") + } +
+
+ { + renderIndicator(usernameAvailable, "Username available") + } +
+
+ { + renderIndicator(validCharacters, "Valid characters (letters, numbers, underscores)") + } +
+
+
+} + +export default { + key: "username", + title: "Step 1", + icon: "User", + description: () =>
+

Enter your username you gonna use for your account, its used to access to your account and give a easy name to identify you.

+

You can set a diferent public name for your account after registration.

+
, + required: true, + content: UsernameStepComponent, +} \ No newline at end of file diff --git a/packages/app/src/pages/auth/forms/selector/index.jsx b/packages/app/src/pages/auth/forms/selector/index.jsx new file mode 100644 index 00000000..ee936c1c --- /dev/null +++ b/packages/app/src/pages/auth/forms/selector/index.jsx @@ -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 <> +
+ +
+ + { + app.userData &&
+ { + app.navigation.goMain() + }} + > + Continue as {app.userData.username} + +
+ } + +
+ } + type="primary" + > + Continue with a Comty™ Account + + + } + type="primary" + disabled + > + Continue with a RageStudio© ID™ + +
+ +

Or create a new account

+ +
+ } + type="primary" + > + Create a Comty™ Account + + +

+ + Registering a new account accepts the app.location.push("/terms")}>Terms and Conditions and app.location.push("/privacy")}>Privacy policy for the services provided by {config.author} +

+
+ +} + +export default MainSelector \ No newline at end of file diff --git a/packages/app/src/pages/auth/index.jsx b/packages/app/src/pages/auth/index.jsx new file mode 100755 index 00000000..2f44f09a --- /dev/null +++ b/packages/app/src/pages/auth/index.jsx @@ -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 + + + + + + + + + + + + + + + + + + +} + +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
+
+ +
+ +
+
+ +
+ { + React.createElement(stagesToComponents[stage] ?? stagesToComponents[0], { + onClickLogin, + onClickRegister, + changeStage, + }) + } +
+
+
+} + +export default AuthPage \ No newline at end of file diff --git a/packages/app/src/pages/login/index.less b/packages/app/src/pages/auth/index.less similarity index 90% rename from packages/app/src/pages/login/index.less rename to packages/app/src/pages/auth/index.less index 6940f32c..ff0ddcfa 100755 --- a/packages/app/src/pages/login/index.less +++ b/packages/app/src/pages/auth/index.less @@ -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; diff --git a/packages/app/src/pages/login/index.mobile.jsx b/packages/app/src/pages/auth/index.mobile.jsx similarity index 100% rename from packages/app/src/pages/login/index.mobile.jsx rename to packages/app/src/pages/auth/index.mobile.jsx diff --git a/packages/app/src/pages/login/index.mobile.less b/packages/app/src/pages/auth/index.mobile.less similarity index 100% rename from packages/app/src/pages/login/index.mobile.less rename to packages/app/src/pages/auth/index.mobile.less diff --git a/packages/app/src/pages/login/index.jsx b/packages/app/src/pages/login/index.jsx deleted file mode 100755 index 62a2a429..00000000 --- a/packages/app/src/pages/login/index.jsx +++ /dev/null @@ -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 - - - - - - - - - - - - - - - - - - -} - -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
-
- -
- -
-
- -
-
- -
- - { - app.userData &&
- { - app.navigation.goMain() - }} - > - Continue as {app.userData.username} - -
- } - -
- } - type="primary" - > - Continue with a Comty™ Account - - - } - type="primary" - disabled - > - Continue with a RageStudio© ID™ - -
- -

Or create a new account

- -
- } - type="primary" - > - Create a Comty™ Account - - -

- - Registering a new account accepts the app.location.push("/terms")}>Terms and Conditions and app.location.push("/privacy")}>Privacy policy for the services provided by {config.author} -

-
-
-
- - {/*
*/} -
-} \ No newline at end of file diff --git a/packages/app/src/pages/privacy/index.jsx b/packages/app/src/pages/privacy/index.jsx index 8110a609..e69422a0 100644 --- a/packages/app/src/pages/privacy/index.jsx +++ b/packages/app/src/pages/privacy/index.jsx @@ -2,8 +2,10 @@ import React from "react" import MarkdownReader from "components/MarkdownReader" import config from "config" -export default () => { +const PrivacyReader = () => { return -} \ No newline at end of file +} + +export default PrivacyReader \ No newline at end of file