diff --git a/.eslintrc.json b/.eslintrc.json index e59d069..0ae7648 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,6 +16,7 @@ }, "rules": { "prettier/prettier": "error", - "@next/next/no-img-element": "off" + "@next/next/no-img-element": "off", + "@typescript-eslint/no-misused-promises": "off" } } diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index 2801756..1bc15ca 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -2,7 +2,7 @@ name: 'Analyze' on: push: - branches: [master, develop] + branches: [develop] pull_request: branches: [master, develop] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31dc821..c824380 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: 'Build' on: push: - branches: [master, develop] + branches: [develop] pull_request: branches: [master, develop] diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8071e79..55a04d6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,7 +2,7 @@ name: 'Lint' on: push: - branches: [master, develop] + branches: [develop] pull_request: branches: [master, develop] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8eb3d59..a232455 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ name: 'Test' on: push: - branches: [master, develop] + branches: [develop] pull_request: branches: [master, develop] diff --git a/Dockerfile b/Dockerfile index d52d349..085cda7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,15 @@ -FROM node:16.13.2 AS dependencies +FROM node:16.14.0 AS dependencies WORKDIR /usr/src/app COPY ./package*.json ./ RUN npm install -FROM node:16.13.2 AS builder +FROM node:16.14.0 AS builder WORKDIR /usr/src/app COPY ./ ./ COPY --from=dependencies /usr/src/app/node_modules ./node_modules RUN npm run build -FROM node:16.13.2 AS runner +FROM node:16.14.0 AS runner WORKDIR /usr/src/app ENV NODE_ENV=production COPY --from=builder /usr/src/app/next.config.js ./next.config.js diff --git a/README.md b/README.md index e4b7c0b..d0daf7c 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,7 @@ 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). - -Using [Thream/api](https://github.com/Thream/api) v1.0.0. +It uses [Thream/api](https://github.com/Thream/api) v1.0.0. ## ⚙️ Getting Started diff --git a/components/Application/Application.tsx b/components/Application/Application.tsx index 9a079d3..d7f4148 100644 --- a/components/Application/Application.tsx +++ b/components/Application/Application.tsx @@ -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 = (props) => { {children} - {typeof path !== 'string' && ( + {isGuildsChannelsPath(path) && ( = (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 => { + try { + const { data } = + await authentication.api.delete( + `/channels/${channel.id}` + ) + await router.push(`/application/${guild.id}/${data.defaultChannelId}`) + } catch (error) { + setFetchState('error') + setMessageTranslationKey('errors:server-error') + } + } + + return ( +
+
+
+
+ +
+
+
+ +
+
+ + +
+ +
+
+ ) +} diff --git a/components/Application/ChannelSettings/index.ts b/components/Application/ChannelSettings/index.ts new file mode 100644 index 0000000..8bfe23a --- /dev/null +++ b/components/Application/ChannelSettings/index.ts @@ -0,0 +1 @@ +export * from './ChannelSettings' diff --git a/components/Application/ConfirmGuildJoin/ConfirmGuildJoin.tsx b/components/Application/ConfirmGuildJoin/ConfirmGuildJoin.tsx index 89695cd..dea7350 100644 --- a/components/Application/ConfirmGuildJoin/ConfirmGuildJoin.tsx +++ b/components/Application/ConfirmGuildJoin/ConfirmGuildJoin.tsx @@ -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 + handleNo: () => void | Promise } export const ConfirmGuildJoin: React.FC = (props) => { - const { className, handleJoinGuild } = props + const { className, handleYes, handleNo } = props + + const [isLoading, setIsLoading] = useState(false) + + const handleYesLoading = async (): Promise => { + setIsLoading((isLoading) => !isLoading) + await handleYes() + } return (
- Joing Guild Illustration -
-

Rejoindre la guild ?

-
- - -
-
+ {isLoading ? ( + + ) : ( + <> + Join Guild Illustration +
+

Rejoindre la guild ?

+
+ + +
+
+ + )}
) } diff --git a/components/Application/CreateChannel/CreateChannel.tsx b/components/Application/CreateChannel/CreateChannel.tsx new file mode 100644 index 0000000..358bdd1 --- /dev/null +++ b/components/Application/CreateChannel/CreateChannel.tsx @@ -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( + `/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 ( +
+
+ + +
+ +
+ ) +} diff --git a/components/Application/CreateChannel/index.ts b/components/Application/CreateChannel/index.ts new file mode 100644 index 0000000..b4a5a4f --- /dev/null +++ b/components/Application/CreateChannel/index.ts @@ -0,0 +1 @@ +export * from './CreateChannel' diff --git a/components/Application/GuildLeftSidebar/GuildLeftSidebar.tsx b/components/Application/GuildLeftSidebar/GuildLeftSidebar.tsx index 267ff3e..962d1b1 100644 --- a/components/Application/GuildLeftSidebar/GuildLeftSidebar.tsx +++ b/components/Application/GuildLeftSidebar/GuildLeftSidebar.tsx @@ -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 = (props) => { const { path } = props - const { guild } = useGuildMember() + const { guild, member } = useGuildMember() return (
@@ -26,12 +27,22 @@ export const GuildLeftSidebar: React.FC = (props) => {
- - - - - - + {member.isOwner && ( + + + + + + + + )} + + + + + + +
) diff --git a/components/Application/GuildSettings/GuildSettings.tsx b/components/Application/GuildSettings/GuildSettings.tsx new file mode 100644 index 0000000..d67efe6 --- /dev/null +++ b/components/Application/GuildSettings/GuildSettings.tsx @@ -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 = 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 => { + try { + await authentication.api.delete(`/guilds/${guild.id}`) + } catch (error) { + setFetchState('error') + setMessageTranslationKey('errors:server-error') + } + } + + const handleLeave = async (): Promise => { + try { + await authentication.api.delete(`/guilds/${guild.id}/members/leave`) + await router.push('/application') + } catch (error) { + setFetchState('error') + setMessageTranslationKey('errors:server-error') + } + } + + return ( +
+ {member.isOwner && ( +
+
+
+
+ +
+
+ Profil Picture +
+
+
+ +