feat(pages): add /application/[guildId]/[channelId]
(#4)
This commit is contained in:
parent
91e246b759
commit
fdc2a2d1de
@ -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 type ApplicationPath =
|
||||
| '/application'
|
||||
| '/application/guilds/join'
|
||||
| '/application/guilds/create'
|
||||
| `/application/users/${number}`
|
||||
| GuildsChannelsPath
|
||||
|
||||
export interface ApplicationProps {
|
||||
path:
|
||||
| '/application'
|
||||
| '/application/guilds/join'
|
||||
| '/application/guilds/create'
|
||||
| '/application/users/[userId]'
|
||||
| GuildsChannelsPath
|
||||
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,13 +216,15 @@ export const Application: React.FC<ApplicationProps> = (props) => {
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<Sidebar
|
||||
direction='right'
|
||||
visible={visibleSidebars.right}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<Members />
|
||||
</Sidebar>
|
||||
{typeof path !== 'string' && (
|
||||
<Sidebar
|
||||
direction='right'
|
||||
visible={visibleSidebars.right}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<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'
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span className='ml-2 mr-4'># Channel {index}</span>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
<div
|
||||
id='channels'
|
||||
className='scrollbar-firefox-support overflow-y-auto flex-1 flex flex-col'
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</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 className='pl-[6px]'>
|
||||
<Image
|
||||
src='/images/icons/Thream.png'
|
||||
alt='logo'
|
||||
width={48}
|
||||
height={48}
|
||||
/>
|
||||
</div>
|
||||
</IconLink>
|
||||
)
|
||||
})}
|
||||
<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'
|
||||
>
|
||||
<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'
|
||||
>
|
||||
<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>
|
||||
)
|
||||
})}
|
||||
<InfiniteScroll
|
||||
className='members-list'
|
||||
dataLength={members.length}
|
||||
next={nextPage}
|
||||
hasMore={hasMore}
|
||||
loader={<Loader />}
|
||||
>
|
||||
{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 = () => {
|
||||
const { messages, hasMore, nextPage } = useMessages()
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<div
|
||||
id='messages'
|
||||
className='w-full scrollbar-firefox-support overflow-y-auto transition-all flex-1 flex flex-col text-center mt-8 text-lg'
|
||||
>
|
||||
<p>
|
||||
Nothing to show here! <Emoji value=':ghost:' size={20} />
|
||||
</p>
|
||||
<p>Start chatting to kill this Ghost!</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full scrollbar-firefox-support overflow-y-auto transition-all'>
|
||||
{new Array(20).fill(null).map((_, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className='p-4 flex transition hover:bg-gray-200 dark:hover:bg-gray-900'
|
||||
>
|
||||
<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!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
<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} />
|
||||
})}
|
||||
</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>
|
||||
</>
|
||||
</InfiniteScroll>
|
||||
</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}>
|
||||
<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>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
.location('pathname')
|
||||
.should('eq', 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', '/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: {
|
||||
|
5658
package-lock.json
generated
5658
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