mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 18:44:16 +00:00
use new register component
This commit is contained in:
parent
0dbb857fff
commit
e881d4f384
@ -388,7 +388,7 @@ class ComtyApp extends React.Component {
|
|||||||
"app.no_session": async () => {
|
"app.no_session": async () => {
|
||||||
const location = window.location.pathname
|
const location = window.location.pathname
|
||||||
|
|
||||||
if (location !== "/" && location !== "/login" && location !== "/register") {
|
if (location !== "/" && location !== "/auth" && location !== "/register") {
|
||||||
antd.notification.info({
|
antd.notification.info({
|
||||||
message: "You are not logged in, to use some features you will need to log in.",
|
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>,
|
btn: <antd.Button type="primary" onClick={() => app.goAuth()}>Login</antd.Button>,
|
||||||
|
215
packages/app/src/pages/auth/forms/register/index.jsx
Normal file
215
packages/app/src/pages/auth/forms/register/index.jsx
Normal 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
|
77
packages/app/src/pages/auth/forms/register/index.less
Normal file
77
packages/app/src/pages/auth/forms/register/index.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
}
|
@ -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,
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
.passwords_fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
}
|
@ -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,
|
||||||
|
}
|
@ -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,
|
||||||
|
}
|
76
packages/app/src/pages/auth/forms/selector/index.jsx
Normal file
76
packages/app/src/pages/auth/forms/selector/index.jsx
Normal 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
|
80
packages/app/src/pages/auth/index.jsx
Executable file
80
packages/app/src/pages/auth/index.jsx
Executable 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
|
@ -39,22 +39,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 55;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
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);
|
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 {
|
.wrapper_background {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 300px;
|
|
||||||
|
min-width: 250px;
|
||||||
|
width: 250px;
|
||||||
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
||||||
@ -70,7 +81,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
max-width: 500px;
|
width: 100%;
|
||||||
|
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
|
|
@ -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>
|
|
||||||
}
|
|
@ -2,8 +2,10 @@ import React from "react"
|
|||||||
import MarkdownReader from "components/MarkdownReader"
|
import MarkdownReader from "components/MarkdownReader"
|
||||||
import config from "config"
|
import config from "config"
|
||||||
|
|
||||||
export default () => {
|
const PrivacyReader = () => {
|
||||||
return <MarkdownReader
|
return <MarkdownReader
|
||||||
url={config.legal.privacy}
|
url={config.legal.privacy}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default PrivacyReader
|
Loading…
x
Reference in New Issue
Block a user