feat: add support for files and math for messages (#5)
This commit is contained in:
		@@ -6,7 +6,7 @@
 | 
			
		||||
    "plugin:storybook/recommended",
 | 
			
		||||
    "prettier"
 | 
			
		||||
  ],
 | 
			
		||||
  "plugins": ["unicorn", "prettier"],
 | 
			
		||||
  "plugins": ["unicorn", "import", "prettier"],
 | 
			
		||||
  "parserOptions": {
 | 
			
		||||
    "project": "./tsconfig.json"
 | 
			
		||||
  },
 | 
			
		||||
@@ -17,6 +17,13 @@
 | 
			
		||||
  },
 | 
			
		||||
  "rules": {
 | 
			
		||||
    "prettier/prettier": "error",
 | 
			
		||||
    "import/order": [
 | 
			
		||||
      "error",
 | 
			
		||||
      {
 | 
			
		||||
        "groups": ["builtin", "external", "internal"],
 | 
			
		||||
        "newlines-between": "always"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "unicorn/prefer-node-protocol": "error",
 | 
			
		||||
    "unicorn/prevent-abbreviations": [
 | 
			
		||||
      "error",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							@@ -21,13 +21,27 @@ jobs:
 | 
			
		||||
      - name: 'Install'
 | 
			
		||||
        run: 'npm install'
 | 
			
		||||
 | 
			
		||||
      - run: 'npm run lint:commit -- --to "${{ github.sha }}"'
 | 
			
		||||
      - run: 'npm run lint:editorconfig'
 | 
			
		||||
      - run: 'npm run lint:markdown'
 | 
			
		||||
      - run: 'npm run lint:docker'
 | 
			
		||||
      - run: 'npm run lint:typescript'
 | 
			
		||||
      - name: 'lint:commit'
 | 
			
		||||
        run: 'npm run lint:commit -- --to "${{ github.sha }}"'
 | 
			
		||||
 | 
			
		||||
      - name: 'dotenv-linter'
 | 
			
		||||
      - name: 'lint:editorconfig'
 | 
			
		||||
        run: 'npm run lint:editorconfig'
 | 
			
		||||
 | 
			
		||||
      - name: 'lint:markdown'
 | 
			
		||||
        run: 'npm run lint:markdown'
 | 
			
		||||
 | 
			
		||||
      - name: 'lint:typescript'
 | 
			
		||||
        run: 'npm run lint:typescript'
 | 
			
		||||
 | 
			
		||||
      - name: 'lint:prettier'
 | 
			
		||||
        run: 'npm run lint:prettier'
 | 
			
		||||
 | 
			
		||||
      - name: 'lint:dotenv'
 | 
			
		||||
        uses: 'dotenv-linter/action-dotenv-linter@v2'
 | 
			
		||||
        with:
 | 
			
		||||
          github_token: ${{ secrets.github_token }}
 | 
			
		||||
 | 
			
		||||
      - name: 'lint:docker'
 | 
			
		||||
        uses: 'hadolint/hadolint-action@v1.6.0'
 | 
			
		||||
        with:
 | 
			
		||||
          dockerfile: './Dockerfile'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							@@ -41,6 +41,9 @@ jobs:
 | 
			
		||||
      - name: 'Build'
 | 
			
		||||
        run: 'npm run build'
 | 
			
		||||
 | 
			
		||||
      - name: 'html-w3c-validator'
 | 
			
		||||
        run: 'npm run test:html-w3c-validator'
 | 
			
		||||
 | 
			
		||||
      - name: 'Lighthouse'
 | 
			
		||||
        run: 'npm run test:lighthouse'
 | 
			
		||||
        env:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								.html-w3c-validatorrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.html-w3c-validatorrc.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
  "urls": [
 | 
			
		||||
    "http://localhost:3000/",
 | 
			
		||||
    "http://localhost:3000/authentication/forgot-password",
 | 
			
		||||
    "http://localhost:3000/authentication/reset-password",
 | 
			
		||||
    "http://localhost:3000/authentication/signin",
 | 
			
		||||
    "http://localhost:3000/authentication/signup"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -6,9 +6,10 @@
 | 
			
		||||
      "startServerReadyTimeout": 20000,
 | 
			
		||||
      "url": [
 | 
			
		||||
        "http://localhost:3000/",
 | 
			
		||||
        "http://localhost:3000/authentication/forgot-password",
 | 
			
		||||
        "http://localhost:3000/authentication/reset-password",
 | 
			
		||||
        "http://localhost:3000/authentication/signin",
 | 
			
		||||
        "http://localhost:3000/authentication/signup",
 | 
			
		||||
        "http://localhost:3000/authentication/forgot-password"
 | 
			
		||||
        "http://localhost:3000/authentication/signup"
 | 
			
		||||
      ],
 | 
			
		||||
      "numberOfRuns": 1
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,5 @@
 | 
			
		||||
    "jest --findRelatedTests"
 | 
			
		||||
  ],
 | 
			
		||||
  "*.{css,yml,json}": ["prettier --write"],
 | 
			
		||||
  "*.{md}": ["prettier --write", "markdownlint --dot --fix"],
 | 
			
		||||
  "./Dockerfile": ["dockerfilelint"]
 | 
			
		||||
  "*.md": ["prettier --write", "markdownlint --dot --fix"]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,3 +6,4 @@ node_modules
 | 
			
		||||
next-env.d.ts
 | 
			
		||||
**/workbox-*.js
 | 
			
		||||
**/sw.js
 | 
			
		||||
*.hbs
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
FROM node:16.13.1 AS dependencies
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
COPY ./package*.json ./
 | 
			
		||||
RUN npm clean-install
 | 
			
		||||
RUN npm install
 | 
			
		||||
 | 
			
		||||
FROM node:16.13.1 AS builder
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
 
 | 
			
		||||
@@ -6,13 +6,13 @@ 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 { IconButton } from '../design/IconButton'
 | 
			
		||||
import { IconLink } from '../design/IconLink'
 | 
			
		||||
import { Guilds } from './Guilds/Guilds'
 | 
			
		||||
import { Divider } from '../design/Divider'
 | 
			
		||||
import { Members } from './Members'
 | 
			
		||||
import { useAuthentication } from 'tools/authentication'
 | 
			
		||||
import { API_URL } from 'tools/api'
 | 
			
		||||
import { useAuthentication } from '../../tools/authentication'
 | 
			
		||||
import { API_URL } from '../../tools/api'
 | 
			
		||||
 | 
			
		||||
export interface GuildsChannelsPath {
 | 
			
		||||
  guildId: number
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
import { channelExample } from '../../../../cypress/fixtures/channels/channel'
 | 
			
		||||
 | 
			
		||||
import { channelExample } from '../../../../cypress/fixtures/channels/channel'
 | 
			
		||||
import { Channel as Component, ChannelProps } from './Channel'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
import { channelExample } from 'cypress/fixtures/channels/channel'
 | 
			
		||||
 | 
			
		||||
import { channelExample } from '../../../../cypress/fixtures/channels/channel'
 | 
			
		||||
import { Channel } from './Channel'
 | 
			
		||||
 | 
			
		||||
describe('<Channel />', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import InfiniteScroll from 'react-infinite-scroll-component'
 | 
			
		||||
 | 
			
		||||
import { useChannels } from '../../../contexts/Channels'
 | 
			
		||||
import { GuildsChannelsPath } from '../Application'
 | 
			
		||||
import { Loader } from 'components/design/Loader'
 | 
			
		||||
import { Loader } from '../../design/Loader'
 | 
			
		||||
import { Channel } from './Channel'
 | 
			
		||||
import { useChannels } from 'contexts/Channels'
 | 
			
		||||
 | 
			
		||||
export interface ChannelsProps {
 | 
			
		||||
  path: GuildsChannelsPath
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { CogIcon, PlusIcon } from '@heroicons/react/solid'
 | 
			
		||||
 | 
			
		||||
import { useGuildMember } from 'contexts/GuildMember'
 | 
			
		||||
import { Divider } from 'components/design/Divider'
 | 
			
		||||
import { Channels } from 'components/Application/Channels'
 | 
			
		||||
import { IconButton } from 'components/design/IconButton'
 | 
			
		||||
import { useGuildMember } from '../../../contexts/GuildMember'
 | 
			
		||||
import { Divider } from '../../design/Divider'
 | 
			
		||||
import { Channels } from '../../Application/Channels'
 | 
			
		||||
import { IconButton } from '../../design/IconButton'
 | 
			
		||||
import { GuildsChannelsPath } from '..'
 | 
			
		||||
 | 
			
		||||
export interface GuildLeftSidebarProps {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import InfiniteScroll from 'react-infinite-scroll-component'
 | 
			
		||||
 | 
			
		||||
import { Loader } from 'components/design/Loader'
 | 
			
		||||
import { Loader } from '../../design/Loader'
 | 
			
		||||
import { Guild } from './Guild'
 | 
			
		||||
import { useGuilds } from 'contexts/Guilds'
 | 
			
		||||
import { useGuilds } from '../../../contexts/Guilds'
 | 
			
		||||
import { GuildsChannelsPath } from '..'
 | 
			
		||||
 | 
			
		||||
export interface GuildsProps {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,11 @@ import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import InfiniteScroll from 'react-infinite-scroll-component'
 | 
			
		||||
 | 
			
		||||
import { useAuthentication } from 'tools/authentication'
 | 
			
		||||
import { GuildPublic as GuildPublicType } from 'models/Guild'
 | 
			
		||||
import { Loader } from 'components/design/Loader'
 | 
			
		||||
import { useAuthentication } from '../../../tools/authentication'
 | 
			
		||||
import { GuildPublic as GuildPublicType } from '../../../models/Guild'
 | 
			
		||||
import { Loader } from '../../design/Loader'
 | 
			
		||||
import { GuildPublic } from './GuildPublic'
 | 
			
		||||
import { usePagination } from 'hooks/usePagination'
 | 
			
		||||
import { usePagination } from '../../../hooks/usePagination'
 | 
			
		||||
 | 
			
		||||
export const JoinGuildsPublic: React.FC = () => {
 | 
			
		||||
  const [search, setSearch] = useState('')
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import InfiniteScroll from 'react-infinite-scroll-component'
 | 
			
		||||
 | 
			
		||||
import { Divider } from '../../design/Divider'
 | 
			
		||||
import { Loader } from 'components/design/Loader'
 | 
			
		||||
import { useMembers } from 'contexts/Members'
 | 
			
		||||
import { Loader } from '../../design/Loader'
 | 
			
		||||
import { useMembers } from '../../../contexts/Members'
 | 
			
		||||
import { Member } from './Member'
 | 
			
		||||
import { capitalize } from '../../../tools/utils/capitalize'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,9 @@ import date from 'date-and-time'
 | 
			
		||||
 | 
			
		||||
import { MessageWithMember } from '../../../../models/Message'
 | 
			
		||||
import { API_URL } from '../../../../tools/api'
 | 
			
		||||
import { MessageContent } from './MessageContent'
 | 
			
		||||
import { MessageText } from './MessageText'
 | 
			
		||||
import { Loader } from '../../../design/Loader'
 | 
			
		||||
import { MessageFile } from './MessageFile'
 | 
			
		||||
 | 
			
		||||
export interface MessageProps {
 | 
			
		||||
  message: MessageWithMember
 | 
			
		||||
@@ -14,7 +16,10 @@ export const Message: React.FC<MessageProps> = (props) => {
 | 
			
		||||
  const { message } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='p-4 flex transition hover:bg-gray-200 dark:hover:bg-gray-900'>
 | 
			
		||||
    <div
 | 
			
		||||
      className='p-4 flex transition hover:bg-gray-200 dark:hover:bg-gray-900'
 | 
			
		||||
      data-cy={`message-${message.id}`}
 | 
			
		||||
    >
 | 
			
		||||
      <Link href={`/application/users/${message.member.user.id}`}>
 | 
			
		||||
        <a>
 | 
			
		||||
          <div className='w-12 h-12 mr-4 flex flex-shrink-0 items-center justify-center'>
 | 
			
		||||
@@ -54,7 +59,13 @@ export const Message: React.FC<MessageProps> = (props) => {
 | 
			
		||||
            {date.format(new Date(message.createdAt), 'DD/MM/YYYY - HH:mm:ss')}
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <MessageContent message={message} />
 | 
			
		||||
        {message.type === 'text' ? (
 | 
			
		||||
          <MessageText message={message} />
 | 
			
		||||
        ) : message.type === 'file' ? (
 | 
			
		||||
          <MessageFile message={message} />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Loader />
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
export * from './MessageContent'
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
export const FileIcon: React.FC = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      className='dark:text-white text-black fill-current'
 | 
			
		||||
      width='21'
 | 
			
		||||
      height='26'
 | 
			
		||||
      viewBox='0 0 21 26'
 | 
			
		||||
      fill='none'
 | 
			
		||||
      xmlns='http://www.w3.org/2000/svg'
 | 
			
		||||
    >
 | 
			
		||||
      <path d='M2.625 0C1.92881 0 1.26113 0.273928 0.768845 0.761522C0.276562 1.24912 0 1.91044 0 2.6V23.4C0 24.0896 0.276562 24.7509 0.768845 25.2385C1.26113 25.7261 1.92881 26 2.625 26H18.375C19.0712 26 19.7389 25.7261 20.2312 25.2385C20.7234 24.7509 21 24.0896 21 23.4V7.8L13.125 0H2.625ZM13.125 9.1H11.8125V2.6L18.375 9.1H13.125Z' />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,96 @@
 | 
			
		||||
import { useState, useEffect } from 'react'
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import prettyBytes from 'pretty-bytes'
 | 
			
		||||
import { DownloadIcon } from '@heroicons/react/solid'
 | 
			
		||||
 | 
			
		||||
import { useAuthentication } from '../../../../../tools/authentication'
 | 
			
		||||
import { MessageWithMember } from '../../../../../models/Message'
 | 
			
		||||
import { Loader } from '../../../../design/Loader'
 | 
			
		||||
import { FileIcon } from './FileIcon'
 | 
			
		||||
 | 
			
		||||
export interface FileData {
 | 
			
		||||
  blob: Blob
 | 
			
		||||
  url: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MessageContentProps {
 | 
			
		||||
  message: MessageWithMember
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const MessageFile: React.FC<MessageContentProps> = (props) => {
 | 
			
		||||
  const { message } = props
 | 
			
		||||
 | 
			
		||||
  const { authentication } = useAuthentication()
 | 
			
		||||
  const [file, setFile] = useState<FileData | null>(null)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const ourRequest = axios.CancelToken.source()
 | 
			
		||||
 | 
			
		||||
    const fetchData = async (): Promise<void> => {
 | 
			
		||||
      const { data } = await authentication.api.get(message.value, {
 | 
			
		||||
        responseType: 'blob',
 | 
			
		||||
        cancelToken: ourRequest.token
 | 
			
		||||
      })
 | 
			
		||||
      const fileURL = URL.createObjectURL(data)
 | 
			
		||||
      setFile({ blob: data, url: fileURL })
 | 
			
		||||
    }
 | 
			
		||||
    fetchData().catch(() => {})
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      ourRequest.cancel()
 | 
			
		||||
    }
 | 
			
		||||
  }, [message.value, authentication.api])
 | 
			
		||||
 | 
			
		||||
  if (file == null) {
 | 
			
		||||
    return <Loader />
 | 
			
		||||
  }
 | 
			
		||||
  if (message.mimetype.startsWith('image/')) {
 | 
			
		||||
    return (
 | 
			
		||||
      <a href={file.url} target='_blank' rel='noreferrer'>
 | 
			
		||||
        <Image
 | 
			
		||||
          data-cy={`message-file-image-${message.id}`}
 | 
			
		||||
          className='max-w-xs max-h-xs'
 | 
			
		||||
          src={file.url}
 | 
			
		||||
          alt={message.value}
 | 
			
		||||
          width={320}
 | 
			
		||||
          height={320}
 | 
			
		||||
        />
 | 
			
		||||
      </a>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  if (message.mimetype.startsWith('audio/')) {
 | 
			
		||||
    return (
 | 
			
		||||
      <audio controls data-cy={`message-file-audio-${message.id}`}>
 | 
			
		||||
        <source src={file.url} type={message.mimetype} />
 | 
			
		||||
      </audio>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  if (message.mimetype.startsWith('video/')) {
 | 
			
		||||
    return (
 | 
			
		||||
      <video
 | 
			
		||||
        className='max-w-xs max-h-xs'
 | 
			
		||||
        controls
 | 
			
		||||
        data-cy={`message-file-video-${message.id}`}
 | 
			
		||||
      >
 | 
			
		||||
        <source src={file.url} type={message.mimetype} />
 | 
			
		||||
      </video>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <a href={file.url} download data-cy={`message-file-download-${message.id}`}>
 | 
			
		||||
      <div className='flex items-center'>
 | 
			
		||||
        <div className='flex items-center'>
 | 
			
		||||
          <div>
 | 
			
		||||
            <FileIcon />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className='ml-4'>
 | 
			
		||||
            <p>{file.blob.type}</p>
 | 
			
		||||
            <p className='mt-1'>{prettyBytes(file.blob.size)}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <DownloadIcon className='ml-4 w-8 h-8' />
 | 
			
		||||
      </div>
 | 
			
		||||
    </a>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
export * from './MessageFile'
 | 
			
		||||
@@ -2,6 +2,10 @@ import { useMemo } from 'react'
 | 
			
		||||
import ReactMarkdown from 'react-markdown'
 | 
			
		||||
import gfm from 'remark-gfm'
 | 
			
		||||
import remarkBreaks from 'remark-breaks'
 | 
			
		||||
import remarkMath from 'remark-math'
 | 
			
		||||
import rehypeKatex from 'rehype-katex'
 | 
			
		||||
 | 
			
		||||
import 'katex/dist/katex.min.css'
 | 
			
		||||
 | 
			
		||||
import { Emoji, emojiPlugin, isStringWithOnlyOneEmoji } from '../../../../Emoji'
 | 
			
		||||
import { MessageWithMember } from '../../../../../models/Message'
 | 
			
		||||
@@ -10,7 +14,7 @@ export interface MessageContentProps {
 | 
			
		||||
  message: MessageWithMember
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const MessageContent: React.FC<MessageContentProps> = (props) => {
 | 
			
		||||
export const MessageText: React.FC<MessageContentProps> = (props) => {
 | 
			
		||||
  const { message } = props
 | 
			
		||||
 | 
			
		||||
  const isMessageWithOnlyOneEmoji = useMemo(() => {
 | 
			
		||||
@@ -31,8 +35,8 @@ export const MessageContent: React.FC<MessageContentProps> = (props) => {
 | 
			
		||||
    <ReactMarkdown
 | 
			
		||||
      disallowedElements={['table']}
 | 
			
		||||
      unwrapDisallowed
 | 
			
		||||
      remarkPlugins={[[gfm], [remarkBreaks]]}
 | 
			
		||||
      rehypePlugins={[emojiPlugin]}
 | 
			
		||||
      remarkPlugins={[[gfm], [remarkBreaks], [remarkMath]]}
 | 
			
		||||
      rehypePlugins={[[emojiPlugin], [rehypeKatex]]}
 | 
			
		||||
      linkTarget='_blank'
 | 
			
		||||
      components={{
 | 
			
		||||
        emoji: (props) => {
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
export * from './MessageText'
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import InfiniteScroll from 'react-infinite-scroll-component'
 | 
			
		||||
 | 
			
		||||
import { Loader } from 'components/design/Loader'
 | 
			
		||||
import { Loader } from '../../design/Loader'
 | 
			
		||||
import { Message } from './Message'
 | 
			
		||||
import { useMessages } from 'contexts/Messages'
 | 
			
		||||
import { Emoji } from 'components/Emoji'
 | 
			
		||||
import { useMessages } from '../../../contexts/Messages'
 | 
			
		||||
import { Emoji } from '../../Emoji'
 | 
			
		||||
 | 
			
		||||
export const Messages: React.FC = () => {
 | 
			
		||||
  const { messages, hasMore, nextPage } = useMessages()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  userExample,
 | 
			
		||||
  userSettingsExample
 | 
			
		||||
} from '../../../cypress/fixtures/users/user'
 | 
			
		||||
 | 
			
		||||
import { UserProfile as Component, UserProfileProps } from './UserProfile'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,15 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { user, userSettings } from '../../../cypress/fixtures/users/user'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  userExample,
 | 
			
		||||
  userSettingsExample
 | 
			
		||||
} from '../../../cypress/fixtures/users/user'
 | 
			
		||||
import { UserProfile } from './UserProfile'
 | 
			
		||||
 | 
			
		||||
describe('<UserProfile />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
    const { baseElement } = render(
 | 
			
		||||
      <UserProfile user={{ ...user, settings: userSettings }} />
 | 
			
		||||
      <UserProfile user={{ ...userExample, settings: userSettingsExample }} />
 | 
			
		||||
    )
 | 
			
		||||
    expect(baseElement).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import { Button } from '../design/Button'
 | 
			
		||||
import { FormState } from '../design/FormState'
 | 
			
		||||
import { AuthenticationForm } from './'
 | 
			
		||||
import { userSchema } from '../../models/User'
 | 
			
		||||
import { api } from 'tools/api'
 | 
			
		||||
import { api } from '../../tools/api'
 | 
			
		||||
import {
 | 
			
		||||
  Tokens,
 | 
			
		||||
  Authentication as AuthenticationClass
 | 
			
		||||
@@ -85,16 +85,16 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Main>
 | 
			
		||||
      <section className='flex flex-col sm:items-center sm:w-full'>
 | 
			
		||||
      <div className='flex flex-col sm:items-center sm:w-full'>
 | 
			
		||||
        <div className='flex flex-col items-center justify-center space-y-6 sm:w-4/6 sm:flex-row sm:space-x-6 sm:space-y-0'>
 | 
			
		||||
          <SocialMediaButton socialMedia='Google' />
 | 
			
		||||
          <SocialMediaButton socialMedia='GitHub' />
 | 
			
		||||
          <SocialMediaButton socialMedia='Discord' />
 | 
			
		||||
        </div>
 | 
			
		||||
      </section>
 | 
			
		||||
      <section className='text-center text-lg font-paragraph pt-8'>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className='text-center text-lg font-paragraph pt-8'>
 | 
			
		||||
        {t('authentication:or')}
 | 
			
		||||
      </section>
 | 
			
		||||
      </div>
 | 
			
		||||
      <AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
 | 
			
		||||
        {mode === 'signup' && (
 | 
			
		||||
          <Input
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,36 @@
 | 
			
		||||
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'
 | 
			
		||||
 | 
			
		||||
export interface ButtonLinkProps extends React.ComponentPropsWithRef<'a'> {}
 | 
			
		||||
 | 
			
		||||
export const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(
 | 
			
		||||
  (props, reference) => {
 | 
			
		||||
    const { children, className: givenClassName, ...rest } = props
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <a
 | 
			
		||||
        ref={reference}
 | 
			
		||||
        className={classNames(className, givenClassName)}
 | 
			
		||||
        {...rest}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
      </a>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
ButtonLink.displayName = 'ButtonLink'
 | 
			
		||||
 | 
			
		||||
export interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {}
 | 
			
		||||
 | 
			
		||||
export const Button: React.FC<ButtonProps> = (props) => {
 | 
			
		||||
  const { children, className, ...rest } = props
 | 
			
		||||
  const { children, className: givenClassName, ...rest } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <button
 | 
			
		||||
      className={classNames(
 | 
			
		||||
        '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',
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...rest}
 | 
			
		||||
    >
 | 
			
		||||
    <button className={classNames(className, givenClassName)} {...rest}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </button>
 | 
			
		||||
  )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import classNames from 'classnames'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
 | 
			
		||||
import { FetchState as FormStateType } from 'hooks/useFetchState'
 | 
			
		||||
import { FetchState as FormStateType } from '../../../hooks/useFetchState'
 | 
			
		||||
import { Loader } from '../Loader'
 | 
			
		||||
 | 
			
		||||
export interface FormStateProps {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
 | 
			
		||||
import { FormState } from '../FormState'
 | 
			
		||||
 | 
			
		||||
export interface InputProps extends React.ComponentPropsWithRef<'input'> {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ describe('<SocialMediaButton />', () => {
 | 
			
		||||
    const { findByTestId } = render(
 | 
			
		||||
      <SocialMediaButton socialMedia={socialMedia} />
 | 
			
		||||
    )
 | 
			
		||||
    const button = await findByTestId('button')
 | 
			
		||||
    const button = await findByTestId('social-media-button')
 | 
			
		||||
    expect(button).toHaveStyle('color: #000')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -8,19 +8,41 @@ type SocialMediaColors = {
 | 
			
		||||
  [key in SocialMedia]: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SocialMediaButtonProps
 | 
			
		||||
  extends React.ComponentPropsWithRef<'button'> {
 | 
			
		||||
  socialMedia: SocialMedia
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const socialMediaColors: SocialMediaColors = {
 | 
			
		||||
  Discord: '#404EED',
 | 
			
		||||
  GitHub: '#24292E',
 | 
			
		||||
  Google: '#FCFCFC'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const className =
 | 
			
		||||
  'py-2 px-6 inline-flex outline-none items-center font-paragraph rounded-lg cursor-pointer transition duration-300 ease-in-out hover:opacity-80 focus:outline-none'
 | 
			
		||||
 | 
			
		||||
interface SocialMediaChildrenProps {
 | 
			
		||||
  socialMedia: SocialMedia
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const SocialMediaChildren: React.FC<SocialMediaChildrenProps> = (props) => {
 | 
			
		||||
  const { socialMedia } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Image
 | 
			
		||||
        width={20}
 | 
			
		||||
        height={20}
 | 
			
		||||
        src={`/images/svg/web/${socialMedia}.svg`}
 | 
			
		||||
        alt={socialMedia}
 | 
			
		||||
      />
 | 
			
		||||
      <span className='ml-2'>{socialMedia}</span>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SocialMediaButtonProps
 | 
			
		||||
  extends React.ComponentPropsWithRef<'button'>,
 | 
			
		||||
    SocialMediaChildrenProps {}
 | 
			
		||||
 | 
			
		||||
export const SocialMediaButton: React.FC<SocialMediaButtonProps> = (props) => {
 | 
			
		||||
  const { socialMedia, className, ...rest } = props
 | 
			
		||||
  const { socialMedia, className: givenClassName, ...rest } = props
 | 
			
		||||
 | 
			
		||||
  const socialMediaColor = useMemo(() => {
 | 
			
		||||
    return socialMediaColors[socialMedia]
 | 
			
		||||
@@ -29,20 +51,11 @@ export const SocialMediaButton: React.FC<SocialMediaButtonProps> = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <button
 | 
			
		||||
        data-testid='button'
 | 
			
		||||
        data-testid='social-media-button'
 | 
			
		||||
        {...rest}
 | 
			
		||||
        className={classNames(
 | 
			
		||||
          `button py-2 px-6 inline-flex outline-none items-center font-paragraph rounded-lg cursor-pointer transition duration-300 ease-in-out hover:opacity-80 focus:outline-none`,
 | 
			
		||||
          className
 | 
			
		||||
        )}
 | 
			
		||||
        className={classNames(className, 'button', givenClassName)}
 | 
			
		||||
      >
 | 
			
		||||
        <Image
 | 
			
		||||
          width={20}
 | 
			
		||||
          height={20}
 | 
			
		||||
          src={`/images/svg/web/${socialMedia}.svg`}
 | 
			
		||||
          alt={socialMedia}
 | 
			
		||||
        />
 | 
			
		||||
        <span className='ml-2'>{socialMedia}</span>
 | 
			
		||||
        <SocialMediaChildren socialMedia={socialMedia} />
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
      <style jsx>
 | 
			
		||||
@@ -60,3 +73,36 @@ export const SocialMediaButton: React.FC<SocialMediaButtonProps> = (props) => {
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SocialMediaLinkProps
 | 
			
		||||
  extends React.ComponentPropsWithRef<'a'>,
 | 
			
		||||
    SocialMediaChildrenProps {}
 | 
			
		||||
 | 
			
		||||
export const SocialMediaLink: React.FC<SocialMediaLinkProps> = (props) => {
 | 
			
		||||
  const { socialMedia, className: givenClassName, ...rest } = props
 | 
			
		||||
 | 
			
		||||
  const socialMediaColor = useMemo(() => {
 | 
			
		||||
    return socialMediaColors[socialMedia]
 | 
			
		||||
  }, [socialMedia])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <a {...rest} className={classNames(className, 'link', givenClassName)}>
 | 
			
		||||
        <SocialMediaChildren socialMedia={socialMedia} />
 | 
			
		||||
      </a>
 | 
			
		||||
 | 
			
		||||
      <style jsx>
 | 
			
		||||
        {`
 | 
			
		||||
          .link {
 | 
			
		||||
            background: ${socialMediaColor};
 | 
			
		||||
            color: ${socialMedia === 'Google' ? '#000' : '#fff'};
 | 
			
		||||
            border: ${socialMedia === 'Google' ? '1px solid #000' : 'none'};
 | 
			
		||||
          }
 | 
			
		||||
          .link:focus {
 | 
			
		||||
            box-shadow: 0 0 0 2px #27b05e;
 | 
			
		||||
          }
 | 
			
		||||
        `}
 | 
			
		||||
      </style>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
 | 
			
		||||
import { channelExample } from '../channel'
 | 
			
		||||
 | 
			
		||||
export const getChannelWithChannelIdHandler: Handler = {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,14 @@
 | 
			
		||||
import { Handler } from '../../../handler'
 | 
			
		||||
import {
 | 
			
		||||
  messageExampleComplete,
 | 
			
		||||
  messageExampleComplete2
 | 
			
		||||
  messageExampleComplete2,
 | 
			
		||||
  messageExampleComplete3,
 | 
			
		||||
  messageExampleComplete4,
 | 
			
		||||
  messageExampleComplete5,
 | 
			
		||||
  messageExampleComplete6,
 | 
			
		||||
  messageExampleComplete7,
 | 
			
		||||
  messageExampleComplete8,
 | 
			
		||||
  messageExampleComplete9
 | 
			
		||||
} from '../../../messages/message'
 | 
			
		||||
import { channelExample } from '../../channel'
 | 
			
		||||
 | 
			
		||||
@@ -10,6 +17,16 @@ export const getMessagesWithChannelIdHandler: Handler = {
 | 
			
		||||
  url: `/channels/${channelExample.id}/messages`,
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: [messageExampleComplete, messageExampleComplete2]
 | 
			
		||||
    body: [
 | 
			
		||||
      messageExampleComplete,
 | 
			
		||||
      messageExampleComplete2,
 | 
			
		||||
      messageExampleComplete3,
 | 
			
		||||
      messageExampleComplete4,
 | 
			
		||||
      messageExampleComplete5,
 | 
			
		||||
      messageExampleComplete6,
 | 
			
		||||
      messageExampleComplete7,
 | 
			
		||||
      messageExampleComplete8,
 | 
			
		||||
      messageExampleComplete9
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
 | 
			
		||||
import { guildExample } from '../guild'
 | 
			
		||||
import { memberExampleComplete } from '../../members/member'
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import { Handler } from '../handler'
 | 
			
		||||
 | 
			
		||||
import { guildExample, guildExample2 } from './guild'
 | 
			
		||||
 | 
			
		||||
export const getGuildsHandler: Handler = {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import { Handler } from '../handler'
 | 
			
		||||
 | 
			
		||||
import { guildExample } from './guild'
 | 
			
		||||
import { channelExample } from '../channels/channel'
 | 
			
		||||
import { memberExampleComplete } from '../members/member'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
 | 
			
		||||
import { guildExample, guildExample2 } from '../guild'
 | 
			
		||||
 | 
			
		||||
export const getGuildsPublicEmptyHandler: Handler = {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ export interface Handler {
 | 
			
		||||
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
 | 
			
		||||
  url: `/${string}`
 | 
			
		||||
  response: {
 | 
			
		||||
    isFile?: boolean
 | 
			
		||||
    body: any
 | 
			
		||||
    statusCode: number
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,63 @@ export const messageExampleComplete = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const messageExampleComplete2 = {
 | 
			
		||||
  ...messageExample,
 | 
			
		||||
  ...messageExampleComplete,
 | 
			
		||||
  id: 2,
 | 
			
		||||
  value: 'Second message',
 | 
			
		||||
  member: memberExampleComplete
 | 
			
		||||
  value: 'Message with bad html: <script>alert("xss")</script>'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const messageExampleComplete3 = {
 | 
			
		||||
  ...messageExampleComplete,
 | 
			
		||||
  id: 3,
 | 
			
		||||
  value:
 | 
			
		||||
    'Message with **bold text** and *italic text*.\nNewlines and some emoji: :smile:'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const messageExampleComplete4 = {
 | 
			
		||||
  ...messageExampleComplete,
 | 
			
		||||
  id: 4,
 | 
			
		||||
  value: `The Quadratic Formula:
 | 
			
		||||
 | 
			
		||||
**Theorem 1**: $(a, b, c) \\in \\mathbb{R}^3$, the solutions of $ax^2 + bx + c = 0$ are:
 | 
			
		||||
 | 
			
		||||
$x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}$
 | 
			
		||||
  `
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const messageExampleComplete5 = {
 | 
			
		||||
  ...messageExampleComplete,
 | 
			
		||||
  id: 5,
 | 
			
		||||
  value: ':wave:'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const messageExampleComplete6 = {
 | 
			
		||||
  ...messageExampleComplete,
 | 
			
		||||
  id: 6,
 | 
			
		||||
  value: '/uploads/messages/image.png',
 | 
			
		||||
  type: 'file',
 | 
			
		||||
  mimetype: 'image/png'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const messageExampleComplete7 = {
 | 
			
		||||
  ...messageExampleComplete,
 | 
			
		||||
  id: 7,
 | 
			
		||||
  value: '/uploads/messages/audio.mp3',
 | 
			
		||||
  type: 'file',
 | 
			
		||||
  mimetype: 'audio/mp3'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const messageExampleComplete8 = {
 | 
			
		||||
  ...messageExampleComplete,
 | 
			
		||||
  id: 8,
 | 
			
		||||
  value: '/uploads/messages/video.mp4',
 | 
			
		||||
  type: 'file',
 | 
			
		||||
  mimetype: 'video/mp4'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const messageExampleComplete9 = {
 | 
			
		||||
  ...messageExampleComplete,
 | 
			
		||||
  id: 9,
 | 
			
		||||
  value: '/uploads/messages/download.zip',
 | 
			
		||||
  type: 'file',
 | 
			
		||||
  mimetype: 'application/zip'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								cypress/fixtures/uploads/audio.mp3
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								cypress/fixtures/uploads/audio.mp3
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								cypress/fixtures/uploads/download.zip
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								cypress/fixtures/uploads/download.zip
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								cypress/fixtures/uploads/image.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								cypress/fixtures/uploads/image.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 350 KiB  | 
							
								
								
									
										47
									
								
								cypress/fixtures/uploads/messages/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								cypress/fixtures/uploads/messages/get.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
import {
 | 
			
		||||
  messageExampleComplete6,
 | 
			
		||||
  messageExampleComplete7,
 | 
			
		||||
  messageExampleComplete8,
 | 
			
		||||
  messageExampleComplete9
 | 
			
		||||
} from '../../messages/message'
 | 
			
		||||
 | 
			
		||||
export const getMessagesUploadsImageHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: messageExampleComplete6.value as `/${string}`,
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    isFile: true,
 | 
			
		||||
    body: ['image.png']
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getMessagesUploadsAudioHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: messageExampleComplete7.value as `/${string}`,
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    isFile: true,
 | 
			
		||||
    body: ['audio.mp3']
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getMessagesUploadsVideoHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: messageExampleComplete8.value as `/${string}`,
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    isFile: true,
 | 
			
		||||
    body: ['video.mp4']
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getMessagesUploadsDownloadHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: messageExampleComplete9.value as `/${string}`,
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    isFile: true,
 | 
			
		||||
    body: ['download.zip']
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								cypress/fixtures/uploads/video.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								cypress/fixtures/uploads/video.mp4
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -1,5 +1,4 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
 | 
			
		||||
import { userExample, userSettingsExample } from '../user'
 | 
			
		||||
 | 
			
		||||
export const getUsersCurrentHandler: Handler = {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
 | 
			
		||||
import { userExample, userSettingsExample } from '../user'
 | 
			
		||||
 | 
			
		||||
export const postUsersSignupHandler: Handler = {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,12 @@ import {
 | 
			
		||||
import { getMembersWithGuildIdHandler } from '../../../../fixtures/guilds/[guildId]/members/get'
 | 
			
		||||
import { memberExampleComplete } from '../../../../fixtures/members/member'
 | 
			
		||||
import { API_URL } from '../../../../../tools/api'
 | 
			
		||||
import {
 | 
			
		||||
  getMessagesUploadsAudioHandler,
 | 
			
		||||
  getMessagesUploadsDownloadHandler,
 | 
			
		||||
  getMessagesUploadsImageHandler,
 | 
			
		||||
  getMessagesUploadsVideoHandler
 | 
			
		||||
} from '../../../../fixtures/uploads/messages/get'
 | 
			
		||||
 | 
			
		||||
describe('Pages > /application/[guildId]/[channelId]', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
@@ -79,36 +85,95 @@ describe('Pages > /application/[guildId]/[channelId]', () => {
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getGuildMemberWithGuildIdHandler,
 | 
			
		||||
      getChannelWithChannelIdHandler,
 | 
			
		||||
      getMessagesWithChannelIdHandler
 | 
			
		||||
      getMessagesWithChannelIdHandler,
 | 
			
		||||
      getMessagesUploadsImageHandler,
 | 
			
		||||
      getMessagesUploadsAudioHandler,
 | 
			
		||||
      getMessagesUploadsVideoHandler,
 | 
			
		||||
      getMessagesUploadsDownloadHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.intercept(`${API_URL}${getMessagesWithChannelIdHandler.url}*`).as(
 | 
			
		||||
      'getMessagesWithChannelIdHandler'
 | 
			
		||||
    )
 | 
			
		||||
    cy.intercept(`${API_URL}${getMessagesUploadsImageHandler.url}`).as(
 | 
			
		||||
      'getMessagesUploadsImageHandler'
 | 
			
		||||
    )
 | 
			
		||||
    cy.intercept(`${API_URL}${getMessagesUploadsAudioHandler.url}`).as(
 | 
			
		||||
      'getMessagesUploadsAudioHandler'
 | 
			
		||||
    )
 | 
			
		||||
    cy.intercept(`${API_URL}${getMessagesUploadsVideoHandler.url}`).as(
 | 
			
		||||
      'getMessagesUploadsVideoHandler'
 | 
			
		||||
    )
 | 
			
		||||
    cy.intercept(`${API_URL}${getMessagesUploadsDownloadHandler.url}`).as(
 | 
			
		||||
      'getMessagesUploadsDownloadHandler'
 | 
			
		||||
    )
 | 
			
		||||
    cy.intercept(`/_next/*`).as('nextStaticAndImages')
 | 
			
		||||
    cy.visit(`/application/${guildExample.id}/${channelExample.id}`)
 | 
			
		||||
    cy.wait(['@getMessagesWithChannelIdHandler', '@nextStaticAndImages']).then(
 | 
			
		||||
      () => {
 | 
			
		||||
        cy.get('.messages-list').children().should('have.length', 2)
 | 
			
		||||
        cy.get('.messages-list p:first').should(
 | 
			
		||||
          'have.text',
 | 
			
		||||
          messageExampleComplete.value
 | 
			
		||||
    cy.wait([
 | 
			
		||||
      '@getMessagesWithChannelIdHandler',
 | 
			
		||||
      '@nextStaticAndImages',
 | 
			
		||||
      '@getMessagesUploadsImageHandler',
 | 
			
		||||
      '@getMessagesUploadsAudioHandler',
 | 
			
		||||
      '@getMessagesUploadsVideoHandler',
 | 
			
		||||
      '@getMessagesUploadsDownloadHandler'
 | 
			
		||||
    ]).then(() => {
 | 
			
		||||
      cy.get('.messages-list').children().should('have.length', 9)
 | 
			
		||||
      cy.get('[data-cy=message-1] p').should(
 | 
			
		||||
        'have.text',
 | 
			
		||||
        messageExampleComplete.value
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('[data-cy=message-1] [data-cy=message-member-user-name]').should(
 | 
			
		||||
        'have.text',
 | 
			
		||||
        messageExampleComplete.member.user.name
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('[data-cy=message-1] [data-cy=message-date]').should(
 | 
			
		||||
        'have.text',
 | 
			
		||||
        date.format(
 | 
			
		||||
          new Date(messageExampleComplete.createdAt),
 | 
			
		||||
          'DD/MM/YYYY - HH:mm:ss'
 | 
			
		||||
        )
 | 
			
		||||
        cy.get(
 | 
			
		||||
          '.messages-list [data-cy=message-member-user-name]:first'
 | 
			
		||||
        ).should('have.text', messageExampleComplete.member.user.name)
 | 
			
		||||
        cy.get('.messages-list [data-cy=message-date]:first').should(
 | 
			
		||||
          'have.text',
 | 
			
		||||
          date.format(
 | 
			
		||||
            new Date(messageExampleComplete.createdAt),
 | 
			
		||||
            'DD/MM/YYYY - HH:mm:ss'
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
        cy.get('.messages-list p:last').should(
 | 
			
		||||
          'have.text',
 | 
			
		||||
          messageExampleComplete2.value
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('[data-cy=message-2] p').should(
 | 
			
		||||
        'have.text',
 | 
			
		||||
        messageExampleComplete2.value
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('[data-cy=message-3] p').should(
 | 
			
		||||
        'have.text',
 | 
			
		||||
        'Message with bold text and italic text.\nNewlines and some emoji: '
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('[data-cy=message-3] strong').should('have.text', 'bold text')
 | 
			
		||||
      cy.get('[data-cy=message-3] em').should('have.text', 'italic text')
 | 
			
		||||
      cy.get('[data-cy=message-3] span[title=smile]').should('exist')
 | 
			
		||||
      cy.get('[data-cy=message-3] span[title=smile]').should(
 | 
			
		||||
        'have.css',
 | 
			
		||||
        'width',
 | 
			
		||||
        '20px'
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('[data-cy=message-3] span[title=smile]').should(
 | 
			
		||||
        'have.css',
 | 
			
		||||
        'height',
 | 
			
		||||
        '20px'
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('[data-cy=message-4] p:first').should(
 | 
			
		||||
        'have.text',
 | 
			
		||||
        'The Quadratic Formula:'
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('[data-cy=message-4] .math').should('have.length', 3)
 | 
			
		||||
      cy.get('[data-cy=message-5] span[title=wave]').should('exist')
 | 
			
		||||
      cy.get('[data-cy=message-5] span[title=wave]').should(
 | 
			
		||||
        'have.css',
 | 
			
		||||
        'width',
 | 
			
		||||
        '40px'
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('[data-cy=message-5] span[title=wave]').should(
 | 
			
		||||
        'have.css',
 | 
			
		||||
        'height',
 | 
			
		||||
        '40px'
 | 
			
		||||
      )
 | 
			
		||||
      cy.get('[data-cy=message-file-image-6]').should('exist')
 | 
			
		||||
      cy.get('[data-cy=message-file-audio-7]').should('exist')
 | 
			
		||||
      cy.get('[data-cy=message-file-video-8]').should('exist')
 | 
			
		||||
      cy.get('[data-cy=message-file-download-9]').should('exist')
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should succeeds and display the members in right sidebar correctly', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import { authenticationHandlers } from '../../../fixtures/handler'
 | 
			
		||||
import {
 | 
			
		||||
  postUsersSigninHandler,
 | 
			
		||||
  postUsersSigninInvalidCredentialsHandler
 | 
			
		||||
} from 'cypress/fixtures/users/signin/post'
 | 
			
		||||
} from '../../../fixtures/users/signin/post'
 | 
			
		||||
import { userExample } from '../../../fixtures/users/user'
 | 
			
		||||
 | 
			
		||||
describe('Pages > /authentication/signin', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import path from 'node:path'
 | 
			
		||||
 | 
			
		||||
import { getLocal } from 'mockttp'
 | 
			
		||||
 | 
			
		||||
import { API_DEFAULT_PORT } from '../../tools/api'
 | 
			
		||||
@@ -7,6 +9,13 @@ import { API_DEFAULT_PORT } from '../../tools/api'
 | 
			
		||||
/** @type {import('mockttp').Mockttp | null}  */
 | 
			
		||||
let server = null
 | 
			
		||||
 | 
			
		||||
const UPLOADS_FIXTURES_DIRECTORY = path.join(
 | 
			
		||||
  process.cwd(),
 | 
			
		||||
  'cypress',
 | 
			
		||||
  'fixtures',
 | 
			
		||||
  'uploads'
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @type {Cypress.PluginConfig}
 | 
			
		||||
 */
 | 
			
		||||
@@ -21,10 +30,18 @@ module.exports = (on, config) => {
 | 
			
		||||
      })
 | 
			
		||||
      await server.start(API_DEFAULT_PORT)
 | 
			
		||||
      for (const handler of handlers) {
 | 
			
		||||
        await server[handler.method.toLowerCase()](handler.url).thenJson(
 | 
			
		||||
          handler.response.statusCode,
 | 
			
		||||
          handler.response.body
 | 
			
		||||
        )
 | 
			
		||||
        const { isFile = false } = handler.response
 | 
			
		||||
        if (isFile) {
 | 
			
		||||
          await server[handler.method.toLowerCase()](handler.url).thenFromFile(
 | 
			
		||||
            handler.response.statusCode,
 | 
			
		||||
            path.join(UPLOADS_FIXTURES_DIRECTORY, ...handler.response.body)
 | 
			
		||||
          )
 | 
			
		||||
        } else {
 | 
			
		||||
          await server[handler.method.toLowerCase()](handler.url).thenJson(
 | 
			
		||||
            handler.response.statusCode,
 | 
			
		||||
            handler.response.body
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return null
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ module.exports = nextTranslate(
 | 
			
		||||
    images: {
 | 
			
		||||
      domains: [
 | 
			
		||||
        'api.thream.divlo.fr',
 | 
			
		||||
        ...(process.env.NODE_ENV === 'development' ? ['localhost'] : [])
 | 
			
		||||
        ...(process.env.NODE_ENV !== 'production' ? ['localhost'] : [])
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    reactStrictMode: true,
 | 
			
		||||
@@ -27,7 +27,7 @@ module.exports = nextTranslate(
 | 
			
		||||
                scriptSrc: ["'self'", "'unsafe-eval'", "'unsafe-inline'"],
 | 
			
		||||
                styleSrc: ["'self'", "'unsafe-inline'"],
 | 
			
		||||
                imgSrc: ['*', 'data:', 'blob:'],
 | 
			
		||||
                mediaSrc: "'none'",
 | 
			
		||||
                mediaSrc: ['*', 'data:', 'blob:'],
 | 
			
		||||
                connectSrc: '*',
 | 
			
		||||
                objectSrc: "'none'",
 | 
			
		||||
                fontSrc: "'self'",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5666
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5666
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										24
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								package.json
									
									
									
									
									
								
							@@ -17,12 +17,13 @@
 | 
			
		||||
    "export": "next export",
 | 
			
		||||
    "generate": "plop",
 | 
			
		||||
    "lint:commit": "commitlint",
 | 
			
		||||
    "lint:docker": "dockerfilelint './Dockerfile'",
 | 
			
		||||
    "lint:editorconfig": "editorconfig-checker",
 | 
			
		||||
    "lint:markdown": "markdownlint '**/*.md' --dot --ignore 'node_modules'",
 | 
			
		||||
    "lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'",
 | 
			
		||||
    "lint:prettier": "prettier '.' --check",
 | 
			
		||||
    "lint:staged": "lint-staged",
 | 
			
		||||
    "test:unit": "jest",
 | 
			
		||||
    "test:html-w3c-validator": "start-server-and-test 'start' 'http://localhost:3000' 'html-w3c-validator'",
 | 
			
		||||
    "test:lighthouse": "lhci autorun",
 | 
			
		||||
    "test:e2e": "start-server-and-test 'start' 'http://localhost:3000' 'cypress run'",
 | 
			
		||||
    "test:e2e:dev": "start-server-and-test 'dev' 'http://localhost:3000' 'cypress open'",
 | 
			
		||||
@@ -44,23 +45,27 @@
 | 
			
		||||
    "classnames": "2.3.1",
 | 
			
		||||
    "date-and-time": "2.0.1",
 | 
			
		||||
    "emoji-mart": "3.0.1",
 | 
			
		||||
    "katex": "0.15.1",
 | 
			
		||||
    "next": "12.0.7",
 | 
			
		||||
    "next-pwa": "5.4.4",
 | 
			
		||||
    "next-themes": "0.0.15",
 | 
			
		||||
    "next-translate": "1.2.0",
 | 
			
		||||
    "pretty-bytes": "5.6.0",
 | 
			
		||||
    "react": "17.0.2",
 | 
			
		||||
    "react-component-form": "2.0.0",
 | 
			
		||||
    "react-dom": "17.0.2",
 | 
			
		||||
    "react-infinite-scroll-component": "6.1.0",
 | 
			
		||||
    "react-markdown": "7.1.1",
 | 
			
		||||
    "react-markdown": "7.1.2",
 | 
			
		||||
    "react-responsive": "8.2.0",
 | 
			
		||||
    "react-swipeable": "6.2.0",
 | 
			
		||||
    "react-textarea-autosize": "8.3.3",
 | 
			
		||||
    "read-pkg": "7.0.0",
 | 
			
		||||
    "rehype-katex": "6.0.2",
 | 
			
		||||
    "remark-breaks": "3.0.2",
 | 
			
		||||
    "remark-gfm": "3.0.1",
 | 
			
		||||
    "remark-math": "5.1.1",
 | 
			
		||||
    "sharp": "0.29.3",
 | 
			
		||||
    "socket.io-client": "4.4.0",
 | 
			
		||||
    "socket.io-client": "4.4.1",
 | 
			
		||||
    "unified": "10.1.1",
 | 
			
		||||
    "unist-util-visit": "4.1.0",
 | 
			
		||||
    "universal-cookie": "4.0.4"
 | 
			
		||||
@@ -82,28 +87,29 @@
 | 
			
		||||
    "@types/emoji-mart": "3.0.9",
 | 
			
		||||
    "@types/hast": "2.3.4",
 | 
			
		||||
    "@types/jest": "27.4.0",
 | 
			
		||||
    "@types/node": "17.0.5",
 | 
			
		||||
    "@types/katex": "0.11.1",
 | 
			
		||||
    "@types/node": "17.0.8",
 | 
			
		||||
    "@types/react": "17.0.38",
 | 
			
		||||
    "@types/react-responsive": "8.0.5",
 | 
			
		||||
    "@types/unist": "2.0.6",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "4.33.0",
 | 
			
		||||
    "autoprefixer": "10.4.1",
 | 
			
		||||
    "cypress": "9.2.0",
 | 
			
		||||
    "dockerfilelint": "1.8.0",
 | 
			
		||||
    "editorconfig-checker": "4.0.2",
 | 
			
		||||
    "eslint": "7.32.0",
 | 
			
		||||
    "eslint-config-next": "12.0.7",
 | 
			
		||||
    "eslint-config-prettier": "8.3.0",
 | 
			
		||||
    "eslint-config-standard-with-typescript": "21.0.1",
 | 
			
		||||
    "eslint-plugin-import": "2.25.3",
 | 
			
		||||
    "eslint-plugin-import": "2.25.4",
 | 
			
		||||
    "eslint-plugin-node": "11.1.0",
 | 
			
		||||
    "eslint-plugin-prettier": "4.0.0",
 | 
			
		||||
    "eslint-plugin-promise": "5.1.1",
 | 
			
		||||
    "eslint-plugin-storybook": "0.5.5",
 | 
			
		||||
    "eslint-plugin-unicorn": "40.0.0",
 | 
			
		||||
    "husky": "7.0.4",
 | 
			
		||||
    "jest": "27.4.5",
 | 
			
		||||
    "lint-staged": "12.1.4",
 | 
			
		||||
    "html-w3c-validator": "1.0.0",
 | 
			
		||||
    "jest": "27.4.7",
 | 
			
		||||
    "lint-staged": "12.1.5",
 | 
			
		||||
    "markdownlint-cli": "0.30.0",
 | 
			
		||||
    "mockttp": "2.5.0",
 | 
			
		||||
    "next-secure-headers": "2.2.0",
 | 
			
		||||
@@ -114,7 +120,7 @@
 | 
			
		||||
    "serve": "13.0.2",
 | 
			
		||||
    "start-server-and-test": "1.14.0",
 | 
			
		||||
    "storybook-tailwind-dark-mode": "1.0.11",
 | 
			
		||||
    "tailwindcss": "3.0.8",
 | 
			
		||||
    "tailwindcss": "3.0.11",
 | 
			
		||||
    "typescript": "4.4.4",
 | 
			
		||||
    "vercel": "23.1.2",
 | 
			
		||||
    "webpack": "5.65.0"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import { NextPage } from 'next'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
 | 
			
		||||
import { Head } from 'components/Head'
 | 
			
		||||
import { Application } from 'components/Application'
 | 
			
		||||
@@ -9,7 +10,6 @@ import {
 | 
			
		||||
} from 'tools/authentication'
 | 
			
		||||
import { CreateGuild } from 'components/Application/CreateGuild'
 | 
			
		||||
import { GuildsProvider } from 'contexts/Guilds'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
 | 
			
		||||
const CreateGuildPage: NextPage<PagePropsWithAuthentication> = (props) => {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { NextPage } from 'next'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import { AuthenticationForm } from 'components/Authentication'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
 | 
			
		||||
import { AuthenticationForm } from 'components/Authentication'
 | 
			
		||||
import { Head } from 'components/Head'
 | 
			
		||||
import { Header } from 'components/Header'
 | 
			
		||||
import { Main } from 'components/design/Main'
 | 
			
		||||
 
 | 
			
		||||
@@ -13,9 +13,9 @@ import { FormState } from 'components/design/FormState'
 | 
			
		||||
import { authenticationFromServerSide } from 'tools/authentication'
 | 
			
		||||
import { AuthenticationForm } from 'components/Authentication'
 | 
			
		||||
import { ScrollableBody } from 'components/ScrollableBody/ScrollableBody'
 | 
			
		||||
import { api } from 'tools/api'
 | 
			
		||||
import { userSchema } from '../../models/User'
 | 
			
		||||
import { HandleSubmitCallback, useForm } from 'hooks/useForm'
 | 
			
		||||
import { api } from 'tools/api'
 | 
			
		||||
import { userSchema } from 'models/User'
 | 
			
		||||
 | 
			
		||||
const ResetPassword: NextPage<FooterProps> = (props) => {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import { GetStaticProps, NextPage } from 'next'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
 | 
			
		||||
import Translation from 'next-translate/Trans'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
 | 
			
		||||
@@ -9,8 +8,8 @@ import { Head } from 'components/Head'
 | 
			
		||||
import { Header } from 'components/Header'
 | 
			
		||||
import { Main } from 'components/design/Main'
 | 
			
		||||
import { Footer, FooterProps } from 'components/Footer'
 | 
			
		||||
import { SocialMediaButton } from 'components/design/SocialMediaButton'
 | 
			
		||||
import { Button } from 'components/design/Button'
 | 
			
		||||
import { SocialMediaLink } from 'components/design/SocialMediaButton'
 | 
			
		||||
import { ButtonLink } from 'components/design/Button'
 | 
			
		||||
import { ScrollableBody } from 'components/ScrollableBody'
 | 
			
		||||
 | 
			
		||||
const Home: NextPage<FooterProps> = (props) => {
 | 
			
		||||
@@ -22,8 +21,8 @@ const Home: NextPage<FooterProps> = (props) => {
 | 
			
		||||
      <Head />
 | 
			
		||||
      <Header />
 | 
			
		||||
      <Main>
 | 
			
		||||
        <section className='flex flex-col items-center w-4/5'>
 | 
			
		||||
          <section className='max-w-xs'>
 | 
			
		||||
        <div className='flex flex-col items-center w-4/5'>
 | 
			
		||||
          <div className='max-w-xs'>
 | 
			
		||||
            <Link href='/authentication/signup'>
 | 
			
		||||
              <a>
 | 
			
		||||
                <Image
 | 
			
		||||
@@ -34,8 +33,8 @@ const Home: NextPage<FooterProps> = (props) => {
 | 
			
		||||
                />
 | 
			
		||||
              </a>
 | 
			
		||||
            </Link>
 | 
			
		||||
          </section>
 | 
			
		||||
          <section className='text-center'>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className='text-center'>
 | 
			
		||||
            <h1 className='my-4 text-3xl font-medium font-headline text-green-800 dark:text-green-400'>
 | 
			
		||||
              Thream
 | 
			
		||||
            </h1>
 | 
			
		||||
@@ -54,21 +53,21 @@ const Home: NextPage<FooterProps> = (props) => {
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className='flex justify-center items-center text-center mt-8 space-x-4'>
 | 
			
		||||
              <Link href='/authentication/signup'>
 | 
			
		||||
                <a data-cy='get-started'>
 | 
			
		||||
                  <Button>{t('home:get-started')}</Button>
 | 
			
		||||
                </a>
 | 
			
		||||
              <Link href='/authentication/signup' passHref>
 | 
			
		||||
                <ButtonLink data-cy='get-started'>
 | 
			
		||||
                  {t('home:get-started')}
 | 
			
		||||
                </ButtonLink>
 | 
			
		||||
              </Link>
 | 
			
		||||
              <a
 | 
			
		||||
 | 
			
		||||
              <SocialMediaLink
 | 
			
		||||
                socialMedia='GitHub'
 | 
			
		||||
                href='https://github.com/Thream'
 | 
			
		||||
                target='_blank'
 | 
			
		||||
                rel='noopener noreferrer'
 | 
			
		||||
              >
 | 
			
		||||
                <SocialMediaButton socialMedia='GitHub' />
 | 
			
		||||
              </a>
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </section>
 | 
			
		||||
        </section>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </Main>
 | 
			
		||||
      <Footer version={version} />
 | 
			
		||||
    </ScrollableBody>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user