feat: create a guild (#1)
This commit is contained in:
parent
a0fa66e8f5
commit
d8fab08585
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import Image from 'next/image'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import {
|
||||
CogIcon,
|
||||
PlusIcon,
|
||||
@ -19,6 +20,7 @@ import { Guilds } from './Guilds/Guilds'
|
||||
import { Divider } from '../design/Divider'
|
||||
import { Members } from './Members'
|
||||
import { useAuthentication } from 'utils/authentication'
|
||||
import { API_URL } from 'utils/api'
|
||||
|
||||
export interface GuildsChannelsPath {
|
||||
guildId: number
|
||||
@ -37,6 +39,7 @@ export interface ApplicationProps {
|
||||
export const Application: React.FC<ApplicationProps> = (props) => {
|
||||
const { children, path } = props
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { user } = useAuthentication()
|
||||
|
||||
const [visibleSidebars, setVisibleSidebars] = useState({
|
||||
@ -138,10 +141,10 @@ export const Application: React.FC<ApplicationProps> = (props) => {
|
||||
return 'Join a Guild'
|
||||
}
|
||||
if (path === '/application/guilds/create') {
|
||||
return 'Create a Guild'
|
||||
return t('application:create-a-guild')
|
||||
}
|
||||
return 'Application'
|
||||
}, [path])
|
||||
}, [path, t])
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
@ -193,7 +196,11 @@ export const Application: React.FC<ApplicationProps> = (props) => {
|
||||
>
|
||||
<Image
|
||||
className='rounded-full'
|
||||
src='/images/data/divlo.png'
|
||||
src={
|
||||
user.logo == null
|
||||
? '/images/data/user-default.png'
|
||||
: API_URL + user.logo
|
||||
}
|
||||
alt='logo'
|
||||
width={48}
|
||||
height={48}
|
||||
|
15
components/Application/CreateGuild/CreateGuild.stories.tsx
Normal file
15
components/Application/CreateGuild/CreateGuild.stories.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { Meta, Story } from '@storybook/react'
|
||||
|
||||
import { CreateGuild as Component } from './CreateGuild'
|
||||
|
||||
const Stories: Meta = {
|
||||
title: 'CreateGuild',
|
||||
component: Component
|
||||
}
|
||||
|
||||
export default Stories
|
||||
|
||||
export const CreateGuild: Story = (arguments_) => {
|
||||
return <Component {...arguments_} />
|
||||
}
|
||||
CreateGuild.args = {}
|
82
components/Application/CreateGuild/CreateGuild.tsx
Normal file
82
components/Application/CreateGuild/CreateGuild.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { Form } from 'react-component-form'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import { AxiosResponse } from 'axios'
|
||||
|
||||
import { useAuthentication } from '../../../utils/authentication'
|
||||
import { HandleSubmitCallback, useForm } from '../../../hooks/useForm'
|
||||
import { GuildComplete, guildSchema } from '../../../models/Guild'
|
||||
import { Input } from '../../design/Input'
|
||||
import { Main } from '../../design/Main'
|
||||
import { Button } from '../../design/Button'
|
||||
import { FormState } from '../../design/FormState'
|
||||
|
||||
export const CreateGuild: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
|
||||
const { formState, message, errors, getErrorTranslation, handleSubmit } =
|
||||
useForm({
|
||||
validateSchemaObject: {
|
||||
name: guildSchema.name,
|
||||
description: guildSchema.description
|
||||
}
|
||||
})
|
||||
|
||||
const { authentication } = useAuthentication()
|
||||
|
||||
const onSubmit: HandleSubmitCallback = async (formData) => {
|
||||
try {
|
||||
const { data } = await authentication.api.post<
|
||||
any,
|
||||
AxiosResponse<{ guild: GuildComplete }>
|
||||
>('/guilds', { name: formData.name, description: formData.description })
|
||||
const guildId = data.guild.id
|
||||
const channelId = data.guild.channels[0].id
|
||||
await router.push(`/application/${guildId}/${channelId}`)
|
||||
return null
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'error',
|
||||
value: 'errors:server-error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Main>
|
||||
<Form className='w-4/6 max-w-xs' onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder={t('common:name')}
|
||||
name='name'
|
||||
label={t('common:name')}
|
||||
error={getErrorTranslation(errors.name)}
|
||||
/>
|
||||
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex justify-between mt-6 mb-2'>
|
||||
<label className='pl-1' htmlFor='description'>
|
||||
Description
|
||||
</label>
|
||||
</div>
|
||||
<div className='mt-0 relative'>
|
||||
<TextareaAutosize
|
||||
className='p-3 rounded-lg bg-[#f1f1f1] text-[#2a2a2a] caret-green-600 font-paragraph w-full focus:border focus:outline-none resize-none focus:shadow-green'
|
||||
placeholder='Description...'
|
||||
id='description'
|
||||
name='description'
|
||||
wrap='soft'
|
||||
></TextareaAutosize>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button className='w-full mt-6' type='submit' data-cy='submit'>
|
||||
{t('application:create')}
|
||||
</Button>
|
||||
</Form>
|
||||
<FormState id='message' state={formState} message={message} />
|
||||
</Main>
|
||||
)
|
||||
}
|
1
components/Application/CreateGuild/index.ts
Normal file
1
components/Application/CreateGuild/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './CreateGuild'
|
@ -1,5 +1,5 @@
|
||||
import { Meta, Story } from '@storybook/react'
|
||||
import { user, userSettings } from '../../cypress/fixtures/users/user'
|
||||
import { user, userSettings } from '../../../cypress/fixtures/users/user'
|
||||
|
||||
import { UserProfile as Component, UserProfileProps } from './UserProfile'
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import { user, userSettings } from '../../cypress/fixtures/users/user'
|
||||
import { user, userSettings } from '../../../cypress/fixtures/users/user'
|
||||
|
||||
import { UserProfile } from './UserProfile'
|
||||
|
@ -4,7 +4,7 @@ import classNames from 'classnames'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import date from 'date-and-time'
|
||||
|
||||
import { UserPublic } from '../../models/User'
|
||||
import { UserPublic } from '../../../models/User'
|
||||
|
||||
export interface UserProfileProps {
|
||||
className?: string
|
||||
@ -15,8 +15,6 @@ export interface UserProfileProps {
|
||||
export const UserProfile: React.FC<UserProfileProps> = (props) => {
|
||||
const { user, isOwner = false } = props
|
||||
|
||||
console.log(user)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleSubmitChanges = (
|
@ -1,11 +1,7 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import Link from 'next/link'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import type { ErrorObject } from 'ajv'
|
||||
import type { HandleForm } from 'react-component-form'
|
||||
import axios from 'axios'
|
||||
|
||||
import { SocialMediaButton } from '../design/SocialMediaButton'
|
||||
@ -13,26 +9,14 @@ import { Main } from '../design/Main'
|
||||
import { Input } from '../design/Input'
|
||||
import { Button } from '../design/Button'
|
||||
import { FormState } from '../design/FormState'
|
||||
import { useFormState } from '../../hooks/useFormState'
|
||||
import { AuthenticationForm } from './'
|
||||
import { userSchema } from '../../models/User'
|
||||
import { ajv } from '../../utils/ajv'
|
||||
import { api } from 'utils/api'
|
||||
import {
|
||||
Tokens,
|
||||
Authentication as AuthenticationClass
|
||||
} from '../../utils/authentication'
|
||||
import { getErrorTranslationKey } from './getErrorTranslationKey'
|
||||
|
||||
interface Errors {
|
||||
[key: string]: ErrorObject<string, any> | null | undefined
|
||||
}
|
||||
|
||||
const findError = (
|
||||
field: string
|
||||
): ((value: ErrorObject, index: number, object: ErrorObject[]) => boolean) => {
|
||||
return (validationError) => validationError.instancePath === field
|
||||
}
|
||||
import { useForm, HandleSubmitCallback } from '../../hooks/useForm'
|
||||
|
||||
export interface AuthenticationProps {
|
||||
mode: 'signup' | 'signin'
|
||||
@ -44,84 +28,57 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
|
||||
const router = useRouter()
|
||||
const { lang, t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const [formState, setFormState] = useFormState()
|
||||
const [messageTranslationKey, setMessageTranslationKey] = useState<
|
||||
string | undefined
|
||||
>(undefined)
|
||||
const [errors, setErrors] = useState<Errors>({
|
||||
name: null,
|
||||
email: null,
|
||||
password: null
|
||||
})
|
||||
|
||||
const validateSchema = useMemo(() => {
|
||||
return Type.Object({
|
||||
...(mode === 'signup' && { name: userSchema.name }),
|
||||
email: userSchema.email,
|
||||
password: userSchema.password
|
||||
const { errors, formState, message, getErrorTranslation, handleSubmit } =
|
||||
useForm({
|
||||
validateSchemaObject: {
|
||||
...(mode === 'signup' && { name: userSchema.name }),
|
||||
email: userSchema.email,
|
||||
password: userSchema.password
|
||||
}
|
||||
})
|
||||
}, [mode])
|
||||
|
||||
const validate = useMemo(() => {
|
||||
return ajv.compile(validateSchema)
|
||||
}, [validateSchema])
|
||||
|
||||
const getErrorTranslation = (error?: ErrorObject | null): string | null => {
|
||||
if (error != null) {
|
||||
return t(getErrorTranslationKey(error)).replace(
|
||||
'{expected}',
|
||||
error?.params?.limit
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const handleSubmit: HandleForm = async (formData, formElement) => {
|
||||
const isValid = validate(formData)
|
||||
if (!isValid) {
|
||||
setFormState('error')
|
||||
const nameError = validate?.errors?.find(findError('/name'))
|
||||
const emailError = validate?.errors?.find(findError('/email'))
|
||||
const passwordError = validate?.errors?.find(findError('/password'))
|
||||
setErrors({
|
||||
name: nameError,
|
||||
email: emailError,
|
||||
password: passwordError
|
||||
})
|
||||
} else {
|
||||
setErrors({})
|
||||
setFormState('loading')
|
||||
if (mode === 'signup') {
|
||||
try {
|
||||
await api.post(
|
||||
`/users/signup?redirectURI=${window.location.origin}/authentication/signin`,
|
||||
{ ...formData, language: lang, theme }
|
||||
)
|
||||
formElement.reset()
|
||||
setFormState('success')
|
||||
setMessageTranslationKey('authentication:success-signup')
|
||||
} catch (error) {
|
||||
setFormState('error')
|
||||
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
||||
setMessageTranslationKey('authentication:alreadyUsed')
|
||||
} else {
|
||||
setMessageTranslationKey('errors:server-error')
|
||||
const onSubmit: HandleSubmitCallback = async (formData) => {
|
||||
if (mode === 'signup') {
|
||||
try {
|
||||
await api.post(
|
||||
`/users/signup?redirectURI=${window.location.origin}/authentication/signin`,
|
||||
{ ...formData, language: lang, theme }
|
||||
)
|
||||
return {
|
||||
type: 'success',
|
||||
value: 'authentication:success-signup'
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
||||
return {
|
||||
type: 'error',
|
||||
value: 'authentication:alreadyUsed'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { data } = await api.post<Tokens>('/users/signin', formData)
|
||||
const authentication = new AuthenticationClass(data)
|
||||
authentication.signin()
|
||||
await router.push('/application')
|
||||
} catch (error) {
|
||||
setFormState('error')
|
||||
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
||||
setMessageTranslationKey('authentication:wrong-credentials')
|
||||
} else {
|
||||
setMessageTranslationKey('errors:server-error')
|
||||
return {
|
||||
type: 'error',
|
||||
value: 'errors:server-error'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { data } = await api.post<Tokens>('/users/signin', formData)
|
||||
const authentication = new AuthenticationClass(data)
|
||||
authentication.signin()
|
||||
await router.push('/application')
|
||||
return null
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
||||
return {
|
||||
type: 'error',
|
||||
value: 'authentication:wrong-credentials'
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'error',
|
||||
value: 'errors:server-error'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -138,13 +95,13 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
|
||||
<section className='text-center text-lg font-paragraph pt-8'>
|
||||
{t('authentication:or')}
|
||||
</section>
|
||||
<AuthenticationForm onSubmit={handleSubmit}>
|
||||
<AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
|
||||
{mode === 'signup' && (
|
||||
<Input
|
||||
type='text'
|
||||
placeholder={t('authentication:name')}
|
||||
placeholder={t('common:name')}
|
||||
name='name'
|
||||
label={t('authentication:name')}
|
||||
label={t('common:name')}
|
||||
error={getErrorTranslation(errors.name)}
|
||||
/>
|
||||
)}
|
||||
@ -182,13 +139,7 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
|
||||
</Link>
|
||||
</p>
|
||||
</AuthenticationForm>
|
||||
<FormState
|
||||
id='message'
|
||||
state={formState}
|
||||
message={
|
||||
messageTranslationKey != null ? t(messageTranslationKey) : null
|
||||
}
|
||||
/>
|
||||
<FormState id='message' state={formState} message={message} />
|
||||
</Main>
|
||||
)
|
||||
}
|
||||
|
9
cypress/fixtures/channels/channel.ts
Normal file
9
cypress/fixtures/channels/channel.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { guild } from '../guilds/guild'
|
||||
|
||||
export const channel = {
|
||||
id: 1,
|
||||
name: 'general',
|
||||
guildId: guild.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
8
cypress/fixtures/guilds/guild.ts
Normal file
8
cypress/fixtures/guilds/guild.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const guild = {
|
||||
id: 1,
|
||||
name: 'GuildExample',
|
||||
description: 'guild example.',
|
||||
icon: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
20
cypress/fixtures/guilds/post.ts
Normal file
20
cypress/fixtures/guilds/post.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Handler } from '../handler'
|
||||
|
||||
import { guild } from './guild'
|
||||
import { channel } from '../channels/channel'
|
||||
import { memberComplete } from '../members/member'
|
||||
|
||||
export const postGuildsHandler: Handler = {
|
||||
method: 'POST',
|
||||
url: '/guilds',
|
||||
response: {
|
||||
statusCode: 201,
|
||||
body: {
|
||||
guild: {
|
||||
...guild,
|
||||
channels: [channel],
|
||||
members: [memberComplete]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
cypress/fixtures/members/member.ts
Normal file
16
cypress/fixtures/members/member.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { guild } from '../guilds/guild'
|
||||
import { user } from '../users/user'
|
||||
|
||||
export const member = {
|
||||
id: 1,
|
||||
isOwner: true,
|
||||
userId: user.id,
|
||||
guildId: guild.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
export const memberComplete = {
|
||||
...member,
|
||||
user
|
||||
}
|
@ -1,15 +1,18 @@
|
||||
import { UserSettings } from '../../../models/UserSettings'
|
||||
import { UserPublic } from '../../../models/User'
|
||||
import { User } from '../../../models/User'
|
||||
|
||||
export const user: UserPublic = {
|
||||
export const user: User = {
|
||||
id: 1,
|
||||
name: 'Divlo',
|
||||
email: 'contact@divlo.fr',
|
||||
logo: undefined,
|
||||
status: undefined,
|
||||
biography: undefined,
|
||||
password: 'somepassword',
|
||||
logo: null,
|
||||
status: null,
|
||||
biography: null,
|
||||
website: 'https://divlo.fr',
|
||||
isConfirmed: true,
|
||||
temporaryToken: 'temporaryUUIDtoken',
|
||||
temporaryExpirationToken: '2021-10-20T20:59:08.485Z',
|
||||
createdAt: '2021-10-20T20:30:51.595Z',
|
||||
updatedAt: '2021-10-20T20:59:08.485Z'
|
||||
}
|
||||
|
37
cypress/integration/pages/application/guilds/create.spec.ts
Normal file
37
cypress/integration/pages/application/guilds/create.spec.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { channel } from '../../../../fixtures/channels/channel'
|
||||
import { guild } from '../../../../fixtures/guilds/guild'
|
||||
import { postGuildsHandler } from '../../../../fixtures/guilds/post'
|
||||
import { authenticationHandlers } from '../../../../fixtures/handler'
|
||||
|
||||
describe('Pages > /application/guilds/create', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('stopMockServer')
|
||||
})
|
||||
|
||||
it('should succeeds and create the guild', () => {
|
||||
cy.task('startMockServer', [
|
||||
...authenticationHandlers,
|
||||
postGuildsHandler
|
||||
]).setCookie('refreshToken', 'refresh-token')
|
||||
cy.visit('/application/guilds/create')
|
||||
cy.get('#error-name').should('not.exist')
|
||||
cy.get('[data-cy=input-name]').type(guild.name)
|
||||
cy.get('[data-cy=submit]').click()
|
||||
cy.location('pathname').should(
|
||||
'eq',
|
||||
`/application/${guild.id}/${channel.id}`
|
||||
)
|
||||
})
|
||||
|
||||
it('should fails with internal api server error', () => {
|
||||
cy.task('startMockServer', [...authenticationHandlers]).setCookie(
|
||||
'refreshToken',
|
||||
'refresh-token'
|
||||
)
|
||||
cy.visit('/application/guilds/create')
|
||||
cy.get('#error-name').should('not.exist')
|
||||
cy.get('[data-cy=input-name]').type(guild.name)
|
||||
cy.get('[data-cy=submit]').click()
|
||||
cy.get('#message').should('have.text', 'Error: Internal Server Error.')
|
||||
})
|
||||
})
|
@ -40,6 +40,9 @@ describe('Pages > /authentication/reset-password', () => {
|
||||
cy.visit('/authentication/reset-password')
|
||||
cy.get('#message').should('not.exist')
|
||||
cy.get('[data-cy=submit]').click()
|
||||
cy.get('#message').should('have.text', 'Error: Invalid value.')
|
||||
cy.get('#message').should(
|
||||
'have.text',
|
||||
'Error: Oops, this field is required 🙈.'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
1
hooks/useForm/index.ts
Normal file
1
hooks/useForm/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './useForm'
|
108
hooks/useForm/useForm.ts
Normal file
108
hooks/useForm/useForm.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import type { FormDataObject, HandleForm } from 'react-component-form'
|
||||
import type { ErrorObject } from 'ajv'
|
||||
|
||||
import { FormState, useFormState } from '../useFormState'
|
||||
import { ajv } from '../../utils/ajv'
|
||||
import { getErrorTranslationKey } from './getErrorTranslationKey'
|
||||
|
||||
interface Errors {
|
||||
[key: string]: ErrorObject<string, any> | null | undefined
|
||||
}
|
||||
|
||||
const findError = (
|
||||
field: string
|
||||
): ((value: ErrorObject, index: number, object: ErrorObject[]) => boolean) => {
|
||||
return (validationError) => validationError.instancePath === field
|
||||
}
|
||||
|
||||
export type GetErrorTranslation = (error?: ErrorObject | null) => string | null
|
||||
|
||||
export interface UseFormOptions {
|
||||
validateSchemaObject: { [key: string]: any }
|
||||
}
|
||||
|
||||
export type HandleSubmit = (callback: HandleSubmitCallback) => HandleForm
|
||||
|
||||
interface Message {
|
||||
type: 'error' | 'success'
|
||||
value: string
|
||||
}
|
||||
|
||||
export type HandleSubmitCallback = (
|
||||
formData: FormDataObject,
|
||||
formElement: HTMLFormElement
|
||||
) => Promise<Message | null>
|
||||
|
||||
export interface UseFormResult {
|
||||
message: string | null
|
||||
formState: FormState
|
||||
getErrorTranslation: GetErrorTranslation
|
||||
handleSubmit: HandleSubmit
|
||||
errors: Errors
|
||||
}
|
||||
|
||||
export const useForm = (options: UseFormOptions): UseFormResult => {
|
||||
const { validateSchemaObject } = options
|
||||
const { t } = useTranslation()
|
||||
const [formState, setFormState] = useFormState()
|
||||
const [messageTranslationKey, setMessageTranslationKey] = useState<
|
||||
string | undefined
|
||||
>(undefined)
|
||||
const [errors, setErrors] = useState<Errors>({})
|
||||
|
||||
const validateSchema = useMemo(() => {
|
||||
return Type.Object(validateSchemaObject)
|
||||
}, [validateSchemaObject])
|
||||
|
||||
const validate = useMemo(() => {
|
||||
return ajv.compile(validateSchema)
|
||||
}, [validateSchema])
|
||||
|
||||
const getErrorTranslation = (error?: ErrorObject | null): string | null => {
|
||||
if (error != null) {
|
||||
return t(getErrorTranslationKey(error)).replace(
|
||||
'{expected}',
|
||||
error?.params?.limit
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const handleSubmit: HandleSubmit = (callback) => {
|
||||
return async (formData, formElement) => {
|
||||
const isValid = validate(formData)
|
||||
if (!isValid) {
|
||||
setFormState('error')
|
||||
const errors: Errors = {}
|
||||
for (const property in validateSchema.properties) {
|
||||
errors[property] = validate.errors?.find(findError(`/${property}`))
|
||||
}
|
||||
setErrors(errors)
|
||||
} else {
|
||||
setErrors({})
|
||||
setFormState('loading')
|
||||
const message = await callback(formData, formElement)
|
||||
if (message != null) {
|
||||
setMessageTranslationKey(message.value)
|
||||
if (message.type === 'success') {
|
||||
setFormState('success')
|
||||
formElement.reset()
|
||||
} else {
|
||||
setFormState('error')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getErrorTranslation,
|
||||
errors,
|
||||
formState,
|
||||
handleSubmit,
|
||||
message: messageTranslationKey != null ? t(messageTranslationKey) : null
|
||||
}
|
||||
}
|
@ -10,6 +10,9 @@
|
||||
"/authentication/reset-password": ["authentication", "errors"],
|
||||
"/authentication/signup": ["authentication", "errors"],
|
||||
"/authentication/signin": ["authentication", "errors"],
|
||||
"/application/users/[userId]": ["application"]
|
||||
"/application/users/[userId]": ["application", "errors"],
|
||||
"/application/guilds/create": ["application", "errors"],
|
||||
"/application/guilds/join": ["application", "errors"],
|
||||
"/application": ["application", "errors"]
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
{
|
||||
"website": "Website"
|
||||
"website": "Website",
|
||||
"create": "Create",
|
||||
"create-a-guild": "Create a Guild"
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"or": "OR",
|
||||
"password": "Password",
|
||||
"name": "Name",
|
||||
"already-have-an-account": "Already have an account?",
|
||||
"dont-have-an-account": "Don't have an account?",
|
||||
"submit": "Submit",
|
||||
|
@ -2,5 +2,6 @@
|
||||
"english": "English",
|
||||
"french": "French",
|
||||
"all-rights-reserved": "All rights reserved",
|
||||
"description": "Stay close with your friends and communities, talk, chat, collaborate, share, and have fun."
|
||||
"description": "Stay close with your friends and communities, talk, chat, collaborate, share, and have fun.",
|
||||
"name": "Name"
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
{
|
||||
"website": "Site web"
|
||||
"website": "Site web",
|
||||
"create": "Crée",
|
||||
"create-a-guild": "Crée une Guilde"
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"or": "OU",
|
||||
"password": "Mot de passe",
|
||||
"name": "Nom",
|
||||
"already-have-an-account": "Vous avez déjà un compte ?",
|
||||
"dont-have-an-account": "Vous n'avez pas de compte ?",
|
||||
"submit": "Soumettre",
|
||||
|
@ -2,5 +2,6 @@
|
||||
"english": "Anglais",
|
||||
"french": "Français",
|
||||
"all-rights-reserved": "Tous droits réservés",
|
||||
"description": "Restez proche de vos amis et de vos communautés, parlez, collaborez, partagez et amusez-vous."
|
||||
"description": "Restez proche de vos amis et de vos communautés, parlez, collaborez, partagez et amusez-vous.",
|
||||
"name": "Nom"
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ export const types = [Type.Literal('text')]
|
||||
|
||||
export const channelSchema = {
|
||||
id,
|
||||
name: Type.String({ maxLength: 255 }),
|
||||
name: Type.String({ minLength: 1, maxLength: 20 }),
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt,
|
||||
guildId: id
|
||||
|
@ -1,12 +1,24 @@
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import { Type, Static } from '@sinclair/typebox'
|
||||
|
||||
import { channelSchema } from './Channel'
|
||||
import { memberSchema } from './Member'
|
||||
import { date, id } from './utils'
|
||||
|
||||
export const guildSchema = {
|
||||
id,
|
||||
name: Type.String({ minLength: 3, maxLength: 30 }),
|
||||
icon: Type.String({ format: 'uri-reference' }),
|
||||
description: Type.String({ maxLength: 160 }),
|
||||
name: Type.String({ minLength: 1, maxLength: 30 }),
|
||||
icon: Type.Union([Type.String({ format: 'uri-reference' }), Type.Null()]),
|
||||
description: Type.Union([Type.String({ maxLength: 160 }), Type.Null()]),
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt
|
||||
}
|
||||
|
||||
export const guildCompleteSchema = {
|
||||
...guildSchema,
|
||||
channels: Type.Array(Type.Object(channelSchema)),
|
||||
members: Type.Array(Type.Object(memberSchema))
|
||||
}
|
||||
|
||||
export const guildCompleteObjectSchema = Type.Object(guildCompleteSchema)
|
||||
|
||||
export type GuildComplete = Static<typeof guildCompleteObjectSchema>
|
||||
|
@ -6,10 +6,13 @@ export const types = [Type.Literal('text'), Type.Literal('file')]
|
||||
|
||||
export const messageSchema = {
|
||||
id,
|
||||
value: Type.String(),
|
||||
value: Type.String({
|
||||
minLength: 1,
|
||||
maxLength: 20_000
|
||||
}),
|
||||
type: Type.Union(types, { default: 'text' }),
|
||||
mimetype: Type.String({
|
||||
maxLength: 255,
|
||||
maxLength: 127,
|
||||
default: 'text/plain',
|
||||
format: 'mimetype'
|
||||
}),
|
||||
|
@ -7,11 +7,11 @@ import { date, id } from './utils'
|
||||
export const userSchema = {
|
||||
id,
|
||||
name: Type.String({ minLength: 1, maxLength: 30 }),
|
||||
email: Type.String({ minLength: 1, maxLength: 255, format: 'email' }),
|
||||
email: Type.String({ minLength: 1, maxLength: 254, format: 'email' }),
|
||||
password: Type.String({ minLength: 1 }),
|
||||
logo: Type.String({ format: 'uri-reference' }),
|
||||
status: Type.String({ maxLength: 255 }),
|
||||
biography: Type.String(),
|
||||
logo: Type.Union([Type.String({ format: 'uri-reference' }), Type.Null()]),
|
||||
status: Type.Union([Type.String({ maxLength: 50 }), Type.Null()]),
|
||||
biography: Type.Union([Type.String({ maxLength: 160 }), Type.Null()]),
|
||||
website: Type.String({ maxLength: 255, format: 'uri-reference' }),
|
||||
isConfirmed: Type.Boolean({ default: false }),
|
||||
temporaryToken: Type.String(),
|
||||
@ -20,32 +20,34 @@ export const userSchema = {
|
||||
updatedAt: date.updatedAt
|
||||
}
|
||||
|
||||
const userSchemaWithSettings = {
|
||||
...userSchema,
|
||||
settings: Type.Object(userSettingsSchema)
|
||||
export const userObjectSchema = Type.Object(userSchema)
|
||||
|
||||
export const userPublicWithoutSettingsSchema = {
|
||||
id,
|
||||
name: userSchema.name,
|
||||
email: Type.Union([userSchema.email, Type.Null()]),
|
||||
logo: userSchema.logo,
|
||||
status: userSchema.status,
|
||||
biography: userSchema.biography,
|
||||
website: Type.Union([userSchema.website, Type.Null()]),
|
||||
isConfirmed: userSchema.isConfirmed,
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt
|
||||
}
|
||||
|
||||
export const userPublicSchema = {
|
||||
id,
|
||||
name: userSchema.name,
|
||||
email: Type.Optional(userSchema.email),
|
||||
logo: Type.Optional(userSchema.logo),
|
||||
status: Type.Optional(userSchema.status),
|
||||
biography: Type.Optional(userSchema.biography),
|
||||
website: Type.Optional(userSchema.website),
|
||||
isConfirmed: userSchema.isConfirmed,
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt,
|
||||
settings: Type.Optional(Type.Object(userSettingsSchema))
|
||||
...userPublicWithoutSettingsSchema,
|
||||
settings: Type.Object(userSettingsSchema)
|
||||
}
|
||||
|
||||
export const userPublicObjectSchema = Type.Object(userPublicSchema)
|
||||
|
||||
export const userCurrentSchema = Type.Object({
|
||||
...userSchemaWithSettings,
|
||||
...userPublicSchema,
|
||||
currentStrategy: Type.Union([...strategiesTypebox]),
|
||||
strategies: Type.Array(Type.Union([...strategiesTypebox]))
|
||||
})
|
||||
|
||||
export type User = Static<typeof userObjectSchema>
|
||||
export type UserPublic = Static<typeof userPublicObjectSchema>
|
||||
export type UserCurrent = Static<typeof userCurrentSchema>
|
||||
|
@ -6,6 +6,12 @@ module.exports = nextTranslate(
|
||||
pwa: {
|
||||
disable: process.env.NODE_ENV !== 'production',
|
||||
dest: 'public'
|
||||
},
|
||||
images: {
|
||||
domains: [
|
||||
'api.thream.divlo.fr',
|
||||
...(process.env.NODE_ENV === 'development' ? ['localhost'] : [])
|
||||
]
|
||||
}
|
||||
})
|
||||
)
|
||||
|
41
package-lock.json
generated
41
package-lock.json
generated
@ -15,6 +15,7 @@
|
||||
"@sinclair/typebox": "0.20.5",
|
||||
"ajv": "8.6.3",
|
||||
"ajv-formats": "2.1.1",
|
||||
"axios": "0.23.0",
|
||||
"classnames": "2.3.1",
|
||||
"date-and-time": "2.0.1",
|
||||
"next": "11.1.2",
|
||||
@ -8388,12 +8389,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"dev": true,
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
|
||||
"integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
"follow-redirects": "^1.14.4"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
@ -16214,7 +16214,6 @@
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@ -37731,6 +37730,15 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wait-on/node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wait-on/node_modules/rxjs": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz",
|
||||
@ -45698,12 +45706,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"dev": true,
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
|
||||
"integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
"follow-redirects": "^1.14.4"
|
||||
}
|
||||
},
|
||||
"axobject-query": {
|
||||
@ -51976,8 +51983,7 @@
|
||||
"follow-redirects": {
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
|
||||
"dev": true
|
||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
|
||||
},
|
||||
"for-in": {
|
||||
"version": "1.0.2",
|
||||
@ -68666,6 +68672,15 @@
|
||||
"rxjs": "^7.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz",
|
||||
|
@ -40,6 +40,7 @@
|
||||
"@sinclair/typebox": "0.20.5",
|
||||
"ajv": "8.6.3",
|
||||
"ajv-formats": "2.1.1",
|
||||
"axios": "0.23.0",
|
||||
"classnames": "2.3.1",
|
||||
"date-and-time": "2.0.1",
|
||||
"next": "11.1.2",
|
||||
|
@ -1,77 +1,19 @@
|
||||
import Image from 'next/image'
|
||||
import { Form } from 'react-component-form'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
|
||||
import { Head } from 'components/Head'
|
||||
import { Application } from 'components/Application'
|
||||
import { Input } from 'components/design/Input'
|
||||
import { Main } from 'components/design/Main'
|
||||
import { Button } from 'components/design/Button'
|
||||
import { FormState } from 'components/design/FormState'
|
||||
|
||||
import {
|
||||
authenticationFromServerSide,
|
||||
AuthenticationProvider,
|
||||
PagePropsWithAuthentication
|
||||
} from 'utils/authentication'
|
||||
import { CreateGuild } from 'components/Application/CreateGuild'
|
||||
|
||||
const CreateGuild: React.FC<PagePropsWithAuthentication> = (props) => {
|
||||
const CreateGuildPage: React.FC<PagePropsWithAuthentication> = (props) => {
|
||||
return (
|
||||
<AuthenticationProvider authentication={props.authentication}>
|
||||
<Head title='Thream | Create a Guild' />
|
||||
<Application path='/application/guilds/create'>
|
||||
<Main>
|
||||
<Form className='w-4/6 max-w-xs'>
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex justify-between mt-6 mb-2'>
|
||||
<label className='pl-1' htmlFor='icon'>
|
||||
Icon
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className='mt-2 relative flex flex-col items-center'>
|
||||
<button className='relative w-14 h-14 flex items-center overflow-hidden justify-center text-green-800 dark:text-green-400 transform scale-125 hover:scale-150'>
|
||||
<Image
|
||||
src='/images/data/guild-default.png'
|
||||
alt='logo'
|
||||
width={56}
|
||||
height={56}
|
||||
className='w-14 h-14 rounded-full'
|
||||
/>
|
||||
<input
|
||||
name='icon'
|
||||
id='icon'
|
||||
type='file'
|
||||
className='absolute w-22 h-20 -top-8 -left-8 opacity-0 cursor-pointer'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input type='text' placeholder='Name' name='name' label='Name' />
|
||||
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex justify-between mt-6 mb-2'>
|
||||
<label className='pl-1' htmlFor='description'>
|
||||
Description
|
||||
</label>
|
||||
</div>
|
||||
<div className='mt-0 relative'>
|
||||
<TextareaAutosize
|
||||
className='p-3 rounded-lg bg-[#f1f1f1] text-[#2a2a2a] caret-green-600 font-paragraph w-full focus:border focus:outline-none resize-none focus:shadow-green'
|
||||
placeholder='Description...'
|
||||
id='description'
|
||||
name='description'
|
||||
wrap='soft'
|
||||
></TextareaAutosize>
|
||||
<FormState state='idle' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button className='w-full mt-6' type='submit'>
|
||||
Create
|
||||
</Button>
|
||||
</Form>
|
||||
</Main>
|
||||
<CreateGuild />
|
||||
</Application>
|
||||
</AuthenticationProvider>
|
||||
)
|
||||
@ -81,4 +23,4 @@ export const getServerSideProps = authenticationFromServerSide({
|
||||
shouldBeAuthenticated: true
|
||||
})
|
||||
|
||||
export default CreateGuild
|
||||
export default CreateGuildPage
|
||||
|
@ -5,8 +5,7 @@ import {
|
||||
AuthenticationProvider,
|
||||
PagePropsWithAuthentication
|
||||
} from 'utils/authentication'
|
||||
|
||||
import { UserProfile } from 'components/UserProfile'
|
||||
import { UserProfile } from 'components/Application/UserProfile'
|
||||
|
||||
const UserProfilePage: React.FC<PagePropsWithAuthentication> = (props) => {
|
||||
return (
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { AuthenticationForm } from 'components/Authentication'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { HandleForm } from 'react-component-form'
|
||||
import axios from 'axios'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
|
||||
import { Head } from 'components/Head'
|
||||
import { Header } from 'components/Header'
|
||||
@ -13,54 +10,40 @@ import { Footer, FooterProps } from 'components/Footer'
|
||||
import { Input } from 'components/design/Input'
|
||||
import { Button } from 'components/design/Button'
|
||||
import { FormState } from 'components/design/FormState'
|
||||
import { useFormState } from 'hooks/useFormState'
|
||||
import { authenticationFromServerSide } from 'utils/authentication'
|
||||
import { ScrollableBody } from 'components/ScrollableBody'
|
||||
import { userSchema } from 'models/User'
|
||||
import { api } from 'utils/api'
|
||||
import { userSchema } from '../../models/User'
|
||||
import { ajv } from '../../utils/ajv'
|
||||
import { HandleSubmitCallback, useForm } from 'hooks/useForm'
|
||||
|
||||
const ForgotPassword: React.FC<FooterProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { version } = props
|
||||
const [formState, setFormState] = useFormState()
|
||||
const [messageTranslationKey, setMessageTranslationKey] = useState<
|
||||
string | undefined
|
||||
>(undefined)
|
||||
|
||||
const validateSchema = useMemo(() => {
|
||||
return Type.Object({
|
||||
email: userSchema.email
|
||||
})
|
||||
}, [])
|
||||
const { formState, message, errors, getErrorTranslation, handleSubmit } =
|
||||
useForm({ validateSchemaObject: { email: userSchema.email } })
|
||||
|
||||
const validate = useMemo(() => {
|
||||
return ajv.compile(validateSchema)
|
||||
}, [validateSchema])
|
||||
|
||||
const handleSubmit: HandleForm = async (formData, formElement) => {
|
||||
const isValid = validate(formData)
|
||||
if (!isValid) {
|
||||
setFormState('error')
|
||||
setMessageTranslationKey('errors:email')
|
||||
} else {
|
||||
setFormState('loading')
|
||||
try {
|
||||
await api.post(
|
||||
`/users/reset-password?redirectURI=${window.location.origin}/authentication/reset-password`,
|
||||
formData
|
||||
)
|
||||
formElement.reset()
|
||||
setFormState('success')
|
||||
setMessageTranslationKey('authentication:success-forgot-password')
|
||||
} catch (error) {
|
||||
setFormState('error')
|
||||
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
||||
setMessageTranslationKey('errors:email')
|
||||
} else {
|
||||
setMessageTranslationKey('errors:server-error')
|
||||
const onSubmit: HandleSubmitCallback = async (formData) => {
|
||||
try {
|
||||
await api.post(
|
||||
`/users/reset-password?redirectURI=${window.location.origin}/authentication/reset-password`,
|
||||
formData
|
||||
)
|
||||
return {
|
||||
type: 'success',
|
||||
value: 'authentication:success-forgot-password'
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
||||
return {
|
||||
type: 'error',
|
||||
value: 'errors:email'
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'error',
|
||||
value: 'errors:server-error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,7 +52,7 @@ const ForgotPassword: React.FC<FooterProps> = (props) => {
|
||||
<Head title={`Thream | ${t('authentication:forgot-password')}`} />
|
||||
<Header />
|
||||
<Main>
|
||||
<AuthenticationForm onSubmit={handleSubmit}>
|
||||
<AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input type='email' placeholder='Email' name='email' label='Email' />
|
||||
<Button data-cy='submit' className='w-full mt-6' type='submit'>
|
||||
{t('authentication:submit')}
|
||||
@ -84,7 +67,7 @@ const ForgotPassword: React.FC<FooterProps> = (props) => {
|
||||
id='message'
|
||||
state={formState}
|
||||
message={
|
||||
messageTranslationKey != null ? t(messageTranslationKey) : null
|
||||
message != null ? message : getErrorTranslation(errors.email)
|
||||
}
|
||||
/>
|
||||
</Main>
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { HandleForm } from 'react-component-form'
|
||||
import axios from 'axios'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
|
||||
import { Head } from 'components/Head'
|
||||
import { Header } from 'components/Header'
|
||||
@ -12,54 +9,40 @@ import { Footer, FooterProps } from 'components/Footer'
|
||||
import { Input } from 'components/design/Input'
|
||||
import { Button } from 'components/design/Button'
|
||||
import { FormState } from 'components/design/FormState'
|
||||
import { useFormState } from 'hooks/useFormState'
|
||||
import { authenticationFromServerSide } from 'utils/authentication'
|
||||
import { AuthenticationForm } from 'components/Authentication'
|
||||
import { ScrollableBody } from 'components/ScrollableBody/ScrollableBody'
|
||||
import { api } from 'utils/api'
|
||||
import { userSchema } from '../../models/User'
|
||||
import { ajv } from '../../utils/ajv'
|
||||
import { HandleSubmitCallback, useForm } from 'hooks/useForm'
|
||||
|
||||
const ResetPassword: React.FC<FooterProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const { version } = props
|
||||
const [formState, setFormState] = useFormState()
|
||||
const [messageTranslationKey, setMessageTranslationKey] = useState<
|
||||
string | undefined
|
||||
>(undefined)
|
||||
|
||||
const validateSchema = useMemo(() => {
|
||||
return Type.Object({
|
||||
password: userSchema.password
|
||||
})
|
||||
}, [])
|
||||
const { formState, message, errors, getErrorTranslation, handleSubmit } =
|
||||
useForm({ validateSchemaObject: { password: userSchema.password } })
|
||||
|
||||
const validate = useMemo(() => {
|
||||
return ajv.compile(validateSchema)
|
||||
}, [validateSchema])
|
||||
|
||||
const handleSubmit: HandleForm = async (formData, formElement) => {
|
||||
const isValid = validate(formData)
|
||||
if (!isValid) {
|
||||
setFormState('error')
|
||||
setMessageTranslationKey('errors:invalid')
|
||||
} else {
|
||||
setFormState('loading')
|
||||
try {
|
||||
await api.put(`/users/reset-password`, {
|
||||
...formData,
|
||||
temporaryToken: router.query.temporaryToken
|
||||
})
|
||||
await router.push('/authentication/signin')
|
||||
} catch (error) {
|
||||
setFormState('error')
|
||||
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
||||
setMessageTranslationKey('errors:invalid')
|
||||
} else {
|
||||
setMessageTranslationKey('errors:server-error')
|
||||
const onSubmit: HandleSubmitCallback = async (formData) => {
|
||||
try {
|
||||
await api.put(`/users/reset-password`, {
|
||||
...formData,
|
||||
temporaryToken: router.query.temporaryToken
|
||||
})
|
||||
await router.push('/authentication/signin')
|
||||
return null
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
||||
return {
|
||||
type: 'error',
|
||||
value: 'errors:invalid'
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'error',
|
||||
value: 'errors:server-error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +51,7 @@ const ResetPassword: React.FC<FooterProps> = (props) => {
|
||||
<Head title={`Thream | ${t('authentication:reset-password')}`} />
|
||||
<Header />
|
||||
<Main>
|
||||
<AuthenticationForm onSubmit={handleSubmit}>
|
||||
<AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input
|
||||
type='password'
|
||||
placeholder='Password'
|
||||
@ -83,7 +66,7 @@ const ResetPassword: React.FC<FooterProps> = (props) => {
|
||||
id='message'
|
||||
state={formState}
|
||||
message={
|
||||
messageTranslationKey != null ? t(messageTranslationKey) : null
|
||||
message != null ? message : getErrorTranslation(errors.password)
|
||||
}
|
||||
/>
|
||||
</Main>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 41 KiB |
BIN
public/images/data/user-default.png
Normal file
BIN
public/images/data/user-default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
@ -1,8 +1,8 @@
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import { io, Socket } from 'socket.io-client'
|
||||
|
||||
import { API_URL } from 'utils/api'
|
||||
import { cookies } from 'utils/cookies'
|
||||
import { API_URL } from '../api'
|
||||
import { cookies } from '../cookies'
|
||||
import { Tokens } from './'
|
||||
import { fetchRefreshToken } from './authenticationFromServerSide'
|
||||
|
||||
@ -46,6 +46,7 @@ export class Authentication {
|
||||
)
|
||||
this.setAccessToken(accessToken)
|
||||
}
|
||||
config.headers = config.headers == null ? {} : config.headers
|
||||
config.headers.Authorization = `${this.tokens.type} ${this.tokens.accessToken}`
|
||||
return config
|
||||
},
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { createContext, useState, useEffect, useMemo, useContext } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import setLanguage from 'next-translate/setLanguage'
|
||||
|
||||
import { Authentication, PagePropsWithAuthentication } from './'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { UserCurrent } from 'models/User'
|
||||
import { UserCurrent } from '../../models/User'
|
||||
|
||||
export interface AuthenticationValue {
|
||||
authentication: Authentication
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { AxiosInstance } from 'axios'
|
||||
import { AxiosInstance, AxiosResponse } from 'axios'
|
||||
import { GetServerSideProps, GetServerSidePropsContext, Redirect } from 'next'
|
||||
|
||||
import { api } from 'utils/api'
|
||||
import { Cookies } from 'utils/cookies'
|
||||
import { api } from '../api'
|
||||
import { Cookies } from '../cookies'
|
||||
import { RefreshTokenResponse, Tokens } from './index'
|
||||
import { Authentication } from './Authentication'
|
||||
import { UserCurrent } from '../../models/User'
|
||||
|
||||
export const fetchRefreshToken = async (
|
||||
refreshToken: string
|
||||
@ -70,9 +71,10 @@ export const authenticationFromServerSide = (
|
||||
} else {
|
||||
let data: Redirect | any = {}
|
||||
const authentication = new Authentication(tokens)
|
||||
const { data: currentUser } = await authentication.api.get(
|
||||
'/users/current'
|
||||
)
|
||||
const { data: currentUser } = await authentication.api.get<
|
||||
unknown,
|
||||
AxiosResponse<UserCurrent>
|
||||
>('/users/current')
|
||||
if (fetchData != null) {
|
||||
data = await fetchData(context, authentication.api)
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { UserCurrent } from 'models/User'
|
||||
import { UserCurrent } from '../../models/User'
|
||||
|
||||
export interface RefreshTokenResponse {
|
||||
accessToken: string
|
||||
|
Reference in New Issue
Block a user