221 lines
6.5 KiB
TypeScript
221 lines
6.5 KiB
TypeScript
import { useNavigate, useParams } from 'react-router-dom'
|
|
import { useEffect, useRef, useState, FormEvent, useCallback, Ref } from 'react'
|
|
import SingleColumnLayout from '../../../layouts/SingleColumnLayout.tsx'
|
|
import TextInput from '../../../components/inputs/TextInput.tsx'
|
|
import Button from '../../../components/buttons/Button.tsx'
|
|
import { invalid, valid, Validation } from '../../../utils/validation.ts'
|
|
import { AuthService } from '../authService.ts'
|
|
import LinkButton from '../../../components/buttons/LinkButton.tsx'
|
|
import NavBar from '../../../components/NavBar.tsx'
|
|
import NavButton from '../../../components/buttons/NavButton.tsx'
|
|
import { useTranslations } from '../../i18n/translations.ts'
|
|
|
|
const SignupCodeKey = 'signupCode'
|
|
|
|
interface SignupPageProps {
|
|
authService: AuthService
|
|
}
|
|
|
|
export default function SignupPage({ authService }: SignupPageProps) {
|
|
const { t } = useTranslations()
|
|
const { code } = useParams()
|
|
const [signupCode, setSignupCode] = useState<string | null>(null)
|
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
const [rememberMe, setRememberMe] = useState(false)
|
|
const [error, setError] = useState<string>('')
|
|
const [username, setUsername, usernameError, validateUsername] =
|
|
useValidatedInput(isValidUsername)
|
|
|
|
const [password, setPassword, passwordError, validatePassword] =
|
|
useValidatedInput(isValidPassword)
|
|
|
|
const userNameInputRef = useRef<HTMLInputElement | null>(null)
|
|
const passwordInputRef = useRef<HTMLInputElement | null>(null)
|
|
|
|
const navigate = useNavigate()
|
|
|
|
useEffect(() => {
|
|
if (signupCode) return
|
|
|
|
let theSignupCode: string | null
|
|
if (code) {
|
|
theSignupCode = code
|
|
setSignupCode(theSignupCode)
|
|
localStorage.setItem(SignupCodeKey, theSignupCode)
|
|
} else {
|
|
theSignupCode = localStorage.getItem(SignupCodeKey)
|
|
setSignupCode(theSignupCode)
|
|
}
|
|
}, [code, signupCode])
|
|
|
|
useEffect(() => {}, [signupCode])
|
|
|
|
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault()
|
|
|
|
if (!signupCode) {
|
|
throw new Error("there's no code")
|
|
}
|
|
|
|
const isUsernameValid = validateUsername()
|
|
const isPasswordValid = validatePassword()
|
|
|
|
if (!isPasswordValid) {
|
|
passwordInputRef.current?.focus()
|
|
}
|
|
|
|
if (!isUsernameValid) {
|
|
userNameInputRef.current?.focus()
|
|
}
|
|
|
|
if (!isUsernameValid || !isPasswordValid) {
|
|
return
|
|
}
|
|
|
|
setIsSubmitting(true)
|
|
|
|
try {
|
|
await authService.signup(username, password, signupCode, rememberMe)
|
|
navigate('/')
|
|
} catch (e: unknown) {
|
|
const err = e as Error
|
|
setError(err.message)
|
|
} finally {
|
|
setIsSubmitting(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<SingleColumnLayout
|
|
navbar={
|
|
<NavBar>
|
|
<NavButton to={'/'}>{t('nav.home')}</NavButton>
|
|
</NavBar>
|
|
}
|
|
>
|
|
<main className="w-full mx-auto p-4">
|
|
<div className="mt-12">
|
|
<form className="flex flex-col gap-4 max-w-md" onSubmit={onSubmit}>
|
|
<FormInput
|
|
id="username"
|
|
label={t('auth.username.label')}
|
|
value={username}
|
|
onInput={setUsername}
|
|
error={usernameError}
|
|
ref={userNameInputRef}
|
|
/>
|
|
|
|
<FormInput
|
|
id="password"
|
|
label={t('auth.password.label')}
|
|
value={password}
|
|
onInput={setPassword}
|
|
error={passwordError}
|
|
type="password"
|
|
ref={passwordInputRef}
|
|
/>
|
|
<div className="flex items-center hidden gap-2 mt-2">
|
|
<input
|
|
type="checkbox"
|
|
id="rememberMe"
|
|
checked={rememberMe}
|
|
onChange={(e) => setRememberMe(e.target.checked)}
|
|
className="h-4 w-4"
|
|
/>
|
|
<label htmlFor="rememberMe" className="text-sm text-gray-600">
|
|
{t('auth.remember_me.label')}
|
|
</label>
|
|
</div>
|
|
<Button
|
|
className="mt-4"
|
|
disabled={isSubmitting || !!usernameError || !!passwordError}
|
|
type="submit"
|
|
>
|
|
{isSubmitting ? t('misc.loading') : t('auth.register.cta')}
|
|
</Button>
|
|
<LinkButton secondary to={'/login'}>
|
|
{t('auth.register.login_instead')}
|
|
</LinkButton>
|
|
|
|
<span className="text-xs h-3 text-red-500">{error}</span>
|
|
</form>
|
|
</div>
|
|
</main>
|
|
</SingleColumnLayout>
|
|
)
|
|
}
|
|
|
|
interface FormInputProps {
|
|
id: string
|
|
label: string
|
|
value: string
|
|
onInput: (value: string) => void
|
|
error: string | null
|
|
type?: 'text' | 'password'
|
|
ref: Ref<HTMLInputElement>
|
|
}
|
|
|
|
function FormInput({ id, label, value, onInput, error, type = 'text', ref }: FormInputProps) {
|
|
return (
|
|
<div className="flex flex-col gap-1">
|
|
<label htmlFor={id} className="text-sm text-gray-600">
|
|
{label}
|
|
</label>
|
|
<TextInput ref={ref} type={type} id={id} value={value} onInput={onInput} />
|
|
<div className="text-xs h-3 text-red-500">{error}</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
type UseValidateInputReturn = [string, (value: string) => void, string | null, () => boolean]
|
|
|
|
function useValidatedInput(validator: (value: string) => Validation): UseValidateInputReturn {
|
|
const [value, setValue] = useState<string>('')
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [keepValidating, setKeepValidating] = useState(false)
|
|
|
|
const validate = useCallback(() => {
|
|
const { isValid, error } = validator(value)
|
|
if (isValid) {
|
|
setError(null)
|
|
} else {
|
|
// We only want to validate on input after they have invalidly submitted once.
|
|
// It's annoying if we set error messages before they've even finished typing.
|
|
setKeepValidating(true)
|
|
setError(error)
|
|
}
|
|
|
|
return isValid
|
|
}, [validator, value])
|
|
|
|
useEffect(() => {
|
|
if (keepValidating) {
|
|
validate()
|
|
}
|
|
}, [keepValidating, validate])
|
|
|
|
return [value, setValue, error, validate]
|
|
}
|
|
|
|
function isValidUsername(username: string): Validation {
|
|
if (!username) return invalid('you need to enter a username :/')
|
|
|
|
if (username.length < 3) {
|
|
return invalid('not long enough :(')
|
|
}
|
|
|
|
const usernameRegex = /^[a-zA-Z0-9_-]+$/
|
|
if (usernameRegex.test(username)) {
|
|
return valid()
|
|
} else {
|
|
return invalid("that's not a good username :'(")
|
|
}
|
|
}
|
|
|
|
function isValidPassword(password: string): Validation {
|
|
if (password.length >= 6) {
|
|
return valid()
|
|
} else {
|
|
return invalid("that isn't a good password :/")
|
|
}
|
|
}
|