feat(pages): add /application/[guildId]/[channelId] (#4)
				
					
				
			This commit is contained in:
		@@ -27,6 +27,7 @@
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
    ],
 | 
			
		||||
    "@typescript-eslint/no-namespace": "off"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,15 @@
 | 
			
		||||
FROM node:16.11.0 AS dependencies
 | 
			
		||||
FROM node:16.13.1 AS dependencies
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
COPY ./package*.json ./
 | 
			
		||||
RUN npm clean-install
 | 
			
		||||
 | 
			
		||||
FROM node:16.11.0 AS builder
 | 
			
		||||
FROM node:16.13.1 AS builder
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
COPY ./ ./
 | 
			
		||||
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
 | 
			
		||||
RUN npm run build
 | 
			
		||||
 | 
			
		||||
FROM node:16.11.0 AS runner
 | 
			
		||||
FROM node:16.13.1 AS runner
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
ENV NODE_ENV=production
 | 
			
		||||
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,6 @@
 | 
			
		||||
import { useState, useEffect, useMemo } from 'react'
 | 
			
		||||
import { useState, useEffect } from 'react'
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import {
 | 
			
		||||
  CogIcon,
 | 
			
		||||
  PlusIcon,
 | 
			
		||||
  MenuIcon,
 | 
			
		||||
  UsersIcon,
 | 
			
		||||
  XIcon
 | 
			
		||||
} from '@heroicons/react/solid'
 | 
			
		||||
import { PlusIcon, MenuIcon, UsersIcon, XIcon } from '@heroicons/react/solid'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import { useMediaQuery } from 'react-responsive'
 | 
			
		||||
import { useSwipeable } from 'react-swipeable'
 | 
			
		||||
@@ -15,31 +8,33 @@ import { useSwipeable } from 'react-swipeable'
 | 
			
		||||
import { Sidebar, DirectionSidebar } from './Sidebar'
 | 
			
		||||
import { IconButton } from 'components/design/IconButton'
 | 
			
		||||
import { IconLink } from 'components/design/IconLink'
 | 
			
		||||
import { Channels } from './Channels'
 | 
			
		||||
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'
 | 
			
		||||
import { useAuthentication } from 'tools/authentication'
 | 
			
		||||
import { API_URL } from 'tools/api'
 | 
			
		||||
 | 
			
		||||
export interface GuildsChannelsPath {
 | 
			
		||||
  guildId: number
 | 
			
