From a0fa66e8f596b6ddf40c8c7e1a3c61541ac59fe7 Mon Sep 17 00:00:00 2001 From: Divlo Date: Sun, 24 Oct 2021 06:09:43 +0200 Subject: [PATCH] feat: design applications and first api calls Co-authored-by: Walid <87608619+WalidKorchi@users.noreply.github.com> --- .env.example | 1 + .github/workflows/release.yml | 21 +- .lighthouserc.json | 7 +- .releaserc.json | 37 + .storybook/preview.js | 6 +- README.md | 7 +- components/Application/Application.tsx | 257 + .../Application/Channels/Channels.stories.tsx | 15 + .../Application/Channels/Channels.test.tsx | 12 + components/Application/Channels/Channels.tsx | 36 + components/Application/Channels/index.ts | 1 + .../Application/Guilds/Guilds.stories.tsx | 15 + components/Application/Guilds/Guilds.test.tsx | 12 + components/Application/Guilds/Guilds.tsx | 34 + components/Application/Guilds/index.ts | 1 + .../Application/Members/Members.stories.tsx | 14 + .../Application/Members/Members.test.tsx | 10 + components/Application/Members/Members.tsx | 57 + components/Application/Members/index.ts | 1 + .../Application/Messages/Messages.stories.tsx | 12 + .../Application/Messages/Messages.test.tsx | 10 + components/Application/Messages/Messages.tsx | 82 + components/Application/Messages/index.ts | 1 + .../PopupGuild/PopupGuild.stories.tsx | 15 + .../PopupGuild/PopupGuild.test.tsx | 10 + .../Application/PopupGuild/PopupGuild.tsx | 56 + .../PopupGuildCard/PopupGuildCard.stories.tsx | 36 + .../PopupGuildCard/PopupGuildCard.test.tsx | 29 + .../PopupGuildCard/PopupGuildCard.tsx | 32 + .../PopupGuild/PopupGuildCard/index.ts | 1 + components/Application/PopupGuild/index.ts | 1 + .../Application/Sidebar/Sidebar.stories.tsx | 14 + .../Application/Sidebar/Sidebar.test.tsx | 12 + components/Application/Sidebar/Sidebar.tsx | 36 + components/Application/Sidebar/index.ts | 1 + components/Application/index.ts | 1 + components/Authentication/Authentication.tsx | 194 + .../Authentication/AuthenticationForm.tsx | 16 + .../getErrorTranslationKey.test.ts | 71 + .../Authentication/getErrorTranslationKey.ts | 19 + components/Authentication/index.ts | 2 + components/Footer/Footer.stories.tsx | 15 + components/Footer/Footer.test.tsx | 16 + components/Footer/Footer.tsx | 14 +- components/Footer/VersionLink.tsx | 19 + .../UserProfile/UserProfile.stories.tsx | 21 + components/UserProfile/UserProfile.test.tsx | 14 + components/UserProfile/UserProfile.tsx | 191 + components/UserProfile/index.ts | 1 + components/design/Divider/Divider.stories.tsx | 12 + components/design/Divider/Divider.test.tsx | 10 + components/design/Divider/Divider.tsx | 7 + components/design/Divider/index.ts | 1 + .../design/FormState/FormState.test.tsx | 34 + components/design/FormState/FormState.tsx | 56 + components/design/FormState/index.ts | 1 + .../design/IconButton/IconButton.test.tsx | 15 + components/design/IconButton/IconButton.tsx | 20 + components/design/IconButton/index.ts | 1 + components/design/IconLink/IconLink.test.tsx | 10 + components/design/IconLink/IconLink.tsx | 32 + components/design/IconLink/index.ts | 1 + components/design/Input/Input.stories.tsx | 30 + components/design/Input/Input.test.tsx | 52 + components/design/Input/Input.tsx | 90 + components/design/Input/index.ts | 1 + components/design/Loader/Loader.stories.tsx | 14 + components/design/Loader/Loader.test.tsx | 20 + components/design/Loader/Loader.tsx | 81 + components/design/Loader/index.ts | 1 + contexts/Channels.tsx | 15 + cypress.json | 2 - cypress/fixtures/handler.ts | 18 + cypress/fixtures/users/current/get.ts | 19 + cypress/fixtures/users/refresh-token/post.ts | 14 + cypress/fixtures/users/reset-password/post.ts | 10 + cypress/fixtures/users/reset-password/put.ts | 23 + cypress/fixtures/users/signin/post.ts | 28 + cypress/fixtures/users/signup/post.ts | 30 + cypress/fixtures/users/user.ts | 26 + .../application/[guildId]/[channelId].spec.ts | 17 + .../pages/application/index.spec.ts | 35 + .../authentication/forgot-password.spec.ts | 37 + .../authentication/reset-password.spec.ts | 45 + .../pages/authentication/signin.spec.ts | 55 + .../pages/authentication/signup.spec.ts | 85 + cypress/integration/pages/index.spec.ts | 12 + cypress/plugins/index.js | 40 + hooks/useFormState.tsx | 15 + i18n.json | 7 +- locales/en/application.json | 3 + locales/en/authentication.json | 17 + locales/en/errors.json | 7 +- locales/en/home.json | 2 +- locales/fr/application.json | 3 + locales/fr/authentication.json | 17 + locales/fr/errors.json | 7 +- locales/fr/home.json | 2 +- models/Channel.ts | 13 + models/Guild.ts | 12 + models/Member.ts | 12 + models/Message.ts | 20 + models/OAuth.ts | 25 + models/RefreshToken.ts | 11 + models/User.ts | 51 + models/UserSettings.ts | 24 + models/utils.ts | 14 + package-lock.json | 12725 ++++++++++++++-- package.json | 19 +- pages/404.tsx | 13 +- pages/500.tsx | 13 +- pages/application/[guildId]/[channelId].tsx | 53 + pages/application/guilds/create.tsx | 84 + pages/application/guilds/join.tsx | 61 + pages/application/index.tsx | 25 + pages/application/users/[userId].tsx | 26 + pages/authentication/forgot-password.tsx | 105 + pages/authentication/reset-password.tsx | 104 + pages/authentication/signin.tsx | 33 + pages/authentication/signup.tsx | 33 + pages/index.tsx | 20 +- public/images/data/divlo.png | Bin 0 -> 41784 bytes public/images/data/guild-default.png | Bin 0 -> 2434 bytes public/images/guilds/Guild_1.svg | 9 + public/images/guilds/Guild_2.svg | 9 + public/images/guilds/Guild_3.svg | 9 + public/images/guilds/Guild_4.svg | 9 + public/images/guilds/Guild_5.svg | 9 + public/images/guilds/Guild_6.svg | 9 + utils/ajv.ts | 26 + utils/api.ts | 13 + utils/authentication/Authentication.ts | 105 + .../authentication/AuthenticationContext.tsx | 54 + .../authenticationFromServerSide.ts | 88 + utils/authentication/index.ts | 24 + utils/cookies.ts | 29 + 136 files changed, 14787 insertions(+), 1668 deletions(-) create mode 100644 .releaserc.json create mode 100644 components/Application/Application.tsx create mode 100644 components/Application/Channels/Channels.stories.tsx create mode 100644 components/Application/Channels/Channels.test.tsx create mode 100644 components/Application/Channels/Channels.tsx create mode 100644 components/Application/Channels/index.ts create mode 100644 components/Application/Guilds/Guilds.stories.tsx create mode 100644 components/Application/Guilds/Guilds.test.tsx create mode 100644 components/Application/Guilds/Guilds.tsx create mode 100644 components/Application/Guilds/index.ts create mode 100644 components/Application/Members/Members.stories.tsx create mode 100644 components/Application/Members/Members.test.tsx create mode 100644 components/Application/Members/Members.tsx create mode 100644 components/Application/Members/index.ts create mode 100644 components/Application/Messages/Messages.stories.tsx create mode 100644 components/Application/Messages/Messages.test.tsx create mode 100644 components/Application/Messages/Messages.tsx create mode 100644 components/Application/Messages/index.ts create mode 100644 components/Application/PopupGuild/PopupGuild.stories.tsx create mode 100644 components/Application/PopupGuild/PopupGuild.test.tsx create mode 100644 components/Application/PopupGuild/PopupGuild.tsx create mode 100644 components/Application/PopupGuild/PopupGuildCard/PopupGuildCard.stories.tsx create mode 100644 components/Application/PopupGuild/PopupGuildCard/PopupGuildCard.test.tsx create mode 100644 components/Application/PopupGuild/PopupGuildCard/PopupGuildCard.tsx create mode 100644 components/Application/PopupGuild/PopupGuildCard/index.ts create mode 100644 components/Application/PopupGuild/index.ts create mode 100644 components/Application/Sidebar/Sidebar.stories.tsx create mode 100644 components/Application/Sidebar/Sidebar.test.tsx create mode 100644 components/Application/Sidebar/Sidebar.tsx create mode 100644 components/Application/Sidebar/index.ts create mode 100644 components/Application/index.ts create mode 100644 components/Authentication/Authentication.tsx create mode 100644 components/Authentication/AuthenticationForm.tsx create mode 100644 components/Authentication/getErrorTranslationKey.test.ts create mode 100644 components/Authentication/getErrorTranslationKey.ts create mode 100644 components/Authentication/index.ts create mode 100644 components/Footer/Footer.stories.tsx create mode 100644 components/Footer/Footer.test.tsx create mode 100644 components/Footer/VersionLink.tsx create mode 100644 components/UserProfile/UserProfile.stories.tsx create mode 100644 components/UserProfile/UserProfile.test.tsx create mode 100644 components/UserProfile/UserProfile.tsx create mode 100644 components/UserProfile/index.ts create mode 100644 components/design/Divider/Divider.stories.tsx create mode 100644 components/design/Divider/Divider.test.tsx create mode 100644 components/design/Divider/Divider.tsx create mode 100644 components/design/Divider/index.ts create mode 100644 components/design/FormState/FormState.test.tsx create mode 100644 components/design/FormState/FormState.tsx create mode 100644 components/design/FormState/index.ts create mode 100644 components/design/IconButton/IconButton.test.tsx create mode 100644 components/design/IconButton/IconButton.tsx create mode 100644 components/design/IconButton/index.ts create mode 100644 components/design/IconLink/IconLink.test.tsx create mode 100644 components/design/IconLink/IconLink.tsx create mode 100644 components/design/IconLink/index.ts create mode 100644 components/design/Input/Input.stories.tsx create mode 100644 components/design/Input/Input.test.tsx create mode 100644 components/design/Input/Input.tsx create mode 100644 components/design/Input/index.ts create mode 100644 components/design/Loader/Loader.stories.tsx create mode 100644 components/design/Loader/Loader.test.tsx create mode 100644 components/design/Loader/Loader.tsx create mode 100644 components/design/Loader/index.ts create mode 100644 contexts/Channels.tsx create mode 100644 cypress/fixtures/handler.ts create mode 100644 cypress/fixtures/users/current/get.ts create mode 100644 cypress/fixtures/users/refresh-token/post.ts create mode 100644 cypress/fixtures/users/reset-password/post.ts create mode 100644 cypress/fixtures/users/reset-password/put.ts create mode 100644 cypress/fixtures/users/signin/post.ts create mode 100644 cypress/fixtures/users/signup/post.ts create mode 100644 cypress/fixtures/users/user.ts create mode 100644 cypress/integration/pages/application/[guildId]/[channelId].spec.ts create mode 100644 cypress/integration/pages/application/index.spec.ts create mode 100644 cypress/integration/pages/authentication/forgot-password.spec.ts create mode 100644 cypress/integration/pages/authentication/reset-password.spec.ts create mode 100644 cypress/integration/pages/authentication/signin.spec.ts create mode 100644 cypress/integration/pages/authentication/signup.spec.ts create mode 100644 cypress/integration/pages/index.spec.ts create mode 100644 cypress/plugins/index.js create mode 100644 hooks/useFormState.tsx create mode 100644 locales/en/application.json create mode 100644 locales/en/authentication.json create mode 100644 locales/fr/application.json create mode 100644 locales/fr/authentication.json create mode 100644 models/Channel.ts create mode 100644 models/Guild.ts create mode 100644 models/Member.ts create mode 100644 models/Message.ts create mode 100644 models/OAuth.ts create mode 100644 models/RefreshToken.ts create mode 100644 models/User.ts create mode 100644 models/UserSettings.ts create mode 100644 models/utils.ts create mode 100644 pages/application/[guildId]/[channelId].tsx create mode 100644 pages/application/guilds/create.tsx create mode 100644 pages/application/guilds/join.tsx create mode 100644 pages/application/index.tsx create mode 100644 pages/application/users/[userId].tsx create mode 100644 pages/authentication/forgot-password.tsx create mode 100644 pages/authentication/reset-password.tsx create mode 100644 pages/authentication/signin.tsx create mode 100644 pages/authentication/signup.tsx create mode 100644 public/images/data/divlo.png create mode 100644 public/images/data/guild-default.png create mode 100644 public/images/guilds/Guild_1.svg create mode 100644 public/images/guilds/Guild_2.svg create mode 100644 public/images/guilds/Guild_3.svg create mode 100644 public/images/guilds/Guild_4.svg create mode 100644 public/images/guilds/Guild_5.svg create mode 100644 public/images/guilds/Guild_6.svg create mode 100644 utils/ajv.ts create mode 100644 utils/api.ts create mode 100644 utils/authentication/Authentication.ts create mode 100644 utils/authentication/AuthenticationContext.tsx create mode 100644 utils/authentication/authenticationFromServerSide.ts create mode 100644 utils/authentication/index.ts create mode 100644 utils/cookies.ts diff --git a/.env.example b/.env.example index 5cd5d57..368ac4a 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ COMPOSE_PROJECT_NAME=thream-website +NEXT_PUBLIC_API_URL=http://localhost:8080 PORT=3000 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f27d0dc..9e8b1d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,10 +8,20 @@ jobs: release: runs-on: 'ubuntu-latest' steps: - - uses: 'actions/checkout@v2.3.5' + - uses: 'actions/checkout@v2.3.4' + with: + fetch-depth: 0 + persist-credentials: false + + - name: 'Import GPG key' + uses: 'crazy-max/ghaction-import-gpg@v4' + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + git_user_signingkey: true + git_commit_gpgsign: true - name: 'Use Node.js' - uses: 'actions/setup-node@v2.4.1' + uses: 'actions/setup-node@v2.4.0' with: node-version: '16.x' cache: 'npm' @@ -19,6 +29,13 @@ jobs: - name: 'Install' run: 'npm install' + - name: 'Release' + run: 'npm run release' + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }} + GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }} + - name: 'Deploy to Vercel' run: 'npm run deploy -- --token="${VERCEL_TOKEN}" --prod' env: diff --git a/.lighthouserc.json b/.lighthouserc.json index 597e069..0d2a96d 100644 --- a/.lighthouserc.json +++ b/.lighthouserc.json @@ -4,7 +4,12 @@ "startServerCommand": "npm run start", "startServerReadyPattern": "ready on", "startServerReadyTimeout": 20000, - "url": ["http://localhost:3000/"], + "url": [ + "http://localhost:3000/", + "http://localhost:3000/authentication/signin", + "http://localhost:3000/authentication/signup", + "http://localhost:3000/authentication/forgot-password" + ], "numberOfRuns": 1 }, "assert": { diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..41ee84f --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,37 @@ +{ + "branches": ["master"], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits" + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits" + } + ], + [ + "@semantic-release/npm", + { + "npmPublish": false + } + ], + [ + "@semantic-release/git", + { + "assets": ["package.json", "package-lock.json"], + "message": "chore(release): ${nextRelease.version} [skip ci]" + } + ], + "@semantic-release/github", + [ + "@saithodev/semantic-release-backmerge", + { + "backmergeStrategy": "merge" + } + ] + ] +} diff --git a/.storybook/preview.js b/.storybook/preview.js index aa7f512..f45656a 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -3,6 +3,8 @@ import I18nProvider from 'next-translate/I18nProvider' import i18n from '../i18n.json' import common from '../locales/en/common.json' +import authentication from '../locales/en/authentication.json' +import application from '../locales/en/application.json' import '../styles/global.css' @@ -18,7 +20,9 @@ addDecorator((story) => ( diff --git a/README.md b/README.md index 37ae675..dc7c35b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@
Conventional Commits + semantic-release Dependabot badge

