feat: add guilds and channels CRUD (#14)
This commit is contained in:
parent
9f56a10305
commit
780788d682
@ -16,6 +16,7 @@
|
|||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error",
|
||||||
"@next/next/no-img-element": "off"
|
"@next/next/no-img-element": "off",
|
||||||
|
"@typescript-eslint/no-misused-promises": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
.github/workflows/analyze.yml
vendored
2
.github/workflows/analyze.yml
vendored
@ -2,7 +2,7 @@ name: 'Analyze'
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master, develop]
|
branches: [develop]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, develop]
|
branches: [master, develop]
|
||||||
|
|
||||||
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -2,7 +2,7 @@ name: 'Build'
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master, develop]
|
branches: [develop]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, develop]
|
branches: [master, develop]
|
||||||
|
|
||||||
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@ -2,7 +2,7 @@ name: 'Lint'
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master, develop]
|
branches: [develop]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, develop]
|
branches: [master, develop]
|
||||||
|
|
||||||
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -2,7 +2,7 @@ name: 'Test'
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master, develop]
|
branches: [develop]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, develop]
|
branches: [master, develop]
|
||||||
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
FROM node:16.13.2 AS dependencies
|
FROM node:16.14.0 AS dependencies
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY ./package*.json ./
|
COPY ./package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
FROM node:16.13.2 AS builder
|
FROM node:16.14.0 AS builder
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
|
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:16.13.2 AS runner
|
FROM node:16.14.0 AS runner
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
|
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
|
||||||
|
@ -18,9 +18,7 @@
|
|||||||
|
|
||||||
Thream's website to stay close with your friends and communities.
|
Thream's website to stay close with your friends and communities.
|
||||||
|
|
||||||
This project was bootstrapped with [create-fullstack-app](https://github.com/Divlo/create-fullstack-app).
|
It uses [Thream/api](https://github.com/Thream/api) v1.0.0.
|
||||||
|
|
||||||
Using [Thream/api](https://github.com/Thream/api) v1.0.0.
|
|
||||||
|
|
||||||
## ⚙️ Getting Started
|
## ⚙️ Getting Started
|
||||||
|
|
||||||
|
@ -14,10 +14,17 @@ import { Members } from './Members'
|
|||||||
import { useAuthentication } from '../../tools/authentication'
|
import { useAuthentication } from '../../tools/authentication'
|
||||||
import { API_URL } from '../../tools/api'
|
import { API_URL } from '../../tools/api'
|
||||||
|
|
||||||
export interface GuildsChannelsPath {
|
export interface ChannelsPath {
|
||||||
guildId: number
|
|
||||||
channelId: number
|
channelId: number
|
||||||
}
|
}
|
||||||
|
export interface GuildsPath {
|
||||||
|
guildId: number
|
||||||
|
}
|
||||||
|
export interface GuildsChannelsPath extends GuildsPath, ChannelsPath {}
|
||||||
|
|
||||||
|
const isGuildsChannelsPath = (path: any): path is GuildsChannelsPath => {
|
||||||
|
return path.guildId !== undefined && path.channelId !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
export type ApplicationPath =
|
export type ApplicationPath =
|
||||||
| '/application'
|
| '/application'
|
||||||
@ -26,6 +33,7 @@ export type ApplicationPath =
|
|||||||
| `/application/users/${number}`
|
| `/application/users/${number}`
|
||||||
| `/application/users/settings`
|
| `/application/users/settings`
|
||||||
| GuildsChannelsPath
|
| GuildsChannelsPath
|
||||||
|
| GuildsPath
|
||||||
|
|
||||||
export interface ApplicationProps {
|
export interface ApplicationProps {
|
||||||
path: ApplicationPath
|
path: ApplicationPath
|
||||||
@ -216,7 +224,7 @@ export const Application: React.FC<ApplicationProps> = (props) => {
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{typeof path !== 'string' && (
|
{isGuildsChannelsPath(path) && (
|
||||||
<Sidebar
|
<Sidebar
|
||||||
direction='right'
|
direction='right'
|
||||||
visible={visibleSidebars.right}
|
visible={visibleSidebars.right}
|
||||||
|
128
components/Application/ChannelSettings/ChannelSettings.tsx
Normal file
128
components/Application/ChannelSettings/ChannelSettings.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Form } from 'react-component-form'
|
||||||
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
|
import { HandleSubmitCallback, useForm } from '../../../hooks/useForm'
|
||||||
|
import { FormState } from '../../design/FormState'
|
||||||
|
import { useGuildMember } from '../../../contexts/GuildMember'
|
||||||
|
import { Input } from '../../design/Input'
|
||||||
|
import { Button } from '../../design/Button'
|
||||||
|
import { useAuthentication } from '../../../tools/authentication'
|
||||||
|
import {
|
||||||
|
Channel,
|
||||||
|
channelSchema,
|
||||||
|
ChannelWithDefaultChannelId
|
||||||
|
} from '../../../models/Channel'
|
||||||
|
|
||||||
|
export interface ChannelSettingsProps {
|
||||||
|
channel: Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const router = useRouter()
|
||||||
|
const { authentication } = useAuthentication()
|
||||||
|
const { guild } = useGuildMember()
|
||||||
|
|
||||||
|
const { channel } = props
|
||||||
|
|
||||||
|
const [inputValues, setInputValues] = useState({
|
||||||
|
name: channel.name
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
fetchState,
|
||||||
|
message,
|
||||||
|
errors,
|
||||||
|
getErrorTranslation,
|
||||||
|
handleSubmit,
|
||||||
|
setFetchState,
|
||||||
|
setMessageTranslationKey
|
||||||
|
} = useForm({
|
||||||
|
validateSchema: {
|
||||||
|
name: channelSchema.name
|
||||||
|
},
|
||||||
|
replaceEmptyStringToNull: true,
|
||||||
|
resetOnSuccess: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit: HandleSubmitCallback = async (formData) => {
|
||||||
|
try {
|
||||||
|
await authentication.api.put(`/channels/${channel.id}`, formData)
|
||||||
|
setInputValues(formData as any)
|
||||||
|
await router.push(`/application/${guild.id}/${channel.id}`)
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
value: 'errors:server-error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange: React.ChangeEventHandler<
|
||||||
|
HTMLInputElement | HTMLTextAreaElement
|
||||||
|
> = (event) => {
|
||||||
|
setInputValues((oldInputValues) => {
|
||||||
|
return {
|
||||||
|
...oldInputValues,
|
||||||
|
[event.target.name]: event.target.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { data } =
|
||||||
|
await authentication.api.delete<ChannelWithDefaultChannelId>(
|
||||||
|
`/channels/${channel.id}`
|
||||||
|
)
|
||||||
|
await router.push(`/application/${guild.id}/${data.defaultChannelId}`)
|
||||||
|
} catch (error) {
|
||||||
|
setFetchState('error')
|
||||||
|
setMessageTranslationKey('errors:server-error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className='my-auto flex flex-col items-center justify-center py-12'
|
||||||
|
>
|
||||||
|
<div className='flex w-full flex-col items-center justify-center sm:w-fit lg:flex-row'>
|
||||||
|
<div className=' flex w-full flex-wrap items-center justify-center px-6 sm:w-max'>
|
||||||
|
<div className='mx-12 flex flex-col'>
|
||||||
|
<Input
|
||||||
|
name='name'
|
||||||
|
label={t('common:name')}
|
||||||
|
placeholder={t('common:name')}
|
||||||
|
className='!mt-0'
|
||||||
|
onChange={onChange}
|
||||||
|
value={inputValues.name}
|
||||||
|
error={getErrorTranslation(errors.name)}
|
||||||
|
data-cy='channel-name-input'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-12 flex flex-col items-center justify-center sm:w-fit'>
|
||||||
|
<div className='space-x-6'>
|
||||||
|
<Button type='submit' data-cy='button-save-channel-settings'>
|
||||||
|
Sauvegarder
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='red'
|
||||||
|
onClick={handleDelete}
|
||||||
|
data-cy='button-delete-channel-settings'
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FormState state={fetchState} message={message} />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
1
components/Application/ChannelSettings/index.ts
Normal file
1
components/Application/ChannelSettings/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './ChannelSettings'
|
@ -1,38 +1,55 @@
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { Loader } from '../../design/Loader'
|
||||||
|
|
||||||
export interface ConfirmGuildJoinProps {
|
export interface ConfirmGuildJoinProps {
|
||||||
className?: string
|
className?: string
|
||||||
handleJoinGuild: () => void
|
handleYes: () => void | Promise<void>
|
||||||
|
handleNo: () => void | Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfirmGuildJoin: React.FC<ConfirmGuildJoinProps> = (props) => {
|
export const ConfirmGuildJoin: React.FC<ConfirmGuildJoinProps> = (props) => {
|
||||||
const { className, handleJoinGuild } = props
|
const { className, handleYes, handleNo } = props
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleYesLoading = async (): Promise<void> => {
|
||||||
|
setIsLoading((isLoading) => !isLoading)
|
||||||
|
await handleYes()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Image
|
{isLoading ? (
|
||||||
src='/images/svg/design/join-guild.svg'
|
<Loader />
|
||||||
alt='Joing Guild Illustration'
|
) : (
|
||||||
height={150}
|
<>
|
||||||
width={150}
|
<Image
|
||||||
/>
|
src='/images/svg/design/join-guild.svg'
|
||||||
<div className='mt-8 flex flex-col'>
|
alt='Join Guild Illustration'
|
||||||
<h1 className='mb-6 text-center text-xl'>Rejoindre la guild ?</h1>
|
height={150}
|
||||||
<div className='flex gap-7'>
|
width={150}
|
||||||
<button
|
/>
|
||||||
className='rounded-3xl bg-success px-8 py-2 transition hover:opacity-50'
|
<div className='mt-8 flex flex-col'>
|
||||||
onClick={handleJoinGuild}
|
<h1 className='mb-6 text-center text-xl'>Rejoindre la guild ?</h1>
|
||||||
>
|
<div className='flex gap-7'>
|
||||||
Oui
|
<button
|
||||||
</button>
|
className='rounded-3xl bg-success px-8 py-2 transition hover:opacity-50'
|
||||||
<button
|
onClick={handleYesLoading}
|
||||||
className='rounded-3xl bg-error px-8 py-2 transition hover:opacity-50'
|
>
|
||||||
onClick={handleJoinGuild}
|
Oui
|
||||||
>
|
</button>
|
||||||
Non
|
<button
|
||||||
</button>
|
className='rounded-3xl bg-error px-8 py-2 transition hover:opacity-50'
|
||||||
</div>
|
onClick={handleNo}
|
||||||
</div>
|
>
|
||||||
|
Non
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
67
components/Application/CreateChannel/CreateChannel.tsx
Normal file
67
components/Application/CreateChannel/CreateChannel.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
import { Form } from 'react-component-form'
|
||||||
|
|
||||||
|
import { useAuthentication } from '../../../tools/authentication'
|
||||||
|
import { HandleSubmitCallback, useForm } from '../../../hooks/useForm'
|
||||||
|
import { Input } from '../../design/Input'
|
||||||
|
import { Main } from '../../design/Main'
|
||||||
|
import { Button } from '../../design/Button'
|
||||||
|
import { FormState } from '../../design/FormState'
|
||||||
|
import { Channel, channelSchema } from '../../../models/Channel'
|
||||||
|
import { useGuildMember } from '../../../contexts/GuildMember'
|
||||||
|
|
||||||
|
export const CreateChannel: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const router = useRouter()
|
||||||
|
const { guild } = useGuildMember()
|
||||||
|
|
||||||
|
const { fetchState, message, errors, getErrorTranslation, handleSubmit } =
|
||||||
|
useForm({
|
||||||
|
validateSchema: {
|
||||||
|
name: channelSchema.name
|
||||||
|
},
|
||||||
|
resetOnSuccess: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const { authentication } = useAuthentication()
|
||||||
|
|
||||||
|
const onSubmit: HandleSubmitCallback = async (formData) => {
|
||||||
|
try {
|
||||||
|
const { data: channel } = await authentication.api.post<Channel>(
|
||||||
|
`/guilds/${guild.id}/channels`,
|
||||||
|
formData
|
||||||
|
)
|
||||||
|
await router.push(`/application/${guild.id}/${channel.id}`)
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
value: 'errors:server-error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Main>
|
||||||
|
<Form className='w-4/6 max-w-xs' onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Input
|
||||||
|
type='text'
|
||||||
|
placeholder={t('common:name')}
|
||||||
|
name='name'
|
||||||
|
label={t('common:name')}
|
||||||
|
error={getErrorTranslation(errors.name)}
|
||||||
|
data-cy='channel-name-input'
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className='mt-6 w-full'
|
||||||
|
type='submit'
|
||||||
|
data-cy='button-create-channel'
|
||||||
|
>
|
||||||
|
{t('application:create')}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
<FormState id='message' state={fetchState} message={message} />
|
||||||
|
</Main>
|
||||||
|
)
|
||||||
|
}
|
1
components/Application/CreateChannel/index.ts
Normal file
1
components/Application/CreateChannel/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './CreateChannel'
|
@ -1,3 +1,4 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
import { CogIcon, PlusIcon } from '@heroicons/react/solid'
|
import { CogIcon, PlusIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
import { useGuildMember } from '../../../contexts/GuildMember'
|
import { useGuildMember } from '../../../contexts/GuildMember'
|
||||||
@ -13,7 +14,7 @@ export interface GuildLeftSidebarProps {
|
|||||||
export const GuildLeftSidebar: React.FC<GuildLeftSidebarProps> = (props) => {
|
export const GuildLeftSidebar: React.FC<GuildLeftSidebarProps> = (props) => {
|
||||||
const { path } = props
|
const { path } = props
|
||||||
|
|
||||||
const { guild } = useGuildMember()
|
const { guild, member } = useGuildMember()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-2 flex w-full flex-col justify-between'>
|
<div className='mt-2 flex w-full flex-col justify-between'>
|
||||||
@ -26,12 +27,22 @@ export const GuildLeftSidebar: React.FC<GuildLeftSidebarProps> = (props) => {
|
|||||||
<Channels path={path} />
|
<Channels path={path} />
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className='mb-1 flex items-center justify-center space-x-6 p-2'>
|
<div className='mb-1 flex items-center justify-center space-x-6 p-2'>
|
||||||
<IconButton className='h-10 w-10' title='Add a Channel'>
|
{member.isOwner && (
|
||||||
<PlusIcon />
|
<Link href={`/application/${path.guildId}/channels/create`} passHref>
|
||||||
</IconButton>
|
<a data-cy='link-add-channel'>
|
||||||
<IconButton className='h-7 w-7' title='Settings'>
|
<IconButton className='h-10 w-10' title='Add a Channel'>
|
||||||
<CogIcon />
|
<PlusIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<Link href={`/application/${path.guildId}/settings`} passHref>
|
||||||
|
<a data-cy='link-settings-guild'>
|
||||||
|
<IconButton className='h-7 w-7' title='Settings'>
|
||||||
|
<CogIcon />
|
||||||
|
</IconButton>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
201
components/Application/GuildSettings/GuildSettings.tsx
Normal file
201
components/Application/GuildSettings/GuildSettings.tsx
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import Image from 'next/image'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Type } from '@sinclair/typebox'
|
||||||
|
import { PhotographIcon } from '@heroicons/react/solid'
|
||||||
|
import { Form } from 'react-component-form'
|
||||||
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
|
import { HandleSubmitCallback, useForm } from 'hooks/useForm'
|
||||||
|
import { guildSchema } from 'models/Guild'
|
||||||
|
import { FormState } from 'components/design/FormState'
|
||||||
|
|
||||||
|
import { API_URL } from '../../../tools/api'
|
||||||
|
import { useGuildMember } from '../../../contexts/GuildMember'
|
||||||
|
import { Textarea } from '../../design/Textarea'
|
||||||
|
import { Input } from '../../design/Input'
|
||||||
|
import { Button } from '../../design/Button'
|
||||||
|
import { useAuthentication } from '../../../tools/authentication'
|
||||||
|
|
||||||
|
export const GuildSettings: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const router = useRouter()
|
||||||
|
const { authentication } = useAuthentication()
|
||||||
|
const { guild, member } = useGuildMember()
|
||||||
|
|
||||||
|
const [inputValues, setInputValues] = useState({
|
||||||
|
name: guild.name,
|
||||||
|
description: guild.description
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
fetchState,
|
||||||
|
message,
|
||||||
|
errors,
|
||||||
|
getErrorTranslation,
|
||||||
|
handleSubmit,
|
||||||
|
setFetchState,
|
||||||
|
setMessageTranslationKey
|
||||||
|
} = useForm({
|
||||||
|
validateSchema: {
|
||||||
|
name: guildSchema.name,
|
||||||
|
description: Type.Optional(guildSchema.description)
|
||||||
|
},
|
||||||
|
replaceEmptyStringToNull: true,
|
||||||
|
resetOnSuccess: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit: HandleSubmitCallback = async (formData) => {
|
||||||
|
try {
|
||||||
|
await authentication.api.put(`/guilds/${guild.id}`, formData)
|
||||||
|
setInputValues(formData as any)
|
||||||
|
return {
|
||||||
|
type: 'success',
|
||||||
|
value: 'common:name'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
type: 'error',
|
||||||
|
value: 'errors:server-error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onChange: React.ChangeEventHandler<
|
||||||
|
HTMLInputElement | HTMLTextAreaElement
|
||||||
|
> = (event) => {
|
||||||
|
setInputValues((oldInputValues) => {
|
||||||
|
return {
|
||||||
|
...oldInputValues,
|
||||||
|
[event.target.name]: event.target.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async (
|
||||||
|
event
|
||||||
|
) => {
|
||||||
|
setFetchState('loading')
|
||||||
|
const files = event?.target?.files
|
||||||
|
if (files != null && files.length === 1) {
|
||||||
|
const file = files[0]
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('icon', file)
|
||||||
|
try {
|
||||||
|
await authentication.api.put(`/guilds/${guild.id}/icon`, formData)
|
||||||
|
setFetchState('idle')
|
||||||
|
} catch (error) {
|
||||||
|
setFetchState('error')
|
||||||
|
setMessageTranslationKey('errors:server-error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await authentication.api.delete(`/guilds/${guild.id}`)
|
||||||
|
} catch (error) {
|
||||||
|
setFetchState('error')
|
||||||
|
setMessageTranslationKey('errors:server-error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLeave = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await authentication.api.delete(`/guilds/${guild.id}/members/leave`)
|
||||||
|
await router.push('/application')
|
||||||
|
} catch (error) {
|
||||||
|
setFetchState('error')
|
||||||
|
setMessageTranslationKey('errors:server-error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className='my-auto flex flex-col items-center justify-center py-12'
|
||||||
|
>
|
||||||
|
{member.isOwner && (
|
||||||
|
<div className='flex w-full flex-col items-center justify-center sm:w-fit lg:flex-row'>
|
||||||
|
<div className=' flex w-full flex-wrap items-center justify-center px-6 sm:w-max'>
|
||||||
|
<div className='relative'>
|
||||||
|
<div className='absolute z-50 h-full w-full'>
|
||||||
|
<button className='relative flex h-full w-full items-center justify-center transition hover:scale-110'>
|
||||||
|
<input
|
||||||
|
type='file'
|
||||||
|
className='absolute h-full w-full cursor-pointer opacity-0'
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
<PhotographIcon color='white' className='h-8 w-8' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center justify-center rounded-full bg-black shadow-xl'>
|
||||||
|
<Image
|
||||||
|
className='rounded-full opacity-50'
|
||||||
|
src={
|
||||||
|
guild.icon == null
|
||||||
|
? '/images/data/guild-default.png'
|
||||||
|
: API_URL + guild.icon
|
||||||
|
}
|
||||||
|
alt='Profil Picture'
|
||||||
|
draggable='false'
|
||||||
|
height={125}
|
||||||
|
width={125}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mx-12 flex flex-col'>
|
||||||
|
<Input
|
||||||
|
name='name'
|
||||||
|
label={t('common:name')}
|
||||||
|
placeholder={t('common:name')}
|
||||||
|
className='!mt-0'
|
||||||
|
onChange={onChange}
|
||||||
|
value={inputValues.name}
|
||||||
|
error={getErrorTranslation(errors.name)}
|
||||||
|
data-cy='guild-name-input'
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
name='description'
|
||||||
|
label={'Description'}
|
||||||
|
placeholder={'Description'}
|
||||||
|
id='textarea-description'
|
||||||
|
onChange={onChange}
|
||||||
|
value={inputValues.description ?? ''}
|
||||||
|
data-cy='guild-description-input'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='mt-12 flex flex-col items-center justify-center sm:w-fit'>
|
||||||
|
<div className='space-x-6'>
|
||||||
|
{member.isOwner ? (
|
||||||
|
<>
|
||||||
|
<Button type='submit' data-cy='button-save-guild-settings'>
|
||||||
|
Sauvegarder
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='red'
|
||||||
|
onClick={handleDelete}
|
||||||
|
data-cy='button-delete-guild-settings'
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
color='red'
|
||||||
|
onClick={handleLeave}
|
||||||
|
data-cy='button-leave-guild-settings'
|
||||||
|
>
|
||||||
|
Quitter {guild.name}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<FormState state={fetchState} message={message} />
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
1
components/Application/GuildSettings/index.ts
Normal file
1
components/Application/GuildSettings/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './GuildSettings'
|
@ -2,11 +2,11 @@ import InfiniteScroll from 'react-infinite-scroll-component'
|
|||||||
|
|
||||||
import { Loader } from '../../design/Loader'
|
import { Loader } from '../../design/Loader'
|
||||||
import { useGuilds } from '../../../contexts/Guilds'
|
import { useGuilds } from '../../../contexts/Guilds'
|
||||||
import { GuildsChannelsPath } from '..'
|
import { GuildsPath } from '..'
|
||||||
import { Guild } from './Guild'
|
import { Guild } from './Guild'
|
||||||
|
|
||||||
export interface GuildsProps {
|
export interface GuildsProps {
|
||||||
path: GuildsChannelsPath | string
|
path: GuildsPath | string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Guilds: React.FC<GuildsProps> = (props) => {
|
export const Guilds: React.FC<GuildsProps> = (props) => {
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { useState } from 'react'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
import { Emoji } from 'components/Emoji'
|
import { Emoji } from 'components/Emoji'
|
||||||
import { ConfirmGuildJoin } from 'components/Application/ConfirmGuildJoin'
|
import { ConfirmGuildJoin } from 'components/Application/ConfirmGuildJoin'
|
||||||
|
import { API_URL } from 'tools/api'
|
||||||
|
|
||||||
import { GuildPublic as GuildPublicType } from '../../../../models/Guild'
|
import {
|
||||||
|
GuildPublic as GuildPublicType,
|
||||||
|
GuildWithDefaultChannelId
|
||||||
|
} from '../../../../models/Guild'
|
||||||
|
import { useAuthentication } from '../../../../tools/authentication'
|
||||||
|
|
||||||
export interface GuildPublicProps {
|
export interface GuildPublicProps {
|
||||||
guild: GuildPublicType
|
guild: GuildPublicType
|
||||||
@ -14,10 +21,37 @@ export interface GuildPublicProps {
|
|||||||
|
|
||||||
export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
|
export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
|
||||||
const { guild } = props
|
const { guild } = props
|
||||||
|
const router = useRouter()
|
||||||
|
const { authentication } = useAuthentication()
|
||||||
const [isConfirmed, setIsConfirmed] = useState(false)
|
const [isConfirmed, setIsConfirmed] = useState(false)
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleIsConfirmed = (): void => {
|
||||||
|
setIsConfirmed((isConfirmed) => !isConfirmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleYes = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { data } = await authentication.api.post<{
|
||||||
|
guild: GuildWithDefaultChannelId
|
||||||
|
}>(`/guilds/${guild.id}/members/join`)
|
||||||
|
await router.push(
|
||||||
|
`/application/${guild.id}/${data.guild.defaultChannelId}`
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
axios.isAxiosError(error) &&
|
||||||
|
error.response?.status === 400 &&
|
||||||
|
typeof error.response?.data.defaultChannelId === 'number'
|
||||||
|
) {
|
||||||
|
const defaultChannelId = error.response.data.defaultChannelId as number
|
||||||
|
await router.push(`/application/${guild.id}/${defaultChannelId}`)
|
||||||
|
} else {
|
||||||
|
await router.push('/application')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='relative overflow-hidden rounded border border-gray-500 shadow-lg transition duration-200 ease-in-out hover:-translate-y-2 hover:shadow-none dark:border-gray-700'>
|
<div className='relative overflow-hidden rounded border border-gray-500 shadow-lg transition duration-200 ease-in-out hover:-translate-y-2 hover:shadow-none dark:border-gray-700'>
|
||||||
<div
|
<div
|
||||||
@ -25,12 +59,14 @@ export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
|
|||||||
'flex h-full cursor-pointer flex-col items-center justify-center p-4 pt-8 transition duration-200 ease-in-out',
|
'flex h-full cursor-pointer flex-col items-center justify-center p-4 pt-8 transition duration-200 ease-in-out',
|
||||||
{ '-translate-x-full': isConfirmed }
|
{ '-translate-x-full': isConfirmed }
|
||||||
)}
|
)}
|
||||||
onClick={() => setIsConfirmed(!isConfirmed)}
|
onClick={handleIsConfirmed}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
className='rounded-full'
|
className='rounded-full'
|
||||||
src={
|
src={
|
||||||
guild.icon != null ? guild.icon : '/images/data/guild-default.png'
|
guild.icon != null
|
||||||
|
? API_URL + guild.icon
|
||||||
|
: '/images/data/guild-default.png'
|
||||||
}
|
}
|
||||||
alt='logo'
|
alt='logo'
|
||||||
width={80}
|
width={80}
|
||||||
@ -65,7 +101,8 @@ export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
|
|||||||
'!left-0': isConfirmed
|
'!left-0': isConfirmed
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
handleJoinGuild={() => setIsConfirmed(!isConfirmed)}
|
handleYes={handleYes}
|
||||||
|
handleNo={handleIsConfirmed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -7,6 +7,7 @@ import { GuildPublic as GuildPublicType } from '../../../models/Guild'
|
|||||||
import { Loader } from '../../design/Loader'
|
import { Loader } from '../../design/Loader'
|
||||||
import { GuildPublic } from './GuildPublic'
|
import { GuildPublic } from './GuildPublic'
|
||||||
import { usePagination } from '../../../hooks/usePagination'
|
import { usePagination } from '../../../hooks/usePagination'
|
||||||
|
import { SocketData, handleSocketData } from '../../../tools/handleSocketData'
|
||||||
|
|
||||||
export const JoinGuildsPublic: React.FC = () => {
|
export const JoinGuildsPublic: React.FC = () => {
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
@ -14,12 +15,22 @@ export const JoinGuildsPublic: React.FC = () => {
|
|||||||
const { authentication } = useAuthentication()
|
const { authentication } = useAuthentication()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const { items, hasMore, nextPage, resetPagination } =
|
const { items, hasMore, nextPage, resetPagination, setItems } =
|
||||||
usePagination<GuildPublicType>({
|
usePagination<GuildPublicType>({
|
||||||
api: authentication.api,
|
api: authentication.api,
|
||||||
url: '/guilds/public'
|
url: '/guilds/public'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
authentication.socket.on('guilds', (data: SocketData<GuildPublicType>) => {
|
||||||
|
handleSocketData({ data, setItems })
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
authentication.socket.off('guilds')
|
||||||
|
}
|
||||||
|
}, [authentication.socket, setItems])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetPagination()
|
resetPagination()
|
||||||
nextPage({ search })
|
nextPage({ search })
|
||||||
|
@ -8,6 +8,13 @@ import { MessageWithMember } from '../../../../../models/Message'
|
|||||||
import { Loader } from '../../../../design/Loader'
|
import { Loader } from '../../../../design/Loader'
|
||||||
import { FileIcon } from './FileIcon'
|
import { FileIcon } from './FileIcon'
|
||||||
|
|
||||||
|
const supportedImageMimetype = [
|
||||||
|
'image/png',
|
||||||
|
'image/jpg',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/gif'
|
||||||
|
]
|
||||||
|
|
||||||
export interface FileData {
|
export interface FileData {
|
||||||
blob: Blob
|
blob: Blob
|
||||||
url: string
|
url: string
|
||||||
@ -44,7 +51,7 @@ export const MessageFile: React.FC<MessageContentProps> = (props) => {
|
|||||||
if (file == null) {
|
if (file == null) {
|
||||||
return <Loader />
|
return <Loader />
|
||||||
}
|
}
|
||||||
if (message.mimetype.startsWith('image/')) {
|
if (supportedImageMimetype.includes(message.mimetype)) {
|
||||||
return (
|
return (
|
||||||
<a href={file.url} target='_blank' rel='noreferrer'>
|
<a href={file.url} target='_blank' rel='noreferrer'>
|
||||||
<img
|
<img
|
||||||
|
@ -155,7 +155,8 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
|
|||||||
'absolute top-0 left-[150%] flex h-full w-full flex-col items-center justify-center transition-all',
|
'absolute top-0 left-[150%] flex h-full w-full flex-col items-center justify-center transition-all',
|
||||||
{ 'left-[0%]': confirmation }
|
{ 'left-[0%]': confirmation }
|
||||||
)}
|
)}
|
||||||
handleJoinGuild={handleConfirmationState}
|
handleYes={handleConfirmationState}
|
||||||
|
handleNo={() => {}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<XIcon
|
<XIcon
|
||||||
|
@ -133,6 +133,7 @@ export const UserSettings: React.FC = () => {
|
|||||||
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async (
|
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async (
|
||||||
event
|
event
|
||||||
) => {
|
) => {
|
||||||
|
setFetchState('loading')
|
||||||
const files = event?.target?.files
|
const files = event?.target?.files
|
||||||
if (files != null && files.length === 1) {
|
if (files != null && files.length === 1) {
|
||||||
const file = files[0]
|
const file = files[0]
|
||||||
@ -149,6 +150,7 @@ export const UserSettings: React.FC = () => {
|
|||||||
logo: data.user.logo
|
logo: data.user.logo
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
setFetchState('idle')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setFetchState('error')
|
setFetchState('error')
|
||||||
setMessageTranslationKey('errors:server-error')
|
setMessageTranslationKey('errors:server-error')
|
||||||
|
@ -2,18 +2,40 @@ import { forwardRef } from 'react'
|
|||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
|
||||||
const className =
|
const className =
|
||||||
'py-2 px-6 font-paragraph rounded-lg bg-transparent border border-green-800 dark:border-green-400 text-green-800 dark:text-green-400 hover:bg-green-800 hover:text-white dark:hover:bg-green-400 dark:hover:text-black fill-current stroke-current transform transition-colors duration-300 ease-in-out focus:outline-none focus:bg-green-800 focus:text-white dark:focus:bg-green-400 dark:focus:text-black'
|
'py-2 px-6 font-paragraph rounded-lg bg-transparent border hover:text-white dark:hover:text-black fill-current stroke-current transform transition-colors duration-300 ease-in-out focus:outline-none focus:text-white dark:focus:text-black'
|
||||||
|
|
||||||
export interface ButtonLinkProps extends React.ComponentPropsWithRef<'a'> {}
|
const classNameGreen =
|
||||||
|
'border-green-800 dark:border-green-400 text-green-800 dark:text-green-400 hover:bg-green-800 focus:bg-green-800 dark:focus:bg-green-400 dark:hover:bg-green-400'
|
||||||
|
|
||||||
|
const classNameRed =
|
||||||
|
'border-red-800 dark:border-red-400 text-red-800 dark:text-red-400 hover:bg-red-800 focus:bg-red-800 dark:focus:bg-red-400 dark:hover:bg-red-400'
|
||||||
|
|
||||||
|
export type ButtonColor = 'green' | 'red'
|
||||||
|
|
||||||
|
export interface ButtonLinkProps extends React.ComponentPropsWithRef<'a'> {
|
||||||
|
color?: ButtonColor
|
||||||
|
}
|
||||||
|
|
||||||
export const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
export const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
||||||
(props, reference) => {
|
(props, reference) => {
|
||||||
const { children, className: givenClassName, ...rest } = props
|
const {
|
||||||
|
children,
|
||||||
|
className: givenClassName,
|
||||||
|
color = 'green',
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
ref={reference}
|
ref={reference}
|
||||||
className={classNames(className, givenClassName)}
|
className={classNames(
|
||||||
|
className,
|
||||||
|
{
|
||||||
|
[classNameGreen]: color === 'green',
|
||||||
|
[classNameRed]: color === 'red'
|
||||||
|
},
|
||||||
|
givenClassName
|
||||||
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -24,13 +46,30 @@ export const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
|||||||
|
|
||||||
ButtonLink.displayName = 'ButtonLink'
|
ButtonLink.displayName = 'ButtonLink'
|
||||||
|
|
||||||
export interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {}
|
export interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
|
||||||
|
color?: ButtonColor
|
||||||
|
}
|
||||||
|
|
||||||
export const Button: React.FC<ButtonProps> = (props) => {
|
export const Button: React.FC<ButtonProps> = (props) => {
|
||||||
const { children, className: givenClassName, ...rest } = props
|
const {
|
||||||
|
children,
|
||||||
|
className: givenClassName,
|
||||||
|
color = 'green',
|
||||||
|
...rest
|
||||||
|
} = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={classNames(className, givenClassName)} {...rest}>
|
<button
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
{
|
||||||
|
[classNameGreen]: color === 'green',
|
||||||
|
[classNameRed]: color === 'red'
|
||||||
|
},
|
||||||
|
givenClassName
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { createContext, useContext, useEffect } from 'react'
|
import { createContext, useContext, useEffect } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
import { NextPage, usePagination } from 'hooks/usePagination'
|
import { NextPage, usePagination } from 'hooks/usePagination'
|
||||||
import { useAuthentication } from 'tools/authentication'
|
import { useAuthentication } from 'tools/authentication'
|
||||||
import { Channel } from 'models/Channel'
|
import { Channel, ChannelWithDefaultChannelId } from 'models/Channel'
|
||||||
import { GuildsChannelsPath } from 'components/Application'
|
import { GuildsChannelsPath } from 'components/Application'
|
||||||
|
import { handleSocketData, SocketData } from 'tools/handleSocketData'
|
||||||
|
|
||||||
export interface Channels {
|
export interface Channels {
|
||||||
channels: Channel[]
|
channels: Channel[]
|
||||||
@ -20,19 +22,38 @@ export interface ChannelsProviderProps {
|
|||||||
|
|
||||||
export const ChannelsProvider: React.FC<ChannelsProviderProps> = (props) => {
|
export const ChannelsProvider: React.FC<ChannelsProviderProps> = (props) => {
|
||||||
const { path, children } = props
|
const { path, children } = props
|
||||||
|
const router = useRouter()
|
||||||
const { authentication } = useAuthentication()
|
const { authentication } = useAuthentication()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
items: channels,
|
items: channels,
|
||||||
hasMore,
|
hasMore,
|
||||||
nextPage,
|
nextPage,
|
||||||
resetPagination
|
resetPagination,
|
||||||
|
setItems
|
||||||
} = usePagination<Channel>({
|
} = usePagination<Channel>({
|
||||||
api: authentication.api,
|
api: authentication.api,
|
||||||
url: `/guilds/${path.guildId}/channels`
|
url: `/guilds/${path.guildId}/channels`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
authentication.socket.on(
|
||||||
|
'channels',
|
||||||
|
async (data: SocketData<ChannelWithDefaultChannelId>) => {
|
||||||
|
handleSocketData({ data, setItems })
|
||||||
|
if (data.action === 'delete') {
|
||||||
|
await router.push(
|
||||||
|
`/application/${path.guildId}/${data.item.defaultChannelId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
authentication.socket.off('channels')
|
||||||
|
}
|
||||||
|
}, [authentication.socket, path.guildId, router, setItems])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetPagination()
|
resetPagination()
|
||||||
nextPage()
|
nextPage()
|
||||||
|
@ -1,48 +1,92 @@
|
|||||||
import { createContext, useContext, useEffect, useState } from 'react'
|
import { createContext, useContext, useEffect, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
import { Guild } from 'models/Guild'
|
import { GuildWithDefaultChannelId } from 'models/Guild'
|
||||||
import { Member } from 'models/Member'
|
import { Member } from 'models/Member'
|
||||||
import { GuildsChannelsPath } from 'components/Application'
|
|
||||||
import { useAuthentication } from 'tools/authentication'
|
import { useAuthentication } from 'tools/authentication'
|
||||||
|
import { SocketData } from 'tools/handleSocketData'
|
||||||
|
|
||||||
export interface GuildMember {
|
export interface GuildMember {
|
||||||
guild: Guild
|
guild: GuildWithDefaultChannelId
|
||||||
member: Member
|
member: Member
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GuildMemberResult extends GuildMember {
|
||||||
|
setGuildMember: React.Dispatch<React.SetStateAction<GuildMember>>
|
||||||
|
}
|
||||||
|
|
||||||
export interface GuildMemberProps {
|
export interface GuildMemberProps {
|
||||||
guildMember: GuildMember
|
guildMember: GuildMember
|
||||||
path: GuildsChannelsPath
|
path: {
|
||||||
|
guildId: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultGuildMemberContext = {} as any
|
const defaultGuildMemberContext = {} as any
|
||||||
const GuildMemberContext = createContext<GuildMember>(defaultGuildMemberContext)
|
const GuildMemberContext = createContext<GuildMemberResult>(
|
||||||
|
defaultGuildMemberContext
|
||||||
|
)
|
||||||
|
|
||||||
export const GuildMemberProvider: React.FC<GuildMemberProps> = (props) => {
|
export const GuildMemberProvider: React.FC<GuildMemberProps> = (props) => {
|
||||||
|
const { path, children } = props
|
||||||
|
const router = useRouter()
|
||||||
const [guildMember, setGuildMember] = useState(props.guildMember)
|
const [guildMember, setGuildMember] = useState(props.guildMember)
|
||||||
const { authentication } = useAuthentication()
|
const { authentication } = useAuthentication()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchGuildMember = async (): Promise<void> => {
|
const fetchGuildMember = async (): Promise<void> => {
|
||||||
const { data } = await authentication.api.get(
|
const { data } = await authentication.api.get(`/guilds/${path.guildId}`)
|
||||||
`/guilds/${props.path.guildId}`
|
|
||||||
)
|
|
||||||
setGuildMember(data)
|
setGuildMember(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchGuildMember().catch((error) => {
|
fetchGuildMember().catch((error) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
})
|
})
|
||||||
}, [props.path, authentication.api])
|
}, [path, authentication.api])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
authentication.socket.on(
|
||||||
|
'guilds',
|
||||||
|
async (data: SocketData<GuildWithDefaultChannelId>) => {
|
||||||
|
if (data.item.id === path.guildId) {
|
||||||
|
switch (data.action) {
|
||||||
|
case 'delete':
|
||||||
|
await router.push('/application')
|
||||||
|
break
|
||||||
|
case 'update':
|
||||||
|
setGuildMember((oldGuildMember) => {
|
||||||
|
return {
|
||||||
|
...oldGuildMember,
|
||||||
|
guild: {
|
||||||
|
...oldGuildMember.guild,
|
||||||
|
...data.item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
authentication.socket.off('guilds')
|
||||||
|
}
|
||||||
|
}, [authentication.socket, path.guildId, router])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GuildMemberContext.Provider value={guildMember}>
|
<GuildMemberContext.Provider
|
||||||
{props.children}
|
value={{
|
||||||
|
...guildMember,
|
||||||
|
setGuildMember
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
</GuildMemberContext.Provider>
|
</GuildMemberContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useGuildMember = (): GuildMember => {
|
export const useGuildMember = (): GuildMemberResult => {
|
||||||
const guildMember = useContext(GuildMemberContext)
|
const guildMember = useContext(GuildMemberContext)
|
||||||
if (guildMember === defaultGuildMemberContext) {
|
if (guildMember === defaultGuildMemberContext) {
|
||||||
throw new Error('useGuildMember must be used within GuildMemberProvider')
|
throw new Error('useGuildMember must be used within GuildMemberProvider')
|
||||||
|
@ -3,6 +3,7 @@ import { createContext, useContext, useEffect } from 'react'
|
|||||||
import { NextPage, usePagination } from 'hooks/usePagination'
|
import { NextPage, usePagination } from 'hooks/usePagination'
|
||||||
import { useAuthentication } from 'tools/authentication'
|
import { useAuthentication } from 'tools/authentication'
|
||||||
import { GuildWithDefaultChannelId } from 'models/Guild'
|
import { GuildWithDefaultChannelId } from 'models/Guild'
|
||||||
|
import { handleSocketData, SocketData } from 'tools/handleSocketData'
|
||||||
|
|
||||||
export interface Guilds {
|
export interface Guilds {
|
||||||
guilds: GuildWithDefaultChannelId[]
|
guilds: GuildWithDefaultChannelId[]
|
||||||
@ -22,12 +23,26 @@ export const GuildsProvider: React.FC = (props) => {
|
|||||||
items: guilds,
|
items: guilds,
|
||||||
hasMore,
|
hasMore,
|
||||||
nextPage,
|
nextPage,
|
||||||
resetPagination
|
resetPagination,
|
||||||
|
setItems
|
||||||
} = usePagination<GuildWithDefaultChannelId>({
|
} = usePagination<GuildWithDefaultChannelId>({
|
||||||
api: authentication.api,
|
api: authentication.api,
|
||||||
url: '/guilds'
|
url: '/guilds'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
authentication.socket.on(
|
||||||
|
'guilds',
|
||||||
|
(data: SocketData<GuildWithDefaultChannelId>) => {
|
||||||
|
handleSocketData({ data, setItems })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
authentication.socket.off('guilds')
|
||||||
|
}
|
||||||
|
}, [authentication.socket, setItems])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetPagination()
|
resetPagination()
|
||||||
nextPage()
|
nextPage()
|
||||||
|
@ -4,6 +4,8 @@ import { NextPage, usePagination } from 'hooks/usePagination'
|
|||||||
import { useAuthentication } from 'tools/authentication'
|
import { useAuthentication } from 'tools/authentication'
|
||||||
import { MemberWithPublicUser } from 'models/Member'
|
import { MemberWithPublicUser } from 'models/Member'
|
||||||
import { GuildsChannelsPath } from 'components/Application'
|
import { GuildsChannelsPath } from 'components/Application'
|
||||||
|
import { handleSocketData, SocketData } from 'tools/handleSocketData'
|
||||||
|
import { User } from 'models/User'
|
||||||
|
|
||||||
export interface Members {
|
export interface Members {
|
||||||
members: MemberWithPublicUser[]
|
members: MemberWithPublicUser[]
|
||||||
@ -27,12 +29,45 @@ export const MembersProviders: React.FC<MembersProviderProps> = (props) => {
|
|||||||
items: members,
|
items: members,
|
||||||
hasMore,
|
hasMore,
|
||||||
nextPage,
|
nextPage,
|
||||||
resetPagination
|
resetPagination,
|
||||||
|
setItems
|
||||||
} = usePagination<MemberWithPublicUser>({
|
} = usePagination<MemberWithPublicUser>({
|
||||||
api: authentication.api,
|
api: authentication.api,
|
||||||
url: `/guilds/${path.guildId}/members`
|
url: `/guilds/${path.guildId}/members`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
authentication.socket.on(
|
||||||
|
'members',
|
||||||
|
(data: SocketData<MemberWithPublicUser>) => {
|
||||||
|
handleSocketData({ data, setItems })
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
authentication.socket.on('users', (data: SocketData<User>) => {
|
||||||
|
setItems((oldItems) => {
|
||||||
|
const newItems = [...oldItems]
|
||||||
|
switch (data.action) {
|
||||||
|
case 'update': {
|
||||||
|
for (const member of newItems) {
|
||||||
|
if (member.user.id === data.item.id) {
|
||||||
|
member.user = data.item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newItems
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
authentication.socket.off('members')
|
||||||
|
authentication.socket.off('users')
|
||||||
|
}
|
||||||
|
}, [authentication.socket, setItems])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetPagination()
|
resetPagination()
|
||||||
nextPage()
|
nextPage()
|
||||||
|
14
cypress/fixtures/channels/[channelId]/delete.ts
Normal file
14
cypress/fixtures/channels/[channelId]/delete.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Handler } from '../../handler'
|
||||||
|
import { channelExample, channelExample2 } from '../channel'
|
||||||
|
|
||||||
|
export const deleteChannelWithChannelIdHandler: Handler = {
|
||||||
|
method: 'DELETE',
|
||||||
|
url: `/channels/${channelExample.id}`,
|
||||||
|
response: {
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
...channelExample,
|
||||||
|
defaultChannelId: channelExample2.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { Handler } from '../../handler'
|
import { Handler } from '../../handler'
|
||||||
import { channelExample } from '../channel'
|
import { channelExample, channelExample2 } from '../channel'
|
||||||
|
|
||||||
export const getChannelWithChannelIdHandler: Handler = {
|
export const getChannelWithChannelIdHandler: Handler = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -11,3 +11,14 @@ export const getChannelWithChannelIdHandler: Handler = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getChannelWithChannelIdHandler2: Handler = {
|
||||||
|
method: 'GET',
|
||||||
|
url: `/channels/${channelExample2.id}`,
|
||||||
|
response: {
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
channel: channelExample2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
15
cypress/fixtures/channels/[channelId]/put.ts
Normal file
15
cypress/fixtures/channels/[channelId]/put.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Handler } from '../../handler'
|
||||||
|
import { channelExample, channelExample2 } from '../channel'
|
||||||
|
|
||||||
|
export const putChannelWithChannelIdHandler: Handler = {
|
||||||
|
method: 'PUT',
|
||||||
|
url: `/channels/${channelExample.id}`,
|
||||||
|
response: {
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
...channelExample,
|
||||||
|
name: 'New channel name',
|
||||||
|
defaultChannelId: channelExample2.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
cypress/fixtures/guilds/[guildId]/channels/post.ts
Normal file
15
cypress/fixtures/guilds/[guildId]/channels/post.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { guildExample } from '../../guild'
|
||||||
|
import { Handler } from '../../../handler'
|
||||||
|
import { channelExample, channelExample2 } from '../../../channels/channel'
|
||||||
|
|
||||||
|
export const postChannelsWithGuildIdHandler: Handler = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `/guilds/${guildExample.id}/channels`,
|
||||||
|
response: {
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
...channelExample2,
|
||||||
|
defaultChannelId: channelExample.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
cypress/fixtures/guilds/[guildId]/delete.ts
Normal file
13
cypress/fixtures/guilds/[guildId]/delete.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Handler } from '../../handler'
|
||||||
|
import { guildExample } from '../guild'
|
||||||
|
|
||||||
|
export const deleteGuildWithGuildIdHandler: Handler = {
|
||||||
|
method: 'DELETE',
|
||||||
|
url: `/guilds/${guildExample.id}`,
|
||||||
|
response: {
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
...guildExample
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,3 +13,18 @@ export const getGuildMemberWithGuildIdHandler: Handler = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getGuildMemberNotOwnerWithGuildIdHandler: Handler = {
|
||||||
|
method: 'GET',
|
||||||
|
url: `/guilds/${guildExample.id}`,
|
||||||
|
response: {
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
guild: guildExample,
|
||||||
|
member: {
|
||||||
|
...memberExampleComplete,
|
||||||
|
isOwner: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
19
cypress/fixtures/guilds/[guildId]/members/join.ts
Normal file
19
cypress/fixtures/guilds/[guildId]/members/join.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { guildExample } from '../../guild'
|
||||||
|
import { Handler } from '../../../handler'
|
||||||
|
import { memberExampleComplete } from '../../../members/member'
|
||||||
|
import { channelExample } from '../../../channels/channel'
|
||||||
|
|
||||||
|
export const postMembersWithGuildIdHandler: Handler = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `/guilds/${guildExample.id}/members/join`,
|
||||||
|
response: {
|
||||||
|
statusCode: 201,
|
||||||
|
body: {
|
||||||
|
...memberExampleComplete,
|
||||||
|
guild: {
|
||||||
|
...guildExample,
|
||||||
|
defaultChannelId: channelExample.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
cypress/fixtures/guilds/[guildId]/members/leave.ts
Normal file
14
cypress/fixtures/guilds/[guildId]/members/leave.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { guildExample } from '../../guild'
|
||||||
|
import { Handler } from '../../../handler'
|
||||||
|
import { memberExample } from '../../../members/member'
|
||||||
|
|
||||||
|
export const deleteLeaveMembersWithGuildIdHandler: Handler = {
|
||||||
|
method: 'DELETE',
|
||||||
|
url: `/guilds/${guildExample.id}/members/leave`,
|
||||||
|
response: {
|
||||||
|
statusCode: 200,
|
||||||
|
body: {
|
||||||
|
...memberExample
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import { channelExample } from '../../../fixtures/channels/channel'
|
import { channelExample } from '../../../fixtures/channels/channel'
|
||||||
import { guildExample } from '../../../fixtures/guilds/guild'
|
import { guildExample } from '../../../fixtures/guilds/guild'
|
||||||
import { userExample } from '../../../fixtures/users/user'
|
import { userExample } from '../../../fixtures/users/user'
|
||||||
import { getGuildsHandler } from '../../../fixtures/guilds/get'
|
|
||||||
import { authenticationHandlers } from '../../../fixtures/handler'
|
import { authenticationHandlers } from '../../../fixtures/handler'
|
||||||
import { getGuildMemberWithGuildIdHandler } from '../../../fixtures/guilds/[guildId]/get'
|
import { getGuildMemberWithGuildIdHandler } from '../../../fixtures/guilds/[guildId]/get'
|
||||||
import { getChannelWithChannelIdHandler } from '../../../fixtures/channels/[channelId]/get'
|
import { getChannelWithChannelIdHandler } from '../../../fixtures/channels/[channelId]/get'
|
||||||
@ -10,9 +9,13 @@ import { getUserByIdHandler } from '../../../fixtures/users/[userId]/get'
|
|||||||
const applicationPaths = [
|
const applicationPaths = [
|
||||||
'/application',
|
'/application',
|
||||||
`/application/users/${userExample.id}`,
|
`/application/users/${userExample.id}`,
|
||||||
|
`/application/users/settings`,
|
||||||
'/application/guilds/create',
|
'/application/guilds/create',
|
||||||
'/application/guilds/join',
|
'/application/guilds/join',
|
||||||
`/application/${guildExample.id}/${channelExample.id}`
|
`/application/${guildExample.id}/${channelExample.id}`,
|
||||||
|
`/application/${guildExample.id}/${channelExample.id}/settings`,
|
||||||
|
`/application/${guildExample.id}/channels/create`,
|
||||||
|
`/application/${guildExample.id}/settings`
|
||||||
]
|
]
|
||||||
|
|
||||||
describe('Common > application/authentication', () => {
|
describe('Common > application/authentication', () => {
|
||||||
@ -31,7 +34,6 @@ describe('Common > application/authentication', () => {
|
|||||||
it('should not redirect the user if signed in', () => {
|
it('should not redirect the user if signed in', () => {
|
||||||
cy.task('startMockServer', [
|
cy.task('startMockServer', [
|
||||||
...authenticationHandlers,
|
...authenticationHandlers,
|
||||||
getGuildsHandler,
|
|
||||||
getGuildMemberWithGuildIdHandler,
|
getGuildMemberWithGuildIdHandler,
|
||||||
getChannelWithChannelIdHandler,
|
getChannelWithChannelIdHandler,
|
||||||
getUserByIdHandler
|
getUserByIdHandler
|
||||||
|
@ -3,34 +3,37 @@ import date from 'date-and-time'
|
|||||||
import {
|
import {
|
||||||
channelExample,
|
channelExample,
|
||||||
channelExample2
|
channelExample2
|
||||||
} from '../../../../fixtures/channels/channel'
|
} from '../../../../../fixtures/channels/channel'
|
||||||
import { guildExample } from '../../../../fixtures/guilds/guild'
|
import { guildExample } from '../../../../../fixtures/guilds/guild'
|
||||||
import { getGuildMemberWithGuildIdHandler } from '../../../../fixtures/guilds/[guildId]/get'
|
import {
|
||||||
import { getChannelWithChannelIdHandler } from '../../../../fixtures/channels/[channelId]/get'
|
getGuildMemberNotOwnerWithGuildIdHandler,
|
||||||
import { authenticationHandlers } from '../../../../fixtures/handler'
|
getGuildMemberWithGuildIdHandler
|
||||||
import { getGuildsHandler } from '../../../../fixtures/guilds/get'
|
} from '../../../../../fixtures/guilds/[guildId]/get'
|
||||||
import { getChannelsWithGuildIdHandler } from '../../../../fixtures/guilds/[guildId]/channels/get'
|
import { getChannelWithChannelIdHandler } from '../../../../../fixtures/channels/[channelId]/get'
|
||||||
import { getMessagesWithChannelIdHandler } from '../../../../fixtures/channels/[channelId]/messages/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 {
|
import {
|
||||||
messageExampleComplete,
|
messageExampleComplete,
|
||||||
messageExampleComplete2
|
messageExampleComplete2
|
||||||
} from '../../../../fixtures/messages/message'
|
} from '../../../../../fixtures/messages/message'
|
||||||
import { getMembersWithGuildIdHandler } from '../../../../fixtures/guilds/[guildId]/members/get'
|
import { getMembersWithGuildIdHandler } from '../../../../../fixtures/guilds/[guildId]/members/get'
|
||||||
import { memberExampleComplete } from '../../../../fixtures/members/member'
|
import { memberExampleComplete } from '../../../../../fixtures/members/member'
|
||||||
import { API_URL } from '../../../../../tools/api'
|
import { API_URL } from '../../../../../../tools/api'
|
||||||
import {
|
import {
|
||||||
getMessagesUploadsAudioHandler,
|
getMessagesUploadsAudioHandler,
|
||||||
getMessagesUploadsDownloadHandler,
|
getMessagesUploadsDownloadHandler,
|
||||||
getMessagesUploadsImageHandler,
|
getMessagesUploadsImageHandler,
|
||||||
getMessagesUploadsVideoHandler
|
getMessagesUploadsVideoHandler
|
||||||
} from '../../../../fixtures/uploads/messages/get'
|
} from '../../../../../fixtures/uploads/messages/get'
|
||||||
|
|
||||||
describe('Pages > /application/[guildId]/[channelId]', () => {
|
describe('Pages > /application/[guildId]/[channelId]', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.task('stopMockServer')
|
cy.task('stopMockServer')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should succeeds and display the guilds in left sidebar correctly', () => {
|
it('should succeeds and display the left sidebar correctly (member is owner)', () => {
|
||||||
cy.task('startMockServer', [
|
cy.task('startMockServer', [
|
||||||
...authenticationHandlers,
|
...authenticationHandlers,
|
||||||
getGuildMemberWithGuildIdHandler,
|
getGuildMemberWithGuildIdHandler,
|
||||||
@ -50,6 +53,43 @@ describe('Pages > /application/[guildId]/[channelId]', () => {
|
|||||||
guildExample.name
|
guildExample.name
|
||||||
)
|
)
|
||||||
cy.get('.guilds-list').children().should('have.length', 2)
|
cy.get('.guilds-list').children().should('have.length', 2)
|
||||||
|
cy.get('[data-cy=link-add-channel]')
|
||||||
|
.should('be.visible')
|
||||||
|
.should(
|
||||||
|
'have.attr',
|
||||||
|
'href',
|
||||||
|
`/application/${guildExample.id}/channels/create`
|
||||||
|
)
|
||||||
|
cy.get('[data-cy=link-settings-guild]')
|
||||||
|
.should('be.visible')
|
||||||
|
.should('have.attr', 'href', `/application/${guildExample.id}/settings`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should succeeds and display the left sidebar correctly (member is not owner)', () => {
|
||||||
|
cy.task('startMockServer', [
|
||||||
|
...authenticationHandlers,
|
||||||
|
getGuildMemberNotOwnerWithGuildIdHandler,
|
||||||
|
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)
|
||||||
|
cy.get('[data-cy=link-add-channel]').should('not.exist')
|
||||||
|
cy.get('[data-cy=link-settings-guild]')
|
||||||
|
.should('be.visible')
|
||||||
|
.should('have.attr', 'href', `/application/${guildExample.id}/settings`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -0,0 +1,133 @@
|
|||||||
|
import { deleteChannelWithChannelIdHandler } from '../../../../../fixtures/channels/[channelId]/delete'
|
||||||
|
import { putChannelWithChannelIdHandler } from '../../../../../fixtures/channels/[channelId]/put'
|
||||||
|
import {
|
||||||
|
channelExample,
|
||||||
|
channelExample2
|
||||||
|
} from '../../../../../fixtures/channels/channel'
|
||||||
|
import { guildExample } from '../../../../../fixtures/guilds/guild'
|
||||||
|
import {
|
||||||
|
getGuildMemberNotOwnerWithGuildIdHandler,
|
||||||
|
getGuildMemberWithGuildIdHandler
|
||||||
|
} from '../../../../../fixtures/guilds/[guildId]/get'
|
||||||
|
import {
|
||||||
|
getChannelWithChannelIdHandler,
|
||||||
|
getChannelWithChannelIdHandler2
|
||||||
|
} from '../../../../../fixtures/channels/[channelId]/get'
|
||||||
|
import { authenticationHandlers } from '../../../../../fixtures/handler'
|
||||||
|
import { API_URL } from '../../../../../../tools/api'
|
||||||
|
|
||||||
|
describe('Pages > /application/[guildId]/[channelId]/settings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.task('stopMockServer')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should succeeds and update the channel name', () => {
|
||||||
|
cy.task('startMockServer', [
|
||||||
|
...authenticationHandlers,
|
||||||
|
getGuildMemberWithGuildIdHandler,
|
||||||
|
getChannelWithChannelIdHandler,
|
||||||
|
putChannelWithChannelIdHandler
|
||||||
|
]).setCookie('refreshToken', 'refresh-token')
|
||||||
|
cy.intercept(`${API_URL}${putChannelWithChannelIdHandler.url}*`).as(
|
||||||
|
'putChannelWithChannelIdHandler'
|
||||||
|
)
|
||||||
|
cy.visit(`/application/${guildExample.id}/${channelExample.id}/settings`)
|
||||||
|
cy.get('[data-cy=channel-name-input]')
|
||||||
|
.clear()
|
||||||
|
.type(putChannelWithChannelIdHandler.response.body.name)
|
||||||
|
cy.get('[data-cy=button-save-channel-settings]').click()
|
||||||
|
cy.wait('@putChannelWithChannelIdHandler').then(() => {
|
||||||
|
cy.location('pathname').should(
|
||||||
|
'eq',
|
||||||
|
`/application/${guildExample.id}/${channelExample.id}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should succeeds and delete the channel', () => {
|
||||||
|
cy.task('startMockServer', [
|
||||||
|
...authenticationHandlers,
|
||||||
|
getGuildMemberWithGuildIdHandler,
|
||||||
|
getChannelWithChannelIdHandler,
|
||||||
|
getChannelWithChannelIdHandler2,
|
||||||
|
deleteChannelWithChannelIdHandler
|
||||||
|
]).setCookie('refreshToken', 'refresh-token')
|
||||||
|
cy.intercept(`${API_URL}${deleteChannelWithChannelIdHandler.url}*`).as(
|
||||||
|
'deleteChannelWithChannelIdHandler'
|
||||||
|
)
|
||||||
|
cy.visit(`/application/${guildExample.id}/${channelExample.id}/settings`)
|
||||||
|
cy.get('[data-cy=button-delete-channel-settings]').click()
|
||||||
|
cy.wait('@deleteChannelWithChannelIdHandler').then(() => {
|
||||||
|
cy.location('pathname').should(
|
||||||
|
'eq',
|
||||||
|
`/application/${guildExample.id}/${channelExample2.id}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fails with too long channel name on update', () => {
|
||||||
|
cy.task('startMockServer', [
|
||||||
|
...authenticationHandlers,
|
||||||
|
getGuildMemberWithGuildIdHandler,
|
||||||
|
getChannelWithChannelIdHandler
|
||||||
|
]).setCookie('refreshToken', 'refresh-token')
|
||||||
|
cy.visit(`/application/${guildExample.id}/${channelExample.id}/settings`)
|
||||||
|
cy.get('[data-cy=channel-name-input]').type(
|
||||||
|
'random channel name that is really too long for a channel name'
|
||||||
|
)
|
||||||
|
cy.get('[data-cy=button-save-channel-settings]').click()
|
||||||
|
cy.get('#error-name').should(
|
||||||
|
'have.text',
|
||||||
|
'Error: The field must contain at most 20 characters.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should redirect the user to `/404` if member is not owner', () => {
|
||||||
|
cy.task('startMockServer', [
|
||||||
|
...authenticationHandlers,
|
||||||
|
getGuildMemberNotOwnerWithGuildIdHandler,
|
||||||
|
getChannelWithChannelIdHandler
|
||||||
|
]).setCookie('refreshToken', 'refresh-token')
|
||||||
|
cy.visit(`/application/${guildExample.id}/${channelExample.id}/settings`, {
|
||||||
|
failOnStatusCode: false
|
||||||
|
})
|
||||||
|
.location('pathname')
|
||||||
|
.should('eq', '/404')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should redirect the user to `/404` if `guildId` or `channelId` are not numbers', () => {
|
||||||
|
cy.task('startMockServer', authenticationHandlers).setCookie(
|
||||||
|
'refreshToken',
|
||||||
|
'refresh-token'
|
||||||
|
)
|
||||||
|
cy.visit('/application/abc/abc/settings', {
|
||||||
|
failOnStatusCode: false
|
||||||
|
})
|
||||||
|
.location('pathname')
|
||||||
|
.should('eq', '/404')
|
||||||
|
})
|
||||||
|
|
||||||
|
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}/settings`, {
|
||||||
|
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/settings`, {
|
||||||
|
failOnStatusCode: false
|
||||||
|
})
|
||||||
|
.location('pathname')
|
||||||
|
.should('eq', '/404')
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,74 @@
|
|||||||
|
import { postChannelsWithGuildIdHandler } from '../../../../../fixtures/guilds/[guildId]/channels/post'
|
||||||
|
import { API_URL } from '../../../../../../tools/api'
|
||||||
|
import { channelExample2 } from '../../../../../fixtures/channels/channel'
|
||||||
|
import { guildExample } from '../../../../../fixtures/guilds/guild'
|
||||||
|
import { getGuildMemberWithGuildIdHandler } from '../../../../../fixtures/guilds/[guildId]/get'
|
||||||
|
import { authenticationHandlers } from '../../../../../fixtures/handler'
|
||||||
|
import { getChannelWithChannelIdHandler2 } from '../../../../../fixtures/channels/[channelId]/get'
|
||||||
|
|
||||||
|
describe('Pages > /application/[guildId]/channels/create', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.task('stopMockServer')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should succeeds and create the channel', () => {
|
||||||
|
cy.task('startMockServer', [
|
||||||
|
...authenticationHandlers,
|
||||||
|
getGuildMemberWithGuildIdHandler,
|
||||||
|
postChannelsWithGuildIdHandler,
|
||||||
|
getChannelWithChannelIdHandler2
|
||||||
|
]).setCookie('refreshToken', 'refresh-token')
|
||||||
|
cy.intercept(`${API_URL}${postChannelsWithGuildIdHandler.url}*`).as(
|
||||||
|
'postChannelsWithGuildIdHandler'
|
||||||
|
)
|
||||||
|
cy.visit(`/application/${guildExample.id}/channels/create`)
|
||||||
|
cy.get('[data-cy=channel-name-input]').type(channelExample2.name)
|
||||||
|
cy.get('[data-cy=button-create-channel]').click()
|
||||||
|
cy.wait('@postChannelsWithGuildIdHandler').then(() => {
|
||||||
|
cy.location('pathname').should(
|
||||||
|
'eq',
|
||||||
|
`/application/${guildExample.id}/${channelExample2.id}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fails with too long channel name on update', () => {
|
||||||
|
cy.task('startMockServer', [
|
||||||
|
...authenticationHandlers,
|
||||||
|
getGuildMemberWithGuildIdHandler
|
||||||
|
]).setCookie('refreshToken', 'refresh-token')
|
||||||
|
cy.visit(`/application/${guildExample.id}/channels/create`)
|
||||||
|
cy.get('[data-cy=channel-name-input]').type(
|
||||||
|
'random channel name that is really too long for a channel name'
|
||||||
|
)
|
||||||
|
cy.get('[data-cy=button-create-channel]').click()
|
||||||
|
cy.get('#error-name').should(
|
||||||
|
'have.text',
|
||||||
|
'Error: The field must contain at most 20 characters.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should redirect the user to `/404` if `guildId` is not a number', () => {
|
||||||
|
cy.task('startMockServer', authenticationHandlers).setCookie(
|
||||||
|
'refreshToken',
|
||||||
|
'refresh-token'
|
||||||
|
)
|
||||||
|
cy.visit('/application/abc/channels/create', {
|
||||||
|
failOnStatusCode: false
|
||||||
|
})
|
||||||
|
.location('pathname')
|
||||||
|
.should('eq', '/404')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should redirect the user to `/404` if `guildId` doesn't exist", () => {
|
||||||
|
cy.task('startMockServer', [...authenticationHandlers]).setCookie(
|
||||||
|
'refreshToken',
|
||||||
|
'refresh-token'
|
||||||
|
)
|
||||||
|
cy.visit(`/application/123/channels/create`, {
|
||||||
|
failOnStatusCode: false
|
||||||
|
})
|
||||||
|
.location('pathname')
|
||||||
|
.should('eq', '/404')
|
||||||
|
})
|
||||||
|
})
|
104
cypress/integration/pages/application/[guildId]/settings.spec.ts
Normal file
104
cypress/integration/pages/application/[guildId]/settings.spec.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { deleteLeaveMembersWithGuildIdHandler } from 'cypress/fixtures/guilds/[guildId]/members/leave'
|
||||||
|
|
||||||
|
import { guildExample } from '../../../../fixtures/guilds/guild'
|
||||||
|
import {
|
||||||
|
getGuildMemberNotOwnerWithGuildIdHandler,
|
||||||
|
getGuildMemberWithGuildIdHandler
|
||||||
|
} from '../../../../fixtures/guilds/[guildId]/get'
|
||||||
|
import { authenticationHandlers } from '../../../../fixtures/handler'
|
||||||
|
import { API_URL } from '../../../../../tools/api'
|
||||||
|
import { deleteGuildWithGuildIdHandler } from '../../../../fixtures/guilds/[guildId]/delete'
|
||||||
|
|
||||||
|
describe('Pages > /application/[guildId]/settings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.task('stopMockServer')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should succeeds and display correctly the settings of the guild (member is owner)', () => {
|
||||||
|
cy.task('startMockServer', [
|
||||||
|
...authenticationHandlers,
|
||||||
|
getGuildMemberWithGuildIdHandler
|
||||||
|
]).setCookie('refreshToken', 'refresh-token')
|
||||||
|
cy.visit(`/application/${guildExample.id}/settings`)
|
||||||
|
cy.get('[data-cy=guild-name-input]').should('have.value', guildExample.name)
|
||||||
|
cy.get('[data-cy=guild-description-input]').should(
|
||||||
|
'have.value',
|
||||||
|
guildExample.description
|
||||||
|
)
|
||||||
|
cy.get('[data-cy=button-save-guild-settings]').should('be.visible')
|
||||||
|
cy.get('[data-cy=button-delete-guild-settings]').should('be.visible')
|
||||||
|
cy.get('[data-cy=button-leave-guild-settings]').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should succeeds and display correctly the settings of the guild (member is not owner)', () => {
|
||||||
|
cy.task('startMockServer', [
|
||||||
|
...authenticationHandlers,
|
||||||
|
getGuildMemberNotOwnerWithGuildIdHandler
|
||||||
|
]).setCookie('refreshToken', 'refresh-token')
|
||||||
|
cy.visit(`/application/${guildExample.id}/settings`)
|
||||||
|
cy.get('[data-cy=guild-name-input]').should('not.exist')
|
||||||
|
cy.get('[data-cy=guild-description-input]').should('not.exist')
|
||||||
|
cy.get('[data-cy=button-save-guild-settings]').should('not.exist')
|
||||||
|
cy.get('[data-cy=button-delete-guild-settings]').should('not.exist')
|
||||||
|
cy.get('[data-cy=button-leave-guild-settings]').should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should succeeds and leave the guild (member is not owner)', () => {
|
||||||
|
cy.task('startMockServer', [
|
||||||
|
...authenticationHandlers,
|
||||||
|
getGuildMemberNotOwnerWithGuildIdHandler,
|
||||||
|
deleteLeaveMembersWithGuildIdHandler
|
||||||
|
]).setCookie('refreshToken', 'refresh-token')
|
||||||
|
cy.intercept(`${API_URL}${deleteLeaveMembersWithGuildIdHandler.url}*`).as(
|
||||||
|
'deleteLeaveMembersWithGuildIdHandler'
|
||||||
|
)
|
||||||
|
cy.visit(`/application/${guildExample.id}/settings`)
|
||||||
|
cy.get('[data-cy=button-leave-guild-settings]').click()
|
||||||
|
cy.wait('@deleteLeaveMembersWithGuildIdHandler').then(() => {
|
||||||
|
cy.location('pathname').should('eq', '/application')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should succeeds and delete the guild', () => {
|
||||||
|
cy.task('startMockServer', [
|
||||||
|
...authenticationHandlers,
|
||||||
|
getGuildMemberWithGuildIdHandler,
|
||||||
|
deleteGuildWithGuildIdHandler
|
||||||
|
]).setCookie('refreshToken', 'refresh-token')
|
||||||
|
cy.intercept(`${API_URL}${deleteGuildWithGuildIdHandler.url}*`).as(
|
||||||
|
'deleteGuildWithGuildIdHandler'
|
||||||
|
)
|
||||||
|
cy.visit(`/application/${guildExample.id}/settings`)
|
||||||
|
cy.get('[data-cy=button-delete-guild-settings]').click()
|
||||||
|
cy.wait('@deleteGuildWithGuildIdHandler').then((interception) => {
|
||||||
|
expect(interception.response).to.not.be.eql(undefined)
|
||||||
|
if (interception.response !== undefined) {
|
||||||
|
expect(interception.response.statusCode).to.eq(200)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should redirect the user to `/404` if `guildId` is not a number', () => {
|
||||||
|
cy.task('startMockServer', authenticationHandlers).setCookie(
|
||||||
|
'refreshToken',
|
||||||
|
'refresh-token'
|
||||||
|
)
|
||||||
|
cy.visit('/application/abc/settings', {
|
||||||
|
failOnStatusCode: false
|
||||||
|
})
|
||||||
|
.location('pathname')
|
||||||
|
.should('eq', '/404')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should redirect the user to `/404` if `guildId` doesn't exist", () => {
|
||||||
|
cy.task('startMockServer', [...authenticationHandlers]).setCookie(
|
||||||
|
'refreshToken',
|
||||||
|
'refresh-token'
|
||||||
|
)
|
||||||
|
cy.visit(`/application/123/settings`, {
|
||||||
|
failOnStatusCode: false
|
||||||
|
})
|
||||||
|
.location('pathname')
|
||||||
|
.should('eq', '/404')
|
||||||
|
})
|
||||||
|
})
|
@ -15,6 +15,9 @@
|
|||||||
"/application/guilds/create": ["application", "errors"],
|
"/application/guilds/create": ["application", "errors"],
|
||||||
"/application/guilds/join": ["application", "errors"],
|
"/application/guilds/join": ["application", "errors"],
|
||||||
"/application": ["application", "errors"],
|
"/application": ["application", "errors"],
|
||||||
"/application/[guildId]/[channelId]": ["application", "errors"]
|
"/application/[guildId]/settings": ["application", "errors"],
|
||||||
|
"/application/[guildId]/[channelId]": ["application", "errors"],
|
||||||
|
"/application/[guildId]/[channelId]/settings": ["application", "errors"],
|
||||||
|
"/application/[guildId]/channels/create": ["application", "errors"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,3 +11,14 @@ export const channelSchema = {
|
|||||||
}
|
}
|
||||||
const channelObjectSchema = Type.Object(channelSchema)
|
const channelObjectSchema = Type.Object(channelSchema)
|
||||||
export type Channel = Static<typeof channelObjectSchema>
|
export type Channel = Static<typeof channelObjectSchema>
|
||||||
|
|
||||||
|
export const channelWithDefaultChannelIdSchema = {
|
||||||
|
...channelSchema,
|
||||||
|
defaultChannelId: channelSchema.id
|
||||||
|
}
|
||||||
|
export const channelWithDefaultChannelObjectSchema = Type.Object(
|
||||||
|
channelWithDefaultChannelIdSchema
|
||||||
|
)
|
||||||
|
export type ChannelWithDefaultChannelId = Static<
|
||||||
|
typeof channelWithDefaultChannelObjectSchema
|
||||||
|
>
|
||||||
|
@ -24,7 +24,12 @@ module.exports = nextTranslate(
|
|||||||
contentSecurityPolicy: {
|
contentSecurityPolicy: {
|
||||||
directives: {
|
directives: {
|
||||||
defaultSrc: ["'self'"],
|
defaultSrc: ["'self'"],
|
||||||
scriptSrc: ["'self'", "'unsafe-eval'", "'unsafe-inline'"],
|
scriptSrc: [
|
||||||
|
"'self'",
|
||||||
|
'data:',
|
||||||
|
"'unsafe-eval'",
|
||||||
|
"'unsafe-inline'"
|
||||||
|
],
|
||||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
imgSrc: ['*', 'data:', 'blob:'],
|
imgSrc: ['*', 'data:', 'blob:'],
|
||||||
mediaSrc: ['*', 'data:', 'blob:'],
|
mediaSrc: ['*', 'data:', 'blob:'],
|
||||||
|
13904
package-lock.json
generated
13904
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@ -37,19 +37,19 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/montserrat": "4.5.5",
|
"@fontsource/montserrat": "4.5.5",
|
||||||
"@fontsource/roboto": "4.5.3",
|
"@fontsource/roboto": "4.5.3",
|
||||||
"@heroicons/react": "1.0.5",
|
"@heroicons/react": "1.0.6",
|
||||||
"@sinclair/typebox": "0.23.4",
|
"@sinclair/typebox": "0.23.4",
|
||||||
"ajv": "8.10.0",
|
"ajv": "8.10.0",
|
||||||
"ajv-formats": "2.1.1",
|
"ajv-formats": "2.1.1",
|
||||||
"axios": "0.26.0",
|
"axios": "0.26.0",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.1",
|
||||||
"date-and-time": "2.1.2",
|
"date-and-time": "2.2.1",
|
||||||
"emoji-mart": "3.0.1",
|
"emoji-mart": "3.0.1",
|
||||||
"katex": "0.15.2",
|
"katex": "0.15.2",
|
||||||
"next": "12.1.0",
|
"next": "12.1.0",
|
||||||
"next-pwa": "5.4.4",
|
"next-pwa": "5.4.5",
|
||||||
"next-themes": "0.0.15",
|
"next-themes": "0.1.1",
|
||||||
"next-translate": "1.3.4",
|
"next-translate": "1.3.5",
|
||||||
"pretty-bytes": "6.0.0",
|
"pretty-bytes": "6.0.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-component-form": "2.0.0",
|
"react-component-form": "2.0.0",
|
||||||
@ -64,7 +64,7 @@
|
|||||||
"remark-breaks": "3.0.2",
|
"remark-breaks": "3.0.2",
|
||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
"remark-math": "5.1.1",
|
"remark-math": "5.1.1",
|
||||||
"sharp": "0.30.1",
|
"sharp": "0.30.2",
|
||||||
"socket.io-client": "4.4.1",
|
"socket.io-client": "4.4.1",
|
||||||
"unified": "10.1.1",
|
"unified": "10.1.1",
|
||||||
"unist-util-visit": "4.1.0",
|
"unist-util-visit": "4.1.0",
|
||||||
@ -74,7 +74,7 @@
|
|||||||
"@commitlint/cli": "16.2.1",
|
"@commitlint/cli": "16.2.1",
|
||||||
"@commitlint/config-conventional": "16.2.1",
|
"@commitlint/config-conventional": "16.2.1",
|
||||||
"@lhci/cli": "0.9.0",
|
"@lhci/cli": "0.9.0",
|
||||||
"@saithodev/semantic-release-backmerge": "2.1.1",
|
"@saithodev/semantic-release-backmerge": "2.1.2",
|
||||||
"@storybook/addon-essentials": "6.4.19",
|
"@storybook/addon-essentials": "6.4.19",
|
||||||
"@storybook/addon-links": "6.4.19",
|
"@storybook/addon-links": "6.4.19",
|
||||||
"@storybook/addon-postcss": "2.0.0",
|
"@storybook/addon-postcss": "2.0.0",
|
||||||
@ -86,44 +86,44 @@
|
|||||||
"@types/date-and-time": "0.13.0",
|
"@types/date-and-time": "0.13.0",
|
||||||
"@types/emoji-mart": "3.0.9",
|
"@types/emoji-mart": "3.0.9",
|
||||||
"@types/hast": "2.3.4",
|
"@types/hast": "2.3.4",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.1",
|
||||||
"@types/katex": "0.11.1",
|
"@types/katex": "0.11.1",
|
||||||
"@types/node": "17.0.18",
|
"@types/node": "17.0.21",
|
||||||
"@types/react": "17.0.39",
|
"@types/react": "17.0.39",
|
||||||
"@types/react-responsive": "8.0.5",
|
"@types/react-responsive": "8.0.5",
|
||||||
"@types/unist": "2.0.6",
|
"@types/unist": "2.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "5.12.0",
|
"@typescript-eslint/eslint-plugin": "5.13.0",
|
||||||
"@typescript-eslint/parser": "5.12.0",
|
"@typescript-eslint/parser": "5.13.0",
|
||||||
"autoprefixer": "10.4.2",
|
"autoprefixer": "10.4.2",
|
||||||
"cypress": "9.5.0",
|
"cypress": "9.5.1",
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "4.0.2",
|
||||||
"eslint": "8.9.0",
|
"eslint": "8.10.0",
|
||||||
"eslint-config-conventions": "1.1.0",
|
"eslint-config-conventions": "1.1.0",
|
||||||
"eslint-config-next": "12.1.0",
|
"eslint-config-next": "12.1.0",
|
||||||
"eslint-config-prettier": "8.4.0",
|
"eslint-config-prettier": "8.5.0",
|
||||||
"eslint-plugin-import": "2.25.4",
|
"eslint-plugin-import": "2.25.4",
|
||||||
"eslint-plugin-prettier": "4.0.0",
|
"eslint-plugin-prettier": "4.0.0",
|
||||||
"eslint-plugin-promise": "6.0.0",
|
"eslint-plugin-promise": "6.0.0",
|
||||||
"eslint-plugin-storybook": "0.5.7",
|
"eslint-plugin-storybook": "0.5.7",
|
||||||
"eslint-plugin-unicorn": "41.0.0",
|
"eslint-plugin-unicorn": "41.0.0",
|
||||||
"html-w3c-validator": "1.0.0",
|
"html-w3c-validator": "1.1.0",
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
"jest": "27.5.1",
|
"jest": "27.5.1",
|
||||||
"lint-staged": "12.3.4",
|
"lint-staged": "12.3.5",
|
||||||
"markdownlint-cli": "0.31.1",
|
"markdownlint-cli": "0.31.1",
|
||||||
"mockttp": "2.6.0",
|
"mockttp": "2.6.0",
|
||||||
"next-secure-headers": "2.2.0",
|
"next-secure-headers": "2.2.0",
|
||||||
"plop": "3.0.5",
|
"plop": "3.0.5",
|
||||||
"postcss": "8.4.6",
|
"postcss": "8.4.7",
|
||||||
"prettier": "2.5.1",
|
"prettier": "2.5.1",
|
||||||
"prettier-plugin-tailwindcss": "0.1.7",
|
"prettier-plugin-tailwindcss": "0.1.8",
|
||||||
"semantic-release": "19.0.2",
|
"semantic-release": "19.0.2",
|
||||||
"serve": "13.0.2",
|
"serve": "13.0.2",
|
||||||
"start-server-and-test": "1.14.0",
|
"start-server-and-test": "1.14.0",
|
||||||
"storybook-tailwind-dark-mode": "1.0.11",
|
"storybook-tailwind-dark-mode": "1.0.11",
|
||||||
"tailwindcss": "3.0.23",
|
"tailwindcss": "3.0.23",
|
||||||
"typescript": "4.4.4",
|
"typescript": "4.6.2",
|
||||||
"vercel": "24.0.0",
|
"vercel": "24.0.0",
|
||||||
"webpack": "5.69.1"
|
"webpack": "5.70.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
92
pages/application/[guildId]/[channelId]/settings.tsx
Normal file
92
pages/application/[guildId]/[channelId]/settings.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { NextPage } from 'next'
|
||||||
|
|
||||||
|
import { Head } from 'components/Head'
|
||||||
|
import { Application } from 'components/Application'
|
||||||
|
import {
|
||||||
|
authenticationFromServerSide,
|
||||||
|
AuthenticationProvider,
|
||||||
|
PagePropsWithAuthentication
|
||||||
|
} from 'tools/authentication'
|
||||||
|
import { GuildMember, GuildMemberProvider } from 'contexts/GuildMember'
|
||||||
|
import { GuildLeftSidebar } from 'components/Application/GuildLeftSidebar'
|
||||||
|
import { ChannelSettings } from 'components/Application/ChannelSettings'
|
||||||
|
import { ChannelsProvider } from 'contexts/Channels'
|
||||||
|
import { GuildsProvider } from 'contexts/Guilds'
|
||||||
|
import { Channel } from 'models/Channel'
|
||||||
|
import { MembersProviders } from 'contexts/Members'
|
||||||
|
|
||||||
|
export interface ChannelSettingsPageProps extends PagePropsWithAuthentication {
|
||||||
|
channelId: number
|
||||||
|
guildId: number
|
||||||
|
guildMember: GuildMember
|
||||||
|
selectedChannel: Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChannelSettingsPage: NextPage<ChannelSettingsPageProps> = (props) => {
|
||||||
|
const { channelId, guildId, authentication, guildMember, selectedChannel } =
|
||||||
|
props
|
||||||
|
|
||||||
|
const path = {
|
||||||
|
channelId,
|
||||||
|
guildId
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticationProvider authentication={authentication}>
|
||||||
|
<GuildsProvider>
|
||||||
|
<GuildMemberProvider guildMember={guildMember} path={path}>
|
||||||
|
<MembersProviders path={path}>
|
||||||
|
<ChannelsProvider path={path}>
|
||||||
|
<Head title='Thream | Application' />
|
||||||
|
<Application
|
||||||
|
path={path}
|
||||||
|
guildLeftSidebar={<GuildLeftSidebar path={path} />}
|
||||||
|
title={`# ${selectedChannel.name}`}
|
||||||
|
>
|
||||||
|
<ChannelSettings channel={selectedChannel} />
|
||||||
|
</Application>
|
||||||
|
</ChannelsProvider>
|
||||||
|
</MembersProviders>
|
||||||
|
</GuildMemberProvider>
|
||||||
|
</GuildsProvider>
|
||||||
|
</AuthenticationProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = authenticationFromServerSide({
|
||||||
|
shouldBeAuthenticated: true,
|
||||||
|
fetchData: async (context, api) => {
|
||||||
|
const channelId = Number(context?.params?.channelId)
|
||||||
|
const guildId = Number(context?.params?.guildId)
|
||||||
|
if (isNaN(channelId) || isNaN(guildId)) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: '/404',
|
||||||
|
permanent: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { data: guildMember } = await api.get<GuildMember>(
|
||||||
|
`/guilds/${guildId}`
|
||||||
|
)
|
||||||
|
if (!guildMember.member.isOwner) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: '/404',
|
||||||
|
permanent: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { data: selectedChannelData } = await api.get(
|
||||||
|
`/channels/${channelId}`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
channelId,
|
||||||
|
guildId,
|
||||||
|
guildMember,
|
||||||
|
selectedChannel: selectedChannelData.channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ChannelSettingsPage
|
61
pages/application/[guildId]/channels/create.tsx
Normal file
61
pages/application/[guildId]/channels/create.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { NextPage } from 'next'
|
||||||
|
|
||||||
|
import { Head } from 'components/Head'
|
||||||
|
import { Application } from 'components/Application'
|
||||||
|
import {
|
||||||
|
authenticationFromServerSide,
|
||||||
|
AuthenticationProvider,
|
||||||
|
PagePropsWithAuthentication
|
||||||
|
} from 'tools/authentication'
|
||||||
|
import { CreateChannel } from 'components/Application/CreateChannel'
|
||||||
|
import { GuildsProvider } from 'contexts/Guilds'
|
||||||
|
import { GuildMember, GuildMemberProvider } from 'contexts/GuildMember'
|
||||||
|
|
||||||
|
export interface CreateChannelPageProps extends PagePropsWithAuthentication {
|
||||||
|
guildId: number
|
||||||
|
guildMember: GuildMember
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateChannelPage: NextPage<CreateChannelPageProps> = (props) => {
|
||||||
|
const { guildId, authentication, guildMember } = props
|
||||||
|
|
||||||
|
const path = { guildId }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticationProvider authentication={authentication}>
|
||||||
|
<GuildsProvider>
|
||||||
|
<GuildMemberProvider guildMember={guildMember} path={path}>
|
||||||
|
<Head
|
||||||
|
title={`Thream | Crée un channel`}
|
||||||
|
description={'Crée un nouveau channel'}
|
||||||
|
/>
|
||||||
|
<Application path={path} title={'Crée un channel'}>
|
||||||
|
<CreateChannel />
|
||||||
|
</Application>
|
||||||
|
</GuildMemberProvider>
|
||||||
|
</GuildsProvider>
|
||||||
|
</AuthenticationProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = authenticationFromServerSide({
|
||||||
|
shouldBeAuthenticated: true,
|
||||||
|
fetchData: async (context, api) => {
|
||||||
|
const guildId = Number(context?.params?.guildId)
|
||||||
|
if (isNaN(guildId)) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: '/404',
|
||||||
|
permanent: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { data: guildMember } = await api.get(`/guilds/${guildId}`)
|
||||||
|
return {
|
||||||
|
guildId,
|
||||||
|
guildMember
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default CreateChannelPage
|
60
pages/application/[guildId]/settings.tsx
Normal file
60
pages/application/[guildId]/settings.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { NextPage } from 'next'
|
||||||
|
|
||||||
|
import { Head } from 'components/Head'
|
||||||
|
import { Application } from 'components/Application'
|
||||||
|
import {
|
||||||
|
authenticationFromServerSide,
|
||||||
|
AuthenticationProvider,
|
||||||
|
PagePropsWithAuthentication
|
||||||
|
} from 'tools/authentication'
|
||||||
|
import { GuildMember, GuildMemberProvider } from 'contexts/GuildMember'
|
||||||
|
import { GuildsProvider } from 'contexts/Guilds'
|
||||||
|
import { GuildSettings } from 'components/Application/GuildSettings'
|
||||||
|
|
||||||
|
export interface GuildSettingsPageProps extends PagePropsWithAuthentication {
|
||||||
|
guildId: number
|
||||||
|
guildMember: GuildMember
|
||||||
|
}
|
||||||
|
|
||||||
|
const GuildSettingsPage: NextPage<GuildSettingsPageProps> = (props) => {
|
||||||
|
const { guildId, authentication, guildMember } = props
|
||||||
|
|
||||||
|
const path = { guildId }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticationProvider authentication={authentication}>
|
||||||
|
<GuildsProvider>
|
||||||
|
<GuildMemberProvider guildMember={guildMember} path={path}>
|
||||||
|
<Head title='Thream | Guild settings' />
|
||||||
|
<Application path={path} title='Guild settings'>
|
||||||
|
<GuildSettings />
|
||||||
|
</Application>
|
||||||
|
</GuildMemberProvider>
|
||||||
|
</GuildsProvider>
|
||||||
|
</AuthenticationProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = authenticationFromServerSide({
|
||||||
|
shouldBeAuthenticated: true,
|
||||||
|
fetchData: async (context, api) => {
|
||||||
|
const guildId = Number(context?.params?.guildId)
|
||||||
|
if (isNaN(guildId)) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: '/404',
|
||||||
|
permanent: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { data: guildMember } = await api.get<GuildMember>(
|
||||||
|
`/guilds/${guildId}`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
guildId,
|
||||||
|
guildMember
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default GuildSettingsPage
|
@ -39,7 +39,10 @@ export const handleSocketData = <T extends Item = Item>(
|
|||||||
case 'update': {
|
case 'update': {
|
||||||
const itemIndex = newItems.findIndex((item) => item.id === data.item.id)
|
const itemIndex = newItems.findIndex((item) => item.id === data.item.id)
|
||||||
if (itemIndex !== -1) {
|
if (itemIndex !== -1) {
|
||||||
newItems[itemIndex] = data.item
|
newItems[itemIndex] = {
|
||||||
|
...newItems[itemIndex],
|
||||||
|
...data.item
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user