feat: design applications and first api calls

Co-authored-by: Walid <87608619+WalidKorchi@users.noreply.github.com>
This commit is contained in:
Divlo
2021-10-24 06:09:43 +02:00
parent 33bd2bb6bf
commit a0fa66e8f5
136 changed files with 14787 additions and 1668 deletions

View File

@ -0,0 +1,257 @@
import { useState, useEffect, useMemo } from 'react'
import Image from 'next/image'
import {
CogIcon,
PlusIcon,
MenuIcon,
UsersIcon,
XIcon
} from '@heroicons/react/solid'
import classNames from 'classnames'
import { useMediaQuery } from 'react-responsive'
import { useSwipeable } from 'react-swipeable'
import { Sidebar, DirectionSidebar } from './Sidebar'
import { IconButton } from 'components/design/IconButton'
import { IconLink } from 'components/design/IconLink'
import { Channels } from './Channels'
import { Guilds } from './Guilds/Guilds'
import { Divider } from '../design/Divider'
import { Members } from './Members'
import { useAuthentication } from 'utils/authentication'
export interface GuildsChannelsPath {
guildId: number
channelId: number
}
export interface ApplicationProps {
path:
| '/application'
| '/application/guilds/join'
| '/application/guilds/create'
| '/application/users/[userId]'
| GuildsChannelsPath
}
export const Application: React.FC<ApplicationProps> = (props) => {
const { children, path } = props
const { user } = useAuthentication()
const [visibleSidebars, setVisibleSidebars] = useState({
left: true,
right: false
})
const [mounted, setMounted] = useState(false)
const isMobile = useMediaQuery({
query: '(max-width: 900px)'
})
const handleToggleSidebars = (direction: DirectionSidebar): void => {
if (!isMobile) {
if (direction === 'left') {
return setVisibleSidebars({
...visibleSidebars,
left: !visibleSidebars.left
})
}
if (direction === 'right') {
return setVisibleSidebars({
...visibleSidebars,
right: !visibleSidebars.right
})
}
} else {
if (direction === 'right' && visibleSidebars.left) {
return setVisibleSidebars({
left: false,
right: true
})
}
if (direction === 'left' && visibleSidebars.right) {
return setVisibleSidebars({
left: true,
right: false
})
}
if (direction === 'left' && !visibleSidebars.right) {
return setVisibleSidebars({
...visibleSidebars,
left: !visibleSidebars.left
})
}
if (direction === 'right' && !visibleSidebars.left) {
return setVisibleSidebars({
...visibleSidebars,
right: !visibleSidebars.right
})
}
}
handleCloseSidebars()
}
const handleCloseSidebars = (): void => {
if (isMobile && (visibleSidebars.left || visibleSidebars.right)) {
setVisibleSidebars({
left: false,
right: false
})
}
}
const swipeableHandlers = useSwipeable({
trackMouse: false,
trackTouch: true,
preventDefaultTouchmoveEvent: true,
onSwipedRight: () => {
if (visibleSidebars.right) {
return setVisibleSidebars({ ...visibleSidebars, right: false })
}
setVisibleSidebars({
...visibleSidebars,
left: true
})
},
onSwipedLeft: () => {
if (visibleSidebars.left) {
return setVisibleSidebars({ ...visibleSidebars, left: false })
}
setVisibleSidebars({
...visibleSidebars,
right: true
})
}
})
const title = useMemo(() => {
if (typeof path !== 'string') {
// TODO: Returns the real name of the channel when doing APIs calls
return `# Channel ${path.channelId}`
}
if (path.startsWith('/application/users/')) {
return 'Settings'
}
if (path === '/application/guilds/join') {
return 'Join a Guild'
}
if (path === '/application/guilds/create') {
return 'Create a Guild'
}
return 'Application'
}, [path])
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
return (
<>
<header className='flex bg-gray-200 dark:bg-gray-800 h-16 px-2 py-3 justify-between items-center shadow-lg z-50'>
<IconButton
className='p-2 h-10 w-10'
onClick={() => handleToggleSidebars('left')}
>
{!visibleSidebars.left ? <MenuIcon /> : <XIcon />}
</IconButton>
<div className='text-md text-green-800 dark:text-green-400 font-semibold'>
{title}
</div>
<div className='flex space-x-2'>
{title.startsWith('#') && (
<IconButton
className='p-2 h-10 w-10'
onClick={() => handleToggleSidebars('right')}
>
{!visibleSidebars.right ? <UsersIcon /> : <XIcon />}
</IconButton>
)}
</div>
</header>
<main
className='relative flex h-full-without-header overflow-hidden'
onClick={handleCloseSidebars}
{...swipeableHandlers}
>
<Sidebar
direction='left'
visible={visibleSidebars.left}
isMobile={isMobile}
>
<div className='flex flex-col min-w-[92px] top-0 left-0 z-50 bg-gray-200 dark:bg-gray-800 border-r-2 border-gray-500 dark:border-white/20 py-2 space-y-2'>
<IconLink
href={`/application/users/${user.id}`}
selected={path === `/application/users/${user.id}`}
title='Settings'
>
<Image
className='rounded-full'
src='/images/data/divlo.png'
alt='logo'
width={48}
height={48}
/>
</IconLink>
<IconLink
href='/application'
selected={path === '/application'}
title='Join or create a Guild'
>
<PlusIcon className='w-12 h-12 text-green-800 dark:text-green-400' />
</IconLink>
<Divider />
<Guilds path={path} />
</div>
{typeof path !== 'string' && (
<div className='flex flex-col justify-between w-full mt-2'>
<div className='text-center p-2 mx-8 mt-2'>
<h2 className='text-xl'>Guild Name</h2>
</div>
<Divider />
<div className='scrollbar-firefox-support overflow-y-auto'>
<Channels path={path} />
</div>
<Divider />
<div className='flex justify-center items-center p-2 mb-1 space-x-6'>
<IconButton className='h-10 w-10' title='Add a Channel'>
<PlusIcon />
</IconButton>
<IconButton className='h-7 w-7' title='Settings'>
<CogIcon />
</IconButton>
</div>
</div>
)}
</Sidebar>
<div
className={classNames(
'top-0 h-full-without-header flex flex-col flex-1 z-0 overflow-y-auto transition',
{
'absolute opacity-20':
isMobile && (visibleSidebars.left || visibleSidebars.right)
}
)}
>
{children}
</div>
<Sidebar
direction='right'
visible={visibleSidebars.right}
isMobile={isMobile}
>
<Members />
</Sidebar>
</main>
</>
)
}

