diff --git a/components/Application/Application.tsx b/components/Application/Application.tsx
index 944f74f..e8214c2 100644
--- a/components/Application/Application.tsx
+++ b/components/Application/Application.tsx
@@ -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 = (props) => {
const { children, path } = props
+ const { t } = useTranslation()
const { user } = useAuthentication()
const [visibleSidebars, setVisibleSidebars] = useState({
@@ -138,10 +141,10 @@ export const Application: React.FC = (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 = (props) => {
>
{
+ return
+}
+CreateGuild.args = {}
diff --git a/components/Application/CreateGuild/CreateGuild.tsx b/components/Application/CreateGuild/CreateGuild.tsx
new file mode 100644
index 0000000..b2fd563
--- /dev/null
+++ b/components/Application/CreateGuild/CreateGuild.tsx
@@ -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 (
+
+
+
+
+ )
+}
diff --git a/components/Application/CreateGuild/index.ts b/components/Application/CreateGuild/index.ts
new file mode 100644
index 0000000..cc03400
--- /dev/null
+++ b/components/Application/CreateGuild/index.ts
@@ -0,0 +1 @@
+export * from './CreateGuild'
diff --git a/components/UserProfile/UserProfile.stories.tsx b/components/Application/UserProfile/UserProfile.stories.tsx
similarity index 84%
rename from components/UserProfile/UserProfile.stories.tsx
rename to components/Application/UserProfile/UserProfile.stories.tsx
index e8e6953..ee76ced 100644
--- a/components/UserProfile/UserProfile.stories.tsx
+++ b/components/Application/UserProfile/UserProfile.stories.tsx
@@ -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'
diff --git a/components/UserProfile/UserProfile.test.tsx b/components/Application/UserProfile/UserProfile.test.tsx
similarity index 81%
rename from components/UserProfile/UserProfile.test.tsx
rename to components/Application/UserProfile/UserProfile.test.tsx
index fa7d128..08a07a7 100644
--- a/components/UserProfile/UserProfile.test.tsx
+++ b/components/Application/UserProfile/UserProfile.test.tsx
@@ -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'
diff --git a/components/UserProfile/UserProfile.tsx b/components/Application/UserProfile/UserProfile.tsx
similarity index 99%
rename from components/UserProfile/UserProfile.tsx
rename to components/Application/UserProfile/UserProfile.tsx
index c1bb81f..38a6f04 100644
--- a/components/UserProfile/UserProfile.tsx
+++ b/components/Application/UserProfile/UserProfile.tsx
@@ -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 = (props) => {
const { user, isOwner = false } = props
- console.log(user)
-
const { t } = useTranslation()
const handleSubmitChanges = (
diff --git a/components/UserProfile/index.ts b/components/Application/UserProfile/index.ts
similarity index 100%
rename from components/UserProfile/index.ts
rename to components/Application/UserProfile/index.ts
diff --git a/components/Authentication/Authentication.tsx b/components/Authentication/Authentication.tsx
index 587b48d..d1621ac 100644
--- a/components/Authentication/Authentication.tsx
+++ b/components/Authentication/Authentication.tsx
@@ -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 | 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 = (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({
- 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('/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('/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 = (props) => {
-
+
{mode === 'signup' && (
)}
@@ -182,13 +139,7 @@ export const Authentication: React.FC = (props) => {
-
+
)
}
diff --git a/cypress/fixtures/channels/channel.ts b/cypress/fixtures/channels/channel.ts
new file mode 100644
index 0000000..dc221f3
--- /dev/null
+++ b/cypress/fixtures/channels/channel.ts
@@ -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()
+}
diff --git a/cypress/fixtures/guilds/guild.ts b/cypress/fixtures/guilds/guild.ts
new file mode 100644
index 0000000..bcd9a50
--- /dev/null
+++ b/cypress/fixtures/guilds/guild.ts
@@ -0,0 +1,8 @@
+export const guild = {
+ id: 1,
+ name: 'GuildExample',
+ description: 'guild example.',
+ icon: null,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
+}
diff --git a/cypress/fixtures/guilds/post.ts b/cypress/fixtures/guilds/post.ts
new file mode 100644
index 0000000..8feefa3
--- /dev/null
+++ b/cypress/fixtures/guilds/post.ts
@@ -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]
+ }
+ }
+ }
+}
diff --git a/cypress/fixtures/members/member.ts b/cypress/fixtures/members/member.ts
new file mode 100644
index 0000000..46872c7
--- /dev/null
+++ b/cypress/fixtures/members/member.ts
@@ -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
+}
diff --git a/cypress/fixtures/users/user.ts b/cypress/fixtures/users/user.ts
index 0024a65..d43dcb9 100644
--- a/cypress/fixtures/users/user.ts
+++ b/cypress/fixtures/users/user.ts
@@ -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'
}
diff --git a/cypress/integration/pages/application/guilds/create.spec.ts b/cypress/integration/pages/application/guilds/create.spec.ts
new file mode 100644
index 0000000..e276376
--- /dev/null
+++ b/cypress/integration/pages/application/guilds/create.spec.ts
@@ -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.')
+ })
+})
diff --git a/cypress/integration/pages/authentication/reset-password.spec.ts b/cypress/integration/pages/authentication/reset-password.spec.ts
index 7195389..74e1460 100644
--- a/cypress/integration/pages/authentication/reset-password.spec.ts
+++ b/cypress/integration/pages/authentication/reset-password.spec.ts
@@ -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 đ.'
+ )
})
})
diff --git a/components/Authentication/getErrorTranslationKey.test.ts b/hooks/useForm/getErrorTranslationKey.test.ts
similarity index 100%
rename from components/Authentication/getErrorTranslationKey.test.ts
rename to hooks/useForm/getErrorTranslationKey.test.ts
diff --git a/components/Authentication/getErrorTranslationKey.ts b/hooks/useForm/getErrorTranslationKey.ts
similarity index 100%
rename from components/Authentication/getErrorTranslationKey.ts
rename to hooks/useForm/getErrorTranslationKey.ts
diff --git a/hooks/useForm/index.ts b/hooks/useForm/index.ts
new file mode 100644
index 0000000..f65863c
--- /dev/null
+++ b/hooks/useForm/index.ts
@@ -0,0 +1 @@
+export * from './useForm'
diff --git a/hooks/useForm/useForm.ts b/hooks/useForm/useForm.ts
new file mode 100644
index 0000000..c3489c2
--- /dev/null
+++ b/hooks/useForm/useForm.ts
@@ -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 | 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
+
+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({})
+
+ 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
+ }
+}
diff --git a/hooks/useFormState.tsx b/hooks/useFormState.ts
similarity index 100%
rename from hooks/useFormState.tsx
rename to hooks/useFormState.ts
diff --git a/i18n.json b/i18n.json
index b2e0b17..e1a9a74 100644
--- a/i18n.json
+++ b/i18n.json
@@ -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"]
}
}
diff --git a/locales/en/application.json b/locales/en/application.json
index 637074e..84f0ec5 100644
--- a/locales/en/application.json
+++ b/locales/en/application.json
@@ -1,3 +1,5 @@
{
- "website": "Website"
+ "website": "Website",
+ "create": "Create",
+ "create-a-guild": "Create a Guild"
}
diff --git a/locales/en/authentication.json b/locales/en/authentication.json
index 56909e7..7febab8 100644
--- a/locales/en/authentication.json
+++ b/locales/en/authentication.json
@@ -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",
diff --git a/locales/en/common.json b/locales/en/common.json
index d5bd8e4..95b71f3 100644
--- a/locales/en/common.json
+++ b/locales/en/common.json
@@ -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"
}
diff --git a/locales/fr/application.json b/locales/fr/application.json
index ca95713..c1af9f5 100644
--- a/locales/fr/application.json
+++ b/locales/fr/application.json
@@ -1,3 +1,5 @@
{
- "website": "Site web"
+ "website": "Site web",
+ "create": "Crée",
+ "create-a-guild": "Crée une Guilde"
}
diff --git a/locales/fr/authentication.json b/locales/fr/authentication.json
index 4fac5ad..9db8638 100644
--- a/locales/fr/authentication.json
+++ b/locales/fr/authentication.json
@@ -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",
diff --git a/locales/fr/common.json b/locales/fr/common.json
index a5980d4..0c3eae3 100644
--- a/locales/fr/common.json
+++ b/locales/fr/common.json
@@ -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"
}
diff --git a/models/Channel.ts b/models/Channel.ts
index eb21334..4246744 100644
--- a/models/Channel.ts
+++ b/models/Channel.ts
@@ -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
diff --git a/models/Guild.ts b/models/Guild.ts
index ee821fd..e1b6522 100644
--- a/models/Guild.ts
+++ b/models/Guild.ts
@@ -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
diff --git a/models/Message.ts b/models/Message.ts
index e45b15c..c8a0d7a 100644
--- a/models/Message.ts
+++ b/models/Message.ts
@@ -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'
}),
diff --git a/models/User.ts b/models/User.ts
index 812837e..4884aa4 100644
--- a/models/User.ts
+++ b/models/User.ts
@@ -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
export type UserPublic = Static
export type UserCurrent = Static
diff --git a/next.config.js b/next.config.js
index 4cdf79a..c22e29c 100644
--- a/next.config.js
+++ b/next.config.js
@@ -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'] : [])
+ ]
}
})
)
diff --git a/package-lock.json b/package-lock.json
index 7690617..9591cdc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 966e90a..a0e7661 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pages/application/guilds/create.tsx b/pages/application/guilds/create.tsx
index 60182aa..663bd45 100644
--- a/pages/application/guilds/create.tsx
+++ b/pages/application/guilds/create.tsx
@@ -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 = (props) => {
+const CreateGuildPage: React.FC = (props) => {
return (
-
-
-
+
)
@@ -81,4 +23,4 @@ export const getServerSideProps = authenticationFromServerSide({
shouldBeAuthenticated: true
})
-export default CreateGuild
+export default CreateGuildPage
diff --git a/pages/application/users/[userId].tsx b/pages/application/users/[userId].tsx
index 3245364..5ca68a9 100644
--- a/pages/application/users/[userId].tsx
+++ b/pages/application/users/[userId].tsx
@@ -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 = (props) => {
return (
diff --git a/pages/authentication/forgot-password.tsx b/pages/authentication/forgot-password.tsx
index 255a271..7344a30 100644
--- a/pages/authentication/forgot-password.tsx
+++ b/pages/authentication/forgot-password.tsx
@@ -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 = (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 = (props) => {
-
+
diff --git a/pages/authentication/reset-password.tsx b/pages/authentication/reset-password.tsx
index 6aabc68..8d21710 100644
--- a/pages/authentication/reset-password.tsx
+++ b/pages/authentication/reset-password.tsx
@@ -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 = (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 = (props) => {
-
+
= (props) => {
id='message'
state={formState}
message={
- messageTranslationKey != null ? t(messageTranslationKey) : null
+ message != null ? message : getErrorTranslation(errors.password)
}
/>
diff --git a/public/images/data/divlo.png b/public/images/data/divlo.png
deleted file mode 100644
index 06bb347..0000000
Binary files a/public/images/data/divlo.png and /dev/null differ
diff --git a/public/images/data/user-default.png b/public/images/data/user-default.png
new file mode 100644
index 0000000..d3d6fbe
Binary files /dev/null and b/public/images/data/user-default.png differ
diff --git a/utils/authentication/Authentication.ts b/utils/authentication/Authentication.ts
index 8ecbfa6..3c19450 100644
--- a/utils/authentication/Authentication.ts
+++ b/utils/authentication/Authentication.ts
@@ -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
},
diff --git a/utils/authentication/AuthenticationContext.tsx b/utils/authentication/AuthenticationContext.tsx
index e27ec54..abac6c3 100644
--- a/utils/authentication/AuthenticationContext.tsx
+++ b/utils/authentication/AuthenticationContext.tsx
@@ -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
diff --git a/utils/authentication/authenticationFromServerSide.ts b/utils/authentication/authenticationFromServerSide.ts
index be9bf9f..08dec58 100644
--- a/utils/authentication/authenticationFromServerSide.ts
+++ b/utils/authentication/authenticationFromServerSide.ts
@@ -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
+ >('/users/current')
if (fetchData != null) {
data = await fetchData(context, authentication.api)
}
diff --git a/utils/authentication/index.ts b/utils/authentication/index.ts
index a37f023..291fe82 100644
--- a/utils/authentication/index.ts
+++ b/utils/authentication/index.ts
@@ -1,4 +1,4 @@
-import { UserCurrent } from 'models/User'
+import { UserCurrent } from '../../models/User'
export interface RefreshTokenResponse {
accessToken: string