hensei-web/components/auth/SignupModal/index.tsx
Justin Edmund 3d67622353
Fix i18n migration to next-intl (#430)
## Summary
- Fixed translation key format compatibility with next-intl
- Fixed pluralization format from i18next to next-intl format
- Fixed dynamic translation key error handling
- Updated server components to match API response structure
- Fixed useSearchParams import location

## Changes
- Changed pluralization from `{{count}} items` to `{count} items` format
- Added proper error handling for missing translation keys
- Fixed import paths for next-intl hooks
- Fixed PartyPageClient trying to set non-existent appState.parties

## Test plan
- [x] Verified translations render correctly
- [x] Tested pluralization works with different counts
- [x] Confirmed no console errors about missing translations
- [x] Tested party page functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-03 16:25:59 -07:00

382 lines
9.9 KiB
TypeScript

'use client'
import React, { useEffect, useState } from 'react'
import { setCookie, getCookie } from 'cookies-next'
import { useRouter } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { AxiosResponse } from 'axios'
import api from '~utils/api'
import { setHeaders } from '~utils/userToken'
import { accountState } from '~utils/accountState'
import Button from '~components/common/Button'
import Input from '~components/common/Input'
import { Dialog } from '~components/common/Dialog'
import DialogHeader from '~components/common/DialogHeader'
import DialogFooter from '~components/common/DialogFooter'
import DialogContent from '~components/common/DialogContent'
import styles from './index.module.scss'
interface Props {
open: boolean
onOpenChange?: (open: boolean) => void
}
interface ErrorMap {
[index: string]: string
username: string
email: string
password: string
passwordConfirmation: string
}
const emailRegex =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const SignupModal = (props: Props) => {
const router = useRouter()
const t = useTranslations('common')
// Set up form states and error handling
const [formValid, setFormValid] = useState(false)
const [errors, setErrors] = useState<ErrorMap>({
username: '',
email: '',
password: '',
passwordConfirmation: '',
})
// States
const [open, setOpen] = useState(false)
// Set up form refs
const usernameInput = React.createRef<HTMLInputElement>()
const emailInput = React.createRef<HTMLInputElement>()
const passwordInput = React.createRef<HTMLInputElement>()
const passwordConfirmationInput = React.createRef<HTMLInputElement>()
const footerRef = React.createRef<HTMLDivElement>()
const form = [
usernameInput,
emailInput,
passwordInput,
passwordConfirmationInput,
]
useEffect(() => {
setOpen(props.open)
}, [props.open])
function register(event: React.FormEvent) {
event.preventDefault()
// In App Router, locale is typically handled via cookies or headers
const currentLocale = getCookie('NEXT_LOCALE') as string || 'en'
const body = {
user: {
username: usernameInput.current?.value,
email: emailInput.current?.value,
password: passwordInput.current?.value,
password_confirmation: passwordConfirmationInput.current?.value,
language: currentLocale,
},
}
if (formValid)
api.endpoints.users
.create(body)
.then((response) => {
storeCookieInfo(response)
return response.data.username
})
.then((username) => fetchUserInfo(username))
.then((infoResponse) => storeUserInfo(infoResponse))
}
function storeCookieInfo(response: AxiosResponse) {
const resp = response.data
const cookieObj: AccountCookie = {
userId: resp.id,
username: resp.username,
role: resp.role,
token: resp.token,
}
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + 60)
setCookie('account', cookieObj, { path: '/', expires: expiresAt })
// Set Axios default headers
setHeaders()
}
function fetchUserInfo(username: string) {
return api.userInfo(username)
}
function storeUserInfo(response: AxiosResponse) {
// Extract the user
const user = response.data
// Set user data in the user cookie
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + 60)
setCookie(
'user',
{
avatar: {
picture: user.avatar.picture,
element: user.avatar.element,
},
language: user.language,
gender: user.gender,
theme: user.theme,
bahamut: false,
},
{ path: '/', expires: expiresAt }
)
// Set the user data in the account state
accountState.account.user = {
id: user.id,
username: user.username,
granblueId: '',
role: user.role,
avatar: {
picture: user.avatar.picture,
element: user.avatar.element,
},
gender: user.gender,
language: user.language,
theme: user.theme,
bahamut: false,
}
console.log('Authorizing account...')
accountState.account.authorized = true
setOpen(false)
}
function handleNameChange(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault()
const fieldName = event.target.name
const value = event.target.value
if (value.length >= 3) {
api.check(fieldName, value).then(
(response) => {
processNameCheck(fieldName, value, response.data.available)
},
(error) => {
console.error(error)
}
)
} else {
validateName(fieldName, value)
}
}
function processNameCheck(
fieldName: string,
value: string,
available: boolean
) {
const newErrors = { ...errors }
if (available) {
// Continue checking for errors
newErrors[fieldName] = ''
setErrors(newErrors)
setFormValid(true)
validateName(fieldName, value)
} else {
newErrors[fieldName] = t('modals.signup.errors.field_in_use', {
field: fieldName,
})
setErrors(newErrors)
setFormValid(false)
}
}
function validateName(fieldName: string, value: string) {
let newErrors = { ...errors }
switch (fieldName) {
case 'username':
if (value.length < 3)
newErrors.username = t('modals.signup.errors.username_too_short')
else if (value.length > 20)
newErrors.username = t('modals.signup.errors.username_too_long')
else newErrors.username = ''
break
case 'email':
newErrors.email = emailRegex.test(value)
? ''
: t('modals.signup.errors.invalid_email')
break
default:
break
}
setFormValid(validateForm(newErrors))
}
function handlePasswordChange(event: React.ChangeEvent<HTMLInputElement>) {
event.preventDefault()
const { name, value } = event.target
let newErrors = { ...errors }
switch (name) {
case 'password':
newErrors.password = passwordInput.current?.value.includes(
usernameInput.current?.value!
)
? t('modals.signup.errors.password_contains_username')
: ''
break
case 'password':
newErrors.password =
value.length < 8 ? t('modals.signup.errors.password_too_short') : ''
break
case 'confirm_password':
newErrors.passwordConfirmation =
passwordInput.current?.value ===
passwordConfirmationInput.current?.value
? ''
: t('modals.signup.errors.passwords_dont_match')
break
default:
break
}
setFormValid(validateForm(newErrors))
}
function validateForm(errors: ErrorMap) {
let valid = true
Object.values(form).forEach(
(input) => input.current?.value.length == 0 && (valid = false)
)
Object.values(errors).forEach(
(error) => error.length > 0 && (valid = false)
)
return valid
}
function openChange(open: boolean) {
setOpen(open)
setErrors({
username: '',
email: '',
password: '',
passwordConfirmation: '',
})
setFormValid(false)
if (props.onOpenChange) props.onOpenChange(open)
}
function onEscapeKeyDown(event: KeyboardEvent) {
setOpen(false)
}
function onOpenAutoFocus(event: Event) {
event.preventDefault()
if (usernameInput.current) usernameInput.current.focus()
}
return (
<Dialog open={open} onOpenChange={openChange}>
<DialogContent
className="signup"
footerRef={footerRef}
onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus}
>
<DialogHeader title={t('modals.signup.title')} />
<form onSubmit={register}>
<div className={styles.fields}>
<Input
bound={true}
className="Bound"
name="username"
placeholder={t('modals.signup.placeholders.username')}
onChange={handleNameChange}
error={errors.username}
ref={usernameInput}
/>
<Input
bound={true}
className="Bound"
name="email"
placeholder={t('modals.signup.placeholders.email')}
onChange={handleNameChange}
error={errors.email}
ref={emailInput}
/>
<Input
bound={true}
className="Bound"
name="password"
placeholder={t('modals.signup.placeholders.password')}
type="password"
onChange={handlePasswordChange}
error={errors.password}
ref={passwordInput}
/>
<Input
bound={true}
name="confirm_password"
placeholder={t('modals.signup.placeholders.password_confirm')}
type="password"
onChange={handlePasswordChange}
error={errors.passwordConfirmation}
ref={passwordConfirmationInput}
/>
</div>
<DialogFooter
ref={footerRef}
rightElements={[
<Button
bound={true}
disabled={!formValid}
key="confirm"
text={t('modals.signup.buttons.confirm')}
/>,
]}
/>
<p className="terms">
{/* <Trans i18nKey="modals.signup.agreement">
By signing up, I agree to the <Link href="/privacy"><span>Privacy Policy</span></Link><Link href="/usage"><span>Usage Guidelines</span></Link>.
</Trans> */}
</p>
</form>
</DialogContent>
</Dialog>
)
}
export default SignupModal