View File

@ -0,0 +1,15 @@
import { Meta, Story } from '@storybook/react'
import { Channels as Component, ChannelsProps } from './'
const Stories: Meta = {
title: 'Channels',
component: Component
}
export default Stories
export const Channels: Story<ChannelsProps> = (arguments_) => (
<Component {...arguments_} />
)
Channels.args = { path: { channelId: 1, guildId: 2 } }

View File

@ -0,0 +1,12 @@
import { render } from '@testing-library/react'
import { Channels } from './'
describe('<Channels />', () => {
it('should render successfully', () => {
const { baseElement } = render(
<Channels path={{ channelId: 1, guildId: 2 }} />
)
expect(baseElement).toBeTruthy()
})
})

View File

@ -0,0 +1,36 @@
import Link from 'next/link'
import classNames from 'classnames'
import { GuildsChannelsPath } from '../Application'
export interface ChannelsProps {
path: GuildsChannelsPath
}
export const Channels: React.FC<ChannelsProps> = (props) => {
const { path } = props
return (
<nav className='w-full'>
{new Array(100).fill(null).map((_, index) => {
return (
<Link key={index} href={`/application/${path.guildId}/${index}`}>
<a
className={classNames(
'hover:bg-gray-100 group flex items-center justify-between text-sm py-2 my-3 mx-3 transition-colors dark:hover:bg-gray-600 duration-200 rounded-lg',
{
'text-green-800 dark:text-green-400 font-semibold':
typeof path !== 'string' && path.channelId === index,
'text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-white font-normal':
typeof path === 'string'
}
)}
>
<span className='ml-2 mr-4'># Channel {index}</span>
</a>
</Link>
)
})}
</nav>
)
}

