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 { useState, useEffect, useMemo } from 'react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import {
|
import {
|
||||||
CogIcon,
|
CogIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
@ -19,6 +20,7 @@ import { Guilds } from './Guilds/Guilds'
|
|||||||
import { Divider } from '../design/Divider'
|
import { Divider } from '../design/Divider'
|
||||||
import { Members } from './Members'
|
import { Members } from './Members'
|
||||||
import { useAuthentication } from 'utils/authentication'
|
import { useAuthentication } from 'utils/authentication'
|
||||||
|
import { API_URL } from 'utils/api'
|
||||||
|
|
||||||
export interface GuildsChannelsPath {
|
export interface GuildsChannelsPath {
|
||||||
guildId: number
|
guildId: number
|
||||||
@ -37,6 +39,7 @@ export interface ApplicationProps {
|
|||||||
export const Application: React.FC<ApplicationProps> = (props) => {
|
export const Application: React.FC<ApplicationProps> = (props) => {
|
||||||
const { children, path } = props
|
const { children, path } = props
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
const { user } = useAuthentication()
|
const { user } = useAuthentication()
|
||||||
|
|
||||||
const [visibleSidebars, setVisibleSidebars] = useState({
|
const [visibleSidebars, setVisibleSidebars] = useState({
|
||||||
@ -138,10 +141,10 @@ export const Application: React.FC<ApplicationProps> = (props) => {
|
|||||||
return 'Join a Guild'
|
return 'Join a Guild'
|
||||||
}
|
}
|
||||||
if (path === '/application/guilds/create') {
|
if (path === '/application/guilds/create') {
|
||||||
return 'Create a Guild'
|
return t('application:create-a-guild')
|
||||||
}
|
}
|
||||||
return 'Application'
|
return 'Application'
|
||||||
}, [path])
|
}, [path, t])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true)
|
||||||
@ -193,7 +196,11 @@ export const Application: React.FC<ApplicationProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
className='rounded-full'
|
className='rounded-full'
|
||||||
src='/images/data/divlo.png'
|
src={
|
||||||
|
user.logo == null
|
||||||
|
? '/images/data/user-default.png'
|
||||||
|
: API_URL + user.logo
|
||||||
|
}
|
||||||
alt='logo'
|
alt='logo'
|
||||||
width={48}
|
width={48}
|
||||||
height={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 { 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'
|
import { UserProfile as Component, UserProfileProps } from './UserProfile'
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
import { render } from '@testing-library/react'
|
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'
|
import { UserProfile } from './UserProfile'
|
||||||
|
|
@ -4,7 +4,7 @@ import classNames from 'classnames'
|
|||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import date from 'date-and-time'
|
import date from 'date-and-time'
|
||||||
|
|
||||||
import { UserPublic } from '../../models/User'
|
import { UserPublic } from '../../../models/User'
|
||||||
|
|
||||||
export interface UserProfileProps {
|
export interface UserProfileProps {
|
||||||
className?: string
|
className?: string
|
||||||
@ -15,8 +15,6 @@ export interface UserProfileProps {
|
|||||||
export const UserProfile: React.FC<UserProfileProps> = (props) => {
|
export const UserProfile: React.FC<UserProfileProps> = (props) => {
|
||||||
const { user, isOwner = false } = props
|
const { user, isOwner = false } = props
|
||||||
|
|
||||||
console.log(user)
|
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const handleSubmitChanges = (
|
const handleSubmitChanges = (
|
@ -1,11 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react'
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import { useTheme } from 'next-themes'
|
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 axios from 'axios'
|
||||||
|
|
||||||
import { SocialMediaButton } from '../design/SocialMediaButton'
|
import { SocialMediaButton } from '../design/SocialMediaButton'
|
||||||
@ -13,26 +9,14 @@ import { Main } from '../design/Main'
|
|||||||
import { Input } from '../design/Input'
|
import { Input } from '../design/Input'
|
||||||
import { Button } from '../design/Button'
|
import { Button } from '../design/Button'
|
||||||
import { FormState } from '../design/FormState'
|
import { FormState } from '../design/FormState'
|
||||||
import { useFormState } from '../../hooks/useFormState'
|
|
||||||
import { AuthenticationForm } from './'
|
import { AuthenticationForm } from './'
|
||||||
import { userSchema } from '../../models/User'
|
import { userSchema } from '../../models/User'
|
||||||
import { ajv } from '../../utils/ajv'
|
|
||||||
import { api } from 'utils/api'
|
import { api } from 'utils/api'
|
||||||
import {
|
import {
|
||||||
Tokens,
|
Tokens,
|
||||||
Authentication as AuthenticationClass
|
Authentication as AuthenticationClass
|
||||||
} from '../../utils/authentication'
|
} from '../../utils/authentication'
|
||||||
import { getErrorTranslationKey } from './getErrorTranslationKey'
|
import { useForm, HandleSubmitCallback } from '../../hooks/useForm'
|
||||||
|
|
||||||
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 interface AuthenticationProps {
|
export interface AuthenticationProps {
|
||||||
mode: 'signup' | 'signin'
|
mode: 'signup' | 'signin'
|
||||||
@ -44,68 +28,37 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { lang, t } = useTranslation()
|
const { lang, t } = useTranslation()
|
||||||
const { theme } = useTheme()
|
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(() => {
|
const { errors, formState, message, getErrorTranslation, handleSubmit } =
|
||||||
return Type.Object({
|
useForm({
|
||||||
|
validateSchemaObject: {
|
||||||
...(mode === 'signup' && { name: userSchema.name }),
|
...(mode === 'signup' && { name: userSchema.name }),
|
||||||
email: userSchema.email,
|
email: userSchema.email,
|
||||||
password: userSchema.password
|
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({})
|
const onSubmit: HandleSubmitCallback = async (formData) => {
|
||||||
setFormState('loading')
|
|
||||||
if (mode === 'signup') {
|
if (mode === 'signup') {
|
||||||
try {
|
try {
|
||||||
await api.post(
|
await api.post(
|
||||||
`/users/signup?redirectURI=${window.location.origin}/authentication/signin`,
|
`/users/signup?redirectURI=${window.location.origin}/authentication/signin`,
|
||||||
{ ...formData, language: lang, theme }
|
{ ...formData, language: lang, theme }
|
||||||
)
|
)
|
||||||
formElement.reset()
|
return {
|
||||||
setFormState('success')
|
type: 'success',
|
||||||
setMessageTranslationKey('authentication:success-signup')
|
value: 'authentication:success-signup'
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFormState('error')
|
|
||||||
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
||||||
setMessageTranslationKey('authentication:alreadyUsed')
|
return {
|
||||||
} else {
|
type: 'error',
|
||||||
setMessageTranslationKey('errors:server-error')
|
value: 'authentication:alreadyUsed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
value: 'errors:server-error'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -114,14 +67,18 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
|
|||||||
const authentication = new AuthenticationClass(data)
|
const authentication = new AuthenticationClass(data)
|
||||||
authentication.signin()
|
authentication.signin()
|
||||||
await router.push('/application')
|
await router.push('/application')
|
||||||
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFormState('error')
|
|
||||||
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
||||||
setMessageTranslationKey('authentication:wrong-credentials')
|
return {
|
||||||
} else {
|
type: 'error',
|
||||||
setMessageTranslationKey('errors:server-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'>
|
<section className='text-center text-lg font-paragraph pt-8'>
|
||||||
{t('authentication:or')}
|
{t('authentication:or')}
|
||||||
</section>
|
</section>
|
||||||
<AuthenticationForm onSubmit={handleSubmit}>
|
<AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
|
||||||
{mode === 'signup' && (
|
{mode === 'signup' && (
|
||||||
<Input
|
<Input
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={t('authentication:name')}
|
placeholder={t('common:name')}
|
||||||
name='name'
|
name='name'
|
||||||
label={t('authentication:name')}
|
label={t('common:name')}
|
||||||
error={getErrorTranslation(errors.name)}
|
error={getErrorTranslation(errors.name)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -182,13 +139,7 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
|
|||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</AuthenticationForm>
|
</AuthenticationForm>
|
||||||
<FormState
|
<FormState id='message' state={formState} message={message} />
|
||||||
id='message'
|
|
||||||
state={formState}
|
|
||||||
message={
|
|
||||||
messageTranslationKey != null ? t(messageTranslationKey) : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Main>
|
</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 { UserSettings } from '../../../models/UserSettings'
|
||||||
import { UserPublic } from '../../../models/User'
|
import { User } from '../../../models/User'
|
||||||
|
|
||||||
export const user: UserPublic = {
|
export const user: User = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Divlo',
|
name: 'Divlo',
|
||||||
email: 'contact@divlo.fr',
|
email: 'contact@divlo.fr',
|
||||||
logo: undefined,
|
password: 'somepassword',
|
||||||
status: undefined,
|
logo: null,
|
||||||
biography: undefined,
|
status: null,
|
||||||
|
biography: null,
|
||||||
website: 'https://divlo.fr',
|
website: 'https://divlo.fr',
|
||||||
isConfirmed: true,
|
isConfirmed: true,
|
||||||
|
temporaryToken: 'temporaryUUIDtoken',
|
||||||
|
temporaryExpirationToken: '2021-10-20T20:59:08.485Z',
|
||||||
createdAt: '2021-10-20T20:30:51.595Z',
|
createdAt: '2021-10-20T20:30:51.595Z',
|
||||||
updatedAt: '2021-10-20T20:59:08.485Z'
|
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.visit('/authentication/reset-password')
|
||||||
cy.get('#message').should('not.exist')
|
cy.get('#message').should('not.exist')
|
||||||
cy.get('[data-cy=submit]').click()
|
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/reset-password": ["authentication", "errors"],
|
||||||
"/authentication/signup": ["authentication", "errors"],
|
"/authentication/signup": ["authentication", "errors"],
|
||||||
"/authentication/signin": ["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",
|
"or": "OR",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"name": "Name",
|
|
||||||
"already-have-an-account": "Already have an account?",
|
"already-have-an-account": "Already have an account?",
|
||||||
"dont-have-an-account": "Don't have an account?",
|
"dont-have-an-account": "Don't have an account?",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
|
@ -2,5 +2,6 @@
|
|||||||
"english": "English",
|
"english": "English",
|
||||||
"french": "French",
|
"french": "French",
|
||||||
"all-rights-reserved": "All rights reserved",
|
"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",
|
"or": "OU",
|
||||||
"password": "Mot de passe",
|
"password": "Mot de passe",
|
||||||
"name": "Nom",
|
|
||||||
"already-have-an-account": "Vous avez déjà un compte ?",
|
"already-have-an-account": "Vous avez déjà un compte ?",
|
||||||
"dont-have-an-account": "Vous n'avez pas de compte ?",
|
"dont-have-an-account": "Vous n'avez pas de compte ?",
|
||||||
"submit": "Soumettre",
|
"submit": "Soumettre",
|
||||||
|
@ -2,5 +2,6 @@
|
|||||||
"english": "Anglais",
|
"english": "Anglais",
|
||||||
"french": "Français",
|
"french": "Français",
|
||||||
"all-rights-reserved": "Tous droits réservés",
|
"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 = {
|
export const channelSchema = {
|
||||||
id,
|
id,
|
||||||
name: Type.String({ maxLength: 255 }),
|
name: Type.String({ minLength: 1, maxLength: 20 }),
|
||||||
createdAt: date.createdAt,
|
createdAt: date.createdAt,
|
||||||
updatedAt: date.updatedAt,
|
updatedAt: date.updatedAt,
|
||||||
guildId: id
|
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'
|
import { date, id } from './utils'
|
||||||
|
|
||||||
export const guildSchema = {
|
export const guildSchema = {
|
||||||
id,
|
id,
|
||||||
name: Type.String({ minLength: 3, maxLength: 30 }),
|
name: Type.String({ minLength: 1, maxLength: 30 }),
|
||||||
icon: Type.String({ format: 'uri-reference' }),
|
icon: Type.Union([Type.String({ format: 'uri-reference' }), Type.Null()]),
|
||||||
description: Type.String({ maxLength: 160 }),
|
description: Type.Union([Type.String({ maxLength: 160 }), Type.Null()]),
|
||||||
createdAt: date.createdAt,
|
createdAt: date.createdAt,
|
||||||
updatedAt: date.updatedAt
|
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 = {
|
export const messageSchema = {
|
||||||
id,
|
id,
|
||||||
value: Type.String(),
|
value: Type.String({
|
||||||
|
minLength: 1,
|
||||||
|
maxLength: 20_000
|
||||||
|
}),
|
||||||
type: Type.Union(types, { default: 'text' }),
|
type: Type.Union(types, { default: 'text' }),
|
||||||
mimetype: Type.String({
|
mimetype: Type.String({
|
||||||
maxLength: 255,
|
maxLength: 127,
|
||||||
default: 'text/plain',
|
default: 'text/plain',
|
||||||
format: 'mimetype'
|
format: 'mimetype'
|
||||||
}),
|
}),
|
||||||
|
@ -7,11 +7,11 @@ import { date, id } from './utils'
|
|||||||
export const userSchema = {
|
export const userSchema = {
|
||||||
id,
|
id,
|
||||||
name: Type.String({ minLength: 1, maxLength: 30 }),
|
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 }),
|
password: Type.String({ minLength: 1 }),
|
||||||
logo: Type.String({ format: 'uri-reference' }),
|
logo: Type.Union([Type.String({ format: 'uri-reference' }), Type.Null()]),
|
||||||
status: Type.String({ maxLength: 255 }),
|
status: Type.Union([Type.String({ maxLength: 50 }), Type.Null()]),
|
||||||
biography: Type.String(),
|
biography: Type.Union([Type.String({ maxLength: 160 }), Type.Null()]),
|
||||||
website: Type.String({ maxLength: 255, format: 'uri-reference' }),
|
website: Type.String({ maxLength: 255, format: 'uri-reference' }),
|
||||||
isConfirmed: Type.Boolean({ default: false }),
|
isConfirmed: Type.Boolean({ default: false }),
|
||||||
temporaryToken: Type.String(),
|
temporaryToken: Type.String(),
|
||||||
@ -20,32 +20,34 @@ export const userSchema = {
|
|||||||
updatedAt: date.updatedAt
|
updatedAt: date.updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
const userSchemaWithSettings = {
|
export const userObjectSchema = Type.Object(userSchema)
|
||||||
...userSchema,
|
|
||||||
settings: Type.Object(userSettingsSchema)
|
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 = {
|
export const userPublicSchema = {
|
||||||
id,
|
...userPublicWithoutSettingsSchema,
|
||||||
name: userSchema.name,
|
settings: Type.Object(userSettingsSchema)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userPublicObjectSchema = Type.Object(userPublicSchema)
|
export const userPublicObjectSchema = Type.Object(userPublicSchema)
|
||||||
|
|
||||||
export const userCurrentSchema = Type.Object({
|
export const userCurrentSchema = Type.Object({
|
||||||
...userSchemaWithSettings,
|
...userPublicSchema,
|
||||||
currentStrategy: Type.Union([...strategiesTypebox]),
|
currentStrategy: Type.Union([...strategiesTypebox]),
|
||||||
strategies: Type.Array(Type.Union([...strategiesTypebox]))
|
strategies: Type.Array(Type.Union([...strategiesTypebox]))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export type User = Static<typeof userObjectSchema>
|
||||||
export type UserPublic = Static<typeof userPublicObjectSchema>
|
export type UserPublic = Static<typeof userPublicObjectSchema>
|
||||||
export type UserCurrent = Static<typeof userCurrentSchema>
|
export type UserCurrent = Static<typeof userCurrentSchema>
|
||||||
|
@ -6,6 +6,12 @@ module.exports = nextTranslate(
|
|||||||
pwa: {
|
pwa: {
|
||||||
disable: process.env.NODE_ENV !== 'production',
|
disable: process.env.NODE_ENV !== 'production',
|
||||||
dest: 'public'
|
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",
|
"@sinclair/typebox": "0.20.5",
|
||||||
"ajv": "8.6.3",
|
"ajv": "8.6.3",
|
||||||
"ajv-formats": "2.1.1",
|
"ajv-formats": "2.1.1",
|
||||||
|
"axios": "0.23.0",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.1",
|
||||||
"date-and-time": "2.0.1",
|
"date-and-time": "2.0.1",
|
||||||
"next": "11.1.2",
|
"next": "11.1.2",
|
||||||
@ -8388,12 +8389,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "0.21.4",
|
"version": "0.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
|
||||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
"integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.14.0"
|
"follow-redirects": "^1.14.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
@ -16214,7 +16214,6 @@
|
|||||||
"version": "1.14.4",
|
"version": "1.14.4",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
|
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@ -37731,6 +37730,15 @@
|
|||||||
"node": ">=10.0.0"
|
"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": {
|
"node_modules/wait-on/node_modules/rxjs": {
|
||||||
"version": "7.4.0",
|
"version": "7.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz",
|
||||||
@ -45698,12 +45706,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "0.21.4",
|
"version": "0.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz",
|
||||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
"integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "^1.14.0"
|
"follow-redirects": "^1.14.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"axobject-query": {
|
"axobject-query": {
|
||||||
@ -51976,8 +51983,7 @@
|
|||||||
"follow-redirects": {
|
"follow-redirects": {
|
||||||
"version": "1.14.4",
|
"version": "1.14.4",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
|
||||||
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
|
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"for-in": {
|
"for-in": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -68666,6 +68672,15 @@
|
|||||||
"rxjs": "^7.1.0"
|
"rxjs": "^7.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"rxjs": {
|
||||||
"version": "7.4.0",
|
"version": "7.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz",
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
"@sinclair/typebox": "0.20.5",
|
"@sinclair/typebox": "0.20.5",
|
||||||
"ajv": "8.6.3",
|
"ajv": "8.6.3",
|
||||||
"ajv-formats": "2.1.1",
|
"ajv-formats": "2.1.1",
|
||||||
|
"axios": "0.23.0",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.1",
|
||||||
"date-and-time": "2.0.1",
|
"date-and-time": "2.0.1",
|
||||||
"next": "11.1.2",
|
"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 { Head } from 'components/Head'
|
||||||
import { Application } from 'components/Application'
|
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 {
|
import {
|
||||||
authenticationFromServerSide,
|
authenticationFromServerSide,
|
||||||
AuthenticationProvider,
|
AuthenticationProvider,
|
||||||
PagePropsWithAuthentication
|
PagePropsWithAuthentication
|
||||||
} from 'utils/authentication'
|
} from 'utils/authentication'
|
||||||
|
import { CreateGuild } from 'components/Application/CreateGuild'
|
||||||
|
|
||||||
const CreateGuild: React.FC<PagePropsWithAuthentication> = (props) => {
|
const CreateGuildPage: React.FC<PagePropsWithAuthentication> = (props) => {
|
||||||
return (
|
return (
|
||||||
<AuthenticationProvider authentication={props.authentication}>
|
<AuthenticationProvider authentication={props.authentication}>
|
||||||
<Head title='Thream | Create a Guild' />
|
<Head title='Thream | Create a Guild' />
|
||||||
<Application path='/application/guilds/create'>
|
<Application path='/application/guilds/create'>
|
||||||
<Main>
|
<CreateGuild />
|
||||||
<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>
|
|
||||||
</Application>
|
</Application>
|
||||||
</AuthenticationProvider>
|
</AuthenticationProvider>
|
||||||
)
|
)
|
||||||
@ -81,4 +23,4 @@ export const getServerSideProps = authenticationFromServerSide({
|
|||||||
shouldBeAuthenticated: true
|
shouldBeAuthenticated: true
|
||||||
})
|
})
|
||||||
|
|
||||||
export default CreateGuild
|
export default CreateGuildPage
|
||||||
|
@ -5,8 +5,7 @@ import {
|
|||||||
AuthenticationProvider,
|
AuthenticationProvider,
|
||||||
PagePropsWithAuthentication
|
PagePropsWithAuthentication
|
||||||
} from 'utils/authentication'
|
} from 'utils/authentication'
|
||||||
|
import { UserProfile } from 'components/Application/UserProfile'
|
||||||
import { UserProfile } from 'components/UserProfile'
|
|
||||||
|
|
||||||
const UserProfilePage: React.FC<PagePropsWithAuthentication> = (props) => {
|
const UserProfilePage: React.FC<PagePropsWithAuthentication> = (props) => {
|
||||||
return (
|
return (
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import { useState, useMemo } from 'react'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { AuthenticationForm } from 'components/Authentication'
|
import { AuthenticationForm } from 'components/Authentication'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import { HandleForm } from 'react-component-form'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Type } from '@sinclair/typebox'
|
|
||||||
|
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
@ -13,53 +10,39 @@ import { Footer, FooterProps } from 'components/Footer'
|
|||||||
import { Input } from 'components/design/Input'
|
import { Input } from 'components/design/Input'
|
||||||
import { Button } from 'components/design/Button'
|
import { Button } from 'components/design/Button'
|
||||||
import { FormState } from 'components/design/FormState'
|
import { FormState } from 'components/design/FormState'
|
||||||
import { useFormState } from 'hooks/useFormState'
|
|
||||||
import { authenticationFromServerSide } from 'utils/authentication'
|
import { authenticationFromServerSide } from 'utils/authentication'
|
||||||
import { ScrollableBody } from 'components/ScrollableBody'
|
import { ScrollableBody } from 'components/ScrollableBody'
|
||||||
|
import { userSchema } from 'models/User'
|
||||||
import { api } from 'utils/api'
|
import { api } from 'utils/api'
|
||||||
import { userSchema } from '../../models/User'
|
import { HandleSubmitCallback, useForm } from 'hooks/useForm'
|
||||||
import { ajv } from '../../utils/ajv'
|
|
||||||
|
|
||||||
const ForgotPassword: React.FC<FooterProps> = (props) => {
|
const ForgotPassword: React.FC<FooterProps> = (props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { version } = props
|
const { version } = props
|
||||||
const [formState, setFormState] = useFormState()
|
|
||||||
const [messageTranslationKey, setMessageTranslationKey] = useState<
|
|
||||||
string | undefined
|
|
||||||
>(undefined)
|
|
||||||
|
|
||||||
const validateSchema = useMemo(() => {
|
const { formState, message, errors, getErrorTranslation, handleSubmit } =
|
||||||
return Type.Object({
|
useForm({ validateSchemaObject: { email: userSchema.email } })
|
||||||
email: userSchema.email
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const validate = useMemo(() => {
|
const onSubmit: HandleSubmitCallback = async (formData) => {
|
||||||
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 {
|
try {
|
||||||
await api.post(
|
await api.post(
|
||||||
`/users/reset-password?redirectURI=${window.location.origin}/authentication/reset-password`,
|
`/users/reset-password?redirectURI=${window.location.origin}/authentication/reset-password`,
|
||||||
formData
|
formData
|
||||||
)
|
)
|
||||||
formElement.reset()
|
return {
|
||||||
setFormState('success')
|
type: 'success',
|
||||||
setMessageTranslationKey('authentication:success-forgot-password')
|
value: 'authentication:success-forgot-password'
|
||||||
} catch (error) {
|
|
||||||
setFormState('error')
|
|
||||||
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
|
||||||
setMessageTranslationKey('errors:email')
|
|
||||||
} else {
|
|
||||||
setMessageTranslationKey('errors:server-error')
|
|
||||||
}
|
}
|
||||||
|
} 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')}`} />
|
<Head title={`Thream | ${t('authentication:forgot-password')}`} />
|
||||||
<Header />
|
<Header />
|
||||||
<Main>
|
<Main>
|
||||||
<AuthenticationForm onSubmit={handleSubmit}>
|
<AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Input type='email' placeholder='Email' name='email' label='Email' />
|
<Input type='email' placeholder='Email' name='email' label='Email' />
|
||||||
<Button data-cy='submit' className='w-full mt-6' type='submit'>
|
<Button data-cy='submit' className='w-full mt-6' type='submit'>
|
||||||
{t('authentication:submit')}
|
{t('authentication:submit')}
|
||||||
@ -84,7 +67,7 @@ const ForgotPassword: React.FC<FooterProps> = (props) => {
|
|||||||
id='message'
|
id='message'
|
||||||
state={formState}
|
state={formState}
|
||||||
message={
|
message={
|
||||||
messageTranslationKey != null ? t(messageTranslationKey) : null
|
message != null ? message : getErrorTranslation(errors.email)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Main>
|
</Main>
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import { useState, useMemo } from 'react'
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import { HandleForm } from 'react-component-form'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { Type } from '@sinclair/typebox'
|
|
||||||
|
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
@ -12,54 +9,40 @@ import { Footer, FooterProps } from 'components/Footer'
|
|||||||
import { Input } from 'components/design/Input'
|
import { Input } from 'components/design/Input'
|
||||||
import { Button } from 'components/design/Button'
|
import { Button } from 'components/design/Button'
|
||||||
import { FormState } from 'components/design/FormState'
|
import { FormState } from 'components/design/FormState'
|
||||||
import { useFormState } from 'hooks/useFormState'
|
|
||||||
import { authenticationFromServerSide } from 'utils/authentication'
|
import { authenticationFromServerSide } from 'utils/authentication'
|
||||||
import { AuthenticationForm } from 'components/Authentication'
|
import { AuthenticationForm } from 'components/Authentication'
|
||||||
import { ScrollableBody } from 'components/ScrollableBody/ScrollableBody'
|
import { ScrollableBody } from 'components/ScrollableBody/ScrollableBody'
|
||||||
import { api } from 'utils/api'
|
import { api } from 'utils/api'
|
||||||
import { userSchema } from '../../models/User'
|
import { userSchema } from '../../models/User'
|
||||||
import { ajv } from '../../utils/ajv'
|
import { HandleSubmitCallback, useForm } from 'hooks/useForm'
|
||||||
|
|
||||||
const ResetPassword: React.FC<FooterProps> = (props) => {
|
const ResetPassword: React.FC<FooterProps> = (props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { version } = props
|
const { version } = props
|
||||||
const [formState, setFormState] = useFormState()
|
|
||||||
const [messageTranslationKey, setMessageTranslationKey] = useState<
|
|
||||||
string | undefined
|
|
||||||
>(undefined)
|
|
||||||
|
|
||||||
const validateSchema = useMemo(() => {
|
const { formState, message, errors, getErrorTranslation, handleSubmit } =
|
||||||
return Type.Object({
|
useForm({ validateSchemaObject: { password: userSchema.password } })
|
||||||
password: userSchema.password
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const validate = useMemo(() => {
|
const onSubmit: HandleSubmitCallback = async (formData) => {
|
||||||
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 {
|
try {
|
||||||
await api.put(`/users/reset-password`, {
|
await api.put(`/users/reset-password`, {
|
||||||
...formData,
|
...formData,
|
||||||
temporaryToken: router.query.temporaryToken
|
temporaryToken: router.query.temporaryToken
|
||||||
})
|
})
|
||||||
await router.push('/authentication/signin')
|
await router.push('/authentication/signin')
|
||||||
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFormState('error')
|
|
||||||
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
||||||
setMessageTranslationKey('errors:invalid')
|
return {
|
||||||
} else {
|
type: 'error',
|
||||||
setMessageTranslationKey('errors:server-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')}`} />
|
<Head title={`Thream | ${t('authentication:reset-password')}`} />
|
||||||
<Header />
|
<Header />
|
||||||
<Main>
|
<Main>
|
||||||
<AuthenticationForm onSubmit={handleSubmit}>
|
<AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Input
|
<Input
|
||||||
type='password'
|
type='password'
|
||||||
placeholder='Password'
|
placeholder='Password'
|
||||||
@ -83,7 +66,7 @@ const ResetPassword: React.FC<FooterProps> = (props) => {
|
|||||||
id='message'
|
id='message'
|
||||||
state={formState}
|
state={formState}
|
||||||
message={
|
message={
|
||||||
messageTranslationKey != null ? t(messageTranslationKey) : null
|
message != null ? message : getErrorTranslation(errors.password)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Main>
|
</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 axios, { AxiosInstance } from 'axios'
|
||||||
import { io, Socket } from 'socket.io-client'
|
import { io, Socket } from 'socket.io-client'
|
||||||
|
|
||||||
import { API_URL } from 'utils/api'
|
import { API_URL } from '../api'
|
||||||
import { cookies } from 'utils/cookies'
|
import { cookies } from '../cookies'
|
||||||
import { Tokens } from './'
|
import { Tokens } from './'
|
||||||
import { fetchRefreshToken } from './authenticationFromServerSide'
|
import { fetchRefreshToken } from './authenticationFromServerSide'
|
||||||
|
|
||||||
@ -46,6 +46,7 @@ export class Authentication {
|
|||||||
)
|
)
|
||||||
this.setAccessToken(accessToken)
|
this.setAccessToken(accessToken)
|
||||||
}
|
}
|
||||||
|
config.headers = config.headers == null ? {} : config.headers
|
||||||
config.headers.Authorization = `${this.tokens.type} ${this.tokens.accessToken}`
|
config.headers.Authorization = `${this.tokens.type} ${this.tokens.accessToken}`
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { createContext, useState, useEffect, useMemo, useContext } from 'react'
|
import { createContext, useState, useEffect, useMemo, useContext } from 'react'
|
||||||
|
import { useTheme } from 'next-themes'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import setLanguage from 'next-translate/setLanguage'
|
import setLanguage from 'next-translate/setLanguage'
|
||||||
|
|
||||||
import { Authentication, PagePropsWithAuthentication } from './'
|
import { Authentication, PagePropsWithAuthentication } from './'
|
||||||
import { useTheme } from 'next-themes'
|
import { UserCurrent } from '../../models/User'
|
||||||
import { UserCurrent } from 'models/User'
|
|
||||||
|
|
||||||
export interface AuthenticationValue {
|
export interface AuthenticationValue {
|
||||||
authentication: Authentication
|
authentication: Authentication
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { AxiosInstance } from 'axios'
|
import { AxiosInstance, AxiosResponse } from 'axios'
|
||||||
import { GetServerSideProps, GetServerSidePropsContext, Redirect } from 'next'
|
import { GetServerSideProps, GetServerSidePropsContext, Redirect } from 'next'
|
||||||
|
|
||||||
import { api } from 'utils/api'
|
import { api } from '../api'
|
||||||
import { Cookies } from 'utils/cookies'
|
import { Cookies } from '../cookies'
|
||||||
import { RefreshTokenResponse, Tokens } from './index'
|
import { RefreshTokenResponse, Tokens } from './index'
|
||||||
import { Authentication } from './Authentication'
|
import { Authentication } from './Authentication'
|
||||||
|
import { UserCurrent } from '../../models/User'
|
||||||
|
|
||||||
export const fetchRefreshToken = async (
|
export const fetchRefreshToken = async (
|
||||||
refreshToken: string
|
refreshToken: string
|
||||||
@ -70,9 +71,10 @@ export const authenticationFromServerSide = (
|
|||||||
} else {
|
} else {
|
||||||
let data: Redirect | any = {}
|
let data: Redirect | any = {}
|
||||||
const authentication = new Authentication(tokens)
|
const authentication = new Authentication(tokens)
|
||||||
const { data: currentUser } = await authentication.api.get(
|
const { data: currentUser } = await authentication.api.get<
|
||||||
'/users/current'
|
unknown,
|
||||||
)
|
AxiosResponse<UserCurrent>
|
||||||
|
>('/users/current')
|
||||||
if (fetchData != null) {
|
if (fetchData != null) {
|
||||||
data = await fetchData(context, authentication.api)
|
data = await fetchData(context, authentication.api)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { UserCurrent } from 'models/User'
|
import { UserCurrent } from '../../models/User'
|
||||||
|
|
||||||
export interface RefreshTokenResponse {
|
export interface RefreshTokenResponse {
|
||||||
accessToken: string
|
accessToken: string
|
||||||
|
Reference in New Issue
Block a user