feat: add guilds and channels CRUD (#14)
This commit is contained in:
@ -14,10 +14,17 @@ import { Members } from './Members'
|
||||
import { useAuthentication } from '../../tools/authentication'
|
||||
import { API_URL } from '../../tools/api'
|
||||
|
||||
export interface GuildsChannelsPath {
|
||||
guildId: number
|
||||
export interface ChannelsPath {
|
||||
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 =
|
||||
| '/application'
|
||||
@ -26,6 +33,7 @@ export type ApplicationPath =
|
||||
| `/application/users/${number}`
|
||||
| `/application/users/settings`
|
||||
| GuildsChannelsPath
|
||||
| GuildsPath
|
||||
|
||||
export interface ApplicationProps {
|
||||
path: ApplicationPath
|
||||
@ -216,7 +224,7 @@ export const Application: React.FC<ApplicationProps> = (props) => {
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{typeof path !== 'string' && (
|
||||
{isGuildsChannelsPath(path) && (
|
||||
<Sidebar
|
||||
direction='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 { useState } from 'react'
|
||||
|
||||
import { Loader } from '../../design/Loader'
|
||||
|
||||
export interface ConfirmGuildJoinProps {
|
||||
className?: string
|
||||
handleJoinGuild: () => void
|
||||
handleYes: () => void | Promise<void>
|
||||
handleNo: () => void | Promise<void>
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={className}>
|
||||
<Image
|
||||
src='/images/svg/design/join-guild.svg'
|
||||
alt='Joing Guild Illustration'
|
||||
height={150}
|
||||
width={150}
|
||||
/>
|
||||
<div className='mt-8 flex flex-col'>
|
||||
<h1 className='mb-6 text-center text-xl'>Rejoindre la guild ?</h1>
|
||||
<div className='flex gap-7'>
|
||||
<button
|
||||
className='rounded-3xl bg-success px-8 py-2 transition hover:opacity-50'
|
||||
onClick={handleJoinGuild}
|
||||
>
|
||||
Oui
|
||||
</button>
|
||||
<button
|
||||
className='rounded-3xl bg-error px-8 py-2 transition hover:opacity-50'
|
||||
onClick={handleJoinGuild}
|
||||
>
|
||||
Non
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<>
|
||||
<Image
|
||||
src='/images/svg/design/join-guild.svg'
|
||||
alt='Join Guild Illustration'
|
||||
height={150}
|
||||
width={150}
|
||||
/>
|
||||
<div className='mt-8 flex flex-col'>
|
||||
<h1 className='mb-6 text-center text-xl'>Rejoindre la guild ?</h1>
|
||||
<div className='flex gap-7'>
|
||||
<button
|
||||
className='rounded-3xl bg-success px-8 py-2 transition hover:opacity-50'
|
||||
onClick={handleYesLoading}
|
||||
>
|
||||
Oui
|
||||
</button>
|
||||
<button
|
||||
className='rounded-3xl bg-error px-8 py-2 transition hover:opacity-50'
|
||||
onClick={handleNo}
|
||||
>
|
||||
Non
|
||||
</button>
|
||||
</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 { useGuildMember } from '../../../contexts/GuildMember'
|
||||
@ -13,7 +14,7 @@ export interface GuildLeftSidebarProps {
|
||||
export const GuildLeftSidebar: React.FC<GuildLeftSidebarProps> = (props) => {
|
||||
const { path } = props
|
||||
|
||||
const { guild } = useGuildMember()
|
||||
const { guild, member } = useGuildMember()
|
||||
|
||||
return (
|
||||
<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} />
|
||||
<Divider />
|
||||
<div className='mb-1 flex items-center justify-center space-x-6 p-2'>
|
||||
<IconButton className='h-10 w-10' title='Add a Channel'>
|
||||
<PlusIcon />
|
||||
</IconButton>
|
||||
<IconButton className='h-7 w-7' title='Settings'>
|
||||
<CogIcon />
|
||||
</IconButton>
|
||||
{member.isOwner && (
|
||||
<Link href={`/application/${path.guildId}/channels/create`} passHref>
|
||||
<a data-cy='link-add-channel'>
|
||||
<IconButton className='h-10 w-10' title='Add a Channel'>
|
||||
<PlusIcon />
|
||||
</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>
|
||||
)
|
||||
|
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 { useGuilds } from '../../../contexts/Guilds'
|
||||
import { GuildsChannelsPath } from '..'
|
||||
import { GuildsPath } from '..'
|
||||
import { Guild } from './Guild'
|
||||
|
||||
export interface GuildsProps {
|
||||
path: GuildsChannelsPath | string
|
||||
path: GuildsPath | string
|
||||
}
|
||||
|
||||
export const Guilds: React.FC<GuildsProps> = (props) => {
|
||||
|
@ -1,12 +1,19 @@
|
||||
import { useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import classNames from 'classnames'
|
||||
import axios from 'axios'
|
||||
|
||||
import { Emoji } from 'components/Emoji'
|
||||
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 {
|
||||
guild: GuildPublicType
|
||||
@ -14,10 +21,37 @@ export interface GuildPublicProps {
|
||||
|
||||
export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
|
||||
const { guild } = props
|
||||
const router = useRouter()
|
||||
const { authentication } = useAuthentication()
|
||||
const [isConfirmed, setIsConfirmed] = useState(false)
|
||||
|
||||
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 (
|
||||
<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
|
||||
@ -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',
|
||||
{ '-translate-x-full': isConfirmed }
|
||||
)}
|
||||
onClick={() => setIsConfirmed(!isConfirmed)}
|
||||
onClick={handleIsConfirmed}
|
||||
>
|
||||
<Image
|
||||
className='rounded-full'
|
||||
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'
|
||||
width={80}
|
||||
@ -65,7 +101,8 @@ export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
|
||||
'!left-0': isConfirmed
|
||||
}
|
||||
)}
|
||||
handleJoinGuild={() => setIsConfirmed(!isConfirmed)}
|
||||
handleYes={handleYes}
|
||||
handleNo={handleIsConfirmed}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -7,6 +7,7 @@ import { GuildPublic as GuildPublicType } from '../../../models/Guild'
|
||||
import { Loader } from '../../design/Loader'
|
||||
import { GuildPublic } from './GuildPublic'
|
||||
import { usePagination } from '../../../hooks/usePagination'
|
||||
import { SocketData, handleSocketData } from '../../../tools/handleSocketData'
|
||||
|
||||
export const JoinGuildsPublic: React.FC = () => {
|
||||
const [search, setSearch] = useState('')
|
||||
@ -14,12 +15,22 @@ export const JoinGuildsPublic: React.FC = () => {
|
||||
const { authentication } = useAuthentication()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { items, hasMore, nextPage, resetPagination } =
|
||||
const { items, hasMore, nextPage, resetPagination, setItems } =
|
||||
usePagination<GuildPublicType>({
|
||||
api: authentication.api,
|
||||
url: '/guilds/public'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
authentication.socket.on('guilds', (data: SocketData<GuildPublicType>) => {
|
||||
handleSocketData({ data, setItems })
|
||||
})
|
||||
|
||||
return () => {
|
||||
authentication.socket.off('guilds')
|
||||
}
|
||||
}, [authentication.socket, setItems])
|
||||
|
||||
useEffect(() => {
|
||||
resetPagination()
|
||||
nextPage({ search })
|
||||
|
@ -8,6 +8,13 @@ import { MessageWithMember } from '../../../../../models/Message'
|
||||
import { Loader } from '../../../../design/Loader'
|
||||
import { FileIcon } from './FileIcon'
|
||||
|
||||
const supportedImageMimetype = [
|
||||
'image/png',
|
||||
'image/jpg',
|
||||
'image/jpeg',
|
||||
'image/gif'
|
||||
]
|
||||
|
||||
export interface FileData {
|
||||
blob: Blob
|
||||
url: string
|
||||
@ -44,7 +51,7 @@ export const MessageFile: React.FC<MessageContentProps> = (props) => {
|
||||
if (file == null) {
|
||||
return <Loader />
|
||||
}
|
||||
if (message.mimetype.startsWith('image/')) {
|
||||
if (supportedImageMimetype.includes(message.mimetype)) {
|
||||
return (
|
||||
<a href={file.url} target='_blank' rel='noreferrer'>
|
||||
<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',
|
||||
{ 'left-[0%]': confirmation }
|
||||
)}
|
||||
handleJoinGuild={handleConfirmationState}
|
||||
handleYes={handleConfirmationState}
|
||||
handleNo={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<XIcon
|
||||
|
@ -133,6 +133,7 @@ export const UserSettings: React.FC = () => {
|
||||
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async (
|
||||
event
|
||||
) => {
|
||||
setFetchState('loading')
|
||||
const files = event?.target?.files
|
||||
if (files != null && files.length === 1) {
|
||||
const file = files[0]
|
||||
@ -149,6 +150,7 @@ export const UserSettings: React.FC = () => {
|
||||
logo: data.user.logo
|
||||
}
|
||||
})
|
||||
setFetchState('idle')
|
||||
} catch (error) {
|
||||
setFetchState('error')
|
||||
setMessageTranslationKey('errors:server-error')
|
||||
|
@ -2,18 +2,40 @@ import { forwardRef } from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
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>(
|
||||
(props, reference) => {
|
||||
const { children, className: givenClassName, ...rest } = props
|
||||
const {
|
||||
children,
|
||||
className: givenClassName,
|
||||
color = 'green',
|
||||
...rest
|
||||
} = props
|
||||
|
||||
return (
|
||||
<a
|
||||
ref={reference}
|
||||
className={classNames(className, givenClassName)}
|
||||
className={classNames(
|
||||
className,
|
||||
{
|
||||
[classNameGreen]: color === 'green',
|
||||
[classNameRed]: color === 'red'
|
||||
},
|
||||
givenClassName
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
@ -24,13 +46,30 @@ export const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
||||
|
||||
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) => {
|
||||
const { children, className: givenClassName, ...rest } = props
|
||||
const {
|
||||
children,
|
||||
className: givenClassName,
|
||||
color = 'green',
|
||||
...rest
|
||||
} = props
|
||||
|
||||
return (
|
||||
<button className={classNames(className, givenClassName)} {...rest}>
|
||||
<button
|
||||
className={classNames(
|
||||
className,
|
||||
{
|
||||
[classNameGreen]: color === 'green',
|
||||
[classNameRed]: color === 'red'
|
||||
},
|
||||
givenClassName
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
Reference in New Issue
Block a user