View File

@ -0,0 +1 @@
export * from './Channels'

View File

@ -0,0 +1,15 @@
import { Meta, Story } from '@storybook/react'
import { Guilds as Component, GuildsProps } from './'
const Stories: Meta = {
title: 'Guilds',
component: Component
}
export default Stories
export const Guilds: Story<GuildsProps> = (arguments_) => (
<Component {...arguments_} />
)
Guilds.args = { path: { channelId: 1, guildId: 2 } }

View File

@ -0,0 +1,12 @@
import { render } from '@testing-library/react'
import { Guilds } from './'
describe('<Guilds />', () => {
it('should render successfully', () => {
const { baseElement } = render(
<Guilds path={{ channelId: 1, guildId: 2 }} />
)
expect(baseElement).toBeTruthy()
})
})

View File

@ -0,0 +1,34 @@
import Image from 'next/image'
import { ApplicationProps } from '../Application'
import { IconLink } from '../../design/IconLink'
export interface GuildsProps extends ApplicationProps {}
export const Guilds: React.FC<GuildsProps> = (props) => {
const { path } = props
return (
<div className='min-w-[92px] mt-[130px] pt-2 h-full border-r-2 border-gray-500 dark:border-white/20 space-y-2 scrollbar-firefox-support overflow-y-auto'>
{new Array(100).fill(null).map((_, index) => {
return (
<IconLink
key={index}
href={`/application/${index}/0`}
selected={typeof path !== 'string' && path.guildId === index}
title='Guild Name'
>
<div className='pl-[6px]'>
<Image
src='/images/icons/Thream.png'
alt='logo'
width={48}
height={48}
/>
</div>
</IconLink>
)
})}
</div>
)
}

View File

@ -0,0 +1 @@
export * from './Guilds'

View File

@ -0,0 +1,14 @@
import { Meta, Story } from '@storybook/react'
import { Members as Component } from './Members'
const Stories: Meta = {
title: 'Members',
component: Component
}
export default Stories
export const Members: Story = (arguments_) => {
return <Component {...arguments_} />
}

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react'
import { Members } from './Members'
describe('<Members />', () => {
it('should render successfully', () => {
const { baseElement } = render(<Members />)
expect(baseElement).toBeTruthy()
})
})

View File

@ -0,0 +1,57 @@
import Image from 'next/image'
import { Divider } from '../../design/Divider'
export const Members: React.FC = () => {
return (
<>
<div className='mb-2'>
<h1 className='text-center pt-2 my-2 text-xl'>Members</h1>
<Divider />
</div>
<div className='flex items-center cursor-pointer py-2 px-4 pr-10 rounded hover:bg-gray-300 dark:hover:bg-gray-900'>
<div className='min-w-[50px] flex rounded-full border-2 border-green-500'>
<Image
src='/images/data/divlo.png'
alt={"Users's profil picture"}
height={50}
width={50}
draggable='false'
className='rounded-full'
/>
</div>
<div className='max-w-[145px] ml-4'>
<p className='overflow-hidden whitespace-nowrap overflow-ellipsis'>
Walidouxssssssssssss
</p>
<span className='text-green-600 dark:text-green-400'>Online</span>
</div>
</div>
{new Array(100).fill(null).map((_, index) => {
return (
<div
key={index}
className='flex items-center cursor-pointer py-2 px-4 pr-10 rounded opacity-40 hover:bg-gray-300 dark:hover:bg-gray-900'
>
<div className='min-w-[50px] flex rounded-full border-2 border-transparent drop-shadow-md'>
<Image
src='/images/data/divlo.png'
alt={"Users's profil picture"}
height={50}
width={50}
draggable='false'
className='rounded-full'
/>
</div>
<div className='max-w-[145px] ml-4'>
<p className='overflow-hidden whitespace-nowrap overflow-ellipsis'>
Walidouxssssssssssssssssssssssssssssss
</p>
<span className='text-red-800 dark:text-red-400'>Offline</span>
</div>
</div>
)
})}
</>
)
}

