feat: add support for files and math for messages (#5)
This commit is contained in:
parent
fdc2a2d1de
commit
5c03a9b944
@ -6,7 +6,7 @@
|
|||||||
"plugin:storybook/recommended",
|
"plugin:storybook/recommended",
|
||||||
"prettier"
|
"prettier"
|
||||||
],
|
],
|
||||||
"plugins": ["unicorn", "prettier"],
|
"plugins": ["unicorn", "import", "prettier"],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"project": "./tsconfig.json"
|
"project": "./tsconfig.json"
|
||||||
},
|
},
|
||||||
@ -17,6 +17,13 @@
|
|||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"prettier/prettier": "error",
|
"prettier/prettier": "error",
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"groups": ["builtin", "external", "internal"],
|
||||||
|
"newlines-between": "always"
|
||||||
|
}
|
||||||
|
],
|
||||||
"unicorn/prefer-node-protocol": "error",
|
"unicorn/prefer-node-protocol": "error",
|
||||||
"unicorn/prevent-abbreviations": [
|
"unicorn/prevent-abbreviations": [
|
||||||
"error",
|
"error",
|
||||||
|
26
.github/workflows/lint.yml
vendored
26
.github/workflows/lint.yml
vendored
@ -21,13 +21,27 @@ jobs:
|
|||||||
- name: 'Install'
|
- name: 'Install'
|
||||||
run: 'npm install'
|
run: 'npm install'
|
||||||
|
|
||||||
- run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
- name: 'lint:commit'
|
||||||
- run: 'npm run lint:editorconfig'
|
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||||
- run: 'npm run lint:markdown'
|
|
||||||
- run: 'npm run lint:docker'
|
|
||||||
- run: 'npm run lint:typescript'
|
|
||||||
|
|
||||||
- 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'
|
uses: 'dotenv-linter/action-dotenv-linter@v2'
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.github_token }}
|
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'
|
- name: 'Build'
|
||||||
run: 'npm run build'
|
run: 'npm run build'
|
||||||
|
|
||||||
|
- name: 'html-w3c-validator'
|
||||||
|
run: 'npm run test:html-w3c-validator'
|
||||||
|
|
||||||
- name: 'Lighthouse'
|
- name: 'Lighthouse'
|
||||||
run: 'npm run test:lighthouse'
|
run: 'npm run test:lighthouse'
|
||||||
env:
|
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,
|
"startServerReadyTimeout": 20000,
|
||||||
"url": [
|
"url": [
|
||||||
"http://localhost:3000/",
|
"http://localhost:3000/",
|
||||||
|
"http://localhost:3000/authentication/forgot-password",
|
||||||
|
"http://localhost:3000/authentication/reset-password",
|
||||||
"http://localhost:3000/authentication/signin",
|
"http://localhost:3000/authentication/signin",
|
||||||
"http://localhost:3000/authentication/signup",
|
"http://localhost:3000/authentication/signup"
|
||||||
"http://localhost:3000/authentication/forgot-password"
|
|
||||||
],
|
],
|
||||||
"numberOfRuns": 1
|
"numberOfRuns": 1
|
||||||
},
|
},
|
||||||
|
@ -6,6 +6,5 @@
|
|||||||
"jest --findRelatedTests"
|
"jest --findRelatedTests"
|
||||||
],
|
],
|
||||||
"*.{css,yml,json}": ["prettier --write"],
|
"*.{css,yml,json}": ["prettier --write"],
|
||||||
"*.{md}": ["prettier --write", "markdownlint --dot --fix"],
|
"*.md": ["prettier --write", "markdownlint --dot --fix"]
|
||||||
"./Dockerfile": ["dockerfilelint"]
|
|
||||||
}
|
}
|
||||||
|
@ -6,3 +6,4 @@ node_modules
|
|||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
**/workbox-*.js
|
**/workbox-*.js
|
||||||
**/sw.js
|
**/sw.js
|
||||||
|
*.hbs
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
FROM node:16.13.1 AS dependencies
|
FROM node:16.13.1 AS dependencies
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY ./package*.json ./
|
COPY ./package*.json ./
|
||||||
RUN npm clean-install
|
RUN npm install
|
||||||
|
|
||||||
FROM node:16.13.1 AS builder
|
FROM node:16.13.1 AS builder
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
@ -6,13 +6,13 @@ import { useMediaQuery } from 'react-responsive'
|
|||||||
import { useSwipeable } from 'react-swipeable'
|
import { useSwipeable } from 'react-swipeable'
|
||||||
|
|
||||||
import { Sidebar, DirectionSidebar } from './Sidebar'
|
import { Sidebar, DirectionSidebar } from './Sidebar'
|
||||||
import { IconButton } from 'components/design/IconButton'
|
import { IconButton } from '../design/IconButton'
|
||||||
import { IconLink } from 'components/design/IconLink'
|
import { IconLink } from '../design/IconLink'
|
||||||
import { Guilds } from './Guilds/Guilds'
|
import { Guilds } from './Guilds/Guilds'
|
||||||
import { Divider } from '../design/Divider'
|
import { Divider } from '../design/Divider'
|
||||||
import { Members } from './Members'
|
import { Members } from './Members'
|
||||||
import { useAuthentication } from 'tools/authentication'
|
import { useAuthentication } from '../../tools/authentication'
|
||||||
import { API_URL } from 'tools/api'
|
import { API_URL } from '../../tools/api'
|
||||||
|
|
||||||
export interface GuildsChannelsPath {
|
export interface GuildsChannelsPath {
|
||||||
guildId: number
|
guildId: number
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Meta, Story } from '@storybook/react'
|
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'
|
import { Channel as Component, ChannelProps } from './Channel'
|
||||||
|
|
||||||
const Stories: Meta = {
|
const Stories: Meta = {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { render } from '@testing-library/react'
|
import { render } from '@testing-library/react'
|
||||||
import { channelExample } from 'cypress/fixtures/channels/channel'
|
|
||||||
|
|
||||||
|
import { channelExample } from '../../../../cypress/fixtures/channels/channel'
|
||||||
import { Channel } from './Channel'
|
import { Channel } from './Channel'
|
||||||
|
|
||||||
describe('<Channel />', () => {
|
describe('<Channel />', () => {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||||
|
|
||||||
|
import { useChannels } from '../../../contexts/Channels'
|
||||||
import { GuildsChannelsPath } from '../Application'
|
import { GuildsChannelsPath } from '../Application'
|
||||||
import { Loader } from 'components/design/Loader'
|
import { Loader } from '../../design/Loader'
|
||||||
import { Channel } from './Channel'
|
import { Channel } from './Channel'
|
||||||
import { useChannels } from 'contexts/Channels'
|
|
||||||
|
|
||||||
export interface ChannelsProps {
|
export interface ChannelsProps {
|
||||||
path: GuildsChannelsPath
|
path: GuildsChannelsPath
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { CogIcon, PlusIcon } from '@heroicons/react/solid'
|
import { CogIcon, PlusIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
import { useGuildMember } from 'contexts/GuildMember'
|
import { useGuildMember } from '../../../contexts/GuildMember'
|
||||||
import { Divider } from 'components/design/Divider'
|
import { Divider } from '../../design/Divider'
|
||||||
import { Channels } from 'components/Application/Channels'
|
import { Channels } from '../../Application/Channels'
|
||||||
import { IconButton } from 'components/design/IconButton'
|
import { IconButton } from '../../design/IconButton'
|
||||||
import { GuildsChannelsPath } from '..'
|
import { GuildsChannelsPath } from '..'
|
||||||
|
|
||||||
export interface GuildLeftSidebarProps {
|
export interface GuildLeftSidebarProps {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||||
|
|
||||||
import { Loader } from 'components/design/Loader'
|
import { Loader } from '../../design/Loader'
|
||||||
import { Guild } from './Guild'
|
import { Guild } from './Guild'
|
||||||
import { useGuilds } from 'contexts/Guilds'
|
import { useGuilds } from '../../../contexts/Guilds'
|
||||||
import { GuildsChannelsPath } from '..'
|
import { GuildsChannelsPath } from '..'
|
||||||
|
|
||||||
export interface GuildsProps {
|
export interface GuildsProps {
|
||||||
|
@ -2,11 +2,11 @@ import useTranslation from 'next-translate/useTranslation'
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||||
|
|
||||||
import { useAuthentication } from 'tools/authentication'
|
import { useAuthentication } from '../../../tools/authentication'
|
||||||
import { GuildPublic as GuildPublicType } from 'models/Guild'
|
import { GuildPublic as GuildPublicType } from '../../../models/Guild'
|
||||||
import { Loader } from 'components/design/Loader'
|
import { Loader } from '../../design/Loader'
|
||||||
import { GuildPublic } from './GuildPublic'
|
import { GuildPublic } from './GuildPublic'
|
||||||
import { usePagination } from 'hooks/usePagination'
|
import { usePagination } from '../../../hooks/usePagination'
|
||||||
|
|
||||||
export const JoinGuildsPublic: React.FC = () => {
|
export const JoinGuildsPublic: React.FC = () => {
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
@ -2,8 +2,8 @@ import useTranslation from 'next-translate/useTranslation'
|
|||||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||||
|
|
||||||
import { Divider } from '../../design/Divider'
|
import { Divider } from '../../design/Divider'
|
||||||
import { Loader } from 'components/design/Loader'
|
import { Loader } from '../../design/Loader'
|
||||||
import { useMembers } from 'contexts/Members'
|
import { useMembers } from '../../../contexts/Members'
|
||||||
import { Member } from './Member'
|
import { Member } from './Member'
|
||||||
import { capitalize } from '../../../tools/utils/capitalize'
|
import { capitalize } from '../../../tools/utils/capitalize'
|
||||||
|
|
||||||
|
@ -4,7 +4,9 @@ import date from 'date-and-time'
|
|||||||
|
|
||||||
import { MessageWithMember } from '../../../../models/Message'
|
import { MessageWithMember } from '../../../../models/Message'
|
||||||
import { API_URL } from '../../../../tools/api'
|
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 {
|
export interface MessageProps {
|
||||||
message: MessageWithMember
|
message: MessageWithMember
|
||||||
@ -14,7 +16,10 @@ export const Message: React.FC<MessageProps> = (props) => {
|
|||||||
const { message } = props
|
const { message } = props
|
||||||
|
|
||||||
return (
|
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}`}>
|
<Link href={`/application/users/${message.member.user.id}`}>
|
||||||
<a>
|
<a>
|
||||||
<div className='w-12 h-12 mr-4 flex flex-shrink-0 items-center justify-center'>
|
<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')}
|
{date.format(new Date(message.createdAt), 'DD/MM/YYYY - HH:mm:ss')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<MessageContent message={message} />
|
{message.type === 'text' ? (
|
||||||
|
<MessageText message={message} />
|
||||||
|
) : message.type === 'file' ? (
|
||||||
|
<MessageFile message={message} />
|
||||||
|
) : (
|
||||||
|
<Loader />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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 ReactMarkdown from 'react-markdown'
|
||||||
import gfm from 'remark-gfm'
|
import gfm from 'remark-gfm'
|
||||||
import remarkBreaks from 'remark-breaks'
|
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 { Emoji, emojiPlugin, isStringWithOnlyOneEmoji } from '../../../../Emoji'
|
||||||
import { MessageWithMember } from '../../../../../models/Message'
|
import { MessageWithMember } from '../../../../../models/Message'
|
||||||
@ -10,7 +14,7 @@ export interface MessageContentProps {
|
|||||||
message: MessageWithMember
|
message: MessageWithMember
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MessageContent: React.FC<MessageContentProps> = (props) => {
|
export const MessageText: React.FC<MessageContentProps> = (props) => {
|
||||||
const { message } = props
|
const { message } = props
|
||||||
|
|
||||||
const isMessageWithOnlyOneEmoji = useMemo(() => {
|
const isMessageWithOnlyOneEmoji = useMemo(() => {
|
||||||
@ -31,8 +35,8 @@ export const MessageContent: React.FC<MessageContentProps> = (props) => {
|
|||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
disallowedElements={['table']}
|
disallowedElements={['table']}
|
||||||
unwrapDisallowed
|
unwrapDisallowed
|
||||||
remarkPlugins={[[gfm], [remarkBreaks]]}
|
remarkPlugins={[[gfm], [remarkBreaks], [remarkMath]]}
|
||||||
rehypePlugins={[emojiPlugin]}
|
rehypePlugins={[[emojiPlugin], [rehypeKatex]]}
|
||||||
linkTarget='_blank'
|
linkTarget='_blank'
|
||||||
components={{
|
components={{
|
||||||
emoji: (props) => {
|
emoji: (props) => {
|
@ -0,0 +1 @@
|
|||||||
|
export * from './MessageText'
|
@ -1,9 +1,9 @@
|
|||||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||||
|
|
||||||
import { Loader } from 'components/design/Loader'
|
import { Loader } from '../../design/Loader'
|
||||||
import { Message } from './Message'
|
import { Message } from './Message'
|
||||||
import { useMessages } from 'contexts/Messages'
|
import { useMessages } from '../../../contexts/Messages'
|
||||||
import { Emoji } from 'components/Emoji'
|
import { Emoji } from '../../Emoji'
|
||||||
|
|
||||||
export const Messages: React.FC = () => {
|
export const Messages: React.FC = () => {
|
||||||
const { messages, hasMore, nextPage } = useMessages()
|
const { messages, hasMore, nextPage } = useMessages()
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Meta, Story } from '@storybook/react'
|
import { Meta, Story } from '@storybook/react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
userExample,
|
userExample,
|
||||||
userSettingsExample
|
userSettingsExample
|
||||||
} from '../../../cypress/fixtures/users/user'
|
} from '../../../cypress/fixtures/users/user'
|
||||||
|
|
||||||
import { UserProfile as Component, UserProfileProps } from './UserProfile'
|
import { UserProfile as Component, UserProfileProps } from './UserProfile'
|
||||||
|
|
||||||
const Stories: Meta = {
|
const Stories: Meta = {
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { render } from '@testing-library/react'
|
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'
|
import { UserProfile } from './UserProfile'
|
||||||
|
|
||||||
describe('<UserProfile />', () => {
|
describe('<UserProfile />', () => {
|
||||||
it('should render successfully', () => {
|
it('should render successfully', () => {
|
||||||
const { baseElement } = render(
|
const { baseElement } = render(
|
||||||
<UserProfile user={{ ...user, settings: userSettings }} />
|
<UserProfile user={{ ...userExample, settings: userSettingsExample }} />
|
||||||
)
|
)
|
||||||
expect(baseElement).toBeTruthy()
|
expect(baseElement).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
@ -11,7 +11,7 @@ import { Button } from '../design/Button'
|
|||||||
import { FormState } from '../design/FormState'
|
import { FormState } from '../design/FormState'
|
||||||
import { AuthenticationForm } from './'
|
import { AuthenticationForm } from './'
|
||||||
import { userSchema } from '../../models/User'
|
import { userSchema } from '../../models/User'
|
||||||
import { api } from 'tools/api'
|
import { api } from '../../tools/api'
|
||||||
import {
|
import {
|
||||||
Tokens,
|
Tokens,
|
||||||
Authentication as AuthenticationClass
|
Authentication as AuthenticationClass
|
||||||
@ -85,16 +85,16 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Main>
|
<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'>
|
<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='Google' />
|
||||||
<SocialMediaButton socialMedia='GitHub' />
|
<SocialMediaButton socialMedia='GitHub' />
|
||||||
<SocialMediaButton socialMedia='Discord' />
|
<SocialMediaButton socialMedia='Discord' />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
<section className='text-center text-lg font-paragraph pt-8'>
|
<div className='text-center text-lg font-paragraph pt-8'>
|
||||||
{t('authentication:or')}
|
{t('authentication:or')}
|
||||||
</section>
|
</div>
|
||||||
<AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
|
<AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
|
||||||
{mode === 'signup' && (
|
{mode === 'signup' && (
|
||||||
<Input
|
<Input
|
||||||
|
@ -1,18 +1,36 @@
|
|||||||
|
import { forwardRef } from 'react'
|
||||||
import classNames from 'classnames'
|
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 interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {}
|
||||||
|
|
||||||
export const Button: React.FC<ButtonProps> = (props) => {
|
export const Button: React.FC<ButtonProps> = (props) => {
|
||||||
const { children, className, ...rest } = props
|
const { children, className: givenClassName, ...rest } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button className={classNames(className, givenClassName)} {...rest}>
|
||||||
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}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { FetchState as FormStateType } from 'hooks/useFetchState'
|
import { FetchState as FormStateType } from '../../../hooks/useFetchState'
|
||||||
import { Loader } from '../Loader'
|
import { Loader } from '../Loader'
|
||||||
|
|
||||||
export interface FormStateProps {
|
export interface FormStateProps {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { FormState } from '../FormState'
|
import { FormState } from '../FormState'
|
||||||
|
|
||||||
export interface InputProps extends React.ComponentPropsWithRef<'input'> {
|
export interface InputProps extends React.ComponentPropsWithRef<'input'> {
|
||||||
|
@ -17,7 +17,7 @@ describe('<SocialMediaButton />', () => {
|
|||||||
const { findByTestId } = render(
|
const { findByTestId } = render(
|
||||||
<SocialMediaButton socialMedia={socialMedia} />
|
<SocialMediaButton socialMedia={socialMedia} />
|
||||||
)
|
)
|
||||||
const button = await findByTestId('button')
|
const button = await findByTestId('social-media-button')
|
||||||
expect(button).toHaveStyle('color: #000')
|
expect(button).toHaveStyle('color: #000')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -8,19 +8,41 @@ type SocialMediaColors = {
|
|||||||
[key in SocialMedia]: string
|
[key in SocialMedia]: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocialMediaButtonProps
|
|
||||||
extends React.ComponentPropsWithRef<'button'> {
|
|
||||||
socialMedia: SocialMedia
|
|
||||||
}
|
|
||||||
|
|
||||||
const socialMediaColors: SocialMediaColors = {
|
const socialMediaColors: SocialMediaColors = {
|
||||||
Discord: '#404EED',
|
Discord: '#404EED',
|
||||||
GitHub: '#24292E',
|
GitHub: '#24292E',
|
||||||
Google: '#FCFCFC'
|
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) => {
|
export const SocialMediaButton: React.FC<SocialMediaButtonProps> = (props) => {
|
||||||
const { socialMedia, className, ...rest } = props
|
const { socialMedia, className: givenClassName, ...rest } = props
|
||||||
|
|
||||||
const socialMediaColor = useMemo(() => {
|
const socialMediaColor = useMemo(() => {
|
||||||
return socialMediaColors[socialMedia]
|
return socialMediaColors[socialMedia]
|
||||||
@ -29,20 +51,11 @@ export const SocialMediaButton: React.FC<SocialMediaButtonProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
data-testid='button'
|
data-testid='social-media-button'
|
||||||
{...rest}
|
{...rest}
|
||||||
className={classNames(
|
className={classNames(className, 'button', givenClassName)}
|
||||||
`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
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Image
|
<SocialMediaChildren socialMedia={socialMedia} />
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
src={`/images/svg/web/${socialMedia}.svg`}
|
|
||||||
alt={socialMedia}
|
|
||||||
/>
|
|
||||||
<span className='ml-2'>{socialMedia}</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<style jsx>
|
<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 { Handler } from '../../handler'
|
||||||
|
|
||||||
import { channelExample } from '../channel'
|
import { channelExample } from '../channel'
|
||||||
|
|
||||||
export const getChannelWithChannelIdHandler: Handler = {
|
export const getChannelWithChannelIdHandler: Handler = {
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
import { Handler } from '../../../handler'
|
import { Handler } from '../../../handler'
|
||||||
import {
|
import {
|
||||||
messageExampleComplete,
|
messageExampleComplete,
|
||||||
messageExampleComplete2
|
messageExampleComplete2,
|
||||||
|
messageExampleComplete3,
|
||||||
|
messageExampleComplete4,
|
||||||
|
messageExampleComplete5,
|
||||||
|
messageExampleComplete6,
|
||||||
|
messageExampleComplete7,
|
||||||
|
messageExampleComplete8,
|
||||||
|
messageExampleComplete9
|
||||||
} from '../../../messages/message'
|
} from '../../../messages/message'
|
||||||
import { channelExample } from '../../channel'
|
import { channelExample } from '../../channel'
|
||||||
|
|
||||||
@ -10,6 +17,16 @@ export const getMessagesWithChannelIdHandler: Handler = {
|
|||||||
url: `/channels/${channelExample.id}/messages`,
|
url: `/channels/${channelExample.id}/messages`,
|
||||||
response: {
|
response: {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
body: [messageExampleComplete, messageExampleComplete2]
|
body: [
|
||||||
|
messageExampleComplete,
|
||||||
|
messageExampleComplete2,
|
||||||
|
messageExampleComplete3,
|
||||||
|
messageExampleComplete4,
|
||||||
|
messageExampleComplete5,
|
||||||
|
messageExampleComplete6,
|
||||||
|
messageExampleComplete7,
|
||||||
|
messageExampleComplete8,
|
||||||
|
messageExampleComplete9
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Handler } from '../../handler'
|
import { Handler } from '../../handler'
|
||||||
|
|
||||||
import { guildExample } from '../guild'
|
import { guildExample } from '../guild'
|
||||||
import { memberExampleComplete } from '../../members/member'
|
import { memberExampleComplete } from '../../members/member'
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Handler } from '../handler'
|
import { Handler } from '../handler'
|
||||||
|
|
||||||
import { guildExample, guildExample2 } from './guild'
|
import { guildExample, guildExample2 } from './guild'
|
||||||
|
|
||||||
export const getGuildsHandler: Handler = {
|
export const getGuildsHandler: Handler = {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Handler } from '../handler'
|
import { Handler } from '../handler'
|
||||||
|
|
||||||
import { guildExample } from './guild'
|
import { guildExample } from './guild'
|
||||||
import { channelExample } from '../channels/channel'
|
import { channelExample } from '../channels/channel'
|
||||||
import { memberExampleComplete } from '../members/member'
|
import { memberExampleComplete } from '../members/member'
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Handler } from '../../handler'
|
import { Handler } from '../../handler'
|
||||||
|
|
||||||
import { guildExample, guildExample2 } from '../guild'
|
import { guildExample, guildExample2 } from '../guild'
|
||||||
|
|
||||||
export const getGuildsPublicEmptyHandler: Handler = {
|
export const getGuildsPublicEmptyHandler: Handler = {
|
||||||
|
@ -5,6 +5,7 @@ export interface Handler {
|
|||||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||||
url: `/${string}`
|
url: `/${string}`
|
||||||
response: {
|
response: {
|
||||||
|
isFile?: boolean
|
||||||
body: any
|
body: any
|
||||||
statusCode: number
|
statusCode: number
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,63 @@ export const messageExampleComplete = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const messageExampleComplete2 = {
|
export const messageExampleComplete2 = {
|
||||||
...messageExample,
|
...messageExampleComplete,
|
||||||
id: 2,
|
id: 2,
|
||||||
value: 'Second message',
|
value: 'Message with bad html: <script>alert("xss")</script>'
|
||||||
member: memberExampleComplete
|
}
|
||||||
|
|
||||||
|
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 { Handler } from '../../handler'
|
||||||
|
|
||||||
import { userExample, userSettingsExample } from '../user'
|
import { userExample, userSettingsExample } from '../user'
|
||||||
|
|
||||||
export const getUsersCurrentHandler: Handler = {
|
export const getUsersCurrentHandler: Handler = {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Handler } from '../../handler'
|
import { Handler } from '../../handler'
|
||||||
|
|
||||||
import { userExample, userSettingsExample } from '../user'
|
import { userExample, userSettingsExample } from '../user'
|
||||||
|
|
||||||
export const postUsersSignupHandler: Handler = {
|
export const postUsersSignupHandler: Handler = {
|
||||||
|
@ -18,6 +18,12 @@ import {
|
|||||||
import { getMembersWithGuildIdHandler } from '../../../../fixtures/guilds/[guildId]/members/get'
|
import { getMembersWithGuildIdHandler } from '../../../../fixtures/guilds/[guildId]/members/get'
|
||||||
import { memberExampleComplete } from '../../../../fixtures/members/member'
|
import { memberExampleComplete } from '../../../../fixtures/members/member'
|
||||||
import { API_URL } from '../../../../../tools/api'
|
import { API_URL } from '../../../../../tools/api'
|
||||||
|
import {
|
||||||
|
getMessagesUploadsAudioHandler,
|
||||||
|
getMessagesUploadsDownloadHandler,
|
||||||
|
getMessagesUploadsImageHandler,
|
||||||
|
getMessagesUploadsVideoHandler
|
||||||
|
} from '../../../../fixtures/uploads/messages/get'
|
||||||
|
|
||||||
describe('Pages > /application/[guildId]/[channelId]', () => {
|
describe('Pages > /application/[guildId]/[channelId]', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -79,36 +85,95 @@ describe('Pages > /application/[guildId]/[channelId]', () => {
|
|||||||
...authenticationHandlers,
|
...authenticationHandlers,
|
||||||
getGuildMemberWithGuildIdHandler,
|
getGuildMemberWithGuildIdHandler,
|
||||||
getChannelWithChannelIdHandler,
|
getChannelWithChannelIdHandler,
|
||||||
getMessagesWithChannelIdHandler
|
getMessagesWithChannelIdHandler,
|
||||||
|
getMessagesUploadsImageHandler,
|
||||||
|
getMessagesUploadsAudioHandler,
|
||||||
|
getMessagesUploadsVideoHandler,
|
||||||
|
getMessagesUploadsDownloadHandler
|
||||||
]).setCookie('refreshToken', 'refresh-token')
|
]).setCookie('refreshToken', 'refresh-token')
|
||||||
cy.intercept(`${API_URL}${getMessagesWithChannelIdHandler.url}*`).as(
|
cy.intercept(`${API_URL}${getMessagesWithChannelIdHandler.url}*`).as(
|
||||||
'getMessagesWithChannelIdHandler'
|
'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.intercept(`/_next/*`).as('nextStaticAndImages')
|
||||||
cy.visit(`/application/${guildExample.id}/${channelExample.id}`)
|
cy.visit(`/application/${guildExample.id}/${channelExample.id}`)
|
||||||
cy.wait(['@getMessagesWithChannelIdHandler', '@nextStaticAndImages']).then(
|
cy.wait([
|
||||||
() => {
|
'@getMessagesWithChannelIdHandler',
|
||||||
cy.get('.messages-list').children().should('have.length', 2)
|
'@nextStaticAndImages',
|
||||||
cy.get('.messages-list p:first').should(
|
'@getMessagesUploadsImageHandler',
|
||||||
|
'@getMessagesUploadsAudioHandler',
|
||||||
|
'@getMessagesUploadsVideoHandler',
|
||||||
|
'@getMessagesUploadsDownloadHandler'
|
||||||
|
]).then(() => {
|
||||||
|
cy.get('.messages-list').children().should('have.length', 9)
|
||||||
|
cy.get('[data-cy=message-1] p').should(
|
||||||
'have.text',
|
'have.text',
|
||||||
messageExampleComplete.value
|
messageExampleComplete.value
|
||||||
)
|
)
|
||||||
cy.get(
|
cy.get('[data-cy=message-1] [data-cy=message-member-user-name]').should(
|
||||||
'.messages-list [data-cy=message-member-user-name]:first'
|
'have.text',
|
||||||
).should('have.text', messageExampleComplete.member.user.name)
|
messageExampleComplete.member.user.name
|
||||||
cy.get('.messages-list [data-cy=message-date]:first').should(
|
)
|
||||||
|
cy.get('[data-cy=message-1] [data-cy=message-date]').should(
|
||||||
'have.text',
|
'have.text',
|
||||||
date.format(
|
date.format(
|
||||||
new Date(messageExampleComplete.createdAt),
|
new Date(messageExampleComplete.createdAt),
|
||||||
'DD/MM/YYYY - HH:mm:ss'
|
'DD/MM/YYYY - HH:mm:ss'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
cy.get('.messages-list p:last').should(
|
cy.get('[data-cy=message-2] p').should(
|
||||||
'have.text',
|
'have.text',
|
||||||
messageExampleComplete2.value
|
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', () => {
|
it('should succeeds and display the members in right sidebar correctly', () => {
|
||||||
|
@ -2,7 +2,7 @@ import { authenticationHandlers } from '../../../fixtures/handler'
|
|||||||
import {
|
import {
|
||||||
postUsersSigninHandler,
|
postUsersSigninHandler,
|
||||||
postUsersSigninInvalidCredentialsHandler
|
postUsersSigninInvalidCredentialsHandler
|
||||||
} from 'cypress/fixtures/users/signin/post'
|
} from '../../../fixtures/users/signin/post'
|
||||||
import { userExample } from '../../../fixtures/users/user'
|
import { userExample } from '../../../fixtures/users/user'
|
||||||
|
|
||||||
describe('Pages > /authentication/signin', () => {
|
describe('Pages > /authentication/signin', () => {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
import { getLocal } from 'mockttp'
|
import { getLocal } from 'mockttp'
|
||||||
|
|
||||||
import { API_DEFAULT_PORT } from '../../tools/api'
|
import { API_DEFAULT_PORT } from '../../tools/api'
|
||||||
@ -7,6 +9,13 @@ import { API_DEFAULT_PORT } from '../../tools/api'
|
|||||||
/** @type {import('mockttp').Mockttp | null} */
|
/** @type {import('mockttp').Mockttp | null} */
|
||||||
let server = null
|
let server = null
|
||||||
|
|
||||||
|
const UPLOADS_FIXTURES_DIRECTORY = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
'cypress',
|
||||||
|
'fixtures',
|
||||||
|
'uploads'
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Cypress.PluginConfig}
|
* @type {Cypress.PluginConfig}
|
||||||
*/
|
*/
|
||||||
@ -21,11 +30,19 @@ module.exports = (on, config) => {
|
|||||||
})
|
})
|
||||||
await server.start(API_DEFAULT_PORT)
|
await server.start(API_DEFAULT_PORT)
|
||||||
for (const handler of handlers) {
|
for (const handler of handlers) {
|
||||||
|
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(
|
await server[handler.method.toLowerCase()](handler.url).thenJson(
|
||||||
handler.response.statusCode,
|
handler.response.statusCode,
|
||||||
handler.response.body
|
handler.response.body
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ module.exports = nextTranslate(
|
|||||||
images: {
|
images: {
|
||||||
domains: [
|
domains: [
|
||||||
'api.thream.divlo.fr',
|
'api.thream.divlo.fr',
|
||||||
...(process.env.NODE_ENV === 'development' ? ['localhost'] : [])
|
...(process.env.NODE_ENV !== 'production' ? ['localhost'] : [])
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
@ -27,7 +27,7 @@ module.exports = nextTranslate(
|
|||||||
scriptSrc: ["'self'", "'unsafe-eval'", "'unsafe-inline'"],
|
scriptSrc: ["'self'", "'unsafe-eval'", "'unsafe-inline'"],
|
||||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||||
imgSrc: ['*', 'data:', 'blob:'],
|
imgSrc: ['*', 'data:', 'blob:'],
|
||||||
mediaSrc: "'none'",
|
mediaSrc: ['*', 'data:', 'blob:'],
|
||||||
connectSrc: '*',
|
connectSrc: '*',
|
||||||
objectSrc: "'none'",
|
objectSrc: "'none'",
|
||||||
fontSrc: "'self'",
|
fontSrc: "'self'",
|
||||||
|
5662
package-lock.json
generated
5662
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",
|
"export": "next export",
|
||||||
"generate": "plop",
|
"generate": "plop",
|
||||||
"lint:commit": "commitlint",
|
"lint:commit": "commitlint",
|
||||||
"lint:docker": "dockerfilelint './Dockerfile'",
|
|
||||||
"lint:editorconfig": "editorconfig-checker",
|
"lint:editorconfig": "editorconfig-checker",
|
||||||
"lint:markdown": "markdownlint '**/*.md' --dot --ignore 'node_modules'",
|
"lint:markdown": "markdownlint '**/*.md' --dot --ignore 'node_modules'",
|
||||||
"lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'",
|
"lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'",
|
||||||
|
"lint:prettier": "prettier '.' --check",
|
||||||
"lint:staged": "lint-staged",
|
"lint:staged": "lint-staged",
|
||||||
"test:unit": "jest",
|
"test:unit": "jest",
|
||||||
|
"test:html-w3c-validator": "start-server-and-test 'start' 'http://localhost:3000' 'html-w3c-validator'",
|
||||||
"test:lighthouse": "lhci autorun",
|
"test:lighthouse": "lhci autorun",
|
||||||
"test:e2e": "start-server-and-test 'start' 'http://localhost:3000' 'cypress run'",
|
"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'",
|
"test:e2e:dev": "start-server-and-test 'dev' 'http://localhost:3000' 'cypress open'",
|
||||||
@ -44,23 +45,27 @@
|
|||||||
"classnames": "2.3.1",
|
"classnames": "2.3.1",
|
||||||
"date-and-time": "2.0.1",
|
"date-and-time": "2.0.1",
|
||||||
"emoji-mart": "3.0.1",
|
"emoji-mart": "3.0.1",
|
||||||
|
"katex": "0.15.1",
|
||||||
"next": "12.0.7",
|
"next": "12.0.7",
|
||||||
"next-pwa": "5.4.4",
|
"next-pwa": "5.4.4",
|
||||||
"next-themes": "0.0.15",
|
"next-themes": "0.0.15",
|
||||||
"next-translate": "1.2.0",
|
"next-translate": "1.2.0",
|
||||||
|
"pretty-bytes": "5.6.0",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-component-form": "2.0.0",
|
"react-component-form": "2.0.0",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-infinite-scroll-component": "6.1.0",
|
"react-infinite-scroll-component": "6.1.0",
|
||||||
"react-markdown": "7.1.1",
|
"react-markdown": "7.1.2",
|
||||||
"react-responsive": "8.2.0",
|
"react-responsive": "8.2.0",
|
||||||
"react-swipeable": "6.2.0",
|
"react-swipeable": "6.2.0",
|
||||||
"react-textarea-autosize": "8.3.3",
|
"react-textarea-autosize": "8.3.3",
|
||||||
"read-pkg": "7.0.0",
|
"read-pkg": "7.0.0",
|
||||||
|
"rehype-katex": "6.0.2",
|
||||||
"remark-breaks": "3.0.2",
|
"remark-breaks": "3.0.2",
|
||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
|
"remark-math": "5.1.1",
|
||||||
"sharp": "0.29.3",
|
"sharp": "0.29.3",
|
||||||
"socket.io-client": "4.4.0",
|
"socket.io-client": "4.4.1",
|
||||||
"unified": "10.1.1",
|
"unified": "10.1.1",
|
||||||
"unist-util-visit": "4.1.0",
|
"unist-util-visit": "4.1.0",
|
||||||
"universal-cookie": "4.0.4"
|
"universal-cookie": "4.0.4"
|
||||||
@ -82,28 +87,29 @@
|
|||||||
"@types/emoji-mart": "3.0.9",
|
"@types/emoji-mart": "3.0.9",
|
||||||
"@types/hast": "2.3.4",
|
"@types/hast": "2.3.4",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/node": "17.0.5",
|
"@types/katex": "0.11.1",
|
||||||
|
"@types/node": "17.0.8",
|
||||||
"@types/react": "17.0.38",
|
"@types/react": "17.0.38",
|
||||||
"@types/react-responsive": "8.0.5",
|
"@types/react-responsive": "8.0.5",
|
||||||
"@types/unist": "2.0.6",
|
"@types/unist": "2.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "4.33.0",
|
"@typescript-eslint/eslint-plugin": "4.33.0",
|
||||||
"autoprefixer": "10.4.1",
|
"autoprefixer": "10.4.1",
|
||||||
"cypress": "9.2.0",
|
"cypress": "9.2.0",
|
||||||
"dockerfilelint": "1.8.0",
|
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "4.0.2",
|
||||||
"eslint": "7.32.0",
|
"eslint": "7.32.0",
|
||||||
"eslint-config-next": "12.0.7",
|
"eslint-config-next": "12.0.7",
|
||||||
"eslint-config-prettier": "8.3.0",
|
"eslint-config-prettier": "8.3.0",
|
||||||
"eslint-config-standard-with-typescript": "21.0.1",
|
"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-node": "11.1.0",
|
||||||
"eslint-plugin-prettier": "4.0.0",
|
"eslint-plugin-prettier": "4.0.0",
|
||||||
"eslint-plugin-promise": "5.1.1",
|
"eslint-plugin-promise": "5.1.1",
|
||||||
"eslint-plugin-storybook": "0.5.5",
|
"eslint-plugin-storybook": "0.5.5",
|
||||||
"eslint-plugin-unicorn": "40.0.0",
|
"eslint-plugin-unicorn": "40.0.0",
|
||||||
"husky": "7.0.4",
|
"husky": "7.0.4",
|
||||||
"jest": "27.4.5",
|
"html-w3c-validator": "1.0.0",
|
||||||
"lint-staged": "12.1.4",
|
"jest": "27.4.7",
|
||||||
|
"lint-staged": "12.1.5",
|
||||||
"markdownlint-cli": "0.30.0",
|
"markdownlint-cli": "0.30.0",
|
||||||
"mockttp": "2.5.0",
|
"mockttp": "2.5.0",
|
||||||
"next-secure-headers": "2.2.0",
|
"next-secure-headers": "2.2.0",
|
||||||
@ -114,7 +120,7 @@
|
|||||||
"serve": "13.0.2",
|
"serve": "13.0.2",
|
||||||
"start-server-and-test": "1.14.0",
|
"start-server-and-test": "1.14.0",
|
||||||
"storybook-tailwind-dark-mode": "1.0.11",
|
"storybook-tailwind-dark-mode": "1.0.11",
|
||||||
"tailwindcss": "3.0.8",
|
"tailwindcss": "3.0.11",
|
||||||
"typescript": "4.4.4",
|
"typescript": "4.4.4",
|
||||||
"vercel": "23.1.2",
|
"vercel": "23.1.2",
|
||||||
"webpack": "5.65.0"
|
"webpack": "5.65.0"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { NextPage } from 'next'
|
import { NextPage } from 'next'
|
||||||
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Application } from 'components/Application'
|
import { Application } from 'components/Application'
|
||||||
@ -9,7 +10,6 @@ import {
|
|||||||
} from 'tools/authentication'
|
} from 'tools/authentication'
|
||||||
import { CreateGuild } from 'components/Application/CreateGuild'
|
import { CreateGuild } from 'components/Application/CreateGuild'
|
||||||
import { GuildsProvider } from 'contexts/Guilds'
|
import { GuildsProvider } from 'contexts/Guilds'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
|
||||||
|
|
||||||
const CreateGuildPage: NextPage<PagePropsWithAuthentication> = (props) => {
|
const CreateGuildPage: NextPage<PagePropsWithAuthentication> = (props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { NextPage } from 'next'
|
import { NextPage } from 'next'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { AuthenticationForm } from 'components/Authentication'
|
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { AuthenticationForm } from 'components/Authentication'
|
||||||
import { Head } from 'components/Head'
|
import { Head } from 'components/Head'
|
||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
import { Main } from 'components/design/Main'
|
import { Main } from 'components/design/Main'
|
||||||
|
@ -13,9 +13,9 @@ import { FormState } from 'components/design/FormState'
|
|||||||
import { authenticationFromServerSide } from 'tools/authentication'
|
import { authenticationFromServerSide } from 'tools/authentication'
|
||||||
import { AuthenticationForm } from 'components/Authentication'
|
import { AuthenticationForm } from 'components/Authentication'
|
||||||
import { ScrollableBody } from 'components/ScrollableBody/ScrollableBody'
|
import { ScrollableBody } from 'components/ScrollableBody/ScrollableBody'
|
||||||
import { api } from 'tools/api'
|
|
||||||
import { userSchema } from '../../models/User'
|
|
||||||
import { HandleSubmitCallback, useForm } from 'hooks/useForm'
|
import { HandleSubmitCallback, useForm } from 'hooks/useForm'
|
||||||
|
import { api } from 'tools/api'
|
||||||
|
import { userSchema } from 'models/User'
|
||||||
|
|
||||||
const ResetPassword: NextPage<FooterProps> = (props) => {
|
const ResetPassword: NextPage<FooterProps> = (props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { GetStaticProps, NextPage } from 'next'
|
import { GetStaticProps, NextPage } from 'next'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
import Translation from 'next-translate/Trans'
|
import Translation from 'next-translate/Trans'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
|
|
||||||
@ -9,8 +8,8 @@ import { Head } from 'components/Head'
|
|||||||
import { Header } from 'components/Header'
|
import { Header } from 'components/Header'
|
||||||
import { Main } from 'components/design/Main'
|
import { Main } from 'components/design/Main'
|
||||||
import { Footer, FooterProps } from 'components/Footer'
|
import { Footer, FooterProps } from 'components/Footer'
|
||||||
import { SocialMediaButton } from 'components/design/SocialMediaButton'
|
import { SocialMediaLink } from 'components/design/SocialMediaButton'
|
||||||
import { Button } from 'components/design/Button'
|
import { ButtonLink } from 'components/design/Button'
|
||||||
import { ScrollableBody } from 'components/ScrollableBody'
|
import { ScrollableBody } from 'components/ScrollableBody'
|
||||||
|
|
||||||
const Home: NextPage<FooterProps> = (props) => {
|
const Home: NextPage<FooterProps> = (props) => {
|
||||||
@ -22,8 +21,8 @@ const Home: NextPage<FooterProps> = (props) => {
|
|||||||
<Head />
|
<Head />
|
||||||
<Header />
|
<Header />
|
||||||
<Main>
|
<Main>
|
||||||
<section className='flex flex-col items-center w-4/5'>
|
<div className='flex flex-col items-center w-4/5'>
|
||||||
<section className='max-w-xs'>
|
<div className='max-w-xs'>
|
||||||
<Link href='/authentication/signup'>
|
<Link href='/authentication/signup'>
|
||||||
<a>
|
<a>
|
||||||
<Image
|
<Image
|
||||||
@ -34,8 +33,8 @@ const Home: NextPage<FooterProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</section>
|
</div>
|
||||||
<section className='text-center'>
|
<div className='text-center'>
|
||||||
<h1 className='my-4 text-3xl font-medium font-headline text-green-800 dark:text-green-400'>
|
<h1 className='my-4 text-3xl font-medium font-headline text-green-800 dark:text-green-400'>
|
||||||
Thream
|
Thream
|
||||||
</h1>
|
</h1>
|
||||||
@ -54,21 +53,21 @@ const Home: NextPage<FooterProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex justify-center items-center text-center mt-8 space-x-4'>
|
<div className='flex justify-center items-center text-center mt-8 space-x-4'>
|
||||||
<Link href='/authentication/signup'>
|
<Link href='/authentication/signup' passHref>
|
||||||
<a data-cy='get-started'>
|
<ButtonLink data-cy='get-started'>
|
||||||
<Button>{t('home:get-started')}</Button>
|
{t('home:get-started')}
|
||||||
</a>
|
</ButtonLink>
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
|
||||||
|
<SocialMediaLink
|
||||||
|
socialMedia='GitHub'
|
||||||
href='https://github.com/Thream'
|
href='https://github.com/Thream'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noopener noreferrer'
|
rel='noopener noreferrer'
|
||||||
>
|
/>
|
||||||
<SocialMediaButton socialMedia='GitHub' />
|
</div>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</Main>
|
</Main>
|
||||||
<Footer version={version} />
|
<Footer version={version} />
|
||||||
</ScrollableBody>
|
</ScrollableBody>
|
||||||
|
Reference in New Issue
Block a user