@@ -20,12 +21,14 @@ 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. + ## ⚙️ Getting Started ### Prerequisites -- [Node.js](https://nodejs.org/) >= 14.0.0 -- [npm](https://www.npmjs.com/) >= 7.0.0 +- [Node.js](https://nodejs.org/) >= 16.0.0 +- [npm](https://www.npmjs.com/) >= 8.0.0 ### Installation diff --git a/components/Application/Application.tsx b/components/Application/Application.tsx new file mode 100644 index 0000000..944f74f --- /dev/null +++ b/components/Application/Application.tsx @@ -0,0 +1,257 @@ +import { useState, useEffect, useMemo } from 'react' +import Image from 'next/image' +import { + CogIcon, + PlusIcon, + MenuIcon, + UsersIcon, + XIcon +} from '@heroicons/react/solid' +import classNames from 'classnames' +import { useMediaQuery } from 'react-responsive' +import { useSwipeable } from 'react-swipeable' + +import { Sidebar, DirectionSidebar } from './Sidebar' +import { IconButton } from 'components/design/IconButton' +import { IconLink } from 'components/design/IconLink' +import { Channels } from './Channels' +import { Guilds } from './Guilds/Guilds' +import { Divider } from '../design/Divider' +import { Members } from './Members' +import { useAuthentication } from 'utils/authentication' + +export interface GuildsChannelsPath { + guildId: number + channelId: number +} + +export interface ApplicationProps { + path: + | '/application' + | '/application/guilds/join' + | '/application/guilds/create' + | '/application/users/[userId]' + | GuildsChannelsPath +} + +export const Application: React.FC = (props) => { + const { children, path } = props + + const { user } = useAuthentication() + + const [visibleSidebars, setVisibleSidebars] = useState({ + left: true, + right: false + }) + + const [mounted, setMounted] = useState(false) + + const isMobile = useMediaQuery({ + query: '(max-width: 900px)' + }) + + const handleToggleSidebars = (direction: DirectionSidebar): void => { + if (!isMobile) { + if (direction === 'left') { + return setVisibleSidebars({ + ...visibleSidebars, + left: !visibleSidebars.left + }) + } + if (direction === 'right') { + return setVisibleSidebars({ + ...visibleSidebars, + right: !visibleSidebars.right + }) + } + } else { + if (direction === 'right' && visibleSidebars.left) { + return setVisibleSidebars({ + left: false, + right: true + }) + } + if (direction === 'left' && visibleSidebars.right) { + return setVisibleSidebars({ + left: true, + right: false + }) + } + if (direction === 'left' && !visibleSidebars.right) { + return setVisibleSidebars({ + ...visibleSidebars, + left: !visibleSidebars.left + }) + } + if (direction === 'right' && !visibleSidebars.left) { + return setVisibleSidebars({ + ...visibleSidebars, + right: !visibleSidebars.right + }) + } + } + handleCloseSidebars() + } + + const handleCloseSidebars = (): void => { + if (isMobile && (visibleSidebars.left || visibleSidebars.right)) { + setVisibleSidebars({ + left: false, + right: false + }) + } + } + + const swipeableHandlers = useSwipeable({ + trackMouse: false, + trackTouch: true, + preventDefaultTouchmoveEvent: true, + onSwipedRight: () => { + if (visibleSidebars.right) { + return setVisibleSidebars({ ...visibleSidebars, right: false }) + } + setVisibleSidebars({ + ...visibleSidebars, + left: true + }) + }, + onSwipedLeft: () => { + if (visibleSidebars.left) { + return setVisibleSidebars({ ...visibleSidebars, left: false }) + } + setVisibleSidebars({ + ...visibleSidebars, + right: true + }) + } + }) + + const title = useMemo(() => { + if (typeof path !== 'string') { + // TODO: Returns the real name of the channel when doing APIs calls + return `# Channel ${path.channelId}` + } + if (path.startsWith('/application/users/')) { + return 'Settings' + } + if (path === '/application/guilds/join') { + return 'Join a Guild' + } + if (path === '/application/guilds/create') { + return 'Create a Guild' + } + return 'Application' + }, [path]) + + useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) { + return null + } + + return ( + <> +
+ handleToggleSidebars('left')} + > + {!visibleSidebars.left ? : } + +
+ {title} +
+
+ {title.startsWith('#') && ( + handleToggleSidebars('right')} + > + {!visibleSidebars.right ? : } + + )} +
+
+ +
+ +
+ + logo + + + + + + +
+ + {typeof path !== 'string' && ( +
+
+

Guild Name

+
+ +
+ +
+ +
+ + + + + + +
+
+ )} +
+ +
+ {children} +
+ + + + +
+ + ) +} diff --git a/components/Application/Channels/Channels.stories.tsx b/components/Application/Channels/Channels.stories.tsx new file mode 100644 index 0000000..5f53bd2 --- /dev/null +++ b/components/Application/Channels/Channels.stories.tsx @@ -0,0 +1,15 @@ +import { Meta, Story } from '@storybook/react' + +import { Channels as Component, ChannelsProps } from './' + +const Stories: Meta = { + title: 'Channels', + component: Component +} + +export default Stories + +export const Channels: Story = (arguments_) => ( + +) +Channels.args = { path: { channelId: 1, guildId: 2 } } diff --git a/components/Application/Channels/Channels.test.tsx b/components/Application/Channels/Channels.test.tsx new file mode 100644 index 0000000..0b15616 --- /dev/null +++ b/components/Application/Channels/Channels.test.tsx @@ -0,0 +1,12 @@ +import { render } from '@testing-library/react' + +import { Channels } from './' + +describe('', () => { + it('should render successfully', () => { + const { baseElement } = render( + + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/components/Application/Channels/Channels.tsx b/components/Application/Channels/Channels.tsx new file mode 100644 index 0000000..55066ce --- /dev/null +++ b/components/Application/Channels/Channels.tsx @@ -0,0 +1,36 @@ +import Link from 'next/link' +import classNames from 'classnames' + +import { GuildsChannelsPath } from '../Application' + +export interface ChannelsProps { + path: GuildsChannelsPath +} + +export const Channels: React.FC = (props) => { + const { path } = props + + return ( + + ) +} diff --git a/components/Application/Channels/index.ts b/components/Application/Channels/index.ts new file mode 100644 index 0000000..e39effa --- /dev/null +++ b/components/Application/Channels/index.ts @@ -0,0 +1 @@ +export * from './Channels' diff --git a/components/Application/Guilds/Guilds.stories.tsx b/components/Application/Guilds/Guilds.stories.tsx new file mode 100644 index 0000000..4c934ee --- /dev/null +++ b/components/Application/Guilds/Guilds.stories.tsx @@ -0,0 +1,15 @@ +import { Meta, Story } from '@storybook/react' + +import { Guilds as Component, GuildsProps } from './' + +const Stories: Meta = { + title: 'Guilds', + component: Component +} + +export default Stories + +export const Guilds: Story = (arguments_) => ( + +) +Guilds.args = { path: { channelId: 1, guildId: 2 } } diff --git a/components/Application/Guilds/Guilds.test.tsx b/components/Application/Guilds/Guilds.test.tsx new file mode 100644 index 0000000..80f4554 --- /dev/null +++ b/components/Application/Guilds/Guilds.test.tsx @@ -0,0 +1,12 @@ +import { render } from '@testing-library/react' + +import { Guilds } from './' + +describe('', () => { + it('should render successfully', () => { + const { baseElement } = render( + + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/components/Application/Guilds/Guilds.tsx b/components/Application/Guilds/Guilds.tsx new file mode 100644 index 0000000..03283da --- /dev/null +++ b/components/Application/Guilds/Guilds.tsx @@ -0,0 +1,34 @@ +import Image from 'next/image' + +import { ApplicationProps } from '../Application' +import { IconLink } from '../../design/IconLink' + +export interface GuildsProps extends ApplicationProps {} + +export const Guilds: React.FC = (props) => { + const { path } = props + + return ( +
+ {new Array(100).fill(null).map((_, index) => { + return ( + +
+ logo +
+
+ ) + })} +
+ ) +} diff --git a/components/Application/Guilds/index.ts b/components/Application/Guilds/index.ts new file mode 100644 index 0000000..2af7ca2 --- /dev/null +++ b/components/Application/Guilds/index.ts @@ -0,0 +1 @@ +export * from './Guilds' diff --git a/components/Application/Members/Members.stories.tsx b/components/Application/Members/Members.stories.tsx new file mode 100644 index 0000000..4e1ee5e --- /dev/null +++ b/components/Application/Members/Members.stories.tsx @@ -0,0 +1,14 @@ +import { Meta, Story } from '@storybook/react' + +import { Members as Component } from './Members' + +const Stories: Meta = { + title: 'Members', + component: Component +} + +export default Stories + +export const Members: Story = (arguments_) => { + return +} diff --git a/components/Application/Members/Members.test.tsx b/components/Application/Members/Members.test.tsx new file mode 100644 index 0000000..37936ab --- /dev/null +++ b/components/Application/Members/Members.test.tsx @@ -0,0 +1,10 @@ +import { render } from '@testing-library/react' + +import { Members } from './Members' + +describe('', () => { + it('should render successfully', () => { + const { baseElement } = render() + expect(baseElement).toBeTruthy() + }) +}) diff --git a/components/Application/Members/Members.tsx b/components/Application/Members/Members.tsx new file mode 100644 index 0000000..aeda557 --- /dev/null +++ b/components/Application/Members/Members.tsx @@ -0,0 +1,57 @@ +import Image from 'next/image' + +import { Divider } from '../../design/Divider' + +export const Members: React.FC = () => { + return ( + <> +
+

Members

+ +
+
+
+ {"Users's +
+
+

+ Walidouxssssssssssss +

+ Online +
+
+ {new Array(100).fill(null).map((_, index) => { + return ( +
+
+ {"Users's +
+
+

+ Walidouxssssssssssssssssssssssssssssss +

+ Offline +
+
+ ) + })} + + ) +} diff --git a/components/Application/Members/index.ts b/components/Application/Members/index.ts new file mode 100644 index 0000000..4f33cc9 --- /dev/null +++ b/components/Application/Members/index.ts @@ -0,0 +1 @@ +export * from './Members' diff --git a/components/Application/Messages/Messages.stories.tsx b/components/Application/Messages/Messages.stories.tsx new file mode 100644 index 0000000..75fb548 --- /dev/null +++ b/components/Application/Messages/Messages.stories.tsx @@ -0,0 +1,12 @@ +import { Meta, Story } from '@storybook/react' + +import { Messages as Component } from './' + +const Stories: Meta = { + title: 'Messages', + component: Component +} + +export default Stories + +export const Messages: Story = (arguments_) => diff --git a/components/Application/Messages/Messages.test.tsx b/components/Application/Messages/Messages.test.tsx new file mode 100644 index 0000000..ffa2468 --- /dev/null +++ b/components/Application/Messages/Messages.test.tsx @@ -0,0 +1,10 @@ +import { render } from '@testing-library/react' + +import { Messages } from './' + +describe('', () => { + it('should render successfully', () => { + const { baseElement } = render() + expect(baseElement).toBeTruthy() + }) +}) diff --git a/components/Application/Messages/Messages.tsx b/components/Application/Messages/Messages.tsx new file mode 100644 index 0000000..057b2cd --- /dev/null +++ b/components/Application/Messages/Messages.tsx @@ -0,0 +1,82 @@ +import Image from 'next/image' +import TextareaAutosize from 'react-textarea-autosize' + +export const Messages: React.FC = () => { + return ( + <> +
+ {new Array(20).fill(null).map((_, index) => { + return ( +
+
+
+ logo +
+
+
+
+ + Divlo + + + 06/04/2021 - 22:28:40 + +
+
+

Message {index}

+

+ Lorem ipsum dolor sit, amet consectetur adipisicing elit. + Eum debitis voluptatum itaque quaerat. Nemo optio voluptas + quas mollitia rerum commodi laboriosam voluptates et sit + quo. Repudiandae eius at inventore magnam. Voluptas nisi + maxime laborum architecto fuga a consequuntur reiciendis + rerum beatae hic possimus, omnis dolorum libero, illo + dolorem assumenda. Repellat, ad! +

+
+
+
+ ) + })} +
+
+
+
+ + +
+ + +
+
+
+ + ) +} diff --git a/components/Application/Messages/index.ts b/components/Application/Messages/index.ts new file mode 100644 index 0000000..c6cba08 --- /dev/null +++ b/components/Application/Messages/index.ts @@ -0,0 +1 @@ +export * from './Messages' diff --git a/components/Application/PopupGuild/PopupGuild.stories.tsx b/components/Application/PopupGuild/PopupGuild.stories.tsx new file mode 100644 index 0000000..fe93ddd --- /dev/null +++ b/components/Application/PopupGuild/PopupGuild.stories.tsx @@ -0,0 +1,15 @@ +import { Meta, Story } from '@storybook/react' + +import { PopupGuild as Component, PopupGuildProps } from './PopupGuild' + +const Stories: Meta = { + title: 'PopupGuild', + component: Component +} + +export default Stories + +export const PopupGuild: Story = (arguments_) => { + return +} +PopupGuild.args = {} diff --git a/components/Application/PopupGuild/PopupGuild.test.tsx b/components/Application/PopupGuild/PopupGuild.test.tsx new file mode 100644 index 0000000..1549371 --- /dev/null +++ b/components/Application/PopupGuild/PopupGuild.test.tsx @@ -0,0 +1,10 @@ +import { render } from '@testing-library/react' + +import { PopupGuild } from './PopupGuild' + +describe('', () => { + it('should render successfully', () => { + const { baseElement } = render() + expect(baseElement).toBeTruthy() + }) +}) diff --git a/components/Application/PopupGuild/PopupGuild.tsx b/components/Application/PopupGuild/PopupGuild.tsx new file mode 100644 index 0000000..391e8dd --- /dev/null +++ b/components/Application/PopupGuild/PopupGuild.tsx @@ -0,0 +1,56 @@ +import { PlusSmIcon, ArrowDownIcon } from '@heroicons/react/solid' +import classNames from 'classnames' +import Image from 'next/image' + +import { PopupGuildCard } from './PopupGuildCard/PopupGuildCard' +export interface PopupGuildProps { + className?: string +} + +export const PopupGuild: React.FC = (props) => { + const { className } = props + + return ( +
+ + } + description='Create your own guild and manage everything within a few clicks !' + link={{ + icon: , + text: 'Create a Guild', + href: '/application/guilds/create' + }} + /> + + } + description='Talk, meet and have fun with new friends by joining any interesting guild !' + link={{ + icon: , + text: 'Join a Guild', + href: '/application/guilds/join' + }} + /> +
+ ) +} diff --git a/components/Application/PopupGuild/PopupGuildCard/PopupGuildCard.stories.tsx b/components/Application/PopupGuild/PopupGuildCard/PopupGuildCard.stories.tsx new file mode 100644 index 0000000..a3b500b --- /dev/null +++ b/components/Application/PopupGuild/PopupGuildCard/PopupGuildCard.stories.tsx @@ -0,0 +1,36 @@ +import { Meta, Story } from '@storybook/react' +import { PlusSmIcon } from '@heroicons/react/solid' +import Image from 'next/image' + +import { + PopupGuildCard as Component, + PopupGuildCardProps +} from './PopupGuildCard' + +const Stories: Meta = { + title: 'PopupGuildCard', + component: Component +} + +export default Stories + +export const PopupGuildCard: Story = (arguments_) => { + return +} +PopupGuildCard.args = { + image: ( + + ), + description: + 'Create your own guild and manage everything within a few clicks !', + link: { + icon: , + text: 'Create a server', + href: '/application/guilds/create' + } +} diff --git a/components/Application/PopupGuild/PopupGuildCard/PopupGuildCard.test.tsx b/components/Application/PopupGuild/PopupGuildCard/PopupGuildCard.test.tsx new file mode 100644 index 0000000..e407328 --- /dev/null +++ b/components/Application/PopupGuild/PopupGuildCard/PopupGuildCard.test.tsx @@ -0,0 +1,29 @@ +import { render } from '@testing-library/react' +import { PlusSmIcon } from '@heroicons/react/solid' +import Image from 'next/image' + +import { PopupGuildCard } from './PopupGuildCard' + +describe('', () => { + it('should render successfully', () => { + const { baseElement } = render( + + } + description='Create your own guild and manage everything within a few clicks !' + link={{ + icon: , + text: 'Create a server', + href: '/application/guilds/create' + }} + /> + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/components/Application/PopupGuild/PopupGuildCard/PopupGuildCard.tsx b/components/Application/PopupGuild/PopupGuildCard/PopupGuildCard.tsx new file mode 100644 index 0000000..08dec60 --- /dev/null +++ b/components/Application/PopupGuild/PopupGuildCard/PopupGuildCard.tsx @@ -0,0 +1,32 @@ +import Link from 'next/link' + +export interface PopupGuildCardProps { + image: JSX.Element + description: string + link: { + href: string + text: string + icon: JSX.Element + } +} + +export const PopupGuildCard: React.FC = (props) => { + const { image, description, link } = props + + return ( +
+
+ {image} +
+
+

{description}

+ + + {link.icon} + {link.text} + + +
+
+ ) +} diff --git a/components/Application/PopupGuild/PopupGuildCard/index.ts b/components/Application/PopupGuild/PopupGuildCard/index.ts new file mode 100644 index 0000000..99d186f --- /dev/null +++ b/components/Application/PopupGuild/PopupGuildCard/index.ts @@ -0,0 +1 @@ +export * from './PopupGuildCard' diff --git a/components/Application/PopupGuild/index.ts b/components/Application/PopupGuild/index.ts new file mode 100644 index 0000000..7a6c537 --- /dev/null +++ b/components/Application/PopupGuild/index.ts @@ -0,0 +1 @@ +export * from './PopupGuild' diff --git a/components/Application/Sidebar/Sidebar.stories.tsx b/components/Application/Sidebar/Sidebar.stories.tsx new file mode 100644 index 0000000..d44bd97 --- /dev/null +++ b/components/Application/Sidebar/Sidebar.stories.tsx @@ -0,0 +1,14 @@ +import { Meta, Story } from '@storybook/react' + +import { Sidebar as Component, SidebarProps } from './Sidebar' + +const Stories: Meta = { + title: 'Sidebar', + component: Component +} + +export default Stories + +export const Sidebar: Story = (arguments_) => { + return +} diff --git a/components/Application/Sidebar/Sidebar.test.tsx b/components/Application/Sidebar/Sidebar.test.tsx new file mode 100644 index 0000000..97593f9 --- /dev/null +++ b/components/Application/Sidebar/Sidebar.test.tsx @@ -0,0 +1,12 @@ +import { render } from '@testing-library/react' + +import { Sidebar } from './Sidebar' + +describe('', () => { + it('should render successfully', () => { + const { baseElement } = render( + + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/components/Application/Sidebar/Sidebar.tsx b/components/Application/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..02cfe51 --- /dev/null +++ b/components/Application/Sidebar/Sidebar.tsx @@ -0,0 +1,36 @@ +import classNames from 'classnames' + +import { ApplicationProps } from '..' + +export type DirectionSidebar = 'left' | 'right' + +export interface SidebarProps { + direction: DirectionSidebar + visible: boolean + path?: ApplicationProps + isMobile: boolean +} + +export const Sidebar: React.FC = (props) => { + const { direction, visible, children, path, isMobile } = props + + return ( + + ) +} diff --git a/components/Application/Sidebar/index.ts b/components/Application/Sidebar/index.ts new file mode 100644 index 0000000..9130e63 --- /dev/null +++ b/components/Application/Sidebar/index.ts @@ -0,0 +1 @@ +export * from './Sidebar' diff --git a/components/Application/index.ts b/components/Application/index.ts new file mode 100644 index 0000000..4513e93 --- /dev/null +++ b/components/Application/index.ts @@ -0,0 +1 @@ +export * from './Application' diff --git a/components/Authentication/Authentication.tsx b/components/Authentication/Authentication.tsx new file mode 100644 index 0000000..587b48d --- /dev/null +++ b/components/Authentication/Authentication.tsx @@ -0,0 +1,194 @@ +import { useMemo, useState } from 'react' +import { useRouter } from 'next/router' +import Link from 'next/link' +import useTranslation from 'next-translate/useTranslation' +import { useTheme } from 'next-themes' +import { Type } from '@sinclair/typebox' +import type { ErrorObject } from 'ajv' +import type { HandleForm } from 'react-component-form' +import axios from 'axios' + +import { SocialMediaButton } from '../design/SocialMediaButton' +import { Main } from '../design/Main' +import { Input } from '../design/Input' +import { Button } from '../design/Button' +import { FormState } from '../design/FormState' +import { useFormState } from '../../hooks/useFormState' +import { AuthenticationForm } from './' +import { userSchema } from '../../models/User' +import { ajv } from '../../utils/ajv' +import { api } from 'utils/api' +import { + Tokens, + Authentication as AuthenticationClass +} from '../../utils/authentication' +import { getErrorTranslationKey } from './getErrorTranslationKey' + +interface Errors { + [key: string]: ErrorObject | null | undefined +} + +const findError = ( + field: string +): ((value: ErrorObject, index: number, object: ErrorObject[]) => boolean) => { + return (validationError) => validationError.instancePath === field +} + +export interface AuthenticationProps { + mode: 'signup' | 'signin' +} + +export const Authentication: React.FC = (props) => { + const { mode } = props + + const router = useRouter() + const { lang, t } = useTranslation() + const { theme } = useTheme() + const [formState, setFormState] = useFormState() + const [messageTranslationKey, setMessageTranslationKey] = useState< + string | undefined + >(undefined) + const [errors, setErrors] = useState({ + name: null, + email: null, + password: null + }) + + const validateSchema = useMemo(() => { + return Type.Object({ + ...(mode === 'signup' && { name: userSchema.name }), + email: userSchema.email, + password: userSchema.password + }) + }, [mode]) + + const validate = useMemo(() => { + return ajv.compile(validateSchema) + }, [validateSchema]) + + const getErrorTranslation = (error?: ErrorObject | null): string | null => { + if (error != null) { + return t(getErrorTranslationKey(error)).replace( + '{expected}', + error?.params?.limit + ) + } + return null + } + + const handleSubmit: HandleForm = async (formData, formElement) => { + const isValid = validate(formData) + if (!isValid) { + setFormState('error') + const nameError = validate?.errors?.find(findError('/name')) + const emailError = validate?.errors?.find(findError('/email')) + const passwordError = validate?.errors?.find(findError('/password')) + setErrors({ + name: nameError, + email: emailError, + password: passwordError + }) + } else { + setErrors({}) + setFormState('loading') + if (mode === 'signup') { + try { + await api.post( + `/users/signup?redirectURI=${window.location.origin}/authentication/signin`, + { ...formData, language: lang, theme } + ) + formElement.reset() + setFormState('success') + setMessageTranslationKey('authentication:success-signup') + } catch (error) { + setFormState('error') + if (axios.isAxiosError(error) && error.response?.status === 400) { + setMessageTranslationKey('authentication:alreadyUsed') + } else { + setMessageTranslationKey('errors:server-error') + } + } + } else { + try { + const { data } = await api.post('/users/signin', formData) + const authentication = new AuthenticationClass(data) + authentication.signin() + await router.push('/application') + } catch (error) { + setFormState('error') + if (axios.isAxiosError(error) && error.response?.status === 400) { + setMessageTranslationKey('authentication:wrong-credentials') + } else { + setMessageTranslationKey('errors:server-error') + } + } + } + } + } + + return ( +
+
+
+ + + +
+
+
+ {t('authentication:or')} +
+ + {mode === 'signup' && ( + + )} + + + +

+ + + {mode === 'signup' + ? t('authentication:already-have-an-account') + : t('authentication:dont-have-an-account')} + + +

+
+ +
+ ) +} diff --git a/components/Authentication/AuthenticationForm.tsx b/components/Authentication/AuthenticationForm.tsx new file mode 100644 index 0000000..52cb56c --- /dev/null +++ b/components/Authentication/AuthenticationForm.tsx @@ -0,0 +1,16 @@ +import classNames from 'classnames' +import { Form, FormProps } from 'react-component-form' + +export const AuthenticationForm: React.FC = (props) => { + const { className, children, ...rest } = props + + return ( +
+ {children} +
+ ) +} diff --git a/components/Authentication/getErrorTranslationKey.test.ts b/components/Authentication/getErrorTranslationKey.test.ts new file mode 100644 index 0000000..c0b3478 --- /dev/null +++ b/components/Authentication/getErrorTranslationKey.test.ts @@ -0,0 +1,71 @@ +import type { ErrorObject } from 'ajv' + +import { getErrorTranslationKey } from './getErrorTranslationKey' + +const errorObject: ErrorObject = { + instancePath: '/path', + keyword: 'keyword', + params: {}, + schemaPath: '/path' +} + +describe('Authentication/getErrorTranslationKey', () => { + it('returns `errors:invalid` with unknown keyword', async () => { + expect( + getErrorTranslationKey({ + ...errorObject, + keyword: 'unknownkeyword' + }) + ).toEqual('errors:invalid') + }) + + it('returns `errors:invalid` with format != email', () => { + expect( + getErrorTranslationKey({ + ...errorObject, + keyword: 'format', + params: { format: 'email' } + }) + ).toEqual('errors:email') + }) + + it('returns `errors:email` with format = email', () => { + expect( + getErrorTranslationKey({ + ...errorObject, + keyword: 'format', + params: { format: 'email' } + }) + ).toEqual('errors:email') + }) + + it('returns `errors:required` with minLength and limit = 1', () => { + expect( + getErrorTranslationKey({ + ...errorObject, + keyword: 'minLength', + params: { limit: 1 } + }) + ).toEqual('errors:required') + }) + + it('returns `errors:minLength` with minLength and limit > 1', () => { + expect( + getErrorTranslationKey({ + ...errorObject, + keyword: 'minLength', + params: { limit: 5 } + }) + ).toEqual('errors:minLength') + }) + + it('returns `errors:maxLength` with maxLength', () => { + expect( + getErrorTranslationKey({ + ...errorObject, + keyword: 'maxLength', + params: { limit: 5 } + }) + ).toEqual('errors:maxLength') + }) +}) diff --git a/components/Authentication/getErrorTranslationKey.ts b/components/Authentication/getErrorTranslationKey.ts new file mode 100644 index 0000000..f74dbf9 --- /dev/null +++ b/components/Authentication/getErrorTranslationKey.ts @@ -0,0 +1,19 @@ +import type { ErrorObject } from 'ajv' + +const knownErrorKeywords = ['minLength', 'maxLength', 'format'] + +export const getErrorTranslationKey = (error: ErrorObject): string => { + if (knownErrorKeywords.includes(error?.keyword)) { + if (error.keyword === 'minLength' && error.params.limit === 1) { + return 'errors:required' + } + if (error.keyword === 'format') { + if (error.params.format === 'email') { + return 'errors:email' + } + return 'errors:invalid' + } + return `errors:${error.keyword}` + } + return 'errors:invalid' +} diff --git a/components/Authentication/index.ts b/components/Authentication/index.ts new file mode 100644 index 0000000..ff369ed --- /dev/null +++ b/components/Authentication/index.ts @@ -0,0 +1,2 @@ +export * from './Authentication' +export * from './AuthenticationForm' diff --git a/components/Footer/Footer.stories.tsx b/components/Footer/Footer.stories.tsx new file mode 100644 index 0000000..34f3d8d --- /dev/null +++ b/components/Footer/Footer.stories.tsx @@ -0,0 +1,15 @@ +import { Meta, Story } from '@storybook/react' + +import { Footer as Component, FooterProps } from './' + +const Stories: Meta = { + title: 'Footer', + component: Component +} + +export default Stories + +export const Footer: Story = (arguments_) => ( + +) +Footer.args = { version: '1.0.0' } diff --git a/components/Footer/Footer.test.tsx b/components/Footer/Footer.test.tsx new file mode 100644 index 0000000..311d4e7 --- /dev/null +++ b/components/Footer/Footer.test.tsx @@ -0,0 +1,16 @@ +import { render } from '@testing-library/react' + +import { Footer } from './' + +describe('