View File

@ -0,0 +1 @@
export * from './Members'

View File

@ -0,0 +1,12 @@
import { Meta, Story } from '@storybook/react'
import { Messages as Component } from './'
const Stories: Meta = {
title: 'Messages',
component: Component
}
export default Stories
export const Messages: Story = (arguments_) => <Component {...arguments_} />

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react'
import { Messages } from './'
describe('<Messages />', () => {
it('should render successfully', () => {
const { baseElement } = render(<Messages />)
expect(baseElement).toBeTruthy()
})
})

View File

@ -0,0 +1,82 @@
import Image from 'next/image'
import TextareaAutosize from 'react-textarea-autosize'
export const Messages: React.FC = () => {
return (
<>
<div className='w-full scrollbar-firefox-support overflow-y-auto transition-all'>
{new Array(20).fill(null).map((_, index) => {
return (
<div
key={index}
className='p-4 flex transition hover:bg-gray-200 dark:hover:bg-gray-900'
>
<div className='w-12 h-12 mr-4 flex flex-shrink-0 items-center justify-center'>
<div className='w-10 h-10 drop-shadow-md'>
<Image
className='rounded-full'
src='/images/data/divlo.png'
alt='logo'
width={50}
height={50}
/>
</div>
</div>
<div className='w-full'>
<div className='w-max flex items-center'>
<span className='font-bold text-gray-900 dark:text-gray-200'>
Divlo
</span>
<span className='text-gray-500 dark:text-gray-200 text-xs ml-4 select-none'>
06/04/2021 - 22:28:40
</span>
</div>
<div className='text-gray-800 dark:text-gray-300 font-paragraph mt-1 break-words'>
<p>Message {index}</p>
<p>
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
Eum debitis voluptatum itaque quaerat. Nemo optio voluptas
quas mollitia rerum commodi laboriosam voluptates et sit
quo. Repudiandae eius at inventore magnam. Voluptas nisi
maxime laborum architecto fuga a consequuntur reiciendis
rerum beatae hic possimus, omnis dolorum libero, illo
dolorem assumenda. Repellat, ad!
</p>
</div>
</div>
</div>
)
})}
</div>
<div className='p-6 pb-4'>
<div className='w-full h-full py-1 flex rounded-lg bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-200'>
<form className='w-full h-full flex items-center'>
<TextareaAutosize
className='w-full scrollbar-firefox-support p-2 px-6 my-2 bg-transparent outline-none font-paragraph tracking-wide resize-none'
placeholder='Write a message...'
wrap='soft'
maxRows={6}
/>
</form>
<div className='h-full flex items-center justify-around pr-6'>
<button className='w-full h-full flex items-center justify-center p-1 text-2xl transition hover:-translate-y-1'>
🙂
</button>
<button className='relative w-full h-full flex items-center justify-center p-1 text-green-800 dark:text-green-400 transition hover:-translate-y-1'>
<input
type='file'
className='absolute w-full h-full opacity-0 cursor-pointer'
/>
<svg width='25' height='25' viewBox='0 0 22 22'>
<path
d='M11 0C4.925 0 0 4.925 0 11C0 17.075 4.925 22 11 22C17.075 22 22 17.075 22 11C22 4.925 17.075 0 11 0ZM12 15C12 15.2652 11.8946 15.5196 11.7071 15.7071C11.5196 15.8946 11.2652 16 11 16C10.7348 16 10.4804 15.8946 10.2929 15.7071C10.1054 15.5196 10 15.2652 10 15V12H7C6.73478 12 6.48043 11.8946 6.29289 11.7071C6.10536 11.5196 6 11.2652 6 11C6 10.7348 6.10536 10.4804 6.29289 10.2929C6.48043 10.1054 6.73478 10 7 10H10V7C10 6.73478 10.1054 6.48043 10.2929 6.29289C10.4804 6.10536 10.7348 6 11 6C11.2652 6 11.5196 6.10536 11.7071 6.29289C11.8946 6.48043 12 6.73478 12 7V10H15C15.2652 10 15.5196 10.1054 15.7071 10.2929C15.8946 10.4804 16 10.7348 16 11C16 11.2652 15.8946 11.5196 15.7071 11.7071C15.5196 11.8946 15.2652 12 15 12H12V15Z'
fill='currentColor'
/>
</svg>
</button>
</div>
</div>
</div>
</>
)
}