		||||
  channelId: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ApplicationProps {
 | 
			
		||||
  path:
 | 
			
		||||
export type ApplicationPath =
 | 
			
		||||
  | '/application'
 | 
			
		||||
  | '/application/guilds/join'
 | 
			
		||||
  | '/application/guilds/create'
 | 
			
		||||
    | '/application/users/[userId]'
 | 
			
		||||
  | `/application/users/${number}`
 | 
			
		||||
  | GuildsChannelsPath
 | 
			
		||||
 | 
			
		||||
export interface ApplicationProps {
 | 
			
		||||
  path: ApplicationPath
 | 
			
		||||
  guildLeftSidebar?: React.ReactNode
 | 
			
		||||
  title: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
  const { children, path } = props
 | 
			
		||||
  const { children, path, guildLeftSidebar, title } = props
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const { user } = useAuthentication()
 | 
			
		||||
 | 
			
		||||
  const [visibleSidebars, setVisibleSidebars] = useState({
 | 
			
		||||
@@ -129,23 +124,6 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const title = useMemo(() => {
 | 
			
		||||
    if (typeof path !== 'string') {
 | 
			
		||||
      // TODO: Returns the real name of the channel when doing APIs calls
 | 
			
		||||
      return `# Channel ${path.channelId}`
 | 
			
		||||
    }
 | 
			
		||||
    if (path.startsWith('/application/users/')) {
 | 
			
		||||
      return 'Settings'
 | 
			
		||||
    }
 | 
			
		||||
    if (path === '/application/guilds/join') {
 | 
			
		||||
      return 'Join a Guild'
 | 
			
		||||
    }
 | 
			
		||||
    if (path === '/application/guilds/create') {
 | 
			
		||||
      return t('application:create-a-guild')
 | 
			
		||||
    }
 | 
			
		||||
    return 'Application'
 | 
			
		||||
  }, [path, t])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setMounted(true)
 | 
			
		||||
  }, [])
 | 
			
		||||
@@ -163,12 +141,16 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
        >
 | 
			
		||||
          {!visibleSidebars.left ? <MenuIcon /> : <XIcon />}
 | 
			
		||||
        </IconButton>
 | 
			
		||||
        <div className='text-md text-green-800 dark:text-green-400 font-semibold'>
 | 
			
		||||
        <div
 | 
			
		||||
          data-cy='application-title'
 | 
			
		||||
          className='text-md text-green-800 dark:text-green-400 font-semibold'
 | 
			
		||||
        >
 | 
			
		||||
          {title}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='flex space-x-2'>
 | 
			
		||||
          {title.startsWith('#') && (
 | 
			
		||||
            <IconButton
 | 
			
		||||
              data-cy='icon-button-right-sidebar-members'
 | 
			
		||||
              className='p-2 h-10 w-10'
 | 
			
		||||
              onClick={() => handleToggleSidebars('right')}
 | 
			
		||||
            >
 | 
			
		||||
@@ -201,7 +183,8 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
                    ? '/images/data/user-default.png'
 | 
			
		||||
                    : API_URL + user.logo
 | 
			
		||||
                }
 | 
			
		||||
                alt='logo'
 | 
			
		||||
                alt={"Users's profil picture"}
 | 
			
		||||
                draggable={false}
 | 
			
		||||
                width={48}
 | 
			
		||||
                height={48}
 | 
			
		||||
              />
 | 
			
		||||
@@ -217,26 +200,7 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
            <Guilds path={path} />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          {typeof path !== 'string' && (
 | 
			
		||||
            <div className='flex flex-col justify-between w-full mt-2'>
 | 
			
		||||
              <div className='text-center p-2 mx-8 mt-2'>
 | 
			
		||||
                <h2 className='text-xl'>Guild Name</h2>
 | 
			
		||||
              </div>
 | 
			
		||||
              <Divider />
 | 
			
		||||
              <div className='scrollbar-firefox-support overflow-y-auto'>
 | 
			
		||||
                <Channels path={path} />
 | 
			
		||||
              </div>
 | 
			
		||||
              <Divider />
 | 
			
		||||
              <div className='flex justify-center items-center p-2 mb-1 space-x-6'>
 | 
			
		||||
                <IconButton className='h-10 w-10' title='Add a Channel'>
 | 
			
		||||
                  <PlusIcon />
 | 
			
		||||
                </IconButton>
 | 
			
		||||
                <IconButton className='h-7 w-7' title='Settings'>
 | 
			
		||||
                  <CogIcon />
 | 
			
		||||
                </IconButton>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
          {guildLeftSidebar}
 | 
			
		||||
        </Sidebar>
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
@@ -252,6 +216,7 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
          {children}
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        {typeof path !== 'string' && (
 | 
			
		||||
          <Sidebar
 | 
			
		||||
            direction='right'
 | 
			
		||||
            visible={visibleSidebars.right}
 | 
			
		||||
@@ -259,6 +224,7 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
          >
 | 
			
		||||
            <Members />
 | 
			
		||||
          </Sidebar>
 | 
			
		||||
        )}
 | 
			
		||||
      </main>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								components/Application/Channels/Channel/Channel.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								components/Application/Channels/Channel/Channel.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
import { channelExample } from '../../../../cypress/fixtures/channels/channel'
 | 
			
		||||
 | 
			
		||||
import { Channel as Component, ChannelProps } from './Channel'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Channel',
 | 
			
		||||
  component: Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Stories
 | 
			
		||||
 | 
			
		||||
export const Channel: Story<ChannelProps> = (arguments_) => {
 | 
			
		||||
  return <Component {...arguments_} />
 | 
			
		||||
}
 | 
			
		||||
Channel.args = { path: { channelId: 1, guildId: 1 }, channel: channelExample }
 | 
			
		||||
							
								
								
									
										13
									
								
								components/Application/Channels/Channel/Channel.test.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								components/Application/Channels/Channel/Channel.test.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
import { channelExample } from 'cypress/fixtures/channels/channel'
 | 
			
		||||
 | 
			
		||||
import { Channel } from './Channel'
 | 
			
		||||
 | 
			
		||||
describe('<Channel />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
    const { baseElement } = render(
 | 
			
		||||
      <Channel channel={channelExample} path={{ channelId: 1, guildId: 1 }} />
 | 
			
		||||
    )
 | 
			
		||||
    expect(baseElement).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										32
									
								
								components/Application/Channels/Channel/Channel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								components/Application/Channels/Channel/Channel.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
 | 
			
		||||
import { GuildsChannelsPath } from '../../Application'
 | 
			
		||||
import { Channel as ChannelType } from '../../../../models/Channel'
 | 
			
		||||
 | 
			
		||||
export interface ChannelProps {
 | 
			
		||||
  path: GuildsChannelsPath
 | 
			
		||||
  channel: ChannelType
 | 
			
		||||
  selected?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Channel: React.FC<ChannelProps> = (props) => {
 | 
			
		||||
  const { channel, path, selected = false } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Link href={`/application/${path.guildId}/${channel.id}`}>
 | 
			
		||||
      <a
 | 
			
		||||
        className={classNames(
 | 
			
		||||
          'hover:bg-gray-100 group flex items-center justify-between text-sm py-2 my-3 mx-3 transition-colors dark:hover:bg-gray-600 duration-200 rounded-lg',
 | 
			
		||||
          {
 | 
			
		||||
            'text-green-800 dark:text-green-400 font-semibold': selected
 | 
			
		||||
          }
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
        <span className='ml-2 mr-4' data-cy='channel-name'>
 | 
			
		||||
          # {channel.name}
 | 
			
		||||
        </span>
 | 
			
		||||
      </a>
 | 
			
		||||
    </Link>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								components/Application/Channels/Channel/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								components/Application/Channels/Channel/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export * from './Channel'
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { Channels as Component, ChannelsProps } from './'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Channels',
 | 
			
		||||
  component: Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Stories
 | 
			
		||||
 | 
			
		||||
export const Channels: Story<ChannelsProps> = (arguments_) => (
 | 
			
		||||
  <Component {...arguments_} />
 | 
			
		||||
)
 | 
			
		||||
Channels.args = { path: { channelId: 1, guildId: 2 } }
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Channels } from './'
 | 
			
		||||
 | 
			
		||||
describe('<Channels />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
    const { baseElement } = render(
 | 
			
		||||
      <Channels path={{ channelId: 1, guildId: 2 }} />
 | 
			
		||||
    )
 | 
			
		||||
    expect(baseElement).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import InfiniteScroll from 'react-infinite-scroll-component'
 | 
			
		||||
 | 
			
		||||
import { GuildsChannelsPath } from '../Application'
 | 
			
		||||
import { Loader } from 'components/design/Loader'
 | 
			
		||||
import { Channel } from './Channel'
 | 
			
		||||
import { useChannels } from 'contexts/Channels'
 | 
			
		||||
 | 
			
		||||
export interface ChannelsProps {
 | 
			
		||||
  path: GuildsChannelsPath
 | 
			
		||||
@@ -10,27 +12,33 @@ export interface ChannelsProps {
 | 
			
		||||
export const Channels: React.FC<ChannelsProps> = (props) => {
 | 
			
		||||
  const { path } = props
 | 
			
		||||
 | 
			
		||||
  const { channels, hasMore, nextPage } = useChannels()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <nav className='w-full'>
 | 
			
		||||
      {new Array(100).fill(null).map((_, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <Link key={index} href={`/application/${path.guildId}/${index}`}>
 | 
			
		||||
            <a
 | 
			
		||||
              className={classNames(
 | 
			
		||||
                'hover:bg-gray-100 group flex items-center justify-between text-sm py-2 my-3 mx-3 transition-colors dark:hover:bg-gray-600 duration-200 rounded-lg',
 | 
			
		||||
                {
 | 
			
		||||
                  'text-green-800 dark:text-green-400 font-semibold':
 | 
			
		||||
                    typeof path !== 'string' && path.channelId === index,
 | 
			
		||||
                  'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-white font-normal':
 | 
			
		||||
                    typeof path === 'string'
 | 
			
		||||
                }
 | 
			
		||||
              )}
 | 
			
		||||
    <div
 | 
			
		||||
      id='channels'
 | 
			
		||||
      className='scrollbar-firefox-support overflow-y-auto flex-1 flex flex-col'
 | 
			
		||||
    >
 | 
			
		||||
              <span className='ml-2 mr-4'># Channel {index}</span>
 | 
			
		||||
            </a>
 | 
			
		||||
          </Link>
 | 
			
		||||
      <InfiniteScroll
 | 
			
		||||
        className='w-full channels-list'
 | 
			
		||||
        scrollableTarget='channels'
 | 
			
		||||
        dataLength={channels.length}
 | 
			
		||||
        next={nextPage}
 | 
			
		||||
        hasMore={hasMore}
 | 
			
		||||
        loader={<Loader />}
 | 
			
		||||
      >
 | 
			
		||||
        {channels.map((channel) => {
 | 
			
		||||
          const selected = channel.id === path.channelId
 | 
			
		||||
          return (
 | 
			
		||||
            <Channel
 | 
			
		||||
              key={channel.id}
 | 
			
		||||
              channel={channel}
 | 
			
		||||
              path={path}
 | 
			
		||||
              selected={selected}
 | 
			
		||||
            />
 | 
			
		||||
          )
 | 
			
		||||
        })}
 | 
			
		||||
    </nav>
 | 
			
		||||
      </InfiniteScroll>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { Form } from 'react-component-form'
 | 
			
		||||
import TextareaAutosize from 'react-textarea-autosize'
 | 
			
		||||
import { AxiosResponse } from 'axios'
 | 
			
		||||
 | 
			
		||||
import { useAuthentication } from '../../../utils/authentication'
 | 
			
		||||
import { useAuthentication } from '../../../tools/authentication'
 | 
			
		||||
import { HandleSubmitCallback, useForm } from '../../../hooks/useForm'
 | 
			
		||||
import { GuildComplete, guildSchema } from '../../../models/Guild'
 | 
			
		||||
import { Input } from '../../design/Input'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								components/Application/GuildLeftSidebar/GuildLeftSidebar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								components/Application/GuildLeftSidebar/GuildLeftSidebar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
import { CogIcon, PlusIcon } from '@heroicons/react/solid'
 | 
			
		||||
 | 
			
		||||
import { useGuildMember } from 'contexts/GuildMember'
 | 
			
		||||
import { Divider } from 'components/design/Divider'
 | 
			
		||||
import { Channels } from 'components/Application/Channels'
 | 
			
		||||
import { IconButton } from 'components/design/IconButton'
 | 
			
		||||
import { GuildsChannelsPath } from '..'
 | 
			
		||||
 | 
			
		||||
export interface GuildLeftSidebarProps {
 | 
			
		||||
  path: GuildsChannelsPath
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const GuildLeftSidebar: React.FC<GuildLeftSidebarProps> = (props) => {
 | 
			
		||||
  const { path } = props
 | 
			
		||||
 | 
			
		||||
  const { guild } = useGuildMember()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='flex flex-col justify-between w-full mt-2'>
 | 
			
		||||
      <div className='text-center p-2 mx-8 mt-2'>
 | 
			
		||||
        <h2 data-cy='guild-left-sidebar-title' className='text-xl'>
 | 
			
		||||
          {guild.name}
 | 
			
		||||
        </h2>
 | 
			
		||||
      </div>
 | 
			
		||||
      <Divider />
 | 
			
		||||
      <Channels path={path} />
 | 
			
		||||
      <Divider />
 | 
			
		||||
      <div className='flex justify-center items-center p-2 mb-1 space-x-6'>
 | 
			
		||||
        <IconButton className='h-10 w-10' title='Add a Channel'>
 | 
			
		||||
          <PlusIcon />
 | 
			
		||||
        </IconButton>
 | 
			
		||||
        <IconButton className='h-7 w-7' title='Settings'>
 | 
			
		||||
          <CogIcon />
 | 
			
		||||
        </IconButton>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								components/Application/GuildLeftSidebar/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								components/Application/GuildLeftSidebar/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export * from './GuildLeftSidebar'
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { Guild as Component, GuildProps } from './Guild'
 | 
			
		||||
import { guildExample } from '../../../../cypress/fixtures/guilds/guild'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Guild',
 | 
			
		||||
@@ -14,12 +15,7 @@ export const Guild: Story<GuildProps> = (arguments_) => {
 | 
			
		||||
}
 | 
			
		||||
Guild.args = {
 | 
			
		||||
  guild: {
 | 
			
		||||
    id: 1,
 | 
			
		||||
    name: 'GuildExample',
 | 
			
		||||
    description: 'guild example.',
 | 
			
		||||
    icon: null,
 | 
			
		||||
    createdAt: new Date().toISOString(),
 | 
			
		||||
    updatedAt: new Date().toISOString(),
 | 
			
		||||
    membersCount: 1
 | 
			
		||||
    ...guildExample,
 | 
			
		||||
    defaultChannelId: 1
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +1,15 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Guild } from './Guild'
 | 
			
		||||
import { guildExample } from '../../../../cypress/fixtures/guilds/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
 | 
			
		||||
          ...guildExample,
 | 
			
		||||
          defaultChannelId: 1
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
							
								
								
									
										35
									
								
								components/Application/Guilds/Guild/Guild.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								components/Application/Guilds/Guild/Guild.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
 | 
			
		||||
import { GuildWithDefaultChannelId } from '../../../../models/Guild'
 | 
			
		||||
import { IconLink } from '../../../design/IconLink'
 | 
			
		||||
 | 
			
		||||
export interface GuildProps {
 | 
			
		||||
  guild: GuildWithDefaultChannelId
 | 
			
		||||
  selected?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Guild: React.FC<GuildProps> = (props) => {
 | 
			
		||||
  const { guild, selected } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <IconLink
 | 
			
		||||
      className='mt-2'
 | 
			
		||||
      href={`/application/${guild.id}/${guild.defaultChannelId}`}
 | 
			
		||||
      selected={selected}
 | 
			
		||||
      title={guild.name}
 | 
			
		||||
    >
 | 
			
		||||
      <div className='pl-[6px]'>
 | 
			
		||||
        <Image
 | 
			
		||||
          className='rounded-full'
 | 
			
		||||
          src={
 | 
			
		||||
            guild.icon != null ? guild.icon : '/images/data/guild-default.png'
 | 
			
		||||
          }
 | 
			
		||||
          alt='logo'
 | 
			
		||||
          width={48}
 | 
			
		||||
          height={48}
 | 
			
		||||
          draggable={false}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </IconLink>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { Guilds as Component, GuildsProps } from './'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Guilds',
 | 
			
		||||
  component: Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Stories
 | 
			
		||||
 | 
			
		||||
export const Guilds: Story<GuildsProps> = (arguments_) => (
 | 
			
		||||
  <Component {...arguments_} />
 | 
			
		||||
)
 | 
			
		||||
Guilds.args = { path: { channelId: 1, guildId: 2 } }
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Guilds } from './'
 | 
			
		||||
 | 
			
		||||
describe('<Guilds />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
    const { baseElement } = render(
 | 
			
		||||
      <Guilds path={{ channelId: 1, guildId: 2 }} />
 | 
			
		||||
    )
 | 
			
		||||
    expect(baseElement).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -1,34 +1,37 @@
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import InfiniteScroll from 'react-infinite-scroll-component'
 | 
			
		||||
 | 
			
		||||
import { ApplicationProps } from '../Application'
 | 
			
		||||
import { IconLink } from '../../design/IconLink'
 | 
			
		||||
import { Loader } from 'components/design/Loader'
 | 
			
		||||
import { Guild } from './Guild'
 | 
			
		||||
import { useGuilds } from 'contexts/Guilds'
 | 
			
		||||
import { GuildsChannelsPath } from '..'
 | 
			
		||||
 | 
			
		||||
export interface GuildsProps extends ApplicationProps {}
 | 
			
		||||
export interface GuildsProps {
 | 
			
		||||
  path: GuildsChannelsPath | string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Guilds: React.FC<GuildsProps> = (props) => {
 | 
			
		||||
  const { path } = props
 | 
			
		||||
 | 
			
		||||
  const { guilds, hasMore, nextPage } = useGuilds()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='min-w-[92px] mt-[130px] pt-2 h-full border-r-2 border-gray-500 dark:border-white/20 space-y-2 scrollbar-firefox-support overflow-y-auto'>
 | 
			
		||||
      {new Array(100).fill(null).map((_, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <IconLink
 | 
			
		||||
            key={index}
 | 
			
		||||
            href={`/application/${index}/0`}
 | 
			
		||||
            selected={typeof path !== 'string' && path.guildId === index}
 | 
			
		||||
            title='Guild Name'
 | 
			
		||||
    <div
 | 
			
		||||
      id='guilds-list'
 | 
			
		||||
      className='min-w-[92px] mt-[130px] pt-2 h-full border-r-2 border-gray-500 dark:border-white/20 space-y-2 scrollbar-firefox-support overflow-y-auto'
 | 
			
		||||
    >
 | 
			
		||||
            <div className='pl-[6px]'>
 | 
			
		||||
              <Image
 | 
			
		||||
                src='/images/icons/Thream.png'
 | 
			
		||||
                alt='logo'
 | 
			
		||||
                width={48}
 | 
			
		||||
                height={48}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </IconLink>
 | 
			
		||||
        )
 | 
			
		||||
      <InfiniteScroll
 | 
			
		||||
        className='guilds-list'
 | 
			
		||||
        dataLength={guilds.length}
 | 
			
		||||
        next={nextPage}
 | 
			
		||||
        hasMore={hasMore}
 | 
			
		||||
        scrollableTarget='guilds-list'
 | 
			
		||||
        loader={<Loader />}
 | 
			
		||||
      >
 | 
			
		||||
        {guilds.map((guild) => {
 | 
			
		||||
          const selected = typeof path !== 'string' && path.guildId === guild.id
 | 
			
		||||
          return <Guild key={guild.id} guild={guild} selected={selected} />
 | 
			
		||||
        })}
 | 
			
		||||
      </InfiniteScroll>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { GuildPublic as Component, GuildPublicProps } from './GuildPublic'
 | 
			
		||||
import { guildExample } from '../../../../cypress/fixtures/guilds/guild'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'GuildPublic',
 | 
			
		||||
  component: Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Stories
 | 
			
		||||
 | 
			
		||||
export const GuildPublic: Story<GuildPublicProps> = (arguments_) => {
 | 
			
		||||
  return <Component {...arguments_} />
 | 
			
		||||
}
 | 
			
		||||
GuildPublic.args = {
 | 
			
		||||
  guild: {
 | 
			
		||||
    ...guildExample,
 | 
			
		||||
    membersCount: 1
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { GuildPublic } from './GuildPublic'
 | 
			
		||||
import { guildExample } from '../../../../cypress/fixtures/guilds/guild'
 | 
			
		||||
 | 
			
		||||
describe('<GuildPublic />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
    const { baseElement } = render(
 | 
			
		||||
      <GuildPublic
 | 
			
		||||
        guild={{
 | 
			
		||||
          ...guildExample,
 | 
			
		||||
          membersCount: 1
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
    expect(baseElement).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
 | 
			
		||||
import { GuildPublic } from 'models/Guild'
 | 
			
		||||
import { GuildPublic as GuildPublicType } from '../../../../models/Guild'
 | 
			
		||||
 | 
			
		||||
export interface GuildProps {
 | 
			
		||||
  guild: GuildPublic
 | 
			
		||||
export interface GuildPublicProps {
 | 
			
		||||
  guild: GuildPublicType
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Guild: React.FC<GuildProps> = (props) => {
 | 
			
		||||
export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
 | 
			
		||||
  const { guild } = props
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
 | 
			
		||||
  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'
 | 
			
		||||
    >
 | 
			
		||||
    <div 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'}
 | 
			
		||||
@@ -28,7 +28,7 @@ export const Guild: React.FC<GuildProps> = (props) => {
 | 
			
		||||
        <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
 | 
			
		||||
        {guild.membersCount} {t('application:members')}
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
export * from './GuildPublic'
 | 
			
		||||
@@ -1,49 +1,31 @@
 | 
			
		||||
import { useCallback, useEffect, useState, useRef } from 'react'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import InfiniteScroll from 'react-infinite-scroll-component'
 | 
			
		||||
 | 
			
		||||
import { useAuthentication } from 'utils/authentication'
 | 
			
		||||
import { GuildPublic } from 'models/Guild'
 | 
			
		||||
import { useAuthentication } from 'tools/authentication'
 | 
			
		||||
import { GuildPublic as GuildPublicType } from 'models/Guild'
 | 
			
		||||
import { Loader } from 'components/design/Loader'
 | 
			
		||||
import { useFetchState } from 'hooks/useFetchState'
 | 
			
		||||
import { Guild } from './Guild'
 | 
			
		||||
import { GuildPublic } from './GuildPublic'
 | 
			
		||||
import { usePagination } from 'hooks/usePagination'
 | 
			
		||||
 | 
			
		||||
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 [search, setSearch] = useState('')
 | 
			
		||||
  const { authentication } = useAuthentication()
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
 | 
			
		||||
  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]
 | 
			
		||||
  const { items, hasMore, nextPage, resetPagination } =
 | 
			
		||||
    usePagination<GuildPublicType>({
 | 
			
		||||
      api: authentication.api,
 | 
			
		||||
      url: '/guilds/public'
 | 
			
		||||
    })
 | 
			
		||||
    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
 | 
			
		||||
    resetPagination()
 | 
			
		||||
    nextPage({ search })
 | 
			
		||||
  }, [resetPagination, nextPage, search])
 | 
			
		||||
 | 
			
		||||
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
 | 
			
		||||
    setInputSearch(event.target.value)
 | 
			
		||||
    setSearch(event.target.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@@ -54,19 +36,19 @@ export const JoinGuildsPublic: React.FC = () => {
 | 
			
		||||
        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...'
 | 
			
		||||
        placeholder={`🔎  ${t('application: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}
 | 
			
		||||
          className='guilds-public-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={items.length}
 | 
			
		||||
          next={nextPage}
 | 
			
		||||
          scrollableTarget='application-page-content'
 | 
			
		||||
          hasMore={hasMore}
 | 
			
		||||
          loader={<Loader />}
 | 
			
		||||
        >
 | 
			
		||||
          {guilds.map((guild) => {
 | 
			
		||||
            return <Guild guild={guild} key={guild.id} />
 | 
			
		||||
          {items.map((guild) => {
 | 
			
		||||
            return <GuildPublic guild={guild} key={guild.id} />
 | 
			
		||||
          })}
 | 
			
		||||
        </InfiniteScroll>
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								components/Application/Members/Member/Member.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								components/Application/Members/Member/Member.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { Member as Component, MemberProps } from './Member'
 | 
			
		||||
import { memberExampleComplete } from '../../../../cypress/fixtures/members/member'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Member',
 | 
			
		||||
  component: Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Stories
 | 
			
		||||
 | 
			
		||||
export const Member: Story<MemberProps> = (arguments_) => {
 | 
			
		||||
  return <Component {...arguments_} />
 | 
			
		||||
}
 | 
			
		||||
Member.args = { member: memberExampleComplete }
 | 
			
		||||
							
								
								
									
										11
									
								
								components/Application/Members/Member/Member.test.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								components/Application/Members/Member/Member.test.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Member } from './Member'
 | 
			
		||||
import { memberExampleComplete } from '../../../../cypress/fixtures/members/member'
 | 
			
		||||
 | 
			
		||||
describe('<Member />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
    const { baseElement } = render(<Member member={memberExampleComplete} />)
 | 
			
		||||
    expect(baseElement).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										45
									
								
								components/Application/Members/Member/Member.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								components/Application/Members/Member/Member.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
 | 
			
		||||
import { MemberWithPublicUser } from '../../../../models/Member'
 | 
			
		||||
import { API_URL } from '../../../../tools/api'
 | 
			
		||||
 | 
			
		||||
export interface MemberProps {
 | 
			
		||||
  member: MemberWithPublicUser
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Member: React.FC<MemberProps> = (props) => {
 | 
			
		||||
  const { member } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Link href={`/application/users/${member.user.id}`}>
 | 
			
		||||
      <a>
 | 
			
		||||
        <div className='flex items-center cursor-pointer py-2 px-4 pr-10 rounded hover:bg-gray-300 dark:hover:bg-gray-900'>
 | 
			
		||||
          <div className='min-w-[50px] flex rounded-full'>
 | 
			
		||||
            <Image
 | 
			
		||||
              src={
 | 
			
		||||
                member.user.logo == null
 | 
			
		||||
                  ? '/images/data/user-default.png'
 | 
			
		||||
                  : API_URL + member.user.logo
 | 
			
		||||
              }
 | 
			
		||||
              alt={"Users's profil picture"}
 | 
			
		||||
              height={50}
 | 
			
		||||
              width={50}
 | 
			
		||||
              draggable={false}
 | 
			
		||||
              className='rounded-full'
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className='max-w-[145px] ml-4'>
 | 
			
		||||
            <p
 | 
			
		||||
              data-cy='member-user-name'
 | 
			
		||||
              className='overflow-hidden whitespace-nowrap overflow-ellipsis'
 | 
			
		||||
            >
 | 
			
		||||
              {member.user.name}
 | 
			
		||||
            </p>
 | 
			
		||||
            {member.user.status != null && <span>{member.user.status}</span>}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </a>
 | 
			
		||||
    </Link>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								components/Application/Members/Member/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								components/Application/Members/Member/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export * from './Member'
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Members } from './Members'
 | 
			
		||||
 | 
			
		||||
describe('<Members />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
    const { baseElement } = render(<Members />)
 | 
			
		||||
    expect(baseElement).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -1,57 +1,36 @@
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import InfiniteScroll from 'react-infinite-scroll-component'
 | 
			
		||||
 | 
			
		||||
import { Divider } from '../../design/Divider'
 | 
			
		||||
import { Loader } from 'components/design/Loader'
 | 
			
		||||
import { useMembers } from 'contexts/Members'
 | 
			
		||||
import { Member } from './Member'
 | 
			
		||||
import { capitalize } from '../../../tools/utils/capitalize'
 | 
			
		||||
 | 
			
		||||
export const Members: React.FC = () => {
 | 
			
		||||
  const { members, hasMore, nextPage } = useMembers()
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className='mb-2'>
 | 
			
		||||
        <h1 className='text-center pt-2 my-2 text-xl'>Members</h1>
 | 
			
		||||
        <h1 data-cy='members-title' className='text-center pt-2 my-2 text-xl'>
 | 
			
		||||
          {capitalize(t('application:members'))}
 | 
			
		||||
        </h1>
 | 
			
		||||
        <Divider />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className='flex items-center cursor-pointer py-2 px-4 pr-10 rounded hover:bg-gray-300 dark:hover:bg-gray-900'>
 | 
			
		||||
        <div className='min-w-[50px] flex rounded-full border-2 border-green-500'>
 | 
			
		||||
          <Image
 | 
			
		||||
            src='/images/data/divlo.png'
 | 
			
		||||
            alt={"Users's profil picture"}
 | 
			
		||||
            height={50}
 | 
			
		||||
            width={50}
 | 
			
		||||
            draggable='false'
 | 
			
		||||
            className='rounded-full'
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='max-w-[145px] ml-4'>
 | 
			
		||||
          <p className='overflow-hidden whitespace-nowrap overflow-ellipsis'>
 | 
			
		||||
            Walidouxssssssssssss
 | 
			
		||||
          </p>
 | 
			
		||||
          <span className='text-green-600 dark:text-green-400'>Online</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      {new Array(100).fill(null).map((_, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div
 | 
			
		||||
            key={index}
 | 
			
		||||
            className='flex items-center cursor-pointer py-2 px-4 pr-10 rounded opacity-40 hover:bg-gray-300 dark:hover:bg-gray-900'
 | 
			
		||||
      <InfiniteScroll
 | 
			
		||||
        className='members-list'
 | 
			
		||||
        dataLength={members.length}
 | 
			
		||||
        next={nextPage}
 | 
			
		||||
        hasMore={hasMore}
 | 
			
		||||
        loader={<Loader />}
 | 
			
		||||
      >
 | 
			
		||||
            <div className='min-w-[50px] flex rounded-full border-2 border-transparent drop-shadow-md'>
 | 
			
		||||
              <Image
 | 
			
		||||
                src='/images/data/divlo.png'
 | 
			
		||||
                alt={"Users's profil picture"}
 | 
			
		||||
                height={50}
 | 
			
		||||
                width={50}
 | 
			
		||||
                draggable='false'
 | 
			
		||||
                className='rounded-full'
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className='max-w-[145px] ml-4'>
 | 
			
		||||
              <p className='overflow-hidden whitespace-nowrap overflow-ellipsis'>
 | 
			
		||||
                Walidouxssssssssssssssssssssssssssssss
 | 
			
		||||
              </p>
 | 
			
		||||
              <span className='text-red-800 dark:text-red-400'>Offline</span>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        )
 | 
			
		||||
        {members.map((member) => {
 | 
			
		||||
          return <Member key={member.id} member={member} />
 | 
			
		||||
        })}
 | 
			
		||||
      </InfiniteScroll>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								components/Application/Messages/Message/Message.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								components/Application/Messages/Message/Message.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { Message as Component, MessageProps } from './Message'
 | 
			
		||||
import { messageExampleComplete } from '../../../../cypress/fixtures/messages/message'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Message',
 | 
			
		||||
  component: Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Stories
 | 
			
		||||
 | 
			
		||||
export const Message: Story<MessageProps> = (arguments_) => {
 | 
			
		||||
  return <Component {...arguments_} />
 | 
			
		||||
}
 | 
			
		||||
Message.args = { message: messageExampleComplete }
 | 
			
		||||
							
								
								
									
										61
									
								
								components/Application/Messages/Message/Message.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								components/Application/Messages/Message/Message.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import date from 'date-and-time'
 | 
			
		||||
 | 
			
		||||
import { MessageWithMember } from '../../../../models/Message'
 | 
			
		||||
import { API_URL } from '../../../../tools/api'
 | 
			
		||||
import { MessageContent } from './MessageContent'
 | 
			
		||||
 | 
			
		||||
export interface MessageProps {
 | 
			
		||||
  message: MessageWithMember
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Message: React.FC<MessageProps> = (props) => {
 | 
			
		||||
  const { message } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='p-4 flex transition hover:bg-gray-200 dark:hover:bg-gray-900'>
 | 
			
		||||
      <Link href={`/application/users/${message.member.user.id}`}>
 | 
			
		||||
        <a>
 | 
			
		||||
          <div className='w-12 h-12 mr-4 flex flex-shrink-0 items-center justify-center'>
 | 
			
		||||
            <div className='w-10 h-10 drop-shadow-md'>
 | 
			
		||||
              <Image
 | 
			
		||||
                className='rounded-full'
 | 
			
		||||
                src={
 | 
			
		||||
                  message.member.user.logo == null
 | 
			
		||||
                    ? '/images/data/user-default.png'
 | 
			
		||||
                    : API_URL + message.member.user.logo
 | 
			
		||||
                }
 | 
			
		||||
                alt={"Users's profil picture"}
 | 
			
		||||
                width={50}
 | 
			
		||||
                height={50}
 | 
			
		||||
                draggable={false}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </a>
 | 
			
		||||
      </Link>
 | 
			
		||||
      <div className='w-full'>
 | 
			
		||||
        <div className='w-max flex items-center'>
 | 
			
		||||
          <Link href={`/application/users/${message.member.user.id}`}>
 | 
			
		||||
            <a>
 | 
			
		||||
              <span
 | 
			
		||||
                data-cy='message-member-user-name'
 | 
			
		||||
                className='font-bold text-gray-900 dark:text-gray-200'
 | 
			
		||||
              >
 | 
			
		||||
                {message.member.user.name}
 | 
			
		||||
              </span>
 | 
			
		||||
            </a>
 | 
			
		||||
          </Link>
 | 
			
		||||
          <span
 | 
			
		||||
            data-cy='message-date'
 | 
			
		||||
            className='text-gray-500 dark:text-gray-200 text-xs ml-4 select-none'
 | 
			
		||||
          >
 | 
			
		||||
            {date.format(new Date(message.createdAt), 'DD/MM/YYYY - HH:mm:ss')}
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <MessageContent message={message} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
import { useMemo } from 'react'
 | 
			
		||||
import ReactMarkdown from 'react-markdown'
 | 
			
		||||
import gfm from 'remark-gfm'
 | 
			
		||||
import remarkBreaks from 'remark-breaks'
 | 
			
		||||
 | 
			
		||||
import { Emoji, emojiPlugin, isStringWithOnlyOneEmoji } from '../../../../Emoji'
 | 
			
		||||
import { MessageWithMember } from '../../../../../models/Message'
 | 
			
		||||
 | 
			
		||||
export interface MessageContentProps {
 | 
			
		||||
  message: MessageWithMember
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const MessageContent: React.FC<MessageContentProps> = (props) => {
 | 
			
		||||
  const { message } = props
 | 
			
		||||
 | 
			
		||||
  const isMessageWithOnlyOneEmoji = useMemo(() => {
 | 
			
		||||
    return isStringWithOnlyOneEmoji(message.value)
 | 
			
		||||
  }, [message.value])
 | 
			
		||||
 | 
			
		||||
  if (isMessageWithOnlyOneEmoji) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        <p>
 | 
			
		||||
          <Emoji value={message.value} size={40} />
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ReactMarkdown
 | 
			
		||||
      disallowedElements={['table']}
 | 
			
		||||
      unwrapDisallowed
 | 
			
		||||
      remarkPlugins={[[gfm], [remarkBreaks]]}
 | 
			
		||||
      rehypePlugins={[emojiPlugin]}
 | 
			
		||||
      linkTarget='_blank'
 | 
			
		||||
      components={{
 | 
			
		||||
        emoji: (props) => {
 | 
			
		||||
          return <Emoji value={props.value} size={20} />
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {message.value}
 | 
			
		||||
    </ReactMarkdown>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
export * from './MessageContent'
 | 
			
		||||
							
								
								
									
										1
									
								
								components/Application/Messages/Message/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								components/Application/Messages/Message/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export * from './Message'
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { Messages as Component } from './'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Messages',
 | 
			
		||||
  component: Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Stories
 | 
			
		||||
 | 
			
		||||
export const Messages: Story = (arguments_) => <Component {...arguments_} />
 | 
			
		||||
@@ -1,82 +1,45 @@
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import TextareaAutosize from 'react-textarea-autosize'
 | 
			
		||||
import InfiniteScroll from 'react-infinite-scroll-component'
 | 
			
		||||
 | 
			
		||||
import { Loader } from 'components/design/Loader'
 | 
			
		||||
import { Message } from './Message'
 | 
			
		||||
import { useMessages } from 'contexts/Messages'
 | 
			
		||||
import { Emoji } from 'components/Emoji'
 | 
			
		||||
 | 
			
		||||
export const Messages: React.FC = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className='w-full scrollbar-firefox-support overflow-y-auto transition-all'>
 | 
			
		||||
        {new Array(20).fill(null).map((_, index) => {
 | 
			
		||||
  const { messages, hasMore, nextPage } = useMessages()
 | 
			
		||||
 | 
			
		||||
  if (messages.length === 0) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
              key={index}
 | 
			
		||||
              className='p-4 flex transition hover:bg-gray-200 dark:hover:bg-gray-900'
 | 
			
		||||
        id='messages'
 | 
			
		||||
        className='w-full scrollbar-firefox-support overflow-y-auto transition-all flex-1 flex flex-col text-center mt-8 text-lg'
 | 
			
		||||
      >
 | 
			
		||||
              <div className='w-12 h-12 mr-4 flex flex-shrink-0 items-center justify-center'>
 | 
			
		||||
                <div className='w-10 h-10 drop-shadow-md'>
 | 
			
		||||
                  <Image
 | 
			
		||||
                    className='rounded-full'
 | 
			
		||||
                    src='/images/data/user-default.png'
 | 
			
		||||
                    alt='logo'
 | 
			
		||||
                    width={50}
 | 
			
		||||
                    height={50}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className='w-full'>
 | 
			
		||||
                <div className='w-max flex items-center'>
 | 
			
		||||
                  <span className='font-bold text-gray-900 dark:text-gray-200'>
 | 
			
		||||
                    Divlo
 | 
			
		||||
                  </span>
 | 
			
		||||
                  <span className='text-gray-500 dark:text-gray-200 text-xs ml-4 select-none'>
 | 
			
		||||
                    06/04/2021 - 22:28:40
 | 
			
		||||
                  </span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className='text-gray-800 dark:text-gray-300 font-paragraph mt-1 break-words'>
 | 
			
		||||
                  <p>Message {index}</p>
 | 
			
		||||
        <p>
 | 
			
		||||
                    Lorem ipsum dolor sit, amet consectetur adipisicing elit.
 | 
			
		||||
                    Eum debitis voluptatum itaque quaerat. Nemo optio voluptas
 | 
			
		||||
                    quas mollitia rerum commodi laboriosam voluptates et sit
 | 
			
		||||
                    quo. Repudiandae eius at inventore magnam. Voluptas nisi
 | 
			
		||||
                    maxime laborum architecto fuga a consequuntur reiciendis
 | 
			
		||||
                    rerum beatae hic possimus, omnis dolorum libero, illo
 | 
			
		||||
                    dolorem assumenda. Repellat, ad!
 | 
			
		||||
          Nothing to show here! <Emoji value=':ghost:' size={20} />
 | 
			
		||||
        </p>
 | 
			
		||||
        <p>Start chatting to kill this Ghost!</p>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      id='messages'
 | 
			
		||||
      className='w-full scrollbar-firefox-support overflow-y-auto transition-all flex-1 flex flex-col-reverse'
 | 
			
		||||
    >
 | 
			
		||||
      <InfiniteScroll
 | 
			
		||||
        scrollableTarget='messages'
 | 
			
		||||
        className='messages-list'
 | 
			
		||||
        dataLength={messages.length}
 | 
			
		||||
        next={nextPage}
 | 
			
		||||
        inverse
 | 
			
		||||
        hasMore={hasMore}
 | 
			
		||||
        loader={<Loader />}
 | 
			
		||||
      >
 | 
			
		||||
        {messages.map((message) => {
 | 
			
		||||
          return <Message key={message.id} message={message} />
 | 
			
		||||
        })}
 | 
			
		||||
      </InfiniteScroll>
 | 
			
		||||
    </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          )
 | 
			
		||||
        })}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className='p-6 pb-4'>
 | 
			
		||||
        <div className='w-full h-full py-1 flex rounded-lg bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-200'>
 | 
			
		||||
          <form className='w-full h-full flex items-center'>
 | 
			
		||||
            <TextareaAutosize
 | 
			
		||||
              className='w-full scrollbar-firefox-support p-2 px-6 my-2 bg-transparent outline-none font-paragraph tracking-wide resize-none'
 | 
			
		||||
              placeholder='Write a message...'
 | 
			
		||||
              wrap='soft'
 | 
			
		||||
              maxRows={6}
 | 
			
		||||
            />
 | 
			
		||||
          </form>
 | 
			
		||||
          <div className='h-full flex items-center justify-around pr-6'>
 | 
			
		||||
            <button className='w-full h-full flex items-center justify-center p-1 text-2xl transition hover:-translate-y-1'>
 | 
			
		||||
              🙂
 | 
			
		||||
            </button>
 | 
			
		||||
            <button className='relative w-full h-full flex items-center justify-center p-1 text-green-800 dark:text-green-400 transition hover:-translate-y-1'>
 | 
			
		||||
              <input
 | 
			
		||||
                type='file'
 | 
			
		||||
                className='absolute w-full h-full opacity-0 cursor-pointer'
 | 
			
		||||
              />
 | 
			
		||||
              <svg width='25' height='25' viewBox='0 0 22 22'>
 | 
			
		||||
                <path
 | 
			
		||||
                  d='M11 0C4.925 0 0 4.925 0 11C0 17.075 4.925 22 11 22C17.075 22 22 17.075 22 11C22 4.925 17.075 0 11 0ZM12 15C12 15.2652 11.8946 15.5196 11.7071 15.7071C11.5196 15.8946 11.2652 16 11 16C10.7348 16 10.4804 15.8946 10.2929 15.7071C10.1054 15.5196 10 15.2652 10 15V12H7C6.73478 12 6.48043 11.8946 6.29289 11.7071C6.10536 11.5196 6 11.2652 6 11C6 10.7348 6.10536 10.4804 6.29289 10.2929C6.48043 10.1054 6.73478 10 7 10H10V7C10 6.73478 10.1054 6.48043 10.2929 6.29289C10.4804 6.10536 10.7348 6 11 6C11.2652 6 11.5196 6.10536 11.7071 6.29289C11.8946 6.48043 12 6.73478 12 7V10H15C15.2652 10 15.5196 10.1054 15.7071 10.2929C15.8946 10.4804 16 10.7348 16 11C16 11.2652 15.8946 11.5196 15.7071 11.7071C15.5196 11.8946 15.2652 12 15 12H12V15Z'
 | 
			
		||||
                  fill='currentColor'
 | 
			
		||||
                />
 | 
			
		||||
              </svg>
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import { PlusSmIcon, ArrowDownIcon } from '@heroicons/react/solid'
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
@@ -10,6 +11,8 @@ export interface PopupGuildProps {
 | 
			
		||||
export const PopupGuild: React.FC<PopupGuildProps> = (props) => {
 | 
			
		||||
  const { className } = props
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={classNames(
 | 
			
		||||
@@ -21,16 +24,16 @@ export const PopupGuild: React.FC<PopupGuildProps> = (props) => {
 | 
			
		||||
        image={
 | 
			
		||||
          <Image
 | 
			
		||||
            src='/images/svg/design/create-guild.svg'
 | 
			
		||||
            alt='Create a guild'
 | 
			
		||||
            alt={t('application:create-a-guild')}
 | 
			
		||||
            draggable='false'
 | 
			
		||||
            width={230}
 | 
			
		||||
            height={230}
 | 
			
		||||
          />
 | 
			
		||||
        }
 | 
			
		||||
        description='Create your own guild and manage everything within a few clicks !'
 | 
			
		||||
        description={t('application:create-a-guild-description')}
 | 
			
		||||
        link={{
 | 
			
		||||
          icon: <PlusSmIcon className='w-8 h-8 mr-2' />,
 | 
			
		||||
          text: 'Create a Guild',
 | 
			
		||||
          text: t('application:create-a-guild'),
 | 
			
		||||
          href: '/application/guilds/create'
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
@@ -38,16 +41,16 @@ export const PopupGuild: React.FC<PopupGuildProps> = (props) => {
 | 
			
		||||
        image={
 | 
			
		||||
          <Image
 | 
			
		||||
            src='/images/svg/design/join-guild.svg'
 | 
			
		||||
            alt='Join a Guild'
 | 
			
		||||
            alt={t('application:join-a-guild')}
 | 
			
		||||
            draggable='false'
 | 
			
		||||
            width={200}
 | 
			
		||||
            height={200}
 | 
			
		||||
          />
 | 
			
		||||
        }
 | 
			
		||||
        description='Talk, meet and have fun with new friends by joining any interesting guild !'
 | 
			
		||||
        description={t('application:join-a-guild-description')}
 | 
			
		||||
        link={{
 | 
			
		||||
          icon: <ArrowDownIcon className='w-6 h-6 mr-2' />,
 | 
			
		||||
          text: 'Join a Guild',
 | 
			
		||||
          text: t('application:join-a-guild'),
 | 
			
		||||
          href: '/application/guilds/join'
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,9 @@ export const PopupGuildCard: React.FC<PopupGuildCardProps> = (props) => {
 | 
			
		||||
        {image}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className='flex justify-between flex-col h-1/2 w-full bg-gray-700 rounded-b-2xl mt-2 shadow-sm'>
 | 
			
		||||
        <p className='text-gray-200 mt-6 text-center px-8'>{description}</p>
 | 
			
		||||
        <p className='text-gray-200 text-sm mt-6 text-center px-8'>
 | 
			
		||||
          {description}
 | 
			
		||||
        </p>
 | 
			
		||||
        <Link href={link.href}>
 | 
			
		||||
          <a className='flex justify-center items-center w-4/5 h-10 rounded-2xl transition duration-200 ease-in-out text-white font-bold tracking-wide bg-green-400 self-center mb-6 hover:bg-green-600'>
 | 
			
		||||
            {link.icon}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,15 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { Members as Component } from './Members'
 | 
			
		||||
import { SendMessage as Component } from './SendMessage'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Members',
 | 
			
		||||
  title: 'SendMessage',
 | 
			
		||||
  component: Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Stories
 | 
			
		||||
 | 
			
		||||
export const Members: Story = (arguments_) => {
 | 
			
		||||
export const SendMessage: Story = (arguments_) => {
 | 
			
		||||
  return <Component {...arguments_} />
 | 
			
		||||
}
 | 
			
		||||
SendMessage.args = {}
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Messages } from './'
 | 
			
		||||
import { SendMessage } from './SendMessage'
 | 
			
		||||
 | 
			
		||||
describe('<Messages />', () => {
 | 
			
		||||
describe('<SendMessage />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
    const { baseElement } = render(<Messages />)
 | 
			
		||||
    const { baseElement } = render(<SendMessage />)
 | 
			
		||||
    expect(baseElement).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										38
									
								
								components/Application/SendMessage/SendMessage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								components/Application/SendMessage/SendMessage.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import TextareaAutosize from 'react-textarea-autosize'
 | 
			
		||||
 | 
			
		||||
export const SendMessage: React.FC = () => {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='p-6 pb-4'>
 | 
			
		||||
      <div className='w-full h-full py-1 flex rounded-lg bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-200'>
 | 
			
		||||
        <form className='w-full h-full flex items-center'>
 | 
			
		||||
          <TextareaAutosize
 | 
			
		||||
            className='w-full scrollbar-firefox-support p-2 px-6 my-2 bg-transparent outline-none font-paragraph tracking-wide resize-none'
 | 
			
		||||
            placeholder={t('application:write-a-message')}
 | 
			
		||||
            wrap='soft'
 | 
			
		||||
            maxRows={6}
 | 
			
		||||
          />
 | 
			
		||||
        </form>
 | 
			
		||||
        <div className='h-full flex items-center justify-around pr-6'>
 | 
			
		||||
          <button className='w-full h-full flex items-center justify-center p-1 text-2xl transition hover:-translate-y-1'>
 | 
			
		||||
            🙂
 | 
			
		||||
          </button>
 | 
			
		||||
          <button className='relative w-full h-full flex items-center justify-center p-1 text-green-800 dark:text-green-400 transition hover:-translate-y-1'>
 | 
			
		||||
            <input
 | 
			
		||||
              type='file'
 | 
			
		||||
              className='absolute w-full h-full opacity-0 cursor-pointer'
 | 
			
		||||
            />
 | 
			
		||||
            <svg width='25' height='25' viewBox='0 0 22 22'>
 | 
			
		||||
              <path
 | 
			
		||||
                d='M11 0C4.925 0 0 4.925 0 11C0 17.075 4.925 22 11 22C17.075 22 22 17.075 22 11C22 4.925 17.075 0 11 0ZM12 15C12 15.2652 11.8946 15.5196 11.7071 15.7071C11.5196 15.8946 11.2652 16 11 16C10.7348 16 10.4804 15.8946 10.2929 15.7071C10.1054 15.5196 10 15.2652 10 15V12H7C6.73478 12 6.48043 11.8946 6.29289 11.7071C6.10536 11.5196 6 11.2652 6 11C6 10.7348 6.10536 10.4804 6.29289 10.2929C6.48043 10.1054 6.73478 10 7 10H10V7C10 6.73478 10.1054 6.48043 10.2929 6.29289C10.4804 6.10536 10.7348 6 11 6C11.2652 6 11.5196 6.10536 11.7071 6.29289C11.8946 6.48043 12 6.73478 12 7V10H15C15.2652 10 15.5196 10.1054 15.7071 10.2929C15.8946 10.4804 16 10.7348 16 11C16 11.2652 15.8946 11.5196 15.7071 11.7071C15.5196 11.8946 15.2652 12 15 12H12V15Z'
 | 
			
		||||
                fill='currentColor'
 | 
			
		||||
              />
 | 
			
		||||
            </svg>
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								components/Application/SendMessage/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								components/Application/SendMessage/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export * from './SendMessage'
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
import { user, userSettings } from '../../../cypress/fixtures/users/user'
 | 
			
		||||
import {
 | 
			
		||||
  userExample,
 | 
			
		||||
  userSettingsExample
 | 
			
		||||
} from '../../../cypress/fixtures/users/user'
 | 
			
		||||
 | 
			
		||||
import { UserProfile as Component, UserProfileProps } from './UserProfile'
 | 
			
		||||
 | 
			
		||||
@@ -15,7 +18,7 @@ export const UserProfile: Story<UserProfileProps> = (arguments_) => {
 | 
			
		||||
}
 | 
			
		||||
UserProfile.args = {
 | 
			
		||||
  user: {
 | 
			
		||||
    ...user,
 | 
			
		||||
    settings: userSettings
 | 
			
		||||
    ...userExample,
 | 
			
		||||
    settings: userSettingsExample
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,11 +11,11 @@ import { Button } from '../design/Button'
 | 
			
		||||
import { FormState } from '../design/FormState'
 | 
			
		||||
import { AuthenticationForm } from './'
 | 
			
		||||
import { userSchema } from '../../models/User'
 | 
			
		||||
import { api } from 'utils/api'
 | 
			
		||||
import { api } from 'tools/api'
 | 
			
		||||
import {
 | 
			
		||||
  Tokens,
 | 
			
		||||
  Authentication as AuthenticationClass
 | 
			
		||||
} from '../../utils/authentication'
 | 
			
		||||
} from '../../tools/authentication'
 | 
			
		||||
import { useForm, HandleSubmitCallback } from '../../hooks/useForm'
 | 
			
		||||
 | 
			
		||||
export interface AuthenticationProps {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								components/Emoji/Emoji.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								components/Emoji/Emoji.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { Emoji as Component, EmojiProps } from './Emoji'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Emoji',
 | 
			
		||||
  component: Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Stories
 | 
			
		||||
 | 
			
		||||
export const Emoji: Story<EmojiProps> = (arguments_) => {
 | 
			
		||||
  return <Component {...arguments_} />
 | 
			
		||||
}
 | 
			
		||||
Emoji.args = { value: ':wave:', size: 20 }
 | 
			
		||||
							
								
								
									
										24
									
								
								components/Emoji/Emoji.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								components/Emoji/Emoji.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
import { Emoji as EmojiMart } from 'emoji-mart'
 | 
			
		||||
 | 
			
		||||
import { EMOJI_SET } from './emojiPlugin'
 | 
			
		||||
 | 
			
		||||
export interface EmojiProps {
 | 
			
		||||
  value: string
 | 
			
		||||
  size: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Emoji: React.FC<EmojiProps> = (props) => {
 | 
			
		||||
  const { value, size } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <EmojiMart
 | 
			
		||||
      set={EMOJI_SET}
 | 
			
		||||
      emoji={value}
 | 
			
		||||
      size={size}
 | 
			
		||||
      tooltip
 | 
			
		||||
      fallback={() => {
 | 
			
		||||
        return <>{value}</>
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								components/Emoji/EmojiPicker/EmojiPicker.stories.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								components/Emoji/EmojiPicker/EmojiPicker.stories.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { EmojiPicker as Component, EmojiPickerProps } from './EmojiPicker'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'EmojiPicker',
 | 
			
		||||
  component: Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Stories
 | 
			
		||||
 | 
			
		||||
export const EmojiPicker: Story<EmojiPickerProps> = (arguments_) => {
 | 
			
		||||
  return <Component {...arguments_} />
 | 
			
		||||
}
 | 
			
		||||
EmojiPicker.args = { onClick: (emoji, event) => console.log(emoji, event) }
 | 
			
		||||
							
								
								
									
										28
									
								
								components/Emoji/EmojiPicker/EmojiPicker.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								components/Emoji/EmojiPicker/EmojiPicker.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
import 'emoji-mart/css/emoji-mart.css'
 | 
			
		||||
import { EmojiData, Picker } from 'emoji-mart'
 | 
			
		||||
import { useTheme } from 'next-themes'
 | 
			
		||||
 | 
			
		||||
import { EMOJI_SET } from '../emojiPlugin'
 | 
			
		||||
 | 
			
		||||
export type EmojiPickerOnClick = (
 | 
			
		||||
  emoji: EmojiData,
 | 
			
		||||
  event: React.MouseEvent<HTMLElement, MouseEvent>
 | 
			
		||||
) => void
 | 
			
		||||
 | 
			
		||||
export interface EmojiPickerProps {
 | 
			
		||||
  onClick?: EmojiPickerOnClick
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const EmojiPicker: React.FC<EmojiPickerProps> = (props) => {
 | 
			
		||||
  const { theme } = useTheme()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Picker
 | 
			
		||||
      set={EMOJI_SET}
 | 
			
		||||
      theme={theme as 'light' | 'dark' | 'auto'}
 | 
			
		||||
      onClick={props.onClick}
 | 
			
		||||
      showPreview={false}
 | 
			
		||||
      showSkinTones={false}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								components/Emoji/EmojiPicker/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								components/Emoji/EmojiPicker/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export * from './EmojiPicker'
 | 
			
		||||
							
								
								
									
										71
									
								
								components/Emoji/emojiPlugin.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								components/Emoji/emojiPlugin.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
import { visit } from 'unist-util-visit'
 | 
			
		||||
import { Plugin, Transformer } from 'unified'
 | 
			
		||||
import { Literal, Parent } from 'unist'
 | 
			
		||||
import type { ElementContent } from 'hast'
 | 
			
		||||
import type { EmojiSet } from 'emoji-mart'
 | 
			
		||||
 | 
			
		||||
import { emojiRegex } from './isStringWithOnlyOneEmoji'
 | 
			
		||||
 | 
			
		||||
export const EMOJI_SET: EmojiSet = 'twitter'
 | 
			
		||||
 | 
			
		||||
const extractText = (
 | 
			
		||||
  string: string,
 | 
			
		||||
  start: number,
 | 
			
		||||
  end: number
 | 
			
		||||
): ElementContent => {
 | 
			
		||||
  return {
 | 
			
		||||
    type: 'text',
 | 
			
		||||
    value: string.slice(start, end)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const emojiPlugin: Plugin<[], Literal<string>> = () => {
 | 
			
		||||
  const transformer: Transformer<Literal<string>> = (tree) => {
 | 
			
		||||
    visit<Literal<string>, string>(
 | 
			
		||||
      tree,
 | 
			
		||||
      'text',
 | 
			
		||||
      (node, position, parent: Parent<ElementContent> | null) => {
 | 
			
		||||
        if (typeof node.value !== 'string') {
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        position = position ?? 0
 | 
			
		||||
        const definition: ElementContent[] = []
 | 
			
		||||
        let lastIndex = 0
 | 
			
		||||
        const match = emojiRegex.exec(node.value)
 | 
			
		||||
        if (match != null) {
 | 
			
		||||
          const value = match[0]
 | 
			
		||||
          if (match.index !== lastIndex) {
 | 
			
		||||
            definition.push(extractText(node.value, lastIndex, match.index))
 | 
			
		||||
          }
 | 
			
		||||
          definition.push({
 | 
			
		||||
            type: 'element',
 | 
			
		||||
            tagName: 'emoji',
 | 
			
		||||
            properties: { value },
 | 
			
		||||
            children: []
 | 
			
		||||
          })
 | 
			
		||||
          lastIndex = match.index + value.length
 | 
			
		||||
          if (lastIndex !== node.value.length) {
 | 
			
		||||
            definition.push(
 | 
			
		||||
              extractText(node.value, lastIndex, node.value.length)
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          if (parent != null) {
 | 
			
		||||
            const last = parent.children.slice(position + 1)
 | 
			
		||||
            parent.children = parent.children.slice(0, position)
 | 
			
		||||
            parent.children = parent.children.concat(definition)
 | 
			
		||||
            parent.children = parent.children.concat(last)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  return transformer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  namespace JSX {
 | 
			
		||||
    interface IntrinsicElements {
 | 
			
		||||
      emoji: { value: string }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								components/Emoji/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								components/Emoji/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
export * from './Emoji'
 | 
			
		||||
export * from './EmojiPicker'
 | 
			
		||||
export * from './emojiPlugin'
 | 
			
		||||
export * from './isStringWithOnlyOneEmoji'
 | 
			
		||||
							
								
								
									
										18
									
								
								components/Emoji/isStringWithOnlyOneEmoji.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								components/Emoji/isStringWithOnlyOneEmoji.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
import { isStringWithOnlyOneEmoji } from './isStringWithOnlyOneEmoji'
 | 
			
		||||
 | 
			
		||||
describe('components/Emoji/isStringWithOnlyOneEmoji', () => {
 | 
			
		||||
  it('returns true with a string with only one emoji', () => {
 | 
			
		||||
    expect(isStringWithOnlyOneEmoji(':wave:')).toBeTruthy()
 | 
			
		||||
    expect(isStringWithOnlyOneEmoji(':smile:')).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('returns false with a string with multiple emoji or with text', () => {
 | 
			
		||||
    expect(isStringWithOnlyOneEmoji(':wave: :smile:')).toBeFalsy()
 | 
			
		||||
    expect(isStringWithOnlyOneEmoji(':wave: some text')).toBeFalsy()
 | 
			
		||||
    expect(isStringWithOnlyOneEmoji('some text :wave:')).toBeFalsy()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('returns false with a string without emoji', () => {
 | 
			
		||||
    expect(isStringWithOnlyOneEmoji('some text')).toBeFalsy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										6
									
								
								components/Emoji/isStringWithOnlyOneEmoji.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								components/Emoji/isStringWithOnlyOneEmoji.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
export const emojiRegex = /:\+1:|:-1:|:[\w-]+:/
 | 
			
		||||
 | 
			
		||||
export const isStringWithOnlyOneEmoji = (value: string): boolean => {
 | 
			
		||||
  const result = emojiRegex.exec(value)
 | 
			
		||||
  return result != null && result.input === result[0]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
 | 
			
		||||
import { API_VERSION } from '../../utils/api'
 | 
			
		||||
import { API_VERSION } from '../../tools/api'
 | 
			
		||||
import { VersionLink } from './VersionLink'
 | 
			
		||||
 | 
			
		||||
export interface FooterProps {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,15 +5,18 @@ export interface IconLinkProps {
 | 
			
		||||
  selected?: boolean
 | 
			
		||||
  href: string
 | 
			
		||||
  title?: string
 | 
			
		||||
  className?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const IconLink: React.FC<IconLinkProps> = (props) => {
 | 
			
		||||
  const { children, selected, href, title } = props
 | 
			
		||||
  const { children, selected, href, title, className } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='w-full flex justify-center group'>
 | 
			
		||||
    <Link href={href}>
 | 
			
		||||
      <a className='w-full flex justify-center relative group' title={title}>
 | 
			
		||||
        <div
 | 
			
		||||
          className={classNames('w-full flex justify-center group', className)}
 | 
			
		||||
        >
 | 
			
		||||
          {children}
 | 
			
		||||
          <div className='absolute flex items-center w-3 h-12 left-0'>
 | 
			
		||||
            <span
 | 
			
		||||
@@ -25,8 +28,8 @@ export const IconLink: React.FC<IconLinkProps> = (props) => {
 | 
			
		||||
              )}
 | 
			
		||||
            ></span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </a>
 | 
			
		||||
    </Link>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,54 @@
 | 
			
		||||
export interface ChannelType {
 | 
			
		||||
  id: number
 | 
			
		||||
  name: string
 | 
			
		||||
  description: string
 | 
			
		||||
  createdAt: string
 | 
			
		||||
  updatedAt: string
 | 
			
		||||
import { createContext, useContext, useEffect } from 'react'
 | 
			
		||||
 | 
			
		||||
import { NextPage, usePagination } from 'hooks/usePagination'
 | 
			
		||||
import { useAuthentication } from 'tools/authentication'
 | 
			
		||||
import { Channel } from 'models/Channel'
 | 
			
		||||
import { GuildsChannelsPath } from 'components/Application'
 | 
			
		||||
 | 
			
		||||
export interface Channels {
 | 
			
		||||
  channels: Channel[]
 | 
			
		||||
  hasMore: boolean
 | 
			
		||||
  nextPage: NextPage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const channelExample: ChannelType = {
 | 
			
		||||
  id: 4,
 | 
			
		||||
  name: 'Channel 4',
 | 
			
		||||
  description: '',
 | 
			
		||||
  createdAt: '',
 | 
			
		||||
  updatedAt: ''
 | 
			
		||||
const defaultChannelsContext = {} as any
 | 
			
		||||
const ChannelsContext = createContext<Channels>(defaultChannelsContext)
 | 
			
		||||
 | 
			
		||||
export interface ChannelsProviderProps {
 | 
			
		||||
  path: GuildsChannelsPath
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ChannelsProvider: React.FC<ChannelsProviderProps> = (props) => {
 | 
			
		||||
  const { path, children } = props
 | 
			
		||||
 | 
			
		||||
  const { authentication } = useAuthentication()
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    items: channels,
 | 
			
		||||
    hasMore,
 | 
			
		||||
    nextPage,
 | 
			
		||||
    resetPagination
 | 
			
		||||
  } = usePagination<Channel>({
 | 
			
		||||
    api: authentication.api,
 | 
			
		||||
    url: `/guilds/${path.guildId}/channels`
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    resetPagination()
 | 
			
		||||
    nextPage()
 | 
			
		||||
  }, [nextPage, resetPagination])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ChannelsContext.Provider value={{ channels, hasMore, nextPage }}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </ChannelsContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useChannels = (): Channels => {
 | 
			
		||||
  const channels = useContext(ChannelsContext)
 | 
			
		||||
  if (channels === defaultChannelsContext) {
 | 
			
		||||
    throw new Error('useChannels must be used within ChannelsProvider')
 | 
			
		||||
  }
 | 
			
		||||
  return channels
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										51
									
								
								contexts/GuildMember.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								contexts/GuildMember.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
import { createContext, useContext, useEffect, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
import { Guild } from 'models/Guild'
 | 
			
		||||
import { Member } from 'models/Member'
 | 
			
		||||
import { GuildsChannelsPath } from 'components/Application'
 | 
			
		||||
import { useAuthentication } from 'tools/authentication'
 | 
			
		||||
 | 
			
		||||
export interface GuildMember {
 | 
			
		||||
  guild: Guild
 | 
			
		||||
  member: Member
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GuildMemberProps {
 | 
			
		||||
  guildMember: GuildMember
 | 
			
		||||
  path: GuildsChannelsPath
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultGuildMemberContext = {} as any
 | 
			
		||||
const GuildMemberContext = createContext<GuildMember>(defaultGuildMemberContext)
 | 
			
		||||
 | 
			
		||||
export const GuildMemberProvider: React.FC<GuildMemberProps> = (props) => {
 | 
			
		||||
  const [guildMember, setGuildMember] = useState(props.guildMember)
 | 
			
		||||
  const { authentication } = useAuthentication()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchGuildMember = async (): Promise<void> => {
 | 
			
		||||
      const { data } = await authentication.api.get(
 | 
			
		||||
        `/guilds/${props.path.guildId}`
 | 
			
		||||
      )
 | 
			
		||||
      setGuildMember(data)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fetchGuildMember().catch((error) => {
 | 
			
		||||
      console.error(error)
 | 
			
		||||
    })
 | 
			
		||||
  }, [props.path, authentication.api])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <GuildMemberContext.Provider value={guildMember}>
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </GuildMemberContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useGuildMember = (): GuildMember => {
 | 
			
		||||
  const guildMember = useContext(GuildMemberContext)
 | 
			
		||||
  if (guildMember === defaultGuildMemberContext) {
 | 
			
		||||
    throw new Error('useGuildMember must be used within GuildMemberProvider')
 | 
			
		||||
  }
 | 
			
		||||
  return guildMember
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								contexts/Guilds.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								contexts/Guilds.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
import { createContext, useContext, useEffect } from 'react'
 | 
			
		||||
 | 
			
		||||
import { NextPage, usePagination } from 'hooks/usePagination'
 | 
			
		||||
import { useAuthentication } from 'tools/authentication'
 | 
			
		||||
import { GuildWithDefaultChannelId } from 'models/Guild'
 | 
			
		||||
 | 
			
		||||
export interface Guilds {
 | 
			
		||||
  guilds: GuildWithDefaultChannelId[]
 | 
			
		||||
  hasMore: boolean
 | 
			
		||||
  nextPage: NextPage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultGuildsContext = {} as any
 | 
			
		||||
const GuildsContext = createContext<Guilds>(defaultGuildsContext)
 | 
			
		||||
 | 
			
		||||
export const GuildsProvider: React.FC = (props) => {
 | 
			
		||||
  const { children } = props
 | 
			
		||||
 | 
			
		||||
  const { authentication } = useAuthentication()
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    items: guilds,
 | 
			
		||||
    hasMore,
 | 
			
		||||
    nextPage,
 | 
			
		||||
    resetPagination
 | 
			
		||||
  } = usePagination<GuildWithDefaultChannelId>({
 | 
			
		||||
    api: authentication.api,
 | 
			
		||||
    url: '/guilds'
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    resetPagination()
 | 
			
		||||
    nextPage()
 | 
			
		||||
  }, [nextPage, resetPagination])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <GuildsContext.Provider value={{ guilds, hasMore, nextPage }}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </GuildsContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useGuilds = (): Guilds => {
 | 
			
		||||
  const guilds = useContext(GuildsContext)
 | 
			
		||||
  if (guilds === defaultGuildsContext) {
 | 
			
		||||
    throw new Error('useGuilds must be used within GuildsProvider')
 | 
			
		||||
  }
 | 
			
		||||
  return guilds
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										54
									
								
								contexts/Members.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								contexts/Members.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
import { createContext, useContext, useEffect } from 'react'
 | 
			
		||||
 | 
			
		||||
import { NextPage, usePagination } from 'hooks/usePagination'
 | 
			
		||||
import { useAuthentication } from 'tools/authentication'
 | 
			
		||||
import { MemberWithPublicUser } from 'models/Member'
 | 
			
		||||
import { GuildsChannelsPath } from 'components/Application'
 | 
			
		||||
 | 
			
		||||
export interface Members {
 | 
			
		||||
  members: MemberWithPublicUser[]
 | 
			
		||||
  hasMore: boolean
 | 
			
		||||
  nextPage: NextPage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultMembersContext = {} as any
 | 
			
		||||
const MembersContext = createContext<Members>(defaultMembersContext)
 | 
			
		||||
 | 
			
		||||
export interface MembersProviderProps {
 | 
			
		||||
  path: GuildsChannelsPath
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const MembersProviders: React.FC<MembersProviderProps> = (props) => {
 | 
			
		||||
  const { children, path } = props
 | 
			
		||||
 | 
			
		||||
  const { authentication } = useAuthentication()
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    items: members,
 | 
			
		||||
    hasMore,
 | 
			
		||||
    nextPage,
 | 
			
		||||
    resetPagination
 | 
			
		||||
  } = usePagination<MemberWithPublicUser>({
 | 
			
		||||
    api: authentication.api,
 | 
			
		||||
    url: `/guilds/${path.guildId}/members`
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    resetPagination()
 | 
			
		||||
    nextPage()
 | 
			
		||||
  }, [nextPage, resetPagination])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <MembersContext.Provider value={{ members, hasMore, nextPage }}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </MembersContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useMembers = (): Members => {
 | 
			
		||||
  const members = useContext(MembersContext)
 | 
			
		||||
  if (members === defaultMembersContext) {
 | 
			
		||||
    throw new Error('useMembers must be used within MembersProvider')
 | 
			
		||||
  }
 | 
			
		||||
  return members
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										60
									
								
								contexts/Messages.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								contexts/Messages.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
import { createContext, useContext, useEffect } from 'react'
 | 
			
		||||
 | 
			
		||||
import { NextPage, usePagination } from 'hooks/usePagination'
 | 
			
		||||
import { useAuthentication } from 'tools/authentication'
 | 
			
		||||
import { MessageWithMember } from 'models/Message'
 | 
			
		||||
import { GuildsChannelsPath } from 'components/Application'
 | 
			
		||||
 | 
			
		||||
export interface Messages {
 | 
			
		||||
  messages: MessageWithMember[]
 | 
			
		||||
  hasMore: boolean
 | 
			
		||||
  nextPage: NextPage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultMessagesContext = {} as any
 | 
			
		||||
const MessagesContext = createContext<Messages>(defaultMessagesContext)
 | 
			
		||||
 | 
			
		||||
export interface MessagesProviderProps {
 | 
			
		||||
  path: GuildsChannelsPath
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const MessagesProvider: React.FC<MessagesProviderProps> = (props) => {
 | 
			
		||||
  const { path, children } = props
 | 
			
		||||
 | 
			
		||||
  const { authentication } = useAuthentication()
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    items: messages,
 | 
			
		||||
    hasMore,
 | 
			
		||||
    nextPage,
 | 
			
		||||
    resetPagination
 | 
			
		||||
  } = usePagination<MessageWithMember>({
 | 
			
		||||
    api: authentication.api,
 | 
			
		||||
    url: `/channels/${path.channelId}/messages`,
 | 
			
		||||
    inverse: true
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    resetPagination()
 | 
			
		||||
    nextPage(undefined, () => {
 | 
			
		||||
      const messagesDiv = window.document.getElementById(
 | 
			
		||||
        'messages'
 | 
			
		||||
      ) as HTMLDivElement
 | 
			
		||||
      messagesDiv.scrollTo(0, messagesDiv.scrollHeight)
 | 
			
		||||
    })
 | 
			
		||||
  }, [nextPage, resetPagination])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <MessagesContext.Provider value={{ messages, hasMore, nextPage }}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </MessagesContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useMessages = (): Messages => {
 | 
			
		||||
  const messages = useContext(MessagesContext)
 | 
			
		||||
  if (messages === defaultMessagesContext) {
 | 
			
		||||
    throw new Error('useMessages must be used within a MessagesProvider')
 | 
			
		||||
  }
 | 
			
		||||
  return messages
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								cypress/fixtures/channels/[channelId]/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								cypress/fixtures/channels/[channelId]/get.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
 | 
			
		||||
import { channelExample } from '../channel'
 | 
			
		||||
 | 
			
		||||
export const getChannelWithChannelIdHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: `/channels/${channelExample.id}`,
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: {
 | 
			
		||||
      channel: channelExample
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								cypress/fixtures/channels/[channelId]/messages/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								cypress/fixtures/channels/[channelId]/messages/get.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
import { Handler } from '../../../handler'
 | 
			
		||||
import {
 | 
			
		||||
  messageExampleComplete,
 | 
			
		||||
  messageExampleComplete2
 | 
			
		||||
} from '../../../messages/message'
 | 
			
		||||
import { channelExample } from '../../channel'
 | 
			
		||||
 | 
			
		||||
export const getMessagesWithChannelIdHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: `/channels/${channelExample.id}/messages`,
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: [messageExampleComplete, messageExampleComplete2]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,15 @@
 | 
			
		||||
import { guild } from '../guilds/guild'
 | 
			
		||||
import { guildExample } from '../guilds/guild'
 | 
			
		||||
 | 
			
		||||
export const channel = {
 | 
			
		||||
export const channelExample = {
 | 
			
		||||
  id: 1,
 | 
			
		||||
  name: 'general',
 | 
			
		||||
  guildId: guild.id,
 | 
			
		||||
  guildId: guildExample.id,
 | 
			
		||||
  createdAt: new Date().toISOString(),
 | 
			
		||||
  updatedAt: new Date().toISOString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const channelExample2 = {
 | 
			
		||||
  ...channelExample,
 | 
			
		||||
  id: 2,
 | 
			
		||||
  name: 'general2'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								cypress/fixtures/guilds/[guildId]/channels/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								cypress/fixtures/guilds/[guildId]/channels/get.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
import { guildExample } from '../../guild'
 | 
			
		||||
import { Handler } from '../../../handler'
 | 
			
		||||
import { channelExample, channelExample2 } from '../../../channels/channel'
 | 
			
		||||
 | 
			
		||||
export const getChannelsWithGuildIdHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: `/guilds/${guildExample.id}/channels`,
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: [channelExample, channelExample2]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								cypress/fixtures/guilds/[guildId]/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								cypress/fixtures/guilds/[guildId]/get.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
 | 
			
		||||
import { guildExample } from '../guild'
 | 
			
		||||
import { memberExampleComplete } from '../../members/member'
 | 
			
		||||
 | 
			
		||||
export const getGuildMemberWithGuildIdHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: `/guilds/${guildExample.id}`,
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: {
 | 
			
		||||
      guild: guildExample,
 | 
			
		||||
      member: memberExampleComplete
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								cypress/fixtures/guilds/[guildId]/members/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								cypress/fixtures/guilds/[guildId]/members/get.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
import { guildExample } from '../../guild'
 | 
			
		||||
import { Handler } from '../../../handler'
 | 
			
		||||
import { memberExampleComplete } from '../../../members/member'
 | 
			
		||||
 | 
			
		||||
export const getMembersWithGuildIdHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: `/guilds/${guildExample.id}/members`,
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: [memberExampleComplete]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								cypress/fixtures/guilds/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								cypress/fixtures/guilds/get.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
import { Handler } from '../handler'
 | 
			
		||||
 | 
			
		||||
import { guildExample, guildExample2 } from './guild'
 | 
			
		||||
 | 
			
		||||
export const getGuildsHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: '/guilds',
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: [
 | 
			
		||||
      { ...guildExample, defaultChannelId: 1 },
 | 
			
		||||
      { ...guildExample2, defaultChannelId: 2 }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
export const guild = {
 | 
			
		||||
import { Guild } from '../../../models/Guild'
 | 
			
		||||
 | 
			
		||||
export const guildExample: Guild = {
 | 
			
		||||
  id: 1,
 | 
			
		||||
  name: 'GuildExample',
 | 
			
		||||
  description: 'guild example.',
 | 
			
		||||
@@ -7,7 +9,8 @@ export const guild = {
 | 
			
		||||
  updatedAt: new Date().toISOString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const guild2 = {
 | 
			
		||||
  ...guild,
 | 
			
		||||
export const guildExample2: Guild = {
 | 
			
		||||
  ...guildExample,
 | 
			
		||||
  id: 2,
 | 
			
		||||
  name: 'app'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import { Handler } from '../handler'
 | 
			
		||||
 | 
			
		||||
import { guild } from './guild'
 | 
			
		||||
import { channel } from '../channels/channel'
 | 
			
		||||
import { memberComplete } from '../members/member'
 | 
			
		||||
import { guildExample } from './guild'
 | 
			
		||||
import { channelExample } from '../channels/channel'
 | 
			
		||||
import { memberExampleComplete } from '../members/member'
 | 
			
		||||
 | 
			
		||||
export const postGuildsHandler: Handler = {
 | 
			
		||||
  method: 'POST',
 | 
			
		||||
@@ -11,9 +11,9 @@ export const postGuildsHandler: Handler = {
 | 
			
		||||
    statusCode: 201,
 | 
			
		||||
    body: {
 | 
			
		||||
      guild: {
 | 
			
		||||
        ...guild,
 | 
			
		||||
        channels: [channel],
 | 
			
		||||
        members: [memberComplete]
 | 
			
		||||
        ...guildExample,
 | 
			
		||||
        channels: [channelExample],
 | 
			
		||||
        members: [memberExampleComplete]
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
 | 
			
		||||
import { guild, guild2 } from '../guild'
 | 
			
		||||
import { guildExample, guildExample2 } from '../guild'
 | 
			
		||||
 | 
			
		||||
export const getGuildsPublicEmptyHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
@@ -17,8 +17,8 @@ export const getGuildsPublicHandler: Handler = {
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: [
 | 
			
		||||
      { ...guild, membersCount: 1 },
 | 
			
		||||
      { ...guild2, membersCount: 1 }
 | 
			
		||||
      { ...guildExample, membersCount: 1 },
 | 
			
		||||
      { ...guildExample2, membersCount: 1 }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -28,6 +28,6 @@ export const getGuildsPublicSearchHandler: Handler = {
 | 
			
		||||
  url: '/guilds/public',
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: [{ ...guild2, membersCount: 1 }]
 | 
			
		||||
    body: [{ ...guildExample2, membersCount: 1 }]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import { postUsersRefreshTokenHandler } from './users/refresh-token/post'
 | 
			
		||||
 | 
			
		||||
export interface Handler {
 | 
			
		||||
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
 | 
			
		||||
  url: string
 | 
			
		||||
  url: `/${string}`
 | 
			
		||||
  response: {
 | 
			
		||||
    body: any
 | 
			
		||||
    statusCode: number
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,16 @@
 | 
			
		||||
import { guild } from '../guilds/guild'
 | 
			
		||||
import { user } from '../users/user'
 | 
			
		||||
import { guildExample } from '../guilds/guild'
 | 
			
		||||
import { userExample } from '../users/user'
 | 
			
		||||
 | 
			
		||||
export const member = {
 | 
			
		||||
export const memberExample = {
 | 
			
		||||
  id: 1,
 | 
			
		||||
  isOwner: true,
 | 
			
		||||
  userId: user.id,
 | 
			
		||||
  guildId: guild.id,
 | 
			
		||||
  userId: userExample.id,
 | 
			
		||||
  guildId: guildExample.id,
 | 
			
		||||
  createdAt: new Date().toISOString(),
 | 
			
		||||
  updatedAt: new Date().toISOString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const memberComplete = {
 | 
			
		||||
  ...member,
 | 
			
		||||
  user
 | 
			
		||||
export const memberExampleComplete = {
 | 
			
		||||
  ...memberExample,
 | 
			
		||||
  user: userExample
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								cypress/fixtures/messages/message.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								cypress/fixtures/messages/message.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
import { channelExample } from '../channels/channel'
 | 
			
		||||
import { memberExampleComplete } from '../members/member'
 | 
			
		||||
 | 
			
		||||
export const messageExample = {
 | 
			
		||||
  id: 1,
 | 
			
		||||
  value: 'Hello, world!',
 | 
			
		||||
  type: 'text' as 'text' | 'file',
 | 
			
		||||
  mimetype: 'text/plain',
 | 
			
		||||
  memberId: memberExampleComplete.id,
 | 
			
		||||
  channelId: channelExample.id,
 | 
			
		||||
  createdAt: new Date().toISOString(),
 | 
			
		||||
  updatedAt: new Date().toISOString()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const messageExampleComplete = {
 | 
			
		||||
  ...messageExample,
 | 
			
		||||
  member: memberExampleComplete
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const messageExampleComplete2 = {
 | 
			
		||||
  ...messageExample,
 | 
			
		||||
  id: 2,
 | 
			
		||||
  value: 'Second message',
 | 
			
		||||
  member: memberExampleComplete
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
 | 
			
		||||
import { user, userSettings } from '../user'
 | 
			
		||||
import { userExample, userSettingsExample } from '../user'
 | 
			
		||||
 | 
			
		||||
export const getUsersCurrentHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
@@ -9,8 +9,8 @@ export const getUsersCurrentHandler: Handler = {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: {
 | 
			
		||||
      user: {
 | 
			
		||||
        ...user,
 | 
			
		||||
        settings: userSettings,
 | 
			
		||||
        ...userExample,
 | 
			
		||||
        settings: userSettingsExample,
 | 
			
		||||
        currentStrategy: 'local',
 | 
			
		||||
        strategies: ['local']
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
 | 
			
		||||
import { user, userSettings } from '../user'
 | 
			
		||||
import { userExample, userSettingsExample } from '../user'
 | 
			
		||||
 | 
			
		||||
export const postUsersSignupHandler: Handler = {
 | 
			
		||||
  method: 'POST',
 | 
			
		||||
@@ -9,8 +9,8 @@ export const postUsersSignupHandler: Handler = {
 | 
			
		||||
    statusCode: 201,
 | 
			
		||||
    body: {
 | 
			
		||||
      user: {
 | 
			
		||||
        ...user,
 | 
			
		||||
        settings: userSettings
 | 
			
		||||
        ...userExample,
 | 
			
		||||
        settings: userSettingsExample
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { UserSettings } from '../../../models/UserSettings'
 | 
			
		||||
import { User } from '../../../models/User'
 | 
			
		||||
 | 
			
		||||
export const user: User = {
 | 
			
		||||
export const userExample: User = {
 | 
			
		||||
  id: 1,
 | 
			
		||||
  name: 'Divlo',
 | 
			
		||||
  email: 'contact@divlo.fr',
 | 
			
		||||
@@ -17,7 +17,7 @@ export const user: User = {
 | 
			
		||||
  updatedAt: '2021-10-20T20:59:08.485Z'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const userSettings: UserSettings = {
 | 
			
		||||
export const userSettingsExample: UserSettings = {
 | 
			
		||||
  id: 1,
 | 
			
		||||
  language: 'en',
 | 
			
		||||
  theme: 'dark',
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,43 @@
 | 
			
		||||
import { channelExample } from '../../../fixtures/channels/channel'
 | 
			
		||||
import { guildExample } from '../../../fixtures/guilds/guild'
 | 
			
		||||
import { userExample } from '../../../fixtures/users/user'
 | 
			
		||||
import { getGuildsHandler } from '../../../fixtures/guilds/get'
 | 
			
		||||
import { authenticationHandlers } from '../../../fixtures/handler'
 | 
			
		||||
import { getGuildMemberWithGuildIdHandler } from '../../../fixtures/guilds/[guildId]/get'
 | 
			
		||||
import { getChannelWithChannelIdHandler } from '../../../fixtures/channels/[channelId]/get'
 | 
			
		||||
 | 
			
		||||
const applicationPaths = [
 | 
			
		||||
  '/application',
 | 
			
		||||
  `/application/users/${userExample.id}`,
 | 
			
		||||
  '/application/guilds/create',
 | 
			
		||||
  '/application/guilds/join',
 | 
			
		||||
  `/application/${guildExample.id}/${channelExample.id}`
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
describe('Common > application/authentication', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.task('stopMockServer')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should redirect the user to `/authentication/signin` if not signed in', () => {
 | 
			
		||||
    for (const applicationPath of applicationPaths) {
 | 
			
		||||
      cy.visit(applicationPath)
 | 
			
		||||
        .location('pathname')
 | 
			
		||||
        .should('eq', '/authentication/signin')
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should not redirect the user if signed in', () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getGuildsHandler,
 | 
			
		||||
      getGuildMemberWithGuildIdHandler,
 | 
			
		||||
      getChannelWithChannelIdHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    for (const applicationPath of applicationPaths) {
 | 
			
		||||
      cy.visit(applicationPath)
 | 
			
		||||
        .location('pathname')
 | 
			
		||||
        .should('eq', applicationPath)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -1,10 +1,143 @@
 | 
			
		||||
import date from 'date-and-time'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  channelExample,
 | 
			
		||||
  channelExample2
 | 
			
		||||
} from '../../../../fixtures/channels/channel'
 | 
			
		||||
import { guildExample } from '../../../../fixtures/guilds/guild'
 | 
			
		||||
import { getGuildMemberWithGuildIdHandler } from '../../../../fixtures/guilds/[guildId]/get'
 | 
			
		||||
import { getChannelWithChannelIdHandler } from '../../../../fixtures/channels/[channelId]/get'
 | 
			
		||||
import { authenticationHandlers } from '../../../../fixtures/handler'
 | 
			
		||||
import { getGuildsHandler } from '../../../../fixtures/guilds/get'
 | 
			
		||||
import { getChannelsWithGuildIdHandler } from '../../../../fixtures/guilds/[guildId]/channels/get'
 | 
			
		||||
import { getMessagesWithChannelIdHandler } from '../../../../fixtures/channels/[channelId]/messages/get'
 | 
			
		||||
import {
 | 
			
		||||
  messageExampleComplete,
 | 
			
		||||
  messageExampleComplete2
 | 
			
		||||
} from '../../../../fixtures/messages/message'
 | 
			
		||||
import { getMembersWithGuildIdHandler } from '../../../../fixtures/guilds/[guildId]/members/get'
 | 
			
		||||
import { memberExampleComplete } from '../../../../fixtures/members/member'
 | 
			
		||||
import { API_URL } from '../../../../../tools/api'
 | 
			
		||||
 | 
			
		||||
describe('Pages > /application/[guildId]/[channelId]', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.task('stopMockServer')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should succeeds and display the guilds in left sidebar correctly', () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getGuildMemberWithGuildIdHandler,
 | 
			
		||||
      getChannelWithChannelIdHandler,
 | 
			
		||||
      getGuildsHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.intercept(`${API_URL}${getGuildsHandler.url}*`).as('getGuildsHandler')
 | 
			
		||||
    cy.intercept(`/_next/*`).as('nextStaticAndImages')
 | 
			
		||||
    cy.visit(`/application/${guildExample.id}/${channelExample.id}`)
 | 
			
		||||
    cy.wait(['@getGuildsHandler', '@nextStaticAndImages']).then(() => {
 | 
			
		||||
      cy.get('[data-cy=application-title]').should(
 | 
			
		||||
        'have.text',
 | 
			
		||||
        `# ${channelExample.name}`
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('[data-cy=guild-left-sidebar-title]').should(
 | 
			
		||||
        'have.text',
 | 
			
		||||
        guildExample.name
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('.guilds-list').children().should('have.length', 2)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should succeeds and display the channels in left sidebar correctly', () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getGuildMemberWithGuildIdHandler,
 | 
			
		||||
      getChannelWithChannelIdHandler,
 | 
			
		||||
      getChannelsWithGuildIdHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.intercept(`${API_URL}${getChannelsWithGuildIdHandler.url}*`).as(
 | 
			
		||||
      'getChannelsWithGuildIdHandler'
 | 
			
		||||
    )
 | 
			
		||||
    cy.intercept(`/_next/*`).as('nextStaticAndImages')
 | 
			
		||||
    cy.visit(`/application/${guildExample.id}/${channelExample.id}`)
 | 
			
		||||
    cy.wait(['@getChannelsWithGuildIdHandler', '@nextStaticAndImages']).then(
 | 
			
		||||
      () => {
 | 
			
		||||
        cy.get('.channels-list').children().should('have.length', 2)
 | 
			
		||||
        cy.get('.channels-list [data-cy=channel-name]:first').should(
 | 
			
		||||
          'have.text',
 | 
			
		||||
          `# ${channelExample.name}`
 | 
			
		||||
        )
 | 
			
		||||
        cy.get('.channels-list [data-cy=channel-name]:last').should(
 | 
			
		||||
          'have.text',
 | 
			
		||||
          `# ${channelExample2.name}`
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should succeeds and display the messages correctly', () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getGuildMemberWithGuildIdHandler,
 | 
			
		||||
      getChannelWithChannelIdHandler,
 | 
			
		||||
      getMessagesWithChannelIdHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.intercept(`${API_URL}${getMessagesWithChannelIdHandler.url}*`).as(
 | 
			
		||||
      'getMessagesWithChannelIdHandler'
 | 
			
		||||
    )
 | 
			
		||||
    cy.intercept(`/_next/*`).as('nextStaticAndImages')
 | 
			
		||||
    cy.visit(`/application/${guildExample.id}/${channelExample.id}`)
 | 
			
		||||
    cy.wait(['@getMessagesWithChannelIdHandler', '@nextStaticAndImages']).then(
 | 
			
		||||
      () => {
 | 
			
		||||
        cy.get('.messages-list').children().should('have.length', 2)
 | 
			
		||||
        cy.get('.messages-list p:first').should(
 | 
			
		||||
          'have.text',
 | 
			
		||||
          messageExampleComplete.value
 | 
			
		||||
        )
 | 
			
		||||
        cy.get(
 | 
			
		||||
          '.messages-list [data-cy=message-member-user-name]:first'
 | 
			
		||||
        ).should('have.text', messageExampleComplete.member.user.name)
 | 
			
		||||
        cy.get('.messages-list [data-cy=message-date]:first').should(
 | 
			
		||||
          'have.text',
 | 
			
		||||
          date.format(
 | 
			
		||||
            new Date(messageExampleComplete.createdAt),
 | 
			
		||||
            'DD/MM/YYYY - HH:mm:ss'
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
        cy.get('.messages-list p:last').should(
 | 
			
		||||
          'have.text',
 | 
			
		||||
          messageExampleComplete2.value
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should succeeds and display the members in right sidebar correctly', () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getGuildMemberWithGuildIdHandler,
 | 
			
		||||
      getChannelWithChannelIdHandler,
 | 
			
		||||
      getMembersWithGuildIdHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.intercept(`${API_URL}${getMembersWithGuildIdHandler.url}*`).as(
 | 
			
		||||
      'getMembersWithGuildIdHandler'
 | 
			
		||||
    )
 | 
			
		||||
    cy.intercept(`/_next/*`).as('nextStaticAndImages')
 | 
			
		||||
    cy.visit(`/application/${guildExample.id}/${channelExample.id}`)
 | 
			
		||||
    cy.wait(['@getMembersWithGuildIdHandler', '@nextStaticAndImages']).then(
 | 
			
		||||
      () => {
 | 
			
		||||
        cy.get('.members-list').should('not.be.visible')
 | 
			
		||||
        cy.get('[data-cy=icon-button-right-sidebar-members]').click()
 | 
			
		||||
        cy.get('.members-list').should('be.visible')
 | 
			
		||||
        cy.get('[data-cy=members-title]').should('have.text', 'Member(s)')
 | 
			
		||||
        cy.get('.members-list').children().should('have.length', 1)
 | 
			
		||||
        cy.get('.members-list [data-cy=member-user-name]:first').should(
 | 
			
		||||
          'have.text',
 | 
			
		||||
          memberExampleComplete.user.name
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should redirect the user to `/application` if `guildId` or `channelId` are not numbers', () => {
 | 
			
		||||
    cy.task('startMockServer', authenticationHandlers).setCookie(
 | 
			
		||||
      'refreshToken',
 | 
			
		||||
@@ -14,4 +147,26 @@ describe('Pages > /application/[guildId]/[channelId]', () => {
 | 
			
		||||
      .location('pathname')
 | 
			
		||||
      .should('eq', '/application')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it("should redirect the user to `/404` if `guildId` doesn't exist", () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getChannelWithChannelIdHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.visit(`/application/123/${channelExample.id}`, {
 | 
			
		||||
      failOnStatusCode: false
 | 
			
		||||
    })
 | 
			
		||||
      .location('pathname')
 | 
			
		||||
      .should('eq', '/404')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it("should redirect the user to `/404` if `channelId` doesn't exist", () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getGuildMemberWithGuildIdHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.visit(`/application/${guildExample.id}/123`, { failOnStatusCode: false })
 | 
			
		||||
      .location('pathname')
 | 
			
		||||
      .should('eq', '/404')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
import { channel } from '../../../../fixtures/channels/channel'
 | 
			
		||||
import { guild } from '../../../../fixtures/guilds/guild'
 | 
			
		||||
import { getChannelWithChannelIdHandler } from '../../../../fixtures/channels/[channelId]/get'
 | 
			
		||||
import { getGuildsHandler } from '../../../../fixtures/guilds/get'
 | 
			
		||||
import { getGuildMemberWithGuildIdHandler } from '../../../../fixtures/guilds/[guildId]/get'
 | 
			
		||||
import { channelExample } from '../../../../fixtures/channels/channel'
 | 
			
		||||
import { guildExample } from '../../../../fixtures/guilds/guild'
 | 
			
		||||
import { postGuildsHandler } from '../../../../fixtures/guilds/post'
 | 
			
		||||
import { authenticationHandlers } from '../../../../fixtures/handler'
 | 
			
		||||
 | 
			
		||||
@@ -11,15 +14,19 @@ describe('Pages > /application/guilds/create', () => {
 | 
			
		||||
  it('should succeeds and create the guild', () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      postGuildsHandler
 | 
			
		||||
      postGuildsHandler,
 | 
			
		||||
      getGuildsHandler,
 | 
			
		||||
      getGuildMemberWithGuildIdHandler,
 | 
			
		||||
      getChannelWithChannelIdHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.visit('/application/guilds/create')
 | 
			
		||||
    cy.get('[data-cy=application-title]').should('have.text', 'Create a Guild')
 | 
			
		||||
    cy.get('#error-name').should('not.exist')
 | 
			
		||||
    cy.get('[data-cy=input-name]').type(guild.name)
 | 
			
		||||
    cy.get('[data-cy=input-name]').type(guildExample.name)
 | 
			
		||||
    cy.get('[data-cy=submit]').click()
 | 
			
		||||
    cy.location('pathname').should(
 | 
			
		||||
      'eq',
 | 
			
		||||
      `/application/${guild.id}/${channel.id}`
 | 
			
		||||
      `/application/${guildExample.id}/${channelExample.id}`
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@@ -30,7 +37,7 @@ describe('Pages > /application/guilds/create', () => {
 | 
			
		||||
    )
 | 
			
		||||
    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=input-name]').type(guildExample.name)
 | 
			
		||||
    cy.get('[data-cy=submit]').click()
 | 
			
		||||
    cy.get('#message').should('have.text', 'Error: Internal Server Error.')
 | 
			
		||||
  })
 | 
			
		||||
 
 | 
			
		||||
@@ -1,45 +1,39 @@
 | 
			
		||||
import { guildExample, guildExample2 } from '../../../../fixtures/guilds/guild'
 | 
			
		||||
import {
 | 
			
		||||
  getGuildsPublicEmptyHandler,
 | 
			
		||||
  getGuildsPublicHandler,
 | 
			
		||||
  getGuildsPublicSearchHandler
 | 
			
		||||
} from '../../../../fixtures/guilds/public/get'
 | 
			
		||||
import { authenticationHandlers } from '../../../../fixtures/handler'
 | 
			
		||||
import { API_URL } from '../../../../../tools/api'
 | 
			
		||||
 | 
			
		||||
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'
 | 
			
		||||
    cy.intercept(`${API_URL}${getGuildsPublicHandler.url}*`).as(
 | 
			
		||||
      'getGuildsPublicHandler'
 | 
			
		||||
    )
 | 
			
		||||
    cy.intercept(`/_next/*`).as('nextStaticAndImages')
 | 
			
		||||
    cy.visit('/application/guilds/join')
 | 
			
		||||
    cy.wait(['@getGuildsPublicHandler', '@nextStaticAndImages']).then(() => {
 | 
			
		||||
      cy.get('[data-cy=application-title]').should('have.text', 'Join a Guild')
 | 
			
		||||
      cy.get('.guilds-public-list').children().should('have.length', 2)
 | 
			
		||||
      cy.get('.guilds-public-list [data-cy=guild-name]:first').should(
 | 
			
		||||
        'have.text',
 | 
			
		||||
        guildExample.name
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('.guilds-public-list [data-cy=guild-name]:last').should(
 | 
			
		||||
        'have.text',
 | 
			
		||||
        guildExample2.name
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should shows the searched guild', () => {
 | 
			
		||||
@@ -48,8 +42,41 @@ describe('Pages > /application/guilds/join', () => {
 | 
			
		||||
      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')
 | 
			
		||||
    cy.intercept(`${API_URL}${getGuildsPublicHandler.url}*`).as(
 | 
			
		||||
      'getGuildsPublicHandler'
 | 
			
		||||
    )
 | 
			
		||||
    cy.intercept(`/_next/*`).as('nextStaticAndImages')
 | 
			
		||||
    cy.wait(['@getGuildsPublicHandler', '@nextStaticAndImages']).then(() => {
 | 
			
		||||
      cy.get('[data-cy=search-guild-input]').type(guildExample2.name)
 | 
			
		||||
      cy.get('.guilds-public-list').children().should('have.length', 1)
 | 
			
		||||
      cy.get('.guilds-public-list [data-cy=guild-name]:first').should(
 | 
			
		||||
        'have.text',
 | 
			
		||||
        guildExample2.name
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should shows no guild if there are no public guilds', () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getGuildsPublicEmptyHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.intercept(`${API_URL}${getGuildsPublicEmptyHandler.url}*`).as(
 | 
			
		||||
      'getGuildsPublicEmptyHandler'
 | 
			
		||||
    )
 | 
			
		||||
    cy.visit('/application/guilds/join')
 | 
			
		||||
    cy.wait('@getGuildsPublicEmptyHandler').then(() => {
 | 
			
		||||
      cy.get('.guilds-public-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-public-list').children().should('have.length', 1)
 | 
			
		||||
    cy.get('[data-testid=progress-spinner]').should('be.visible')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,35 +1,32 @@
 | 
			
		||||
import { authenticationHandlers } from '../../../fixtures/handler'
 | 
			
		||||
 | 
			
		||||
const applicationPaths = [
 | 
			
		||||
  '/application',
 | 
			
		||||
  '/application/users/0',
 | 
			
		||||
  '/application/guilds/create',
 | 
			
		||||
  '/application/guilds/join',
 | 
			
		||||
  '/application/0/0'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
describe('Pages > /application', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.task('stopMockServer')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should redirect the user to `/authentication/signin` if not signed in', () => {
 | 
			
		||||
    for (const applicationPath of applicationPaths) {
 | 
			
		||||
      cy.visit(applicationPath)
 | 
			
		||||
        .location('pathname')
 | 
			
		||||
        .should('eq', '/authentication/signin')
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should not redirect the user if signed in', () => {
 | 
			
		||||
    cy.task('startMockServer', authenticationHandlers).setCookie(
 | 
			
		||||
  it('should redirect user to `/application/guilds/create` on click on "Create a Guild"', () => {
 | 
			
		||||
    cy.task('startMockServer', [...authenticationHandlers]).setCookie(
 | 
			
		||||
      'refreshToken',
 | 
			
		||||
      'refresh-token'
 | 
			
		||||
    )
 | 
			
		||||
    for (const applicationPath of applicationPaths) {
 | 
			
		||||
      cy.visit(applicationPath)
 | 
			
		||||
    cy.visit('/application')
 | 
			
		||||
    cy.get('[data-cy=application-title]').should('have.text', 'Application')
 | 
			
		||||
    cy.get('a[href="/application/guilds/create"]')
 | 
			
		||||
      .click()
 | 
			
		||||
      .location('pathname')
 | 
			
		||||
        .should('eq', applicationPath)
 | 
			
		||||
    }
 | 
			
		||||
      .should('eq', '/application/guilds/create')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should redirect user to `/application/guilds/join` on click on "Join a Guild"', () => {
 | 
			
		||||
    cy.task('startMockServer', [...authenticationHandlers]).setCookie(
 | 
			
		||||
      'refreshToken',
 | 
			
		||||
      'refresh-token'
 | 
			
		||||
    )
 | 
			
		||||
    cy.visit('/application')
 | 
			
		||||
    cy.get('a[href="/application/guilds/join"]')
 | 
			
		||||
      .click()
 | 
			
		||||
      .location('pathname')
 | 
			
		||||
      .should('eq', '/application/guilds/join')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { postUsersResetPasswordHandler } from '../../../fixtures/users/reset-password/post'
 | 
			
		||||
import { user } from '../../../fixtures/users/user'
 | 
			
		||||
import { userExample } from '../../../fixtures/users/user'
 | 
			
		||||
 | 
			
		||||
describe('Pages > /authentication/forgot-password', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
@@ -10,7 +10,7 @@ describe('Pages > /authentication/forgot-password', () => {
 | 
			
		||||
  it('should succeeds and sends a password-reset request', () => {
 | 
			
		||||
    cy.task('startMockServer', [postUsersResetPasswordHandler])
 | 
			
		||||
    cy.get('#message').should('not.exist')
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(user.email)
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(userExample.email)
 | 
			
		||||
    cy.get('[data-cy=submit]').click()
 | 
			
		||||
    cy.get('#message').should(
 | 
			
		||||
      'have.text',
 | 
			
		||||
@@ -20,7 +20,7 @@ describe('Pages > /authentication/forgot-password', () => {
 | 
			
		||||
 | 
			
		||||
  it('should fails with unreachable api server', () => {
 | 
			
		||||
    cy.get('#message').should('not.exist')
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(user.email)
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(userExample.email)
 | 
			
		||||
    cy.get('[data-cy=submit]').click()
 | 
			
		||||
    cy.get('#message').should('have.text', 'Error: Internal Server Error.')
 | 
			
		||||
  })
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import {
 | 
			
		||||
  postUsersSigninHandler,
 | 
			
		||||
  postUsersSigninInvalidCredentialsHandler
 | 
			
		||||
} from 'cypress/fixtures/users/signin/post'
 | 
			
		||||
import { user } from '../../../fixtures/users/user'
 | 
			
		||||
import { userExample } from '../../../fixtures/users/user'
 | 
			
		||||
 | 
			
		||||
describe('Pages > /authentication/signin', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
@@ -18,7 +18,7 @@ describe('Pages > /authentication/signin', () => {
 | 
			
		||||
    ])
 | 
			
		||||
    cy.get('#error-email').should('not.exist')
 | 
			
		||||
    cy.get('#error-password').should('not.exist')
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(user.email)
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(userExample.email)
 | 
			
		||||
    cy.get('[data-cy=input-password]').type('randompassword')
 | 
			
		||||
    cy.get('[data-cy=submit]').click()
 | 
			
		||||
    cy.location('pathname').should('eq', '/application')
 | 
			
		||||
@@ -27,7 +27,7 @@ describe('Pages > /authentication/signin', () => {
 | 
			
		||||
  it('should fails with unreachable api server', () => {
 | 
			
		||||
    cy.get('#error-email').should('not.exist')
 | 
			
		||||
    cy.get('#error-password').should('not.exist')
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(user.email)
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(userExample.email)
 | 
			
		||||
    cy.get('[data-cy=input-password]').type('randompassword')
 | 
			
		||||
    cy.get('[data-cy=submit]').click()
 | 
			
		||||
    cy.get('#message').should('have.text', 'Error: Internal Server Error.')
 | 
			
		||||
@@ -42,7 +42,7 @@ describe('Pages > /authentication/signin', () => {
 | 
			
		||||
    ])
 | 
			
		||||
    cy.get('#error-email').should('not.exist')
 | 
			
		||||
    cy.get('#error-password').should('not.exist')
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(user.email)
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(userExample.email)
 | 
			
		||||
    cy.get('[data-cy=input-password]').type('randompassword')
 | 
			
		||||
    cy.get('[data-cy=submit]').click()
 | 
			
		||||
    cy.get('#message').should(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { user } from '../../../fixtures/users/user'
 | 
			
		||||
import { userExample } from '../../../fixtures/users/user'
 | 
			
		||||
import {
 | 
			
		||||
  postUsersSignupHandler,
 | 
			
		||||
  postUsersSignupAlreadyUsedHandler
 | 
			
		||||
@@ -15,8 +15,8 @@ describe('Pages > /authentication/signup', () => {
 | 
			
		||||
    cy.get('#error-name').should('not.exist')
 | 
			
		||||
    cy.get('#error-email').should('not.exist')
 | 
			
		||||
    cy.get('#error-password').should('not.exist')
 | 
			
		||||
    cy.get('[data-cy=input-name]').type(user.name)
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(user.email)
 | 
			
		||||
    cy.get('[data-cy=input-name]').type(userExample.name)
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(userExample.email)
 | 
			
		||||
    cy.get('[data-cy=input-password]').type('randompassword')
 | 
			
		||||
    cy.get('[data-cy=submit]').click()
 | 
			
		||||
    cy.get('#message').should(
 | 
			
		||||
@@ -30,8 +30,8 @@ describe('Pages > /authentication/signup', () => {
 | 
			
		||||
    cy.get('#error-name').should('not.exist')
 | 
			
		||||
    cy.get('#error-email').should('not.exist')
 | 
			
		||||
    cy.get('#error-password').should('not.exist')
 | 
			
		||||
    cy.get('[data-cy=input-name]').type(user.name)
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(user.email)
 | 
			
		||||
    cy.get('[data-cy=input-name]').type(userExample.name)
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(userExample.email)
 | 
			
		||||
    cy.get('[data-cy=input-password]').type('randompassword')
 | 
			
		||||
    cy.get('[data-cy=submit]').click()
 | 
			
		||||
    cy.get('#message').should('have.text', 'Error: Name or Email already used.')
 | 
			
		||||
@@ -44,8 +44,8 @@ describe('Pages > /authentication/signup', () => {
 | 
			
		||||
    cy.get('#error-name').should('not.exist')
 | 
			
		||||
    cy.get('#error-email').should('not.exist')
 | 
			
		||||
    cy.get('#error-password').should('not.exist')
 | 
			
		||||
    cy.get('[data-cy=input-name]').type(user.name)
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(user.email)
 | 
			
		||||
    cy.get('[data-cy=input-name]').type(userExample.name)
 | 
			
		||||
    cy.get('[data-cy=input-email]').type(userExample.email)
 | 
			
		||||
    cy.get('[data-cy=input-password]').type('randompassword')
 | 
			
		||||
    cy.get('[data-cy=submit]').click()
 | 
			
		||||
    cy.get('#message').should('have.text', 'Error: Internal Server Error.')
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import { getLocal } from 'mockttp'
 | 
			
		||||
 | 
			
		||||
import { API_DEFAULT_PORT } from '../../tools/api'
 | 
			
		||||
 | 
			
		||||
/// <reference types="cypress" />
 | 
			
		||||
 | 
			
		||||
/** @type {import('mockttp').Mockttp | null}  */
 | 
			
		||||
@@ -17,7 +19,7 @@ module.exports = (on, config) => {
 | 
			
		||||
      server = getLocal({
 | 
			
		||||
        cors: true
 | 
			
		||||
      })
 | 
			
		||||
      await server.start(8080)
 | 
			
		||||
      await server.start(API_DEFAULT_PORT)
 | 
			
		||||
      for (const handler of handlers) {
 | 
			
		||||
        await server[handler.method.toLowerCase()](handler.url).thenJson(
 | 
			
		||||
          handler.response.statusCode,
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import type { FormDataObject, HandleForm } from 'react-component-form'
 | 
			
		||||
import type { ErrorObject } from 'ajv'
 | 
			
		||||
 | 
			
		||||
import { FetchState, useFetchState } from '../useFetchState'
 | 
			
		||||
import { ajv } from '../../utils/ajv'
 | 
			
		||||
import { ajv } from '../../tools/ajv'
 | 
			
		||||
import { getErrorTranslationKey } from './getErrorTranslationKey'
 | 
			
		||||
 | 
			
		||||
interface Errors {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										85
									
								
								hooks/usePagination.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								hooks/usePagination.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
import { useState, useRef, useCallback } from 'react'
 | 
			
		||||
import { AxiosInstance } from 'axios'
 | 
			
		||||
 | 
			
		||||
import { FetchState } from './useFetchState'
 | 
			
		||||
 | 
			
		||||
export interface Query {
 | 
			
		||||
  [key: string]: string
 | 
			
		||||
}
 | 
			
		||||
export type NextPageAsync = (query?: Query) => Promise<void>
 | 
			
		||||
export type NextPage = (query?: Query, callback?: () => void) => void
 | 
			
		||||
 | 
			
		||||
export interface UsePaginationOptions {
 | 
			
		||||
  api: AxiosInstance
 | 
			
		||||
  url: string
 | 
			
		||||
  inverse?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UsePaginationResult<T> {
 | 
			
		||||
  items: T[]
 | 
			
		||||
  nextPage: NextPage
 | 
			
		||||
  resetPagination: () => void
 | 
			
		||||
  hasMore: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const usePagination = <T extends { id: number }>(
 | 
			
		||||
  options: UsePaginationOptions
 | 
			
		||||
): UsePaginationResult<T> => {
 | 
			
		||||
  const { api, url, inverse = false } = options
 | 
			
		||||
 | 
			
		||||
  const [items, setItems] = useState<T[]>([])
 | 
			
		||||
  const [hasMore, setHasMore] = useState(true)
 | 
			
		||||
  const fetchState = useRef<FetchState>('idle')
 | 
			
		||||
  const afterId = useRef<number | null>(null)
 | 
			
		||||
 | 
			
		||||
  const nextPageAsync: NextPageAsync = useCallback(
 | 
			
		||||
    async (query) => {
 | 
			
		||||
      if (fetchState.current !== 'idle') {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      fetchState.current = 'loading'
 | 
			
		||||
      const searchParameters = new URLSearchParams(query)
 | 
			
		||||
      searchParameters.append('limit', '20')
 | 
			
		||||
      if (afterId.current != null) {
 | 
			
		||||
        searchParameters.append('after', afterId.current.toString())
 | 
			
		||||
      }
 | 
			
		||||
      const { data: newItems } = await api.get<T[]>(
 | 
			
		||||
        `${url}?${searchParameters.toString()}`
 | 
			
		||||
      )
 | 
			
		||||
      if (!inverse) {
 | 
			
		||||
        afterId.current =
 | 
			
		||||
          newItems.length > 0 ? newItems[newItems.length - 1].id : null
 | 
			
		||||
      } else {
 | 
			
		||||
        afterId.current = newItems.length > 0 ? newItems[0].id : null
 | 
			
		||||
      }
 | 
			
		||||
      setItems((oldItems) => {
 | 
			
		||||
        return inverse ? [...newItems, ...oldItems] : [...oldItems, ...newItems]
 | 
			
		||||
      })
 | 
			
		||||
      setHasMore(newItems.length > 0)
 | 
			
		||||
      fetchState.current = 'idle'
 | 
			
		||||
    },
 | 
			
		||||
    [api, url, inverse]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const nextPage: NextPage = useCallback(
 | 
			
		||||
    (query, callback) => {
 | 
			
		||||
      nextPageAsync(query)
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          if (callback != null) {
 | 
			
		||||
            callback()
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        .catch((error) => {
 | 
			
		||||
          console.error(error)
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
    [nextPageAsync]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const resetPagination = useCallback((): void => {
 | 
			
		||||
    afterId.current = null
 | 
			
		||||
    setItems([])
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return { items, hasMore, nextPage, resetPagination }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,6 +13,7 @@
 | 
			
		||||
    "/application/users/[userId]": ["application", "errors"],
 | 
			
		||||
    "/application/guilds/create": ["application", "errors"],
 | 
			
		||||
    "/application/guilds/join": ["application", "errors"],
 | 
			
		||||
    "/application": ["application", "errors"]
 | 
			
		||||
    "/application": ["application", "errors"],
 | 
			
		||||
    "/application/[guildId]/[channelId]": ["application", "errors"]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,11 @@
 | 
			
		||||
{
 | 
			
		||||
  "website": "Website",
 | 
			
		||||
  "create": "Create",
 | 
			
		||||
  "create-a-guild": "Create a Guild"
 | 
			
		||||
  "create-a-guild": "Create a Guild",
 | 
			
		||||
  "create-a-guild-description": "Create your own guild and manage everything.",
 | 
			
		||||
  "join-a-guild": "Join a Guild",
 | 
			
		||||
  "join-a-guild-description": "Talk, collaborate, share and have fun with your friends by joining an already existing guild!",
 | 
			
		||||
  "members": "member(s)",
 | 
			
		||||
  "search": "Search",
 | 
			
		||||
  "write-a-message": "Write a message..."
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,11 @@
 | 
			
		||||
{
 | 
			
		||||
  "website": "Site web",
 | 
			
		||||
  "create": "Crée",
 | 
			
		||||
  "create-a-guild": "Crée une Guilde"
 | 
			
		||||
  "create": "Créer",
 | 
			
		||||
  "create-a-guild": "Créer une Guilde",
 | 
			
		||||
  "create-a-guild-description": "Créez votre propre guilde et gérez tout.",
 | 
			
		||||
  "join-a-guild": "Rejoindre une Guilde",
 | 
			
		||||
  "join-a-guild-description": "Discutez, collaborez, partagez et amusez-vous avec vos amis en rejoignant une guilde déjà existante!",
 | 
			
		||||
  "members": "membre(s)",
 | 
			
		||||
  "search": "Rechercher",
 | 
			
		||||
  "write-a-message": "Écrire un message..."
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,7 @@
 | 
			
		||||
import { Type } from '@sinclair/typebox'
 | 
			
		||||
import { Type, Static } from '@sinclair/typebox'
 | 
			
		||||
 | 
			
		||||
import { date, id } from './utils'
 | 
			
		||||
 | 
			
		||||
export const types = [Type.Literal('text')]
 | 
			
		||||
 | 
			
		||||
export const channelSchema = {
 | 
			
		||||
  id,
 | 
			
		||||
  name: Type.String({ minLength: 1, maxLength: 20 }),
 | 
			
		||||
@@ -11,3 +9,5 @@ export const channelSchema = {
 | 
			
		||||
  updatedAt: date.updatedAt,
 | 
			
		||||
  guildId: id
 | 
			
		||||
}
 | 
			
		||||
const channelObjectSchema = Type.Object(channelSchema)
 | 
			
		||||
export type Channel = Static<typeof channelObjectSchema>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,20 +12,30 @@ export const guildSchema = {
 | 
			
		||||
  createdAt: date.createdAt,
 | 
			
		||||
  updatedAt: date.updatedAt
 | 
			
		||||
}
 | 
			
		||||
export const guildObjectSchema = Type.Object(guildSchema)
 | 
			
		||||
export type Guild = Static<typeof guildObjectSchema>
 | 
			
		||||
 | 
			
		||||
export const guildWithDefaultChannelIdSchema = {
 | 
			
		||||
  ...guildSchema,
 | 
			
		||||
  defaultChannelId: id
 | 
			
		||||
}
 | 
			
		||||
export const guildWithDefaultChannelObjectSchema = Type.Object(
 | 
			
		||||
  guildWithDefaultChannelIdSchema
 | 
			
		||||
)
 | 
			
		||||
export type GuildWithDefaultChannelId = Static<
 | 
			
		||||
  typeof guildWithDefaultChannelObjectSchema
 | 
			
		||||
>
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import { Type } from '@sinclair/typebox'
 | 
			
		||||
import { Type, Static } from '@sinclair/typebox'
 | 
			
		||||
 | 
			
		||||
import { date, id } from './utils'
 | 
			
		||||
import { UserPublicWithoutSettings } from './User'
 | 
			
		||||
 | 
			
		||||
export const memberSchema = {
 | 
			
		||||
  id,
 | 
			
		||||
@@ -10,3 +11,9 @@ export const memberSchema = {
 | 
			
		||||
  userId: id,
 | 
			
		||||
  guildId: id
 | 
			
		||||
}
 | 
			
		||||
const memberObjectSchema = Type.Object(memberSchema)
 | 
			
		||||
export type Member = Static<typeof memberObjectSchema>
 | 
			
		||||
 | 
			
		||||
export interface MemberWithPublicUser extends Member {
 | 
			
		||||
  user: UserPublicWithoutSettings
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import { Type } from '@sinclair/typebox'
 | 
			
		||||
import { Type, Static } from '@sinclair/typebox'
 | 
			
		||||
 | 
			
		||||
import { date, id } from './utils'
 | 
			
		||||
import { MemberWithPublicUser } from './Member'
 | 
			
		||||
 | 
			
		||||
export const types = [Type.Literal('text'), Type.Literal('file')]
 | 
			
		||||
 | 
			
		||||
@@ -21,3 +22,9 @@ export const messageSchema = {
 | 
			
		||||
  memberId: id,
 | 
			
		||||
  channelId: id
 | 
			
		||||
}
 | 
			
		||||
const messageObjectSchema = Type.Object(messageSchema)
 | 
			
		||||
export type Message = Static<typeof messageObjectSchema>
 | 
			
		||||
 | 
			
		||||
export interface MessageWithMember extends Message {
 | 
			
		||||
  member: MemberWithPublicUser
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -50,6 +50,9 @@ export const userPublicWithoutSettingsSchema = {
 | 
			
		||||
  createdAt: date.createdAt,
 | 
			
		||||
  updatedAt: date.updatedAt
 | 
			
		||||
}
 | 
			
		||||
export const userPublicWithoutSettingsObjectSchema = Type.Object(
 | 
			
		||||
  userPublicWithoutSettingsSchema
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export const userPublicSchema = {
 | 
			
		||||
  ...userPublicWithoutSettingsSchema,
 | 
			
		||||
@@ -66,4 +69,7 @@ export const userCurrentSchema = Type.Object({
 | 
			
		||||
 | 
			
		||||
export type User = Static<typeof userObjectSchema>
 | 
			
		||||
export type UserPublic = Static<typeof userPublicObjectSchema>
 | 
			
		||||
export type UserPublicWithoutSettings = Static<
 | 
			
		||||
  typeof userPublicWithoutSettingsObjectSchema
 | 
			
		||||
>
 | 
			
		||||
export type UserCurrent = Static<typeof userCurrentSchema>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ const nextPWA = require('next-pwa')
 | 
			
		||||
const nextTranslate = require('next-translate')
 | 
			
		||||
const { createSecureHeaders } = require('next-secure-headers')
 | 
			
		||||
 | 
			
		||||
/** @type {import("next").NextConfig} */
 | 
			
		||||
module.exports = nextTranslate(
 | 
			
		||||
  nextPWA({
 | 
			
		||||
    images: {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5656
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5656
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										21
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								package.json
									
									
									
									
									
								
							@@ -43,6 +43,7 @@
 | 
			
		||||
    "axios": "0.24.0",
 | 
			
		||||
    "classnames": "2.3.1",
 | 
			
		||||
    "date-and-time": "2.0.1",
 | 
			
		||||
    "emoji-mart": "3.0.1",
 | 
			
		||||
    "next": "12.0.7",
 | 
			
		||||
    "next-pwa": "5.4.4",
 | 
			
		||||
    "next-themes": "0.0.15",
 | 
			
		||||
@@ -51,12 +52,17 @@
 | 
			
		||||
    "react-component-form": "2.0.0",
 | 
			
		||||
    "react-dom": "17.0.2",
 | 
			
		||||
    "react-infinite-scroll-component": "6.1.0",
 | 
			
		||||
    "react-markdown": "7.1.1",
 | 
			
		||||
    "react-responsive": "8.2.0",
 | 
			
		||||
    "react-swipeable": "6.2.0",
 | 
			
		||||
    "react-textarea-autosize": "8.3.3",
 | 
			
		||||
    "read-pkg": "7.0.0",
 | 
			
		||||
    "remark-breaks": "3.0.2",
 | 
			
		||||
    "remark-gfm": "3.0.1",
 | 
			
		||||
    "sharp": "0.29.3",
 | 
			
		||||
    "socket.io-client": "4.4.0",
 | 
			
		||||
    "unified": "10.1.1",
 | 
			
		||||
    "unist-util-visit": "4.1.0",
 | 
			
		||||
    "universal-cookie": "4.0.4"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
@@ -73,17 +79,20 @@
 | 
			
		||||
    "@testing-library/jest-dom": "5.16.1",
 | 
			
		||||
    "@testing-library/react": "12.1.2",
 | 
			
		||||
    "@types/date-and-time": "0.13.0",
 | 
			
		||||
    "@types/jest": "27.0.3",
 | 
			
		||||
    "@types/emoji-mart": "3.0.9",
 | 
			
		||||
    "@types/hast": "2.3.4",
 | 
			
		||||
    "@types/jest": "27.4.0",
 | 
			
		||||
    "@types/node": "17.0.5",
 | 
			
		||||
    "@types/react": "17.0.38",
 | 
			
		||||
    "@types/react-responsive": "8.0.5",
 | 
			
		||||
    "@types/unist": "2.0.6",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "4.33.0",
 | 
			
		||||
    "autoprefixer": "10.4.0",
 | 
			
		||||
    "autoprefixer": "10.4.1",
 | 
			
		||||
    "cypress": "9.2.0",
 | 
			
		||||
    "dockerfilelint": "1.8.0",
 | 
			
		||||
    "editorconfig-checker": "4.0.2",
 | 
			
		||||
    "eslint": "7.32.0",
 | 
			
		||||
    "eslint-config-next": "11.1.2",
 | 
			
		||||
    "eslint-config-next": "12.0.7",
 | 
			
		||||
    "eslint-config-prettier": "8.3.0",
 | 
			
		||||
    "eslint-config-standard-with-typescript": "21.0.1",
 | 
			
		||||
    "eslint-plugin-import": "2.25.3",
 | 
			
		||||
@@ -91,7 +100,7 @@
 | 
			
		||||
    "eslint-plugin-prettier": "4.0.0",
 | 
			
		||||
    "eslint-plugin-promise": "5.1.1",
 | 
			
		||||
    "eslint-plugin-storybook": "0.5.5",
 | 
			
		||||
    "eslint-plugin-unicorn": "39.0.0",
 | 
			
		||||
    "eslint-plugin-unicorn": "40.0.0",
 | 
			
		||||
    "husky": "7.0.4",
 | 
			
		||||
    "jest": "27.4.5",
 | 
			
		||||
    "lint-staged": "12.1.4",
 | 
			
		||||
@@ -105,8 +114,8 @@
 | 
			
		||||
    "serve": "13.0.2",
 | 
			
		||||
    "start-server-and-test": "1.14.0",
 | 
			
		||||
    "storybook-tailwind-dark-mode": "1.0.11",
 | 
			
		||||
    "tailwindcss": "3.0.7",
 | 
			
		||||
    "typescript": "4.5.4",
 | 
			
		||||
    "tailwindcss": "3.0.8",
 | 
			
		||||
    "typescript": "4.4.4",
 | 
			
		||||
    "vercel": "23.1.2",
 | 
			
		||||
    "webpack": "5.65.0"
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user