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
+
+
+ >
+}
+
+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
-
-
-
-
-
- {/*
*/}
-
-}
\ 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