View File

@ -0,0 +1 @@
export * from './Messages'

View File

@ -0,0 +1,15 @@
import { Meta, Story } from '@storybook/react'
import { PopupGuild as Component, PopupGuildProps } from './PopupGuild'
const Stories: Meta = {
title: 'PopupGuild',
component: Component
}
export default Stories
export const PopupGuild: Story<PopupGuildProps> = (arguments_) => {
return <Component {...arguments_} />
}
PopupGuild.args = {}

View File

@ -0,0 +1,10 @@
import { render } from '@testing-library/react'
import { PopupGuild } from './PopupGuild'
describe('<PopupGuild />', () => {
it('should render successfully', () => {
const { baseElement } = render(<PopupGuild />)
expect(baseElement).toBeTruthy()
})
})

View File

@ -0,0 +1,56 @@
import { PlusSmIcon, ArrowDownIcon } from '@heroicons/react/solid'
import classNames from 'classnames'
import Image from 'next/image'
import { PopupGuildCard } from './PopupGuildCard/PopupGuildCard'
export interface PopupGuildProps {
className?: string
}
export const PopupGuild: React.FC<PopupGuildProps> = (props) => {
const { className } = props
return (
<div
className={classNames(
className,
'flex p-8 flex-wrap justify-center items-center overflow-y-auto h-full-without-header min-w-full'
)}
>
<PopupGuildCard
image={
<Image
src='/images/svg/design/create-guild.svg'
alt='Create a guild'
draggable='false'
width={230}
height={230}
/>
}
description='Create your own guild and manage everything within a few clicks !'
link={{
icon: <PlusSmIcon className='w-8 h-8 mr-2' />,
text: 'Create a Guild',
href: '/application/guilds/create'
}}
/>
<PopupGuildCard
image={
<Image
src='/images/svg/design/join-guild.svg'
alt='Join a Guild'
draggable='false'
width={200}
height={200}
/>
}
description='Talk, meet and have fun with new friends by joining any interesting guild !'
link={{
icon: <ArrowDownIcon className='w-6 h-6 mr-2' />,
text: 'Join a Guild',
href: '/application/guilds/join'
}}
/>
</div>
)
}

View File

@ -0,0 +1,36 @@
import { Meta, Story } from '@storybook/react'
import { PlusSmIcon } from '@heroicons/react/solid'
import Image from 'next/image'
import {
PopupGuildCard as Component,
PopupGuildCardProps
} from './PopupGuildCard'
const Stories: Meta = {
title: 'PopupGuildCard',
component: Component
}
export default Stories
export const PopupGuildCard: Story<PopupGuildCardProps> = (arguments_) => {
return <Component {...arguments_} />
}
PopupGuildCard.args = {
image: (
<Image
src='/images/svg/design/create-server.svg'
alt=''
width={230}
height={230}
/>
),
description:
'Create your own guild and manage everything within a few clicks !',
link: {
icon: <PlusSmIcon className='w-8 h-8 mr-2' />,
text: 'Create a server',
href: '/application/guilds/create'
}
}

