feat(pages): add /application/guilds/join (#2)
				
					
				
			This commit is contained in:
		@@ -240,6 +240,7 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
        </Sidebar>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
          id='application-page-content'
 | 
			
		||||
          className={classNames(
 | 
			
		||||
            'top-0 h-full-without-header flex flex-col flex-1 z-0 overflow-y-auto transition',
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ export const CreateGuild: React.FC = () => {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
 | 
			
		||||
  const { formState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
  const { fetchState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({
 | 
			
		||||
      validateSchemaObject: {
 | 
			
		||||
        name: guildSchema.name,
 | 
			
		||||
@@ -76,7 +76,7 @@ export const CreateGuild: React.FC = () => {
 | 
			
		||||
          {t('application:create')}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Form>
 | 
			
		||||
      <FormState id='message' state={formState} message={message} />
 | 
			
		||||
      <FormState id='message' state={fetchState} message={message} />
 | 
			
		||||
    </Main>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { Guild as Component, GuildProps } from './Guild'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Guild',
 | 
			
		||||
  component: Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Stories
 | 
			
		||||
 | 
			
		||||
export const Guild: Story<GuildProps> = (arguments_) => {
 | 
			
		||||
  return <Component {...arguments_} />
 | 
			
		||||
}
 | 
			
		||||
Guild.args = {
 | 
			
		||||
  guild: {
 | 
			
		||||
    id: 1,
 | 
			
		||||
    name: 'GuildExample',
 | 
			
		||||
    description: 'guild example.',
 | 
			
		||||
    icon: null,
 | 
			
		||||
    createdAt: new Date().toISOString(),
 | 
			
		||||
    updatedAt: new Date().toISOString(),
 | 
			
		||||
    membersCount: 1
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								components/Application/JoinGuildsPublic/Guild/Guild.test.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								components/Application/JoinGuildsPublic/Guild/Guild.test.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Guild } from './Guild'
 | 
			
		||||
 | 
			
		||||
describe('<Guild />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
    const { baseElement } = render(
 | 
			
		||||
      <Guild
 | 
			
		||||
        guild={{
 | 
			
		||||
          id: 1,
 | 
			
		||||
          name: 'GuildExample',
 | 
			
		||||
          description: 'guild example.',
 | 
			
		||||
          icon: null,
 | 
			
		||||
          createdAt: new Date().toISOString(),
 | 
			
		||||
          updatedAt: new Date().toISOString(),
 | 
			
		||||
          membersCount: 1
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
    expect(baseElement).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										35
									
								
								components/Application/JoinGuildsPublic/Guild/Guild.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								components/Application/JoinGuildsPublic/Guild/Guild.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
 | 
			
		||||
import { GuildPublic } from 'models/Guild'
 | 
			
		||||
 | 
			
		||||
export interface GuildProps {
 | 
			
		||||
  guild: GuildPublic
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Guild: React.FC<GuildProps> = (props) => {
 | 
			
		||||
  const { guild } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      key={guild.id}
 | 
			
		||||
      className='max-w-sm flex flex-col items-center justify-center border-gray-500 dark:border-gray-700 p-4 cursor-pointer rounded shadow-lg border transition duration-200 ease-in-out hover:-translate-y-2 hover:shadow-none'
 | 
			
		||||
    >
 | 
			
		||||
      <Image
 | 
			
		||||
        className='rounded-full'
 | 
			
		||||
        src={guild.icon != null ? guild.icon : '/images/data/guild-default.png'}
 | 
			
		||||
        alt='logo'
 | 
			
		||||
        width={80}
 | 
			
		||||
        height={80}
 | 
			
		||||
      />
 | 
			
		||||
      <div className='m-2 text-center mt-3'>
 | 
			
		||||
        <h3 data-cy='guild-name' className='font-bold text-xl mb-2'>
 | 
			
		||||
          {guild.name}
 | 
			
		||||
        </h3>
 | 
			
		||||
        <p className='text-base w-11/12 mx-auto'>{guild.description}</p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <p className='flex flex-col text-green-800 dark:text-green-400 mt-4'>
 | 
			
		||||
        {guild.membersCount} members
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								components/Application/JoinGuildsPublic/Guild/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								components/Application/JoinGuildsPublic/Guild/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export * from './Guild'
 | 
			
		||||
							
								
								
									
										75
									
								
								components/Application/JoinGuildsPublic/JoinGuildsPublic.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								components/Application/JoinGuildsPublic/JoinGuildsPublic.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
import { useCallback, useEffect, useState, useRef } from 'react'
 | 
			
		||||
import InfiniteScroll from 'react-infinite-scroll-component'
 | 
			
		||||
 | 
			
		||||
import { useAuthentication } from 'utils/authentication'
 | 
			
		||||
import { GuildPublic } from 'models/Guild'
 | 
			
		||||
import { Loader } from 'components/design/Loader'
 | 
			
		||||
import { useFetchState } from 'hooks/useFetchState'
 | 
			
		||||
import { Guild } from './Guild'
 | 
			
		||||
 | 
			
		||||
export const JoinGuildsPublic: React.FC = () => {
 | 
			
		||||
  const [guilds, setGuilds] = useState<GuildPublic[]>([])
 | 
			
		||||
  const [hasMore, setHasMore] = useState(true)
 | 
			
		||||
  const [inputSearch, setInputSearch] = useState('')
 | 
			
		||||
  const [fetchState, setFetchState] = useFetchState('idle')
 | 
			
		||||
  const afterId = useRef<number | null>(null)
 | 
			
		||||
 | 
			
		||||
  const { authentication } = useAuthentication()
 | 
			
		||||
 | 
			
		||||
  const fetchGuilds = useCallback(async (): Promise<void> => {
 | 
			
		||||
    if (fetchState !== 'idle') {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    setFetchState('loading')
 | 
			
		||||
    const { data } = await authentication.api.get<GuildPublic[]>(
 | 
			
		||||
      `/guilds/public?limit=20&search=${inputSearch}${
 | 
			
		||||
        afterId.current != null ? `&after=${afterId.current}` : ''
 | 
			
		||||
      }`
 | 
			
		||||
    )
 | 
			
		||||
    afterId.current = data.length > 0 ? data[data.length - 1].id : null
 | 
			
		||||
    setGuilds((oldGuilds) => {
 | 
			
		||||
      return [...oldGuilds, ...data]
 | 
			
		||||
    })
 | 
			
		||||
    setHasMore(data.length > 0)
 | 
			
		||||
    setFetchState('idle')
 | 
			
		||||
  }, [authentication, fetchState, setFetchState, inputSearch])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    afterId.current = null
 | 
			
		||||
    setGuilds([])
 | 
			
		||||
    fetchGuilds().catch((error) => {
 | 
			
		||||
      console.error(error)
 | 
			
		||||
    })
 | 
			
		||||
  }, [inputSearch]) // eslint-disable-line react-hooks/exhaustive-deps
 | 
			
		||||
 | 
			
		||||
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
 | 
			
		||||
    setInputSearch(event.target.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <input
 | 
			
		||||
        data-cy='search-guild-input'
 | 
			
		||||
        onChange={handleChange}
 | 
			
		||||
        className='w-10/12 sm:w-8/12 md:w-6/12 lg:w-5/12 bg-white dark:bg-[#3B3B3B] border-gray-500 dark:border-gray-700 p-3 my-6 mt-16 mx-auto rounded-md border'
 | 
			
		||||
        type='search'
 | 
			
		||||
        name='search-guild'
 | 
			
		||||
        placeholder='🔎  Search...'
 | 
			
		||||
      />
 | 
			
		||||
      <div className='w-full flex items-center justify-center p-12'>
 | 
			
		||||
        <InfiniteScroll
 | 
			
		||||
          className='guilds-list max-w-[1600px] grid grid-cols-1 xl:grid-cols-3 md:grid-cols-2 sm:grid-cols-1 gap-8 !overflow-hidden'
 | 
			
		||||
          dataLength={guilds.length}
 | 
			
		||||
          next={fetchGuilds}
 | 
			
		||||
          scrollableTarget='application-page-content'
 | 
			
		||||
          hasMore={hasMore}
 | 
			
		||||
          loader={<Loader />}
 | 
			
		||||
        >
 | 
			
		||||
          {guilds.map((guild) => {
 | 
			
		||||
            return <Guild guild={guild} key={guild.id} />
 | 
			
		||||
          })}
 | 
			
		||||
        </InfiniteScroll>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								components/Application/JoinGuildsPublic/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								components/Application/JoinGuildsPublic/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export * from './JoinGuildsPublic'
 | 
			
		||||
@@ -29,7 +29,7 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
			
		||||
  const { lang, t } = useTranslation()
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
 | 
			
		||||
  const { errors, formState, message, getErrorTranslation, handleSubmit } =
 | 
			
		||||
  const { errors, fetchState, message, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({
 | 
			
		||||
      validateSchemaObject: {
 | 
			
		||||
        ...(mode === 'signup' && { name: userSchema.name }),
 | 
			
		||||
@@ -139,7 +139,7 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
			
		||||
          </Link>
 | 
			
		||||
        </p>
 | 
			
		||||
      </AuthenticationForm>
 | 
			
		||||
      <FormState id='message' state={formState} message={message} />
 | 
			
		||||
      <FormState id='message' state={fetchState} message={message} />
 | 
			
		||||
    </Main>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
 | 
			
		||||
import { FormState as FormStateType } from 'hooks/useFormState'
 | 
			
		||||
import { FetchState as FormStateType } from 'hooks/useFetchState'
 | 
			
		||||
import { Loader } from '../Loader'
 | 
			
		||||
 | 
			
		||||
export interface FormStateProps {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,3 +6,8 @@ export const guild = {
 | 
			
		||||
  createdAt: new Date().toISOString(),
 | 
			
		||||
  updatedAt: new Date().toISOString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const guild2 = {
 | 
			
		||||
  ...guild,
 | 
			
		||||
  name: 'app'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								cypress/fixtures/guilds/public/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								cypress/fixtures/guilds/public/get.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
 | 
			
		||||
import { guild, guild2 } from '../guild'
 | 
			
		||||
 | 
			
		||||
export const getGuildsPublicEmptyHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: '/guilds/public',
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: []
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getGuildsPublicHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: '/guilds/public',
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: [
 | 
			
		||||
      { ...guild, membersCount: 1 },
 | 
			
		||||
      { ...guild2, membersCount: 1 }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getGuildsPublicSearchHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: '/guilds/public',
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: [{ ...guild2, membersCount: 1 }]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								cypress/integration/pages/application/guilds/join.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								cypress/integration/pages/application/guilds/join.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
import {
 | 
			
		||||
  getGuildsPublicEmptyHandler,
 | 
			
		||||
  getGuildsPublicHandler,
 | 
			
		||||
  getGuildsPublicSearchHandler
 | 
			
		||||
} from '../../../../fixtures/guilds/public/get'
 | 
			
		||||
import { authenticationHandlers } from '../../../../fixtures/handler'
 | 
			
		||||
 | 
			
		||||
describe('Pages > /application/guilds/join', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.task('stopMockServer')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should shows no guild if there are no public guilds', () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getGuildsPublicEmptyHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.visit('/application/guilds/join')
 | 
			
		||||
    cy.get('.guilds-list').children().should('have.length', 0)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should shows loader with internal api server error', () => {
 | 
			
		||||
    cy.task('startMockServer', [...authenticationHandlers]).setCookie(
 | 
			
		||||
      'refreshToken',
 | 
			
		||||
      'refresh-token'
 | 
			
		||||
    )
 | 
			
		||||
    cy.visit('/application/guilds/join')
 | 
			
		||||
    cy.get('.guilds-list').children().should('have.length', 1)
 | 
			
		||||
    cy.get('[data-testid=progress-spinner]').should('be.visible')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should shows all the guilds', () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getGuildsPublicHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.visit('/application/guilds/join')
 | 
			
		||||
    cy.get('.guilds-list').children().should('have.length', 2)
 | 
			
		||||
    cy.get('.guilds-list [data-cy=guild-name]:first').should(
 | 
			
		||||
      'have.text',
 | 
			
		||||
      'GuildExample'
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should shows the searched guild', () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getGuildsPublicSearchHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.visit('/application/guilds/join')
 | 
			
		||||
    cy.get('[data-cy=search-guild-input]').type('app')
 | 
			
		||||
    cy.get('.guilds-list').children().should('have.length', 1)
 | 
			
		||||
    cy.get('.guilds-list [data-cy=guild-name]:first').should('have.text', 'app')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										15
									
								
								hooks/useFetchState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								hooks/useFetchState.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
 | 
			
		||||
export const fetchState = ['idle', 'loading', 'error', 'success'] as const
 | 
			
		||||
 | 
			
		||||
export type FetchState = typeof fetchState[number]
 | 
			
		||||
 | 
			
		||||
export const useFetchState = (
 | 
			
		||||
  initialFetchState: FetchState = 'idle'
 | 
			
		||||
): [
 | 
			
		||||
  fetchState: FetchState,
 | 
			
		||||
  setFetchState: React.Dispatch<React.SetStateAction<FetchState>>
 | 
			
		||||
] => {
 | 
			
		||||
  const [fetchState, setFetchState] = useState<FetchState>(initialFetchState)
 | 
			
		||||
  return [fetchState, setFetchState]
 | 
			
		||||
}
 | 
			
		||||
@@ -4,7 +4,7 @@ import { Type } from '@sinclair/typebox'
 | 
			
		||||
import type { FormDataObject, HandleForm } from 'react-component-form'
 | 
			
		||||
import type { ErrorObject } from 'ajv'
 | 
			
		||||
 | 
			
		||||
import { FormState, useFormState } from '../useFormState'
 | 
			
		||||
import { FetchState, useFetchState } from '../useFetchState'
 | 
			
		||||
import { ajv } from '../../utils/ajv'
 | 
			
		||||
import { getErrorTranslationKey } from './getErrorTranslationKey'
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +38,7 @@ export type HandleSubmitCallback = (
 | 
			
		||||
 | 
			
		||||
export interface UseFormResult {
 | 
			
		||||
  message: string | null
 | 
			
		||||
  formState: FormState
 | 
			
		||||
  fetchState: FetchState
 | 
			
		||||
  getErrorTranslation: GetErrorTranslation
 | 
			
		||||
  handleSubmit: HandleSubmit
 | 
			
		||||
  errors: Errors
 | 
			
		||||
@@ -47,7 +47,7 @@ export interface UseFormResult {
 | 
			
		||||
export const useForm = (options: UseFormOptions): UseFormResult => {
 | 
			
		||||
  const { validateSchemaObject } = options
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const [formState, setFormState] = useFormState()
 | 
			
		||||
  const [fetchState, setFetchState] = useFetchState()
 | 
			
		||||
  const [messageTranslationKey, setMessageTranslationKey] = useState<
 | 
			
		||||
    string | undefined
 | 
			
		||||
  >(undefined)
 | 
			
		||||
@@ -75,7 +75,7 @@ export const useForm = (options: UseFormOptions): UseFormResult => {
 | 
			
		||||
    return async (formData, formElement) => {
 | 
			
		||||
      const isValid = validate(formData)
 | 
			
		||||
      if (!isValid) {
 | 
			
		||||
        setFormState('error')
 | 
			
		||||
        setFetchState('error')
 | 
			
		||||
        const errors: Errors = {}
 | 
			
		||||
        for (const property in validateSchema.properties) {
 | 
			
		||||
          errors[property] = validate.errors?.find(findError(`/${property}`))
 | 
			
		||||
@@ -83,15 +83,15 @@ export const useForm = (options: UseFormOptions): UseFormResult => {
 | 
			
		||||
        setErrors(errors)
 | 
			
		||||
      } else {
 | 
			
		||||
        setErrors({})
 | 
			
		||||
        setFormState('loading')
 | 
			
		||||
        setFetchState('loading')
 | 
			
		||||
        const message = await callback(formData, formElement)
 | 
			
		||||
        if (message != null) {
 | 
			
		||||
          setMessageTranslationKey(message.value)
 | 
			
		||||
          if (message.type === 'success') {
 | 
			
		||||
            setFormState('success')
 | 
			
		||||
            setFetchState('success')
 | 
			
		||||
            formElement.reset()
 | 
			
		||||
          } else {
 | 
			
		||||
            setFormState('error')
 | 
			
		||||
            setFetchState('error')
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@@ -101,7 +101,7 @@ export const useForm = (options: UseFormOptions): UseFormResult => {
 | 
			
		||||
  return {
 | 
			
		||||
    getErrorTranslation,
 | 
			
		||||
    errors,
 | 
			
		||||
    formState,
 | 
			
		||||
    fetchState,
 | 
			
		||||
    handleSubmit,
 | 
			
		||||
    message: messageTranslationKey != null ? t(messageTranslationKey) : null
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
 | 
			
		||||
export const formState = ['idle', 'loading', 'error', 'success'] as const
 | 
			
		||||
 | 
			
		||||
export type FormState = typeof formState[number]
 | 
			
		||||
 | 
			
		||||
export const useFormState = (
 | 
			
		||||
  initialFormState: FormState = 'idle'
 | 
			
		||||
): [
 | 
			
		||||
  formState: FormState,
 | 
			
		||||
  setFormState: React.Dispatch<React.SetStateAction<FormState>>
 | 
			
		||||
] => {
 | 
			
		||||
  const [formState, setFormState] = useState<FormState>(initialFormState)
 | 
			
		||||
  return [formState, setFormState]
 | 
			
		||||
}
 | 
			
		||||
@@ -19,6 +19,13 @@ export const guildCompleteSchema = {
 | 
			
		||||
  members: Type.Array(Type.Object(memberSchema))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const guildPublicObjectSchema = Type.Object({
 | 
			
		||||
  ...guildSchema,
 | 
			
		||||
  membersCount: Type.Integer({ min: 1 })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const guildCompleteObjectSchema = Type.Object(guildCompleteSchema)
 | 
			
		||||
 | 
			
		||||
export type GuildComplete = Static<typeof guildCompleteObjectSchema>
 | 
			
		||||
 | 
			
		||||
export type GuildPublic = Static<typeof guildPublicObjectSchema>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,10 +9,26 @@ export const userSchema = {
 | 
			
		||||
  name: Type.String({ minLength: 1, maxLength: 30 }),
 | 
			
		||||
  email: Type.String({ minLength: 1, maxLength: 254, format: 'email' }),
 | 
			
		||||
  password: Type.String({ minLength: 1 }),
 | 
			
		||||
  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' }),
 | 
			
		||||
  logo: Type.Union([
 | 
			
		||||
    Type.String({ minLength: 1, format: 'uri-reference' }),
 | 
			
		||||
    Type.Null()
 | 
			
		||||
  ]),
 | 
			
		||||
  status: Type.Union([
 | 
			
		||||
    Type.String({ minLength: 1, maxLength: 50 }),
 | 
			
		||||
    Type.Null()
 | 
			
		||||
  ]),
 | 
			
		||||
  biography: Type.Union([
 | 
			
		||||
    Type.String({ minLength: 1, maxLength: 160 }),
 | 
			
		||||
    Type.Null()
 | 
			
		||||
  ]),
 | 
			
		||||
  website: Type.Union([
 | 
			
		||||
    Type.String({
 | 
			
		||||
      minLength: 1,
 | 
			
		||||
      maxLength: 255,
 | 
			
		||||
      format: 'uri-reference'
 | 
			
		||||
    }),
 | 
			
		||||
    Type.Null()
 | 
			
		||||
  ]),
 | 
			
		||||
  isConfirmed: Type.Boolean({ default: false }),
 | 
			
		||||
  temporaryToken: Type.String(),
 | 
			
		||||
  temporaryExpirationToken: Type.String({ format: 'date-time' }),
 | 
			
		||||
@@ -29,7 +45,7 @@ export const userPublicWithoutSettingsSchema = {
 | 
			
		||||
  logo: userSchema.logo,
 | 
			
		||||
  status: userSchema.status,
 | 
			
		||||
  biography: userSchema.biography,
 | 
			
		||||
  website: Type.Union([userSchema.website, Type.Null()]),
 | 
			
		||||
  website: userSchema.website,
 | 
			
		||||
  isConfirmed: userSchema.isConfirmed,
 | 
			
		||||
  createdAt: date.createdAt,
 | 
			
		||||
  updatedAt: date.updatedAt
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9697
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9697
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										65
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										65
									
								
								package.json
									
									
									
									
									
								
							@@ -28,7 +28,7 @@
 | 
			
		||||
    "test:e2e:dev": "start-server-and-test 'dev' 'http://localhost:3000' 'cypress open'",
 | 
			
		||||
    "storybook": "start-storybook --port 6006 --static-dir public",
 | 
			
		||||
    "storybook:build": "build-storybook --static-dir public",
 | 
			
		||||
    "storybook:serve": "serve storybook-static",
 | 
			
		||||
    "storybook:serve": "serve -p 6006 storybook-static",
 | 
			
		||||
    "release": "semantic-release",
 | 
			
		||||
    "deploy": "vercel",
 | 
			
		||||
    "postinstall": "husky install"
 | 
			
		||||
@@ -36,75 +36,76 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@fontsource/montserrat": "4.5.1",
 | 
			
		||||
    "@fontsource/roboto": "4.5.1",
 | 
			
		||||
    "@heroicons/react": "1.0.4",
 | 
			
		||||
    "@heroicons/react": "1.0.5",
 | 
			
		||||
    "@sinclair/typebox": "0.20.5",
 | 
			
		||||
    "ajv": "8.6.3",
 | 
			
		||||
    "ajv": "8.7.1",
 | 
			
		||||
    "ajv-formats": "2.1.1",
 | 
			
		||||
    "axios": "0.23.0",
 | 
			
		||||
    "axios": "0.24.0",
 | 
			
		||||
    "classnames": "2.3.1",
 | 
			
		||||
    "date-and-time": "2.0.1",
 | 
			
		||||
    "next": "11.1.2",
 | 
			
		||||
    "next-pwa": "5.3.1",
 | 
			
		||||
    "next-pwa": "5.4.0",
 | 
			
		||||
    "next-themes": "0.0.15",
 | 
			
		||||
    "next-translate": "1.1.0",
 | 
			
		||||
    "next-translate": "1.2.0",
 | 
			
		||||
    "react": "17.0.2",
 | 
			
		||||
    "react-component-form": "2.0.0",
 | 
			
		||||
    "react-dom": "17.0.2",
 | 
			
		||||
    "react-infinite-scroll-component": "6.1.0",
 | 
			
		||||
    "react-responsive": "8.2.0",
 | 
			
		||||
    "react-swipeable": "6.2.0",
 | 
			
		||||
    "react-textarea-autosize": "8.3.3",
 | 
			
		||||
    "read-pkg": "7.0.0",
 | 
			
		||||
    "sharp": "0.29.1",
 | 
			
		||||
    "sharp": "0.29.2",
 | 
			
		||||
    "socket.io-client": "4.3.2",
 | 
			
		||||
    "universal-cookie": "4.0.4"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@commitlint/cli": "13.2.1",
 | 
			
		||||
    "@commitlint/config-conventional": "13.2.0",
 | 
			
		||||
    "@commitlint/cli": "14.1.0",
 | 
			
		||||
    "@commitlint/config-conventional": "14.1.0",
 | 
			
		||||
    "@lhci/cli": "0.8.2",
 | 
			
		||||
    "@saithodev/semantic-release-backmerge": "1.5.3",
 | 
			
		||||
    "@storybook/addon-essentials": "6.3.10",
 | 
			
		||||
    "@storybook/addon-links": "6.3.10",
 | 
			
		||||
    "@saithodev/semantic-release-backmerge": "2.1.0",
 | 
			
		||||
    "@storybook/addon-essentials": "6.3.12",
 | 
			
		||||
    "@storybook/addon-links": "6.3.12",
 | 
			
		||||
    "@storybook/addon-postcss": "2.0.0",
 | 
			
		||||
    "@storybook/react": "6.3.10",
 | 
			
		||||
    "@testing-library/jest-dom": "5.14.1",
 | 
			
		||||
    "@storybook/react": "6.3.12",
 | 
			
		||||
    "@testing-library/jest-dom": "5.15.0",
 | 
			
		||||
    "@testing-library/react": "12.1.2",
 | 
			
		||||
    "@types/date-and-time": "0.13.0",
 | 
			
		||||
    "@types/jest": "27.0.2",
 | 
			
		||||
    "@types/node": "16.10.3",
 | 
			
		||||
    "@types/react": "17.0.27",
 | 
			
		||||
    "@types/node": "16.11.7",
 | 
			
		||||
    "@types/react": "17.0.34",
 | 
			
		||||
    "@types/react-responsive": "8.0.4",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "4.33.0",
 | 
			
		||||
    "autoprefixer": "10.3.7",
 | 
			
		||||
    "babel-jest": "27.2.5",
 | 
			
		||||
    "babel-loader": "8.2.2",
 | 
			
		||||
    "autoprefixer": "10.4.0",
 | 
			
		||||
    "babel-jest": "27.3.1",
 | 
			
		||||
    "babel-loader": "8.2.3",
 | 
			
		||||
    "babel-register": "6.26.0",
 | 
			
		||||
    "cypress": "8.5.0",
 | 
			
		||||
    "cypress": "9.0.0",
 | 
			
		||||
    "dockerfilelint": "1.8.0",
 | 
			
		||||
    "editorconfig-checker": "4.0.2",
 | 
			
		||||
    "eslint": "7.32.0",
 | 
			
		||||
    "eslint-config-next": "11.1.2",
 | 
			
		||||
    "eslint-config-prettier": "8.3.0",
 | 
			
		||||
    "eslint-config-standard-with-typescript": "21.0.1",
 | 
			
		||||
    "eslint-plugin-import": "2.24.2",
 | 
			
		||||
    "eslint-plugin-import": "2.25.3",
 | 
			
		||||
    "eslint-plugin-node": "11.1.0",
 | 
			
		||||
    "eslint-plugin-prettier": "4.0.0",
 | 
			
		||||
    "eslint-plugin-promise": "5.1.0",
 | 
			
		||||
    "eslint-plugin-unicorn": "36.0.0",
 | 
			
		||||
    "husky": "7.0.2",
 | 
			
		||||
    "jest": "27.2.5",
 | 
			
		||||
    "lint-staged": "11.2.1",
 | 
			
		||||
    "eslint-plugin-promise": "5.1.1",
 | 
			
		||||
    "eslint-plugin-unicorn": "38.0.1",
 | 
			
		||||
    "husky": "7.0.4",
 | 
			
		||||
    "jest": "27.3.1",
 | 
			
		||||
    "lint-staged": "11.2.6",
 | 
			
		||||
    "markdownlint-cli": "0.29.0",
 | 
			
		||||
    "mockttp": "2.3.1",
 | 
			
		||||
    "plop": "2.7.4",
 | 
			
		||||
    "postcss": "8.3.9",
 | 
			
		||||
    "mockttp": "2.4.0",
 | 
			
		||||
    "plop": "2.7.6",
 | 
			
		||||
    "postcss": "8.3.11",
 | 
			
		||||
    "prettier": "2.4.1",
 | 
			
		||||
    "semantic-release": "18.0.0",
 | 
			
		||||
    "serve": "12.0.1",
 | 
			
		||||
    "serve": "13.0.2",
 | 
			
		||||
    "start-server-and-test": "1.14.0",
 | 
			
		||||
    "storybook-tailwind-dark-mode": "1.0.11",
 | 
			
		||||
    "tailwindcss": "2.2.16",
 | 
			
		||||
    "typescript": "4.4.3",
 | 
			
		||||
    "tailwindcss": "2.2.19",
 | 
			
		||||
    "typescript": "4.4.4",
 | 
			
		||||
    "vercel": "23.1.2"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,3 @@
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
 | 
			
		||||
import { Head } from 'components/Head'
 | 
			
		||||
import { Application } from 'components/Application'
 | 
			
		||||
import {
 | 
			
		||||
@@ -7,48 +5,14 @@ import {
 | 
			
		||||
  AuthenticationProvider,
 | 
			
		||||
  PagePropsWithAuthentication
 | 
			
		||||
} from 'utils/authentication'
 | 
			
		||||
import { JoinGuildsPublic } from 'components/Application/JoinGuildsPublic'
 | 
			
		||||
 | 
			
		||||
const JoinGuildPage: React.FC<PagePropsWithAuthentication> = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <AuthenticationProvider authentication={props.authentication}>
 | 
			
		||||
      <Head title='Thream | Application' />
 | 
			
		||||
      <Application path='/application/guilds/join'>
 | 
			
		||||
        <input
 | 
			
		||||
          className='w-10/12 sm:w-8/12 md:w-6/12 lg:w-5/12 bg-white dark:bg-[#3B3B3B] border-gray-500 dark:border-gray-700 p-3 my-6 mt-16 mx-auto rounded-md border'
 | 
			
		||||
          type='search'
 | 
			
		||||
          name='search_guild'
 | 
			
		||||
          placeholder='🔎  Search...'
 | 
			
		||||
        />
 | 
			
		||||
        <div className='w-full flex items-center justify-center p-12'>
 | 
			
		||||
          <div className='max-w-[1600px] grid grid-cols-1 xl:grid-cols-3 md:grid-cols-2 sm:grid-cols-1 gap-8'>
 | 
			
		||||
            {new Array(100).fill(null).map((_, index) => {
 | 
			
		||||
              return (
 | 
			
		||||
                <div
 | 
			
		||||
                  key={index}
 | 
			
		||||
                  className='max-w-sm flex flex-col items-center justify-center border-gray-500 dark:border-gray-700 p-4 cursor-pointer rounded shadow-lg border transition duration-200 ease-in-out hover:-translate-y-2 hover:shadow-none'
 | 
			
		||||
                >
 | 
			
		||||
                  <Image
 | 
			
		||||
                    src='/images/icons/Thream.png'
 | 
			
		||||
                    alt='logo'
 | 
			
		||||
                    width={80}
 | 
			
		||||
                    height={80}
 | 
			
		||||
                  />
 | 
			
		||||
                  <div className='m-2 text-center mt-3'>
 | 
			
		||||
                    <h3 className='font-bold text-xl mb-2'>Guild</h3>
 | 
			
		||||
                    <p className='text-base w-11/12 mx-auto'>
 | 
			
		||||
                      Lorem ipsum dolor sit amet, consectetur adipisicing elit.
 | 
			
		||||
                      Voluptatibus quia, nulla! Maiores et perferendis eaque,
 | 
			
		||||
                      exercitationem praesentium nihil.
 | 
			
		||||
                    </p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <p className='flex flex-col text-green-800 dark:text-green-400 mt-4'>
 | 
			
		||||
                    54 members
 | 
			
		||||
                  </p>
 | 
			
		||||
                </div>
 | 
			
		||||
              )
 | 
			
		||||
            })}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <JoinGuildsPublic />
 | 
			
		||||
      </Application>
 | 
			
		||||
    </AuthenticationProvider>
 | 
			
		||||
  )
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ const ForgotPassword: React.FC<FooterProps> = (props) => {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const { version } = props
 | 
			
		||||
 | 
			
		||||
  const { formState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
  const { fetchState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({ validateSchemaObject: { email: userSchema.email } })
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
@@ -65,7 +65,7 @@ const ForgotPassword: React.FC<FooterProps> = (props) => {
 | 
			
		||||
        </AuthenticationForm>
 | 
			
		||||
        <FormState
 | 
			
		||||
          id='message'
 | 
			
		||||
          state={formState}
 | 
			
		||||
          state={fetchState}
 | 
			
		||||
          message={
 | 
			
		||||
            message != null ? message : getErrorTranslation(errors.email)
 | 
			
		||||
          }
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ const ResetPassword: React.FC<FooterProps> = (props) => {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const { version } = props
 | 
			
		||||
 | 
			
		||||
  const { formState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
  const { fetchState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({ validateSchemaObject: { password: userSchema.password } })
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
@@ -64,7 +64,7 @@ const ResetPassword: React.FC<FooterProps> = (props) => {
 | 
			
		||||
        </AuthenticationForm>
 | 
			
		||||
        <FormState
 | 
			
		||||
          id='message'
 | 
			
		||||
          state={formState}
 | 
			
		||||
          state={fetchState}
 | 
			
		||||
          message={
 | 
			
		||||
            message != null ? message : getErrorTranslation(errors.password)
 | 
			
		||||
          }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user