View File

@ -0,0 +1,29 @@
import { render } from '@testing-library/react'
import { PlusSmIcon } from '@heroicons/react/solid'
import Image from 'next/image'
import { PopupGuildCard } from './PopupGuildCard'
describe('<PopupGuildCard />', () => {
it('should render successfully', () => {
const { baseElement } = render(
<PopupGuildCard
image={
<Image
src='/images/svg/design/create-server.svg'
alt=''
width={230}
height={230}
/>
}
description='Create your own guild and manage everything within a few clicks !'
link={{
icon: <PlusSmIcon className='w-8 h-8 mr-2' />,
text: 'Create a server',
href: '/application/guilds/create'
}}
/>
)
expect(baseElement).toBeTruthy()
})
})

View File

@ -0,0 +1,32 @@
import Link from 'next/link'
export interface PopupGuildCardProps {
image: JSX.Element
description: string
link: {
href: string
text: string
icon: JSX.Element
}
}
export const PopupGuildCard: React.FC<PopupGuildCardProps> = (props) => {
const { image, description, link } = props
return (
<div className='w-80 h-96 m-8 rounded-2xl bg-gray-800'>
<div className='flex justify-center items-center h-1/2 w-full'>
{image}
</div>
<div className='flex justify-between flex-col h-1/2 w-full bg-gray-700 rounded-b-2xl mt-2 shadow-sm'>
<p className='text-gray-200 mt-6 text-center px-8'>{description}</p>
<Link href={link.href}>
<a className='flex justify-center items-center w-4/5 h-10 rounded-2xl transition duration-200 ease-in-out text-white font-bold tracking-wide bg-green-400 self-center mb-6 hover:bg-green-600'>
{link.icon}
{link.text}
</a>
</Link>
</div>
</div>
)
}

View File

@ -0,0 +1 @@
export * from './PopupGuildCard'

View File

@ -0,0 +1 @@
export * from './PopupGuild'

View File

@ -0,0 +1,14 @@
import { Meta, Story } from '@storybook/react'
import { Sidebar as Component, SidebarProps } from './Sidebar'
const Stories: Meta = {
title: 'Sidebar',
component: Component
}
export default Stories
export const Sidebar: Story<SidebarProps> = (arguments_) => {
return <Component {...arguments_} />
}

View File

@ -0,0 +1,12 @@
import { render } from '@testing-library/react'
import { Sidebar } from './Sidebar'
describe('<Sidebar />', () => {
it('should render successfully', () => {
const { baseElement } = render(
<Sidebar direction='left' visible={true} isMobile={false} />
)
expect(baseElement).toBeTruthy()
})
})

View File

@ -0,0 +1,36 @@
import classNames from 'classnames'
import { ApplicationProps } from '..'
export type DirectionSidebar = 'left' | 'right'
export interface SidebarProps {
direction: DirectionSidebar
visible: boolean
path?: ApplicationProps
isMobile: boolean
}
export const Sidebar: React.FC<SidebarProps> = (props) => {
const { direction, visible, children, path, isMobile } = props
return (
<nav
className={classNames(
'h-full-without-header flex z-50 drop-shadow-2xl bg-gray-200 dark:bg-gray-800 transition-all',
{
'top-0 right-0 scrollbar-firefox-support overflow-y-auto flex-col space-y-1':
direction === 'right',
'w-64': direction === 'right' && visible,
'w-0 opacity-0': !visible,
'w-80': direction === 'left' && visible,
'max-w-max': typeof path !== 'string' && direction === 'left',
'top-0 right-0': direction === 'right' && isMobile,
absolute: isMobile
}
)}
>
{children}
</nav>
)
}

View File

@ -0,0 +1 @@
export * from './Sidebar'

View File

@ -0,0 +1 @@
export * from './Application'