feat: interact with user settings/profile (#9)
This commit is contained in:
		@@ -1,8 +0,0 @@
 | 
			
		||||
.next
 | 
			
		||||
.lighthouseci
 | 
			
		||||
storybook-static
 | 
			
		||||
coverage
 | 
			
		||||
node_modules
 | 
			
		||||
next-env.d.ts
 | 
			
		||||
**/workbox-*.js
 | 
			
		||||
**/sw.js
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": [
 | 
			
		||||
    "standard-with-typescript",
 | 
			
		||||
    "next",
 | 
			
		||||
    "conventions",
 | 
			
		||||
    "next/core-web-vitals",
 | 
			
		||||
    "plugin:storybook/recommended",
 | 
			
		||||
    "plugin:cypress/recommended",
 | 
			
		||||
    "prettier"
 | 
			
		||||
  ],
 | 
			
		||||
  "plugins": ["unicorn", "import", "prettier"],
 | 
			
		||||
  "parser": "@typescript-eslint/parser",
 | 
			
		||||
  "plugins": ["prettier"],
 | 
			
		||||
  "parserOptions": {
 | 
			
		||||
    "project": "./tsconfig.json"
 | 
			
		||||
  },
 | 
			
		||||
@@ -17,25 +18,6 @@
 | 
			
		||||
  },
 | 
			
		||||
  "rules": {
 | 
			
		||||
    "prettier/prettier": "error",
 | 
			
		||||
    "import/order": [
 | 
			
		||||
      "error",
 | 
			
		||||
      {
 | 
			
		||||
        "groups": ["builtin", "external", "internal"],
 | 
			
		||||
        "newlines-between": "always"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "unicorn/prefer-node-protocol": "error",
 | 
			
		||||
    "unicorn/prevent-abbreviations": [
 | 
			
		||||
      "error",
 | 
			
		||||
      {
 | 
			
		||||
        "replacements": {
 | 
			
		||||
          "props": {
 | 
			
		||||
            "properties": false
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "@typescript-eslint/no-namespace": "off",
 | 
			
		||||
    "@next/next/no-img-element": "off"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
* text=auto eol=lf
 | 
			
		||||
@@ -16,6 +16,8 @@
 | 
			
		||||
    "assert": {
 | 
			
		||||
      "preset": "lighthouse:recommended",
 | 
			
		||||
      "assertions": {
 | 
			
		||||
        "image-size-responsive": "warning",
 | 
			
		||||
        "unsized-images": "warning",
 | 
			
		||||
        "csp-xss": "warning",
 | 
			
		||||
        "non-composited-animations": "warning",
 | 
			
		||||
        "unused-javascript": "warning"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
  "*": ["editorconfig-checker"],
 | 
			
		||||
  "*.{js,ts,jsx,tsx}": [
 | 
			
		||||
  "*.{js,jsx,ts,tsx}": [
 | 
			
		||||
    "prettier --write",
 | 
			
		||||
    "eslint --fix",
 | 
			
		||||
    "jest --findRelatedTests"
 | 
			
		||||
  ],
 | 
			
		||||
  "*.{css,yml,json}": ["prettier --write"],
 | 
			
		||||
  "*.{css,scss,sass,json,jsonc,yml,yaml}": ["prettier --write"],
 | 
			
		||||
  "*.md": ["prettier --write", "markdownlint --dot --fix"]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@
 | 
			
		||||
storybook-static
 | 
			
		||||
coverage
 | 
			
		||||
node_modules
 | 
			
		||||
next-env.d.ts
 | 
			
		||||
**/workbox-*.js
 | 
			
		||||
**/sw.js
 | 
			
		||||
*.hbs
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,15 @@
 | 
			
		||||
FROM node:16.13.1 AS dependencies
 | 
			
		||||
FROM node:16.13.2 AS dependencies
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
COPY ./package*.json ./
 | 
			
		||||
RUN npm install
 | 
			
		||||
 | 
			
		||||
FROM node:16.13.1 AS builder
 | 
			
		||||
FROM node:16.13.2 AS builder
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
COPY ./ ./
 | 
			
		||||
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
 | 
			
		||||
RUN npm run build
 | 
			
		||||
 | 
			
		||||
FROM node:16.13.1 AS runner
 | 
			
		||||
FROM node:16.13.2 AS runner
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
ENV NODE_ENV=production
 | 
			
		||||
COPY --from=builder /usr/src/app/next.config.js ./next.config.js
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@
 | 
			
		||||
  <br />
 | 
			
		||||
  <a href="https://conventionalcommits.org"><img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg" alt="Conventional Commits" /></a>
 | 
			
		||||
  <a href="https://github.com/semantic-release/semantic-release"><img src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg" alt="semantic-release" /></a>
 | 
			
		||||
  <a href="https://dependabot.com/"><img src="https://badgen.net/github/dependabot/Thream/website?icon=dependabot" alt="Dependabot badge" /></a>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
## 📜 About
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ export type ApplicationPath =
 | 
			
		||||
  | '/application/guilds/join'
 | 
			
		||||
  | '/application/guilds/create'
 | 
			
		||||
  | `/application/users/${number}`
 | 
			
		||||
  | `/application/users/${number}/settings`
 | 
			
		||||
  | `/application/users/settings`
 | 
			
		||||
  | GuildsChannelsPath
 | 
			
		||||
 | 
			
		||||
export interface ApplicationProps {
 | 
			
		||||
@@ -135,16 +135,16 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
  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'>
 | 
			
		||||
      <header className='z-50 flex h-16 items-center justify-between bg-gray-200 px-2 py-3 shadow-lg dark:bg-gray-800'>
 | 
			
		||||
        <IconButton
 | 
			
		||||
          className='p-2 h-10 w-10'
 | 
			
		||||
          className='h-10 w-10 p-2'
 | 
			
		||||
          onClick={() => handleToggleSidebars('left')}
 | 
			
		||||
        >
 | 
			
		||||
          {!visibleSidebars.left ? <MenuIcon /> : <XIcon />}
 | 
			
		||||
        </IconButton>
 | 
			
		||||
        <div
 | 
			
		||||
          data-cy='application-title'
 | 
			
		||||
          className='text-md text-green-800 dark:text-green-400 font-semibold'
 | 
			
		||||
          className='text-md font-semibold text-green-800 dark:text-green-400'
 | 
			
		||||
        >
 | 
			
		||||
          {title}
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -152,7 +152,7 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
          {title.startsWith('#') && (
 | 
			
		||||
            <IconButton
 | 
			
		||||
              data-cy='icon-button-right-sidebar-members'
 | 
			
		||||
              className='p-2 h-10 w-10'
 | 
			
		||||
              className='h-10 w-10 p-2'
 | 
			
		||||
              onClick={() => handleToggleSidebars('right')}
 | 
			
		||||
            >
 | 
			
		||||
              {!visibleSidebars.right ? <UsersIcon /> : <XIcon />}
 | 
			
		||||
@@ -162,7 +162,7 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
      </header>
 | 
			
		||||
 | 
			
		||||
      <main
 | 
			
		||||
        className='relative flex h-full-without-header overflow-hidden'
 | 
			
		||||
        className='h-full-without-header relative flex overflow-hidden'
 | 
			
		||||
        onClick={handleCloseSidebars}
 | 
			
		||||
        {...swipeableHandlers}
 | 
			
		||||
      >
 | 
			
		||||
@@ -171,10 +171,10 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
          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-4'>
 | 
			
		||||
          <div className='top-0 left-0 z-50 flex min-w-[92px] flex-col space-y-4 border-r-2 border-gray-500 bg-gray-200 py-2 dark:border-white/20 dark:bg-gray-800'>
 | 
			
		||||
            <IconLink
 | 
			
		||||
              href={`/application/users/${user.id}/settings`}
 | 
			
		||||
              selected={path === `/application/users/${user.id}/settings`}
 | 
			
		||||
              href={`/application/users/settings`}
 | 
			
		||||
              selected={path === `/application/users/settings`}
 | 
			
		||||
              title='Settings'
 | 
			
		||||
            >
 | 
			
		||||
              <Image
 | 
			
		||||
@@ -195,7 +195,7 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
              selected={path === '/application'}
 | 
			
		||||
              title='Join or create a Guild'
 | 
			
		||||
            >
 | 
			
		||||
              <PlusIcon className='w-12 h-12 text-green-800 dark:text-green-400' />
 | 
			
		||||
              <PlusIcon className='h-12 w-12 text-green-800 dark:text-green-400' />
 | 
			
		||||
            </IconLink>
 | 
			
		||||
            <Divider />
 | 
			
		||||
            <Guilds path={path} />
 | 
			
		||||
@@ -206,7 +206,7 @@ export const Application: React.FC<ApplicationProps> = (props) => {
 | 
			
		||||
        <div
 | 
			
		||||
          id='application-page-content'
 | 
			
		||||
          className={classNames(
 | 
			
		||||
            'top-0 h-full-without-header w-full flex flex-col flex-1 z-0 overflow-y-auto transition',
 | 
			
		||||
            'h-full-without-header top-0 z-0 flex w-full flex-1 flex-col overflow-y-auto transition',
 | 
			
		||||
            {
 | 
			
		||||
              'absolute opacity-20':
 | 
			
		||||
                isMobile && (visibleSidebars.left || visibleSidebars.right)
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,9 @@ export const Channel: React.FC<ChannelProps> = (props) => {
 | 
			
		||||
    <Link href={`/application/${path.guildId}/${channel.id}`}>
 | 
			
		||||
      <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',
 | 
			
		||||
          'group my-3 mx-3 flex items-center justify-between rounded-lg py-2 text-sm transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-600',
 | 
			
		||||
          {
 | 
			
		||||
            'text-green-800 dark:text-green-400 font-semibold': selected
 | 
			
		||||
            'font-semibold text-green-800 dark:text-green-400': selected
 | 
			
		||||
          }
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
 
 | 
			
		||||
@@ -17,10 +17,10 @@ export const Channels: React.FC<ChannelsProps> = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      id='channels'
 | 
			
		||||
      className='scrollbar-firefox-support overflow-y-auto flex-1 flex flex-col'
 | 
			
		||||
      className='scrollbar-firefox-support flex flex-1 flex-col overflow-y-auto'
 | 
			
		||||
    >
 | 
			
		||||
      <InfiniteScroll
 | 
			
		||||
        className='w-full channels-list'
 | 
			
		||||
        className='channels-list w-full'
 | 
			
		||||
        scrollableTarget='channels'
 | 
			
		||||
        dataLength={channels.length}
 | 
			
		||||
        next={nextPage}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,17 +16,17 @@ export const ConfirmGuildJoin: React.FC<ConfirmGuildJoinProps> = (props) => {
 | 
			
		||||
        height={150}
 | 
			
		||||
        width={150}
 | 
			
		||||
      />
 | 
			
		||||
      <div className='flex flex-col mt-8'>
 | 
			
		||||
        <h1 className='text-xl mb-6 text-center'>Rejoindre la guild ?</h1>
 | 
			
		||||
      <div className='mt-8 flex flex-col'>
 | 
			
		||||
        <h1 className='mb-6 text-center text-xl'>Rejoindre la guild ?</h1>
 | 
			
		||||
        <div className='flex gap-7'>
 | 
			
		||||
          <button
 | 
			
		||||
            className='px-8 py-2 rounded-3xl bg-success hover:opacity-50 transition'
 | 
			
		||||
            className='rounded-3xl bg-success px-8 py-2 transition hover:opacity-50'
 | 
			
		||||
            onClick={handleJoinGuild}
 | 
			
		||||
          >
 | 
			
		||||
            Oui
 | 
			
		||||
          </button>
 | 
			
		||||
          <button
 | 
			
		||||
            className='px-8 py-2 rounded-3xl bg-error hover:opacity-50 transition'
 | 
			
		||||
            className='rounded-3xl bg-error px-8 py-2 transition hover:opacity-50'
 | 
			
		||||
            onClick={handleJoinGuild}
 | 
			
		||||
          >
 | 
			
		||||
            Non
 | 
			
		||||
 
 | 
			
		||||
@@ -18,10 +18,11 @@ export const CreateGuild: React.FC = () => {
 | 
			
		||||
 | 
			
		||||
  const { fetchState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({
 | 
			
		||||
      validateSchemaObject: {
 | 
			
		||||
      validateSchema: {
 | 
			
		||||
        name: guildSchema.name,
 | 
			
		||||
        description: guildSchema.description
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
      resetOnSuccess: true
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
  const { authentication } = useAuthentication()
 | 
			
		||||
@@ -59,7 +60,7 @@ export const CreateGuild: React.FC = () => {
 | 
			
		||||
          placeholder='Description'
 | 
			
		||||
          id='description'
 | 
			
		||||
        />
 | 
			
		||||
        <Button className='w-full mt-6' type='submit' data-cy='submit'>
 | 
			
		||||
        <Button className='mt-6 w-full' type='submit' data-cy='submit'>
 | 
			
		||||
          {t('application:create')}
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Form>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import { CogIcon, PlusIcon } from '@heroicons/react/solid'
 | 
			
		||||
 | 
			
		||||
import { useGuildMember } from '../../../contexts/GuildMember'
 | 
			
		||||
import { Divider } from '../../design/Divider'
 | 
			
		||||
import { Channels } from '../../Application/Channels'
 | 
			
		||||
import { Channels } from '../Channels'
 | 
			
		||||
import { IconButton } from '../../design/IconButton'
 | 
			
		||||
import { GuildsChannelsPath } from '..'
 | 
			
		||||
 | 
			
		||||
@@ -16,8 +16,8 @@ export const GuildLeftSidebar: React.FC<GuildLeftSidebarProps> = (props) => {
 | 
			
		||||
  const { guild } = useGuildMember()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='flex flex-col justify-between w-full mt-2'>
 | 
			
		||||
      <div className='text-center p-2 mx-8 mt-2'>
 | 
			
		||||
    <div className='mt-2 flex w-full flex-col justify-between'>
 | 
			
		||||
      <div className='mx-8 mt-2 p-2 text-center'>
 | 
			
		||||
        <h2 data-cy='guild-left-sidebar-title' className='text-xl'>
 | 
			
		||||
          {guild.name}
 | 
			
		||||
        </h2>
 | 
			
		||||
@@ -25,7 +25,7 @@ export const GuildLeftSidebar: React.FC<GuildLeftSidebarProps> = (props) => {
 | 
			
		||||
      <Divider />
 | 
			
		||||
      <Channels path={path} />
 | 
			
		||||
      <Divider />
 | 
			
		||||
      <div className='flex justify-center items-center p-2 mb-1 space-x-6'>
 | 
			
		||||
      <div className='mb-1 flex items-center justify-center space-x-6 p-2'>
 | 
			
		||||
        <IconButton className='h-10 w-10' title='Add a Channel'>
 | 
			
		||||
          <PlusIcon />
 | 
			
		||||
        </IconButton>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ export const Guilds: React.FC<GuildsProps> = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      id='guilds-list'
 | 
			
		||||
      className='min-w-[92px] mt-[130px] pt-2 h-full border-r-1 border-gray-500 dark:border-white/20 space-y-2 scrollbar-firefox-support overflow-y-auto'
 | 
			
		||||
      className='border-r-1 scrollbar-firefox-support mt-[130px] h-full min-w-[92px] space-y-2 overflow-y-auto border-gray-500 pt-2 dark:border-white/20'
 | 
			
		||||
    >
 | 
			
		||||
      <InfiniteScroll
 | 
			
		||||
        className='guilds-list'
 | 
			
		||||
 
 | 
			
		||||
@@ -19,10 +19,10 @@ export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='relative overflow-hidden rounded border border-gray-500 dark:border-gray-700 hover:-translate-y-2 hover:shadow-none shadow-lg transition duration-200 ease-in-out'>
 | 
			
		||||
    <div className='relative overflow-hidden rounded border border-gray-500 shadow-lg transition duration-200 ease-in-out hover:-translate-y-2 hover:shadow-none dark:border-gray-700'>
 | 
			
		||||
      <div
 | 
			
		||||
        className={classNames(
 | 
			
		||||
          'flex flex-col items-center h-full justify-center cursor-pointer p-4 pt-8 transition duration-200 ease-in-out',
 | 
			
		||||
          'flex h-full cursor-pointer flex-col items-center justify-center p-4 pt-8 transition duration-200 ease-in-out',
 | 
			
		||||
          { '-translate-x-full': isConfirmed }
 | 
			
		||||
        )}
 | 
			
		||||
        onClick={() => setIsConfirmed(!isConfirmed)}
 | 
			
		||||
@@ -36,10 +36,10 @@ export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
 | 
			
		||||
          width={80}
 | 
			
		||||
          height={80}
 | 
			
		||||
        />
 | 
			
		||||
        <div className='w-full px-4 m-2 text-center mt-6'>
 | 
			
		||||
        <div className='m-2 mt-6 w-full px-4 text-center'>
 | 
			
		||||
          <h3
 | 
			
		||||
            data-cy='guild-name'
 | 
			
		||||
            className='w-full center font-bold text-xl mb-2 truncate'
 | 
			
		||||
            className='center mb-2 w-full truncate text-xl font-bold'
 | 
			
		||||
          >
 | 
			
		||||
            {guild.name}
 | 
			
		||||
          </h3>
 | 
			
		||||
@@ -47,20 +47,20 @@ export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
 | 
			
		||||
            {guild.description != null ? (
 | 
			
		||||
              guild.description
 | 
			
		||||
            ) : (
 | 
			
		||||
              <span className='flex h-full opacity-25 justify-center items-center'>
 | 
			
		||||
              <span className='flex h-full items-center justify-center opacity-25'>
 | 
			
		||||
                <Emoji value=':eyes:' size={25} />
 | 
			
		||||
                <span className='ml-2'>Nothing's here...</span>
 | 
			
		||||
              </span>
 | 
			
		||||
            )}
 | 
			
		||||
          </p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <p className='flex flex-col text-green-800 dark:text-green-400 mt-auto'>
 | 
			
		||||
        <p className='mt-auto flex flex-col text-green-800 dark:text-green-400'>
 | 
			
		||||
          {guild.membersCount} {t('application:members')}
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <ConfirmGuildJoin
 | 
			
		||||
        className={classNames(
 | 
			
		||||
          'absolute w-full h-full flex flex-col w-ful h-ful top-1/2 -translate-y-1/2 left-full translate-x- rounded-2xl justify-center items-center transition-all',
 | 
			
		||||
          'w-ful h-ful translate-x- absolute top-1/2 left-full flex h-full w-full -translate-y-1/2 flex-col items-center justify-center rounded-2xl transition-all',
 | 
			
		||||
          {
 | 
			
		||||
            '!left-0': isConfirmed
 | 
			
		||||
          }
 | 
			
		||||
 
 | 
			
		||||
@@ -30,18 +30,18 @@ export const JoinGuildsPublic: React.FC = () => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='flex flex-col w-full h-full transition-all'>
 | 
			
		||||
    <div className='flex h-full w-full flex-col transition-all'>
 | 
			
		||||
      <input
 | 
			
		||||
        data-cy='search-guild-input'
 | 
			
		||||
        onChange={handleChange}
 | 
			
		||||
        className='w-10/12 sm:w-8/12 md:w-6/12 lg:w-5/12 bg-white dark:bg-[#3B3B3B] border-gray-500 dark:border-gray-700 p-3 my-6 mt-16 mx-auto rounded-md border'
 | 
			
		||||
        className='my-6 mx-auto mt-16 w-10/12 rounded-md border border-gray-500 bg-white p-3 dark:border-gray-700 dark:bg-[#3B3B3B] sm:w-8/12 md:w-6/12 lg:w-5/12'
 | 
			
		||||
        type='search'
 | 
			
		||||
        name='search-guild'
 | 
			
		||||
        placeholder={`🔎  ${t('application:search')}...`}
 | 
			
		||||
      />
 | 
			
		||||
      <div className='w-full p-12'>
 | 
			
		||||
        <InfiniteScroll
 | 
			
		||||
          className='guilds-public-list max-w-[1400px] mx-auto grid gap-8 grid-cols-[repeat(auto-fill,_minmax(20em,_1fr))] !overflow-visible'
 | 
			
		||||
          className='guilds-public-list mx-auto grid max-w-[1400px] grid-cols-[repeat(auto-fill,_minmax(20em,_1fr))] gap-8 !overflow-visible'
 | 
			
		||||
          dataLength={items.length}
 | 
			
		||||
          next={nextPage}
 | 
			
		||||
          scrollableTarget='application-page-content'
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,8 @@ export const Member: React.FC<MemberProps> = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Link href={`/application/users/${member.user.id}`}>
 | 
			
		||||
      <a>
 | 
			
		||||
        <div className='flex items-center cursor-pointer py-2 px-6 pr-10 overflow-hidden hover:bg-gray-300 dark:hover:bg-gray-900'>
 | 
			
		||||
          <div className='min-w-[50px] flex rounded-full'>
 | 
			
		||||
        <div className='flex cursor-pointer items-center overflow-hidden py-2 px-6 pr-10 hover:bg-gray-300 dark:hover:bg-gray-900'>
 | 
			
		||||
          <div className='flex min-w-[50px] rounded-full'>
 | 
			
		||||
            <Image
 | 
			
		||||
              src={
 | 
			
		||||
                member.user.logo == null
 | 
			
		||||
@@ -34,7 +34,7 @@ export const Member: React.FC<MemberProps> = (props) => {
 | 
			
		||||
              {member.user.name}
 | 
			
		||||
            </p>
 | 
			
		||||
            {member.user.status != null && (
 | 
			
		||||
              <span className='block truncate w-44'>{member.user.status}</span>
 | 
			
		||||
              <span className='block w-44 truncate'>{member.user.status}</span>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ export const Members: React.FC = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className='mb-2'>
 | 
			
		||||
        <h1 data-cy='members-title' className='text-center pt-2 my-2 text-xl'>
 | 
			
		||||
        <h1 data-cy='members-title' className='my-2 pt-2 text-center text-xl'>
 | 
			
		||||
          {capitalize(t('application:members'))}
 | 
			
		||||
        </h1>
 | 
			
		||||
        <Divider />
 | 
			
		||||
 
 | 
			
		||||
@@ -17,13 +17,13 @@ export const Message: React.FC<MessageProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className='p-4 flex transition hover:bg-gray-200 dark:hover:bg-gray-900'
 | 
			
		||||
      className='flex p-4 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'>
 | 
			
		||||
            <div className='w-10 h-10 drop-shadow-md'>
 | 
			
		||||
          <div className='mr-4 flex h-12 w-12 flex-shrink-0 items-center justify-center'>
 | 
			
		||||
            <div className='h-10 w-10 drop-shadow-md'>
 | 
			
		||||
              <Image
 | 
			
		||||
                className='rounded-full'
 | 
			
		||||
                src={
 | 
			
		||||
@@ -41,7 +41,7 @@ export const Message: React.FC<MessageProps> = (props) => {
 | 
			
		||||
        </a>
 | 
			
		||||
      </Link>
 | 
			
		||||
      <div className='w-full'>
 | 
			
		||||
        <div className='w-max flex items-center'>
 | 
			
		||||
        <div className='flex w-max items-center'>
 | 
			
		||||
          <Link href={`/application/users/${message.member.user.id}`}>
 | 
			
		||||
            <a>
 | 
			
		||||
              <span
 | 
			
		||||
@@ -54,7 +54,7 @@ export const Message: React.FC<MessageProps> = (props) => {
 | 
			
		||||
          </Link>
 | 
			
		||||
          <span
 | 
			
		||||
            data-cy='message-date'
 | 
			
		||||
            className='text-gray-500 dark:text-gray-200 text-xs ml-4 select-none'
 | 
			
		||||
            className='ml-4 select-none text-xs text-gray-500 dark:text-gray-200'
 | 
			
		||||
          >
 | 
			
		||||
            {date.format(new Date(message.createdAt), 'DD/MM/YYYY - HH:mm:ss')}
 | 
			
		||||
          </span>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
export const FileIcon: React.FC = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      className='dark:text-white text-black fill-current'
 | 
			
		||||
      className='fill-current text-black dark:text-white'
 | 
			
		||||
      width='21'
 | 
			
		||||
      height='26'
 | 
			
		||||
      viewBox='0 0 21 26'
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ export const MessageFile: React.FC<MessageContentProps> = (props) => {
 | 
			
		||||
      <a href={file.url} target='_blank' rel='noreferrer'>
 | 
			
		||||
        <img
 | 
			
		||||
          data-cy={`message-file-image-${message.id}`}
 | 
			
		||||
          className='sm:max-w-xs max-h-80'
 | 
			
		||||
          className='max-h-80 sm:max-w-xs'
 | 
			
		||||
          src={file.url}
 | 
			
		||||
          alt={message.value}
 | 
			
		||||
        />
 | 
			
		||||
@@ -66,7 +66,7 @@ export const MessageFile: React.FC<MessageContentProps> = (props) => {
 | 
			
		||||
  if (message.mimetype.startsWith('video/')) {
 | 
			
		||||
    return (
 | 
			
		||||
      <video
 | 
			
		||||
        className='max-w-xs max-h-80'
 | 
			
		||||
        className='max-h-80 max-w-xs'
 | 
			
		||||
        controls
 | 
			
		||||
        data-cy={`message-file-video-${message.id}`}
 | 
			
		||||
      >
 | 
			
		||||
@@ -86,7 +86,7 @@ export const MessageFile: React.FC<MessageContentProps> = (props) => {
 | 
			
		||||
            <p className='mt-1'>{prettyBytes(file.blob.size)}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <DownloadIcon className='ml-4 w-8 h-8' />
 | 
			
		||||
        <DownloadIcon className='ml-4 h-8 w-8' />
 | 
			
		||||
      </div>
 | 
			
		||||
    </a>
 | 
			
		||||
  )
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ export const Messages: React.FC = () => {
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        id='messages'
 | 
			
		||||
        className='w-full scrollbar-firefox-support overflow-y-auto transition-all flex-1 flex flex-col text-center mt-8 text-lg'
 | 
			
		||||
        className='scrollbar-firefox-support mt-8 flex w-full flex-1 flex-col overflow-y-auto text-center text-lg transition-all'
 | 
			
		||||
      >
 | 
			
		||||
        <p>
 | 
			
		||||
          Nothing to show here! <Emoji value=':ghost:' size={20} />
 | 
			
		||||
@@ -25,7 +25,7 @@ export const Messages: React.FC = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      id='messages'
 | 
			
		||||
      className='w-full scrollbar-firefox-support overflow-y-auto transition-all flex-1 flex flex-col-reverse'
 | 
			
		||||
      className='scrollbar-firefox-support flex w-full flex-1 flex-col-reverse overflow-y-auto transition-all'
 | 
			
		||||
    >
 | 
			
		||||
      <InfiniteScroll
 | 
			
		||||
        scrollableTarget='messages'
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ export const PopupGuild: React.FC<PopupGuildProps> = (props) => {
 | 
			
		||||
    <div
 | 
			
		||||
      className={classNames(
 | 
			
		||||
        className,
 | 
			
		||||
        'flex p-8 flex-wrap justify-center items-center overflow-y-auto h-full-without-header min-w-full'
 | 
			
		||||
        'h-full-without-header flex min-w-full flex-wrap items-center justify-center overflow-y-auto p-8'
 | 
			
		||||
      )}
 | 
			
		||||
    >
 | 
			
		||||
      <PopupGuildCard
 | 
			
		||||
@@ -32,7 +32,7 @@ export const PopupGuild: React.FC<PopupGuildProps> = (props) => {
 | 
			
		||||
        }
 | 
			
		||||
        description={t('application:create-a-guild-description')}
 | 
			
		||||
        link={{
 | 
			
		||||
          icon: <PlusSmIcon className='w-8 h-8 mr-2' />,
 | 
			
		||||
          icon: <PlusSmIcon className='mr-2 h-8 w-8' />,
 | 
			
		||||
          text: t('application:create-a-guild'),
 | 
			
		||||
          href: '/application/guilds/create'
 | 
			
		||||
        }}
 | 
			
		||||
@@ -49,7 +49,7 @@ export const PopupGuild: React.FC<PopupGuildProps> = (props) => {
 | 
			
		||||
        }
 | 
			
		||||
        description={t('application:join-a-guild-description')}
 | 
			
		||||
        link={{
 | 
			
		||||
          icon: <ArrowDownIcon className='w-6 h-6 mr-2' />,
 | 
			
		||||
          icon: <ArrowDownIcon className='mr-2 h-6 w-6' />,
 | 
			
		||||
          text: t('application:join-a-guild'),
 | 
			
		||||
          href: '/application/guilds/join'
 | 
			
		||||
        }}
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ PopupGuildCard.args = {
 | 
			
		||||
  description:
 | 
			
		||||
    'Create your own guild and manage everything within a few clicks !',
 | 
			
		||||
  link: {
 | 
			
		||||
    icon: <PlusSmIcon className='w-8 h-8 mr-2' />,
 | 
			
		||||
    icon: <PlusSmIcon className='mr-2 h-8 w-8' />,
 | 
			
		||||
    text: 'Create a server',
 | 
			
		||||
    href: '/application/guilds/create'
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ describe('<PopupGuildCard />', () => {
 | 
			
		||||
        }
 | 
			
		||||
        description='Create your own guild and manage everything within a few clicks !'
 | 
			
		||||
        link={{
 | 
			
		||||
          icon: <PlusSmIcon className='w-8 h-8 mr-2' />,
 | 
			
		||||
          icon: <PlusSmIcon className='mr-2 h-8 w-8' />,
 | 
			
		||||
          text: 'Create a server',
 | 
			
		||||
          href: '/application/guilds/create'
 | 
			
		||||
        }}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
 | 
			
		||||
export interface PopupGuildCardProps {
 | 
			
		||||
@@ -14,16 +15,16 @@ 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'>
 | 
			
		||||
    <div className='m-8 h-96 w-80 rounded-2xl bg-gray-800'>
 | 
			
		||||
      <div className='flex h-1/2 w-full items-center justify-center'>
 | 
			
		||||
        {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 text-sm mt-6 text-center px-8'>
 | 
			
		||||
      <div className='mt-2 flex h-1/2 w-full flex-col justify-between rounded-b-2xl bg-gray-700 shadow-sm'>
 | 
			
		||||
        <p className='mt-6 px-8 text-center text-sm text-gray-200'>
 | 
			
		||||
          {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'>
 | 
			
		||||
          <a className='mb-6 flex h-10 w-4/5 items-center justify-center self-center rounded-2xl bg-green-400 font-bold tracking-wide text-white transition duration-200 ease-in-out hover:bg-green-600'>
 | 
			
		||||
            {link.icon}
 | 
			
		||||
            {link.text}
 | 
			
		||||
          </a>
 | 
			
		||||
 
 | 
			
		||||
@@ -80,14 +80,14 @@ export const SendMessage: React.FC<SendMessageProps> = (props) => {
 | 
			
		||||
    <>
 | 
			
		||||
      {isVisibleEmojiPicker && <EmojiPicker onClick={handleEmojiPicker} />}
 | 
			
		||||
      <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'>
 | 
			
		||||
        <div className='flex h-full w-full rounded-lg bg-gray-200 py-1 text-gray-600 dark:bg-gray-800 dark:text-gray-200'>
 | 
			
		||||
          <form
 | 
			
		||||
            className='w-full h-full flex items-center'
 | 
			
		||||
            className='flex h-full w-full items-center'
 | 
			
		||||
            onSubmit={handleSubmit}
 | 
			
		||||
            onKeyDown={handleTextareaKeyDown}
 | 
			
		||||
          >
 | 
			
		||||
            <TextareaAutosize
 | 
			
		||||
              className='w-full scrollbar-firefox-support p-2 px-6 my-2 bg-transparent outline-none font-paragraph tracking-wide resize-none'
 | 
			
		||||
              className='scrollbar-firefox-support my-2 w-full resize-none bg-transparent p-2 px-6 font-paragraph tracking-wide outline-none'
 | 
			
		||||
              placeholder={t('application:write-a-message')}
 | 
			
		||||
              wrap='soft'
 | 
			
		||||
              maxRows={6}
 | 
			
		||||
@@ -98,17 +98,17 @@ export const SendMessage: React.FC<SendMessageProps> = (props) => {
 | 
			
		||||
              autoFocus
 | 
			
		||||
            />
 | 
			
		||||
          </form>
 | 
			
		||||
          <div className='h-full flex items-center justify-around pr-6'>
 | 
			
		||||
          <div className='flex h-full 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'
 | 
			
		||||
              className='flex h-full w-full items-center justify-center p-1 text-2xl transition hover:-translate-y-1'
 | 
			
		||||
              onClick={handleVisibleEmojiPicker}
 | 
			
		||||
            >
 | 
			
		||||
              🙂
 | 
			
		||||
            </button>
 | 
			
		||||
            <button className='cursor-pointer relative w-full h-full flex items-center justify-center p-1 text-green-800 dark:text-green-400 transition hover:-translate-y-1'>
 | 
			
		||||
            <button className='relative flex h-full w-full cursor-pointer items-center justify-center p-1 text-green-800 transition hover:-translate-y-1 dark:text-green-400'>
 | 
			
		||||
              <input
 | 
			
		||||
                type='file'
 | 
			
		||||
                className='absolute w-full h-full opacity-0 cursor-pointer'
 | 
			
		||||
                className='absolute h-full w-full cursor-pointer opacity-0'
 | 
			
		||||
                onChange={handleFileChange}
 | 
			
		||||
              />
 | 
			
		||||
              <svg width='25' height='25' viewBox='0 0 22 22'>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,9 +17,9 @@ export const Sidebar: React.FC<SidebarProps> = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <nav
 | 
			
		||||
      className={classNames(
 | 
			
		||||
        'h-full-without-header flex z-50 drop-shadow-2xl bg-gray-200 dark:bg-gray-800 transition-all',
 | 
			
		||||
        'h-full-without-header z-50 flex bg-gray-200 drop-shadow-2xl transition-all dark:bg-gray-800',
 | 
			
		||||
        {
 | 
			
		||||
          'top-0 right-0 scrollbar-firefox-support overflow-y-auto flex-col space-y-1':
 | 
			
		||||
          'scrollbar-firefox-support top-0 right-0 flex-col space-y-1 overflow-y-auto':
 | 
			
		||||
            direction === 'right',
 | 
			
		||||
          'w-72': direction === 'right' && visible,
 | 
			
		||||
          'w-0 opacity-0': !visible,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,9 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  guildExample,
 | 
			
		||||
  guildExample2
 | 
			
		||||
} from '../../../cypress/fixtures/guilds/guild'
 | 
			
		||||
import {
 | 
			
		||||
  userExample,
 | 
			
		||||
  userSettingsExample
 | 
			
		||||
@@ -20,5 +24,6 @@ UserProfile.args = {
 | 
			
		||||
  user: {
 | 
			
		||||
    ...userExample,
 | 
			
		||||
    settings: userSettingsExample
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
  guilds: [guildExample, guildExample2]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,9 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  guildExample,
 | 
			
		||||
  guildExample2
 | 
			
		||||
} from '../../../cypress/fixtures/guilds/guild'
 | 
			
		||||
import {
 | 
			
		||||
  userExample,
 | 
			
		||||
  userSettingsExample
 | 
			
		||||
@@ -9,7 +13,10 @@ import { UserProfile } from './UserProfile'
 | 
			
		||||
describe('<UserProfile />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
    const { baseElement } = render(
 | 
			
		||||
      <UserProfile user={{ ...userExample, settings: userSettingsExample }} />
 | 
			
		||||
      <UserProfile
 | 
			
		||||
        user={{ ...userExample, settings: userSettingsExample }}
 | 
			
		||||
        guilds={[guildExample, guildExample2]}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
    expect(baseElement).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
 
 | 
			
		||||
@@ -9,15 +9,17 @@ import { API_URL } from '../../../tools/api'
 | 
			
		||||
import { UserPublic } from '../../../models/User'
 | 
			
		||||
import { UserProfileGuilds } from './UserProfileGuilds'
 | 
			
		||||
import { UserProfileGuild } from './UserProfileGuilds/UserProfileGuild'
 | 
			
		||||
import { Guild } from '../../../models/Guild'
 | 
			
		||||
import { ConfirmGuildJoin } from '../ConfirmGuildJoin'
 | 
			
		||||
 | 
			
		||||
export interface UserProfileProps {
 | 
			
		||||
  className?: string
 | 
			
		||||
  user: UserPublic
 | 
			
		||||
  guilds: Guild[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const UserProfile: React.FC<UserProfileProps> = (props) => {
 | 
			
		||||
  const { user } = props
 | 
			
		||||
  const { user, guilds } = props
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
 | 
			
		||||
  const [showPopup, setShowPopup] = useState<boolean>(false)
 | 
			
		||||
@@ -32,16 +34,16 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='relative h-full flex flex-col items-center justify-center'>
 | 
			
		||||
    <div className='relative flex h-full flex-col items-center justify-center'>
 | 
			
		||||
      <div
 | 
			
		||||
        className={classNames('transition', {
 | 
			
		||||
          'blur-3xl select-none': showPopup
 | 
			
		||||
          'select-none blur-3xl': showPopup
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        <div className='max-w-[1000px] px-12'>
 | 
			
		||||
          <div className='flex justify-between items-center'>
 | 
			
		||||
            <div className='w-max flex items-center'>
 | 
			
		||||
              <div className='relative flex justify-center items-center rounded-full overflow-hidden transition-all shadow-lg'>
 | 
			
		||||
          <div className='flex items-center justify-between'>
 | 
			
		||||
            <div className='flex w-max items-center'>
 | 
			
		||||
              <div className='relative flex items-center justify-center overflow-hidden rounded-full shadow-lg transition-all'>
 | 
			
		||||
                <Image
 | 
			
		||||
                  className='rounded-full'
 | 
			
		||||
                  src={
 | 
			
		||||
@@ -55,24 +57,31 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
 | 
			
		||||
                  width={125}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className='flex flex-col ml-10'>
 | 
			
		||||
                <div className='flex items-center mb-2'>
 | 
			
		||||
                  <p className='text-3xl font-bold space tracking-wide text-white'>
 | 
			
		||||
              <div className='ml-10 flex flex-col'>
 | 
			
		||||
                <div className='mb-2 flex items-center'>
 | 
			
		||||
                  <p
 | 
			
		||||
                    className='space text-3xl font-bold tracking-wide text-white'
 | 
			
		||||
                    data-cy='user-name'
 | 
			
		||||
                  >
 | 
			
		||||
                    {user.name}
 | 
			
		||||
                  </p>
 | 
			
		||||
                  <p className='ml-8 text-sm tracking-widest text-white opacity-40 select-none'>
 | 
			
		||||
                  <p
 | 
			
		||||
                    className='ml-8 select-none text-sm tracking-widest text-white opacity-40'
 | 
			
		||||
                    data-cy='user-createdAt'
 | 
			
		||||
                  >
 | 
			
		||||
                    {date.format(new Date(user.createdAt), 'DD/MM/YYYY')}
 | 
			
		||||
                  </p>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className='text-left my-2'>
 | 
			
		||||
                <div className='my-2 text-left'>
 | 
			
		||||
                  {user.email != null && (
 | 
			
		||||
                    <p className='font-bold'>
 | 
			
		||||
                      Email:{' '}
 | 
			
		||||
                      <a
 | 
			
		||||
                        href={`mailto:${user.email}`}
 | 
			
		||||
                        target='_blank'
 | 
			
		||||
                        className='relative ml-2 opacity-80 hover:opacity-100 transition-all no-underline font-normal tracking-wide after:absolute after:left-0 after:bottom-[-1px] after:bg-black dark:after:bg-white after:h-[1px] after:w-0 after:transition-all hover:after:w-full'
 | 
			
		||||
                        className='relative ml-2 font-normal tracking-wide no-underline opacity-80 transition-all after:absolute after:left-0 after:bottom-[-1px] after:h-[1px] after:w-0 after:bg-black after:transition-all hover:opacity-100 hover:after:w-full dark:after:bg-white'
 | 
			
		||||
                        rel='noreferrer'
 | 
			
		||||
                        data-cy='user-email'
 | 
			
		||||
                      >
 | 
			
		||||
                        {user.email}
 | 
			
		||||
                      </a>
 | 
			
		||||
@@ -83,7 +92,7 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
 | 
			
		||||
                      {t('application:website')}:{' '}
 | 
			
		||||
                      <a
 | 
			
		||||
                        href={user.website}
 | 
			
		||||
                        className='relative ml-2 opacity-80 hover:opacity-100 transition-all no-underline font-normal tracking-wide after:absolute after:left-0 after:bottom-[-2px] after:bg-black dark:after:bg-white after:h-[1px] after:w-0 after:transition-all hover:after:w-full'
 | 
			
		||||
                        className='relative ml-2 font-normal tracking-wide no-underline opacity-80 transition-all after:absolute after:left-0 after:bottom-[-2px] after:h-[1px] after:w-0 after:bg-black after:transition-all hover:opacity-100 hover:after:w-full dark:after:bg-white'
 | 
			
		||||
                      >
 | 
			
		||||
                        {user.website}
 | 
			
		||||
                      </a>
 | 
			
		||||
@@ -104,6 +113,7 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
 | 
			
		||||
            <div className='py-8 px-4' onClick={handlePopupVisibility}>
 | 
			
		||||
              <UserProfileGuilds
 | 
			
		||||
                isPublicGuilds={user.settings.isPublicGuilds}
 | 
			
		||||
                guilds={guilds}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -114,22 +124,24 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* TODO: We might want to remove this code */}
 | 
			
		||||
      <div
 | 
			
		||||
        className={classNames(
 | 
			
		||||
          'absolute flex justify-center items-center top-0 h-full w-full bg-zinc-900/75 transition opacity-0 pointer-events-none',
 | 
			
		||||
          'pointer-events-none absolute top-0 flex h-full w-full items-center justify-center bg-zinc-900/75 opacity-0 transition',
 | 
			
		||||
          {
 | 
			
		||||
            'opacity-100 visible pointer-events-auto': showPopup
 | 
			
		||||
            'pointer-events-auto visible opacity-100': showPopup
 | 
			
		||||
          }
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          className={classNames(
 | 
			
		||||
            'relative h-[400px] w-[400px] py-2 rounded-2xl shadow-xl bg-gray-200 dark:bg-gray-800 scale-0 transition overflow-y-auto overflow-x-hidden',
 | 
			
		||||
            'relative h-[400px] w-[400px] scale-0 overflow-y-auto overflow-x-hidden rounded-2xl bg-gray-200 py-2 shadow-xl transition dark:bg-gray-800',
 | 
			
		||||
            { 'scale-100': showPopup }
 | 
			
		||||
          )}
 | 
			
		||||
        >
 | 
			
		||||
          <div
 | 
			
		||||
            className={classNames('relative transition h-full', {
 | 
			
		||||
            className={classNames('relative h-full transition', {
 | 
			
		||||
              '-translate-x-[150%]': confirmation
 | 
			
		||||
            })}
 | 
			
		||||
          >
 | 
			
		||||
@@ -140,7 +152,7 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
          <ConfirmGuildJoin
 | 
			
		||||
            className={classNames(
 | 
			
		||||
              'absolute w-full h-full flex flex-col justify-center items-center transition-all top-0 left-[150%]',
 | 
			
		||||
              'absolute top-0 left-[150%] flex h-full w-full flex-col items-center justify-center transition-all',
 | 
			
		||||
              { 'left-[0%]': confirmation }
 | 
			
		||||
            )}
 | 
			
		||||
            handleJoinGuild={handleConfirmationState}
 | 
			
		||||
@@ -149,7 +161,7 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
 | 
			
		||||
        <XIcon
 | 
			
		||||
          height={40}
 | 
			
		||||
          onClick={() => setShowPopup(false)}
 | 
			
		||||
          className='absolute top-8 right-8 cursor-pointer hover:rotate-180 transition'
 | 
			
		||||
          className='absolute top-8 right-8 cursor-pointer transition hover:rotate-180'
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -11,12 +11,12 @@ export const UserProfileGuild: React.FC<UserProfileGuildProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className='relative flex w-full cursor-pointer transition group'
 | 
			
		||||
      className='group relative flex w-full cursor-pointer transition'
 | 
			
		||||
      onClick={handleConfirmationState}
 | 
			
		||||
    >
 | 
			
		||||
      <div className='relative group-hover:-translate-x-20 px-8 py-5 w-full transition'>
 | 
			
		||||
        <div className='flex group-hover:opacity-40 transition'>
 | 
			
		||||
          <div className='flex justify-center rounded-full filter drop-shadow-lg mr-8 min-w-[60px] min-h-[60px] select-none'>
 | 
			
		||||
      <div className='relative w-full px-8 py-5 transition group-hover:-translate-x-20'>
 | 
			
		||||
        <div className='flex transition group-hover:opacity-40'>
 | 
			
		||||
          <div className='mr-8 flex min-h-[60px] min-w-[60px] select-none justify-center rounded-full drop-shadow-lg filter'>
 | 
			
		||||
            <Image
 | 
			
		||||
              className='rounded-full'
 | 
			
		||||
              src='/images/guilds/Guild_1.svg'
 | 
			
		||||
@@ -28,13 +28,13 @@ export const UserProfileGuild: React.FC<UserProfileGuildProps> = (props) => {
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className='flex flex-col'>
 | 
			
		||||
            <h1 className='text-xl font-bold'>Guild Name</h1>
 | 
			
		||||
            <p className='text-gray-300 mt-2'>
 | 
			
		||||
            <p className='mt-2 text-gray-300'>
 | 
			
		||||
              Lorem ipsum dolor sit amet consectetur adipisicing elit. Debitis,
 | 
			
		||||
              nam.
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='absolute top-0 right-[-80px] flex justify-center items-center w-[80px] h-full'>
 | 
			
		||||
        <div className='absolute top-0 right-[-80px] flex h-full w-[80px] items-center justify-center'>
 | 
			
		||||
          <LoginIcon
 | 
			
		||||
            height={40}
 | 
			
		||||
            className='fill-green-600 drop-shadow-[0_0_15px_rgba(22,163,74,0.50)]'
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,11 @@ import classNames from 'classnames'
 | 
			
		||||
import { EyeOffIcon } from '@heroicons/react/solid'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
 | 
			
		||||
import { Guild } from '../../../../models/Guild'
 | 
			
		||||
 | 
			
		||||
export interface UserProfileGuildsProps {
 | 
			
		||||
  isPublicGuilds?: boolean
 | 
			
		||||
  guilds: Guild[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const UserProfileGuilds: React.FC<UserProfileGuildsProps> = (props) => {
 | 
			
		||||
@@ -19,10 +22,10 @@ export const UserProfileGuilds: React.FC<UserProfileGuildsProps> = (props) => {
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        className={classNames('flex -space-x-7', {
 | 
			
		||||
          'blur-lg select-none': !isPublicGuilds
 | 
			
		||||
          'select-none blur-lg': !isPublicGuilds
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        <div className='flex justify-center items-center rounded-full filter drop-shadow-lg'>
 | 
			
		||||
        <div className='flex items-center justify-center rounded-full drop-shadow-lg filter'>
 | 
			
		||||
          <Image
 | 
			
		||||
            className='rounded-full'
 | 
			
		||||
            src='/images/guilds/Guild_1.svg'
 | 
			
		||||
@@ -32,7 +35,7 @@ export const UserProfileGuilds: React.FC<UserProfileGuildsProps> = (props) => {
 | 
			
		||||
            width={60}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='flex justify-center items-center rounded-full filter drop-shadow-lg'>
 | 
			
		||||
        <div className='flex items-center justify-center rounded-full drop-shadow-lg filter'>
 | 
			
		||||
          <Image
 | 
			
		||||
            className='rounded-full'
 | 
			
		||||
            src='/images/guilds/Guild_2.svg'
 | 
			
		||||
@@ -42,7 +45,7 @@ export const UserProfileGuilds: React.FC<UserProfileGuildsProps> = (props) => {
 | 
			
		||||
            width={60}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='flex justify-center items-center rounded-full filter drop-shadow-lg'>
 | 
			
		||||
        <div className='flex items-center justify-center rounded-full drop-shadow-lg filter'>
 | 
			
		||||
          <Image
 | 
			
		||||
            className='rounded-full'
 | 
			
		||||
            src='/images/guilds/Guild_3.svg'
 | 
			
		||||
@@ -52,7 +55,7 @@ export const UserProfileGuilds: React.FC<UserProfileGuildsProps> = (props) => {
 | 
			
		||||
            width={60}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='flex justify-center items-center rounded-full filter drop-shadow-lg'>
 | 
			
		||||
        <div className='flex items-center justify-center rounded-full drop-shadow-lg filter'>
 | 
			
		||||
          <Image
 | 
			
		||||
            className='rounded-full'
 | 
			
		||||
            src='/images/guilds/Guild_4.svg'
 | 
			
		||||
@@ -62,7 +65,7 @@ export const UserProfileGuilds: React.FC<UserProfileGuildsProps> = (props) => {
 | 
			
		||||
            width={60}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='flex justify-center items-center rounded-full filter drop-shadow-lg'>
 | 
			
		||||
        <div className='flex items-center justify-center rounded-full drop-shadow-lg filter'>
 | 
			
		||||
          <Image
 | 
			
		||||
            className='rounded-full'
 | 
			
		||||
            src='/images/guilds/Guild_5.svg'
 | 
			
		||||
@@ -72,7 +75,7 @@ export const UserProfileGuilds: React.FC<UserProfileGuildsProps> = (props) => {
 | 
			
		||||
            width={60}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='flex justify-center items-center rounded-full filter drop-shadow-lg'>
 | 
			
		||||
        <div className='flex items-center justify-center rounded-full drop-shadow-lg filter'>
 | 
			
		||||
          <Image
 | 
			
		||||
            className='rounded-full'
 | 
			
		||||
            src='/images/guilds/Guild_6.svg'
 | 
			
		||||
@@ -82,20 +85,20 @@ export const UserProfileGuilds: React.FC<UserProfileGuildsProps> = (props) => {
 | 
			
		||||
            width={60}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='w-[60px] h-[60px] flex justify-center items-center rounded-full filter drop-shadow-lg bg-gray-300 dark:bg-gray-800 z-10'>
 | 
			
		||||
          <span className='font-bold text-black dark:text-white text-xl select-none'>
 | 
			
		||||
        <div className='z-10 flex h-[60px] w-[60px] items-center justify-center rounded-full bg-gray-300 drop-shadow-lg filter dark:bg-gray-800'>
 | 
			
		||||
          <span className='select-none text-xl font-bold text-black dark:text-white'>
 | 
			
		||||
            +4
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div
 | 
			
		||||
        className={classNames(
 | 
			
		||||
          'absolute flex items-center top-1/2 -translate-y-1/2',
 | 
			
		||||
          'absolute top-1/2 flex -translate-y-1/2 items-center',
 | 
			
		||||
          { hidden: isPublicGuilds }
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
        <EyeOffIcon height={25} />
 | 
			
		||||
        <p className='drop-shadow-2xl ml-4'>
 | 
			
		||||
        <p className='ml-4 drop-shadow-2xl'>
 | 
			
		||||
          {t('application:private-user-guilds-list')}
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  userExample,
 | 
			
		||||
  userSettingsExample
 | 
			
		||||
} from '../../../cypress/fixtures/users/user'
 | 
			
		||||
import { UserSettings as Component, UserSettingsProps } from './UserSettings'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'UserSettings',
 | 
			
		||||
  component: Component
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Stories
 | 
			
		||||
 | 
			
		||||
export const UserSettings: Story<UserSettingsProps> = (arguments_) => {
 | 
			
		||||
  return <Component {...arguments_} />
 | 
			
		||||
}
 | 
			
		||||
UserSettings.args = { user: { ...userExample, settings: userSettingsExample } }
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  userExample,
 | 
			
		||||
  userSettingsExample
 | 
			
		||||
} from '../../../cypress/fixtures/users/user'
 | 
			
		||||
import { UserSettings } from './UserSettings'
 | 
			
		||||
 | 
			
		||||
describe('<UserSettings />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
    const { baseElement } = render(
 | 
			
		||||
      <UserSettings user={{ ...userExample, settings: userSettingsExample }} />
 | 
			
		||||
    )
 | 
			
		||||
    expect(baseElement).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -1,9 +1,12 @@
 | 
			
		||||
import Image from 'next/image'
 | 
			
		||||
import { PhotographIcon } from '@heroicons/react/solid'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
import { Form } from 'react-component-form'
 | 
			
		||||
import { PhotographIcon } from '@heroicons/react/solid'
 | 
			
		||||
import { Type } from '@sinclair/typebox'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
 | 
			
		||||
import { API_URL } from '../../../tools/api'
 | 
			
		||||
import { UserPublic } from '../../../models/User'
 | 
			
		||||
import { UserProfileGuilds } from '../UserProfile/UserProfileGuilds'
 | 
			
		||||
import { Input } from '../../design/Input'
 | 
			
		||||
import { Checkbox } from '../../design/Checkbox'
 | 
			
		||||
@@ -11,32 +14,169 @@ import { Textarea } from '../../design/Textarea'
 | 
			
		||||
import { SocialMediaButton } from '../../design/SocialMediaButton'
 | 
			
		||||
import { SwitchTheme } from '../../Header/SwitchTheme'
 | 
			
		||||
import { Language } from '../../Header/Language'
 | 
			
		||||
import { useAuthentication } from '../../../tools/authentication'
 | 
			
		||||
import { Button } from '../../design/Button'
 | 
			
		||||
import { FormState } from '../../design/FormState'
 | 
			
		||||
import { useForm, HandleSubmitCallback } from '../../../hooks/useForm'
 | 
			
		||||
import { userCurrentSchema, userSchema } from '../../../models/User'
 | 
			
		||||
import { userSettingsSchema } from '../../../models/UserSettings'
 | 
			
		||||
import { useGuilds } from '../../../contexts/Guilds'
 | 
			
		||||
 | 
			
		||||
export interface UserSettingsProps {
 | 
			
		||||
  user: UserPublic
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const UserSettings: React.FC<UserSettingsProps> = (props) => {
 | 
			
		||||
  const { user } = props
 | 
			
		||||
export const UserSettings: React.FC = () => {
 | 
			
		||||
  const { user, setUser, authentication } = useAuthentication()
 | 
			
		||||
  const { guilds } = useGuilds()
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const [inputValues, setInputValues] = useState({
 | 
			
		||||
    name: user.name,
 | 
			
		||||
    status: user.status,
 | 
			
		||||
    email: user.email,
 | 
			
		||||
    website: user.website,
 | 
			
		||||
    biography: user.biography,
 | 
			
		||||
    isPublicGuilds: user.settings.isPublicGuilds,
 | 
			
		||||
    isPublicEmail: user.settings.isPublicEmail
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    fetchState,
 | 
			
		||||
    setFetchState,
 | 
			
		||||
    message,
 | 
			
		||||
    setMessageTranslationKey,
 | 
			
		||||
    errors,
 | 
			
		||||
    getErrorTranslation,
 | 
			
		||||
    handleSubmit
 | 
			
		||||
  } = useForm({
 | 
			
		||||
    validateSchema: {
 | 
			
		||||
      name: userSchema.name,
 | 
			
		||||
      status: Type.Optional(userSchema.status),
 | 
			
		||||
      email: Type.Optional(userCurrentSchema.email),
 | 
			
		||||
      website: Type.Optional(userSchema.website),
 | 
			
		||||
      biography: Type.Optional(userSchema.biography),
 | 
			
		||||
      isPublicGuilds: userSettingsSchema.isPublicGuilds,
 | 
			
		||||
      isPublicEmail: userSettingsSchema.isPublicEmail
 | 
			
		||||
    },
 | 
			
		||||
    replaceEmptyStringToNull: true,
 | 
			
		||||
    resetOnSuccess: false
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const { isPublicGuilds, isPublicEmail, ...userData } = formData
 | 
			
		||||
      const userSettings = { isPublicEmail, isPublicGuilds }
 | 
			
		||||
      const { data: userCurrentData } = await authentication.api.put(
 | 
			
		||||
        `/users/current?redirectURI=${window.location.origin}/authentication/signin`,
 | 
			
		||||
        userData
 | 
			
		||||
      )
 | 
			
		||||
      const { data: userCurrentSettings } = await authentication.api.put(
 | 
			
		||||
        '/users/current/settings',
 | 
			
		||||
        userSettings
 | 
			
		||||
      )
 | 
			
		||||
      setUser((oldUser) => {
 | 
			
		||||
        return {
 | 
			
		||||
          ...oldUser,
 | 
			
		||||
          ...userCurrentData,
 | 
			
		||||
          settings: userCurrentSettings.settings
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      setInputValues(formData as any)
 | 
			
		||||
      return {
 | 
			
		||||
        type: 'success',
 | 
			
		||||
        value: 'common:name'
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      if (axios.isAxiosError(error) && error.response?.status === 400) {
 | 
			
		||||
        const message = error.response.data.message as string
 | 
			
		||||
        if (message.endsWith('already taken.')) {
 | 
			
		||||
          return {
 | 
			
		||||
            type: 'error',
 | 
			
		||||
            value: 'authentication:already-used'
 | 
			
		||||
          }
 | 
			
		||||
        } else if (message.endsWith('email to sign in.')) {
 | 
			
		||||
          return {
 | 
			
		||||
            type: 'error',
 | 
			
		||||
            value: 'authentication:email-required-to-sign-in'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return {
 | 
			
		||||
          type: 'error',
 | 
			
		||||
          value: 'errors:server-error'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return {
 | 
			
		||||
        type: 'error',
 | 
			
		||||
        value: 'errors:server-error'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onChange: React.ChangeEventHandler<
 | 
			
		||||
    HTMLInputElement | HTMLTextAreaElement
 | 
			
		||||
  > = (event) => {
 | 
			
		||||
    setInputValues((oldInputValues) => {
 | 
			
		||||
      return {
 | 
			
		||||
        ...oldInputValues,
 | 
			
		||||
        [event.target.name]: event.target.value
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onChangeCheckbox: React.ChangeEventHandler<HTMLInputElement> = (
 | 
			
		||||
    event
 | 
			
		||||
  ) => {
 | 
			
		||||
    setInputValues((oldInputValues) => {
 | 
			
		||||
      return {
 | 
			
		||||
        ...oldInputValues,
 | 
			
		||||
        [event.target.name]: event.target.checked
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async (
 | 
			
		||||
    event
 | 
			
		||||
  ) => {
 | 
			
		||||
    const files = event?.target?.files
 | 
			
		||||
    if (files != null && files.length === 1) {
 | 
			
		||||
      const file = files[0]
 | 
			
		||||
      const formData = new FormData()
 | 
			
		||||
      formData.append('logo', file)
 | 
			
		||||
      try {
 | 
			
		||||
        const { data } = await authentication.api.put(
 | 
			
		||||
          `/users/current/logo`,
 | 
			
		||||
          formData
 | 
			
		||||
        )
 | 
			
		||||
        setUser((oldUser) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...oldUser,
 | 
			
		||||
            logo: data.user.logo
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        setFetchState('error')
 | 
			
		||||
        setMessageTranslationKey('errors:server-error')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='my-auto lg:min-w-[875px] py-12 justify-center items-center flex flex-col'>
 | 
			
		||||
      <div className='flex flex-col w-full justify-center items-center lg:flex-row sm:w-fit'>
 | 
			
		||||
        <div className=' flex justify-center items-center flex-wrap px-6 w-full sm:w-max'>
 | 
			
		||||
    <Form
 | 
			
		||||
      onSubmit={handleSubmit(onSubmit)}
 | 
			
		||||
      className='my-auto flex flex-col items-center justify-center py-12 lg:min-w-[875px]'
 | 
			
		||||
    >
 | 
			
		||||
      <div className='flex w-full flex-col items-center justify-center sm:w-fit lg:flex-row'>
 | 
			
		||||
        <div className=' flex w-full flex-wrap items-center justify-center px-6 sm:w-max'>
 | 
			
		||||
          <div className='relative'>
 | 
			
		||||
            <div className='absolute w-full h-full z-50'>
 | 
			
		||||
              <button className='relative w-full h-full flex items-center justify-center transition hover:scale-110'>
 | 
			
		||||
            <div className='absolute z-50 h-full w-full'>
 | 
			
		||||
              <button className='relative flex h-full w-full items-center justify-center transition hover:scale-110'>
 | 
			
		||||
                <input
 | 
			
		||||
                  type='file'
 | 
			
		||||
                  className='absolute w-full h-full opacity-0 cursor-pointer'
 | 
			
		||||
                  className='absolute h-full w-full cursor-pointer opacity-0'
 | 
			
		||||
                  onChange={handleFileChange}
 | 
			
		||||
                />
 | 
			
		||||
                <PhotographIcon color='white' className='w-8 h-8' />
 | 
			
		||||
                <PhotographIcon color='white' className='h-8 w-8' />
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className='flex justify-center items-center rounded-full shadow-xl bg-black'>
 | 
			
		||||
            <div className='flex items-center justify-center rounded-full bg-black shadow-xl'>
 | 
			
		||||
              <Image
 | 
			
		||||
                className='opacity-50 rounded-full'
 | 
			
		||||
                className='rounded-full opacity-50'
 | 
			
		||||
                src={
 | 
			
		||||
                  user.logo != null
 | 
			
		||||
                    ? API_URL + user.logo
 | 
			
		||||
@@ -49,53 +189,78 @@ export const UserSettings: React.FC<UserSettingsProps> = (props) => {
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className='flex flex-col mx-12'>
 | 
			
		||||
          <div className='mx-12 flex flex-col'>
 | 
			
		||||
            <Input
 | 
			
		||||
              name='name'
 | 
			
		||||
              label={t('common:name')}
 | 
			
		||||
              placeholder={t('common:name')}
 | 
			
		||||
              className='!mt-0'
 | 
			
		||||
              defaultValue={user.name}
 | 
			
		||||
              onChange={onChange}
 | 
			
		||||
              value={inputValues.name ?? ''}
 | 
			
		||||
              error={getErrorTranslation(errors.name)}
 | 
			
		||||
            />
 | 
			
		||||
            <Input
 | 
			
		||||
              name='status'
 | 
			
		||||
              label={t('application:status')}
 | 
			
		||||
              placeholder={t('application:status')}
 | 
			
		||||
              className='!mt-4'
 | 
			
		||||
              defaultValue={user.status ?? ''}
 | 
			
		||||
              onChange={onChange}
 | 
			
		||||
              value={inputValues.status ?? ''}
 | 
			
		||||
              error={getErrorTranslation(errors.status)}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='flex mt-10 flex-col items-center ml-0 lg:ml-24 lg:mt-0'>
 | 
			
		||||
          <UserProfileGuilds isPublicGuilds={user.settings.isPublicGuilds} />
 | 
			
		||||
        <div className='mt-10 ml-0 flex flex-col items-center lg:ml-24 lg:mt-0'>
 | 
			
		||||
          <UserProfileGuilds
 | 
			
		||||
            isPublicGuilds={inputValues.isPublicGuilds}
 | 
			
		||||
            guilds={guilds}
 | 
			
		||||
          />
 | 
			
		||||
          <Checkbox
 | 
			
		||||
            name='isPublicGuilds'
 | 
			
		||||
            label={t('application:label-checkbox-guilds')}
 | 
			
		||||
            defaultChecked={user.settings.isPublicGuilds}
 | 
			
		||||
            onChange={onChangeCheckbox}
 | 
			
		||||
            checked={inputValues.isPublicGuilds}
 | 
			
		||||
            id='checkbox-public-guilds'
 | 
			
		||||
            className='px-8'
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className='flex flex-col w-full justify-between items-center mt-12 lg:flex-row sm:w-fit'>
 | 
			
		||||
        <div className='w-4/5 sm:w-[450px] pr-0 lg:pr-12 lg:border-r-[1px] lg:border-neutral-700'>
 | 
			
		||||
          <Input label='Email' defaultValue={user.email ?? ''} />
 | 
			
		||||
      <div className='mt-12 flex w-full flex-col items-center justify-between sm:w-fit lg:flex-row'>
 | 
			
		||||
        <div className='w-4/5 pr-0 sm:w-[450px] lg:border-r-[1px] lg:border-neutral-700 lg:pr-12'>
 | 
			
		||||
          <Input
 | 
			
		||||
            name='email'
 | 
			
		||||
            label='Email'
 | 
			
		||||
            placeholder='Email'
 | 
			
		||||
            onChange={onChange}
 | 
			
		||||
            value={inputValues.email ?? ''}
 | 
			
		||||
            error={getErrorTranslation(errors.email)}
 | 
			
		||||
          />
 | 
			
		||||
          <Checkbox
 | 
			
		||||
            name='isPublicEmail'
 | 
			
		||||
            label={t('application:label-checkbox-email')}
 | 
			
		||||
            id='checkbox-email-visibility'
 | 
			
		||||
            defaultChecked={user.settings.isPublicEmail}
 | 
			
		||||
            onChange={onChangeCheckbox}
 | 
			
		||||
            checked={inputValues.isPublicEmail}
 | 
			
		||||
          />
 | 
			
		||||
          <Input
 | 
			
		||||
            name='website'
 | 
			
		||||
            label={t('application:website')}
 | 
			
		||||
            placeholder={t('application:website')}
 | 
			
		||||
            defaultValue={user.website ?? ''}
 | 
			
		||||
            onChange={onChange}
 | 
			
		||||
            value={inputValues.website ?? ''}
 | 
			
		||||
            error={getErrorTranslation(errors.website)}
 | 
			
		||||
          />
 | 
			
		||||
          <Textarea
 | 
			
		||||
            name='biography'
 | 
			
		||||
            label={t('application:biography')}
 | 
			
		||||
            placeholder={t('application:biography')}
 | 
			
		||||
            id='textarea-biography'
 | 
			
		||||
            defaultValue={user.biography ?? ''}
 | 
			
		||||
            onChange={onChange}
 | 
			
		||||
            value={inputValues.biography ?? ''}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='flex flex-col justify-between items-center w-4/5 sm:w-[415px] h-full pr-0 lg:pl-12'>
 | 
			
		||||
          <div className='flex w-full flex-col gap-4 mt-14'>
 | 
			
		||||
        <div className='flex h-full w-4/5 flex-col items-center justify-between pr-0 sm:w-[415px] lg:pl-12'>
 | 
			
		||||
          <div className='mt-14 flex w-full flex-col gap-4'>
 | 
			
		||||
            <SocialMediaButton
 | 
			
		||||
              socialMedia='Google'
 | 
			
		||||
              className='w-full justify-center'
 | 
			
		||||
@@ -109,12 +274,17 @@ export const UserSettings: React.FC<UserSettingsProps> = (props) => {
 | 
			
		||||
              className='w-full justify-center'
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className='flex justify-between w-full pt-14'>
 | 
			
		||||
          <div className='flex w-full justify-between pt-14'>
 | 
			
		||||
            <Language />
 | 
			
		||||
            <SwitchTheme />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className='mt-12 flex flex-col items-center justify-center sm:w-fit'>
 | 
			
		||||
        <Button type='submit'>Sauvegarder</Button>
 | 
			
		||||
        <FormState state={fetchState} message={message} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </Form>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import { Main } from '../design/Main'
 | 
			
		||||
import { Input } from '../design/Input'
 | 
			
		||||
import { Button } from '../design/Button'
 | 
			
		||||
import { FormState } from '../design/FormState'
 | 
			
		||||
import { AuthenticationForm } from './'
 | 
			
		||||
import { AuthenticationForm } from '.'
 | 
			
		||||
import { userSchema } from '../../models/User'
 | 
			
		||||
import { api } from '../../tools/api'
 | 
			
		||||
import {
 | 
			
		||||
@@ -31,11 +31,12 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
  const { errors, fetchState, message, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({
 | 
			
		||||
      validateSchemaObject: {
 | 
			
		||||
      validateSchema: {
 | 
			
		||||
        ...(mode === 'signup' && { name: userSchema.name }),
 | 
			
		||||
        email: userSchema.email,
 | 
			
		||||
        password: userSchema.password
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
      resetOnSuccess: true
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
@@ -51,9 +52,16 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        if (axios.isAxiosError(error) && error.response?.status === 400) {
 | 
			
		||||
          const message = error.response.data.message as string
 | 
			
		||||
          if (message.endsWith('already taken.')) {
 | 
			
		||||
            return {
 | 
			
		||||
              type: 'error',
 | 
			
		||||
            value: 'authentication:alreadyUsed'
 | 
			
		||||
              value: 'authentication:already-used'
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return {
 | 
			
		||||
            type: 'error',
 | 
			
		||||
            value: 'errors:server-error'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return {
 | 
			
		||||
@@ -85,14 +93,14 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Main>
 | 
			
		||||
      <div className='flex flex-col sm:items-center sm:w-full'>
 | 
			
		||||
      <div className='flex flex-col sm:w-full sm:items-center'>
 | 
			
		||||
        <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>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className='text-center text-lg font-paragraph pt-8'>
 | 
			
		||||
      <div className='pt-8 text-center font-paragraph text-lg'>
 | 
			
		||||
        {t('authentication:or')}
 | 
			
		||||
      </div>
 | 
			
		||||
      <AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
 | 
			
		||||
@@ -120,10 +128,10 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
			
		||||
          showForgotPassword={mode === 'signin'}
 | 
			
		||||
          error={getErrorTranslation(errors.password)}
 | 
			
		||||
        />
 | 
			
		||||
        <Button data-cy='submit' className='w-full mt-6' type='submit'>
 | 
			
		||||
        <Button data-cy='submit' className='mt-6 w-full' type='submit'>
 | 
			
		||||
          {t('authentication:submit')}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <p className='mt-3 font-headline text-sm text-green-800 dark:text-green-400 hover:underline'>
 | 
			
		||||
        <p className='mt-3 font-headline text-sm text-green-800 hover:underline dark:text-green-400'>
 | 
			
		||||
          <Link
 | 
			
		||||
            href={
 | 
			
		||||
              mode === 'signup'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { ErrorPage } from '../ErrorPage'
 | 
			
		||||
import { ErrorPage } from '.'
 | 
			
		||||
 | 
			
		||||
describe('<ErrorPage />', () => {
 | 
			
		||||
  it('should render the message and statusCode', async () => {
 | 
			
		||||
  it('should render the message and statusCode', () => {
 | 
			
		||||
    const messageContent = 'message content'
 | 
			
		||||
    const statusCode = 404
 | 
			
		||||
    const { getByText } = render(
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <h1 className='my-6 font-semibold text-4xl'>
 | 
			
		||||
      <h1 className='my-6 text-4xl font-semibold'>
 | 
			
		||||
        {t('errors:error')}{' '}
 | 
			
		||||
        <span
 | 
			
		||||
          className='text-green-800 dark:text-green-400'
 | 
			
		||||
@@ -24,7 +24,7 @@ export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
 | 
			
		||||
      <p className='text-center text-lg'>
 | 
			
		||||
        {message}{' '}
 | 
			
		||||
        <Link href='/'>
 | 
			
		||||
          <a className='text-green-800 dark:text-green-400 hover:underline'>
 | 
			
		||||
          <a className='text-green-800 hover:underline dark:text-green-400'>
 | 
			
		||||
            {t('errors:return-to-home-page')}
 | 
			
		||||
          </a>
 | 
			
		||||
        </Link>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { Footer as Component, FooterProps } from './'
 | 
			
		||||
import { Footer as Component, FooterProps } from '.'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Footer',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Footer } from './'
 | 
			
		||||
import { Footer } from '.'
 | 
			
		||||
 | 
			
		||||
describe('<Footer />', () => {
 | 
			
		||||
  it('should render with appropriate link tag version', async () => {
 | 
			
		||||
  it('should render with appropriate link tag version', () => {
 | 
			
		||||
    const version = '1.0.0'
 | 
			
		||||
    const { getByText } = render(<Footer version={version} />)
 | 
			
		||||
    const versionLink = getByText(`website v${version}`) as HTMLAnchorElement
 | 
			
		||||
 
 | 
			
		||||
@@ -13,10 +13,10 @@ export const Footer: React.FC<FooterProps> = (props) => {
 | 
			
		||||
  const { version } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <footer className='bg-white flex flex-col items-center justify-center py-6 text-lg border-t-2 border-gray-600 dark:border-gray-400 dark:bg-black'>
 | 
			
		||||
    <footer className='flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black'>
 | 
			
		||||
      <p>
 | 
			
		||||
        <Link href='/'>
 | 
			
		||||
          <a className='hover:underline text-green-800 dark:text-green-400'>
 | 
			
		||||
          <a className='text-green-800 hover:underline dark:text-green-400'>
 | 
			
		||||
            Thream
 | 
			
		||||
          </a>
 | 
			
		||||
        </Link>{' '}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const VersionLink: React.FC<VersionLinkProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <a
 | 
			
		||||
      className='hover:underline text-green-800 dark:text-green-400'
 | 
			
		||||
      className='text-green-800 hover:underline dark:text-green-400'
 | 
			
		||||
      href={`https://github.com/Thream/${repository}/releases/tag/v${version}`}
 | 
			
		||||
      target='_blank'
 | 
			
		||||
      rel='noopener noreferrer'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { Header as Component } from './'
 | 
			
		||||
import { Header as Component } from '.'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Header',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Header } from './'
 | 
			
		||||
import { Header } from '.'
 | 
			
		||||
 | 
			
		||||
describe('<Header />', () => {
 | 
			
		||||
  it('should render', async () => {
 | 
			
		||||
  it('should render', () => {
 | 
			
		||||
    const { getByText } = render(<Header />)
 | 
			
		||||
    expect(getByText('Thream')).toBeInTheDocument()
 | 
			
		||||
  })
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import { SwitchTheme } from './SwitchTheme'
 | 
			
		||||
 | 
			
		||||
export const Header: React.FC = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <header className='bg-white flex justify-center sticky top-0 z-50 w-full px-6 py-2 border-b-2 border-gray-600 dark:border-gray-400 dark:bg-black'>
 | 
			
		||||
    <header className='sticky top-0 z-50 flex w-full justify-center border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
 | 
			
		||||
      <div className='container flex justify-between'>
 | 
			
		||||
        <Link href='/'>
 | 
			
		||||
          <a>
 | 
			
		||||
@@ -17,7 +17,7 @@ export const Header: React.FC = () => {
 | 
			
		||||
                src='/images/icons/Thream.png'
 | 
			
		||||
                alt='Thream'
 | 
			
		||||
              />
 | 
			
		||||
              <span className='ml-1 font-medium font-headline hidden xs:block text-green-800 dark:text-green-400'>
 | 
			
		||||
              <span className='ml-1 hidden font-headline font-medium text-green-800 dark:text-green-400 xs:block'>
 | 
			
		||||
                Thream
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -33,10 +33,10 @@ export const Language: React.FC = () => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='relative flex flex-col justify-center items-center cursor-pointer'>
 | 
			
		||||
    <div className='relative flex cursor-pointer flex-col items-center justify-center'>
 | 
			
		||||
      <div
 | 
			
		||||
        data-cy='language-click'
 | 
			
		||||
        className='flex items-center mr-5'
 | 
			
		||||
        className='mr-5 flex items-center'
 | 
			
		||||
        onClick={handleHiddenMenu}
 | 
			
		||||
      >
 | 
			
		||||
        <LanguageFlag language={currentLanguage} />
 | 
			
		||||
@@ -46,7 +46,7 @@ export const Language: React.FC = () => {
 | 
			
		||||
      <ul
 | 
			
		||||
        data-cy='languages-list'
 | 
			
		||||
        className={classNames(
 | 
			
		||||
          'flex flex-col justify-center items-center absolute p-0 -bottom-16 z-10 w-24 mt-3 mr-4 rounded-lg list-none shadow-light dark:shadow-dark bg-white dark:bg-black',
 | 
			
		||||
          'absolute -bottom-16 z-10 mt-3 mr-4 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-light dark:bg-black dark:shadow-dark',
 | 
			
		||||
          { hidden: hiddenMenu }
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
@@ -57,7 +57,7 @@ export const Language: React.FC = () => {
 | 
			
		||||
          return (
 | 
			
		||||
            <li
 | 
			
		||||
              key={index}
 | 
			
		||||
              className='flex items-center justify-center w-full h-12 hover:bg-[#4f545c] hover:bg-opacity-20 pl-2'
 | 
			
		||||
              className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
 | 
			
		||||
              onClick={async () => await handleLanguage(language)}
 | 
			
		||||
            >
 | 
			
		||||
              <LanguageFlag language={language} />
 | 
			
		||||
 
 | 
			
		||||
@@ -24,13 +24,13 @@ export const SwitchTheme: React.FC = () => {
 | 
			
		||||
        data-cy='switch-theme-click'
 | 
			
		||||
        onClick={handleClick}
 | 
			
		||||
      >
 | 
			
		||||
        <div className='toggle-theme-button relative cursor-pointer bg-transparent inline-block'>
 | 
			
		||||
        <div className='toggle-theme-button relative inline-block cursor-pointer bg-transparent'>
 | 
			
		||||
          <div className='toggle-track'>
 | 
			
		||||
            <div
 | 
			
		||||
              data-cy='switch-theme-dark'
 | 
			
		||||
              className='toggle-track-check absolute'
 | 
			
		||||
            >
 | 
			
		||||
              <span className='toggle_Dark flex justify-center items-center relative'>
 | 
			
		||||
              <span className='toggle_Dark relative flex items-center justify-center'>
 | 
			
		||||
                🌜
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
@@ -38,7 +38,7 @@ export const SwitchTheme: React.FC = () => {
 | 
			
		||||
              data-cy='switch-theme-light'
 | 
			
		||||
              className='toggle-track-x absolute'
 | 
			
		||||
            >
 | 
			
		||||
              <span className='toggle_Light flex justify-center items-center relative'>
 | 
			
		||||
              <span className='toggle_Light relative flex items-center justify-center'>
 | 
			
		||||
                🌞
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Button } from './'
 | 
			
		||||
import { Button } from '.'
 | 
			
		||||
 | 
			
		||||
describe('<Button />', () => {
 | 
			
		||||
  it('should render', async () => {
 | 
			
		||||
  it('should render', () => {
 | 
			
		||||
    const { getByText } = render(<Button>Submit</Button>)
 | 
			
		||||
    expect(getByText('Submit')).toBeInTheDocument()
 | 
			
		||||
  })
 | 
			
		||||
 
 | 
			
		||||
@@ -8,15 +8,15 @@ export interface CheckboxProps extends React.ComponentPropsWithRef<'input'> {
 | 
			
		||||
export const Checkbox: React.FC<CheckboxProps> = (props) => {
 | 
			
		||||
  const { label, id, className } = props
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={classNames('flex items-center mt-4', className)}>
 | 
			
		||||
    <div className={classNames('mt-4 flex items-center', className)}>
 | 
			
		||||
      <input
 | 
			
		||||
        {...props}
 | 
			
		||||
        type='checkbox'
 | 
			
		||||
        id={id}
 | 
			
		||||
        className='relative appearance-none min-h-[25px] min-w-[25px] bg-gradient-to-t from-[#bcc7d4] to-[#d3dfed] dark:from-[#1f2937] dark:to-[#273547] mr-3 cursor-pointer rounded-md after:absolute before:absolute after:w-[2px] before:w-[2px] after:bg-black before:bg-black dark:after:bg-white dark:before:bg-white transition-all after:transition-all before:transition-all after:top-[62.5%] after:left-[36%] after:h-[7px] after:translate-x-[-35%] after:translate-y-[-62.5%] after:rotate-[-50deg] after:scale-0 after:duration-200 before:top-[50%] before:left-[59%] before:h-[12px] before:translate-x-[-59%] before:translate-y-[-50%] before:rotate-[40deg] before:scale-0 checked:after:scale-100 checked:before:scale-100'
 | 
			
		||||
        className='relative mr-3 min-h-[25px] min-w-[25px] cursor-pointer appearance-none rounded-md bg-gradient-to-t from-[#bcc7d4] to-[#d3dfed] transition-all before:absolute before:top-[50%] before:left-[59%] before:h-[12px] before:w-[2px] before:translate-x-[-59%] before:translate-y-[-50%] before:rotate-[40deg] before:scale-0 before:bg-black before:transition-all after:absolute after:top-[62.5%] after:left-[36%] after:h-[7px] after:w-[2px] after:translate-x-[-35%] after:translate-y-[-62.5%] after:rotate-[-50deg] after:scale-0 after:bg-black after:transition-all after:duration-200 checked:before:scale-100 checked:after:scale-100 dark:from-[#1f2937] dark:to-[#273547] dark:before:bg-white dark:after:bg-white'
 | 
			
		||||
      />
 | 
			
		||||
      <label
 | 
			
		||||
        className='cursor-pointer opacity-80 hover:opacity-100 transition duration-400 select-none '
 | 
			
		||||
        className='duration-400 cursor-pointer select-none opacity-80 transition hover:opacity-100 '
 | 
			
		||||
        htmlFor={id}
 | 
			
		||||
      >
 | 
			
		||||
        {label}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { Meta, Story } from '@storybook/react'
 | 
			
		||||
 | 
			
		||||
import { Divider as Component } from './'
 | 
			
		||||
import { Divider as Component } from '.'
 | 
			
		||||
 | 
			
		||||
const Stories: Meta = {
 | 
			
		||||
  title: 'Divider',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Divider } from './'
 | 
			
		||||
import { Divider } from '.'
 | 
			
		||||
 | 
			
		||||
describe('<Divider />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
export const Divider: React.FC = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='relative flex justify-center h-[2px] w-full mb-3'>
 | 
			
		||||
      <div className='absolute h-[2px] w-8/12 bg-gray-600 dark:bg-white/20 rounded-full'></div>
 | 
			
		||||
    <div className='relative mb-3 flex h-[2px] w-full justify-center'>
 | 
			
		||||
      <div className='absolute h-[2px] w-8/12 rounded-full bg-gray-600 dark:bg-white/20'></div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,24 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { FormState } from './'
 | 
			
		||||
import { FormState } from '.'
 | 
			
		||||
 | 
			
		||||
describe('<FormState />', () => {
 | 
			
		||||
  it('should return nothing if the state is idle', async () => {
 | 
			
		||||
  it('should return nothing if the state is idle', () => {
 | 
			
		||||
    const { container } = render(<FormState state='idle' />)
 | 
			
		||||
    expect(container.innerHTML.length).toEqual(0)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should return nothing if the message is null', async () => {
 | 
			
		||||
  it('should return nothing if the message is null', () => {
 | 
			
		||||
    const { container } = render(<FormState state='error' />)
 | 
			
		||||
    expect(container.innerHTML.length).toEqual(0)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should render the <Loader /> if state is loading', async () => {
 | 
			
		||||
  it('should render the <Loader /> if state is loading', () => {
 | 
			
		||||
    const { getByTestId } = render(<FormState state='loading' />)
 | 
			
		||||
    expect(getByTestId('loader')).toBeInTheDocument()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should render the success message if state is success', async () => {
 | 
			
		||||
  it('should render the success message if state is success', () => {
 | 
			
		||||
    const message = 'Success Message'
 | 
			
		||||
    const { getByText } = render(
 | 
			
		||||
      <FormState state='success' message={message} />
 | 
			
		||||
@@ -26,7 +26,7 @@ describe('<FormState />', () => {
 | 
			
		||||
    expect(getByText(message)).toBeInTheDocument()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should render the error message if state is error', async () => {
 | 
			
		||||
  it('should render the error message if state is error', () => {
 | 
			
		||||
    const message = 'Error Message'
 | 
			
		||||
    const { getByText } = render(<FormState state='error' message={message} />)
 | 
			
		||||
    expect(getByText(message)).toBeInTheDocument()
 | 
			
		||||
 
 | 
			
		||||
@@ -30,14 +30,14 @@ export const FormState: React.FC<FormStateProps> = (props) => {
 | 
			
		||||
    <>
 | 
			
		||||
      <div
 | 
			
		||||
        className={classNames(
 | 
			
		||||
          'mt-6 relative flex flex-row items-center font-medium text-center max-w-xl',
 | 
			
		||||
          'relative mt-6 flex max-w-xl flex-row items-center text-center font-medium',
 | 
			
		||||
          {
 | 
			
		||||
            'text-red-800 dark:text-red-400': state === 'error',
 | 
			
		||||
            'text-green-800 dark:text-green-400': state === 'success'
 | 
			
		||||
          }
 | 
			
		||||
        )}
 | 
			
		||||
      >
 | 
			
		||||
        <div className='thumbnail bg-cover absolute top-0 inline-block font-headline'></div>
 | 
			
		||||
        <div className='thumbnail absolute top-0 inline-block bg-cover font-headline'></div>
 | 
			
		||||
        <span id={id} className={classNames({ 'pl-6': state === 'error' })}>
 | 
			
		||||
          <b>{t(`errors:${state}`)}:</b> {message}
 | 
			
		||||
        </span>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ export const IconButton: React.FC<IconButtonProps> = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <button
 | 
			
		||||
      className={classNames(
 | 
			
		||||
        'text-center flex items-center justify-center text-green-800 dark:text-green-400 focus:outline-none focus:animate-pulse hover:animate-pulse',
 | 
			
		||||
        'flex items-center justify-center text-center text-green-800 hover:animate-pulse focus:animate-pulse focus:outline-none dark:text-green-400',
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...rest}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,15 +13,15 @@ export const IconLink: React.FC<IconLinkProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Link href={href}>
 | 
			
		||||
      <a className='w-full flex justify-center relative group' title={title}>
 | 
			
		||||
      <a className='group relative flex w-full justify-center' title={title}>
 | 
			
		||||
        <div
 | 
			
		||||
          className={classNames('w-full flex justify-center group', className)}
 | 
			
		||||
          className={classNames('group flex w-full justify-center', className)}
 | 
			
		||||
        >
 | 
			
		||||
          {children}
 | 
			
		||||
          <div className='absolute flex items-center w-3 h-12 left-0'>
 | 
			
		||||
          <div className='absolute left-0 flex h-12 w-3 items-center'>
 | 
			
		||||
            <span
 | 
			
		||||
              className={classNames(
 | 
			
		||||
                'absolute w-4/12 bg-green-700 rounded-r-lg group-hover:h-5',
 | 
			
		||||
                'absolute w-4/12 rounded-r-lg bg-green-700 group-hover:h-5',
 | 
			
		||||
                {
 | 
			
		||||
                  'h-full': selected
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,15 @@
 | 
			
		||||
import { render, fireEvent } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Input, getInputType } from './'
 | 
			
		||||
import { Input, getInputType } from '.'
 | 
			
		||||
 | 
			
		||||
describe('<Input />', () => {
 | 
			
		||||
  it('should render the label', async () => {
 | 
			
		||||
  it('should render the label', () => {
 | 
			
		||||
    const labelContent = 'label content'
 | 
			
		||||
    const { getByText } = render(<Input label={labelContent} />)
 | 
			
		||||
    expect(getByText(labelContent)).toBeInTheDocument()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should not render forgot password link', async () => {
 | 
			
		||||
  it('should not render forgot password link', () => {
 | 
			
		||||
    const { queryByTestId } = render(
 | 
			
		||||
      <Input type='text' label='content' showForgotPassword />
 | 
			
		||||
    )
 | 
			
		||||
@@ -17,7 +17,7 @@ describe('<Input />', () => {
 | 
			
		||||
    expect(forgotPasswordLink).not.toBeInTheDocument()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should render forgot password link', async () => {
 | 
			
		||||
  it('should render forgot password link', () => {
 | 
			
		||||
    const { queryByTestId } = render(
 | 
			
		||||
      <Input type='password' label='content' showForgotPassword />
 | 
			
		||||
    )
 | 
			
		||||
@@ -25,7 +25,7 @@ describe('<Input />', () => {
 | 
			
		||||
    expect(forgotPasswordLink).toBeInTheDocument()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should not render the eye icon if the input is not of type "password"', async () => {
 | 
			
		||||
  it('should not render the eye icon if the input is not of type "password"', () => {
 | 
			
		||||
    const { queryByTestId } = render(<Input type='text' label='content' />)
 | 
			
		||||
    const passwordEye = queryByTestId('password-eye')
 | 
			
		||||
    expect(passwordEye).not.toBeInTheDocument()
 | 
			
		||||
@@ -42,11 +42,11 @@ describe('<Input />', () => {
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
describe('getInputType', () => {
 | 
			
		||||
  it('should return `text`', async () => {
 | 
			
		||||
  it('should return `text`', () => {
 | 
			
		||||
    expect(getInputType('password')).toEqual('text')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should return `password`', async () => {
 | 
			
		||||
  it('should return `password`', () => {
 | 
			
		||||
    expect(getInputType('text')).toEqual('password')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ export const Input: React.FC<InputProps> = (props) => {
 | 
			
		||||
    <>
 | 
			
		||||
      <div className='flex flex-col'>
 | 
			
		||||
        <div
 | 
			
		||||
          className={classNames('flex justify-between mt-6 mb-2', className)}
 | 
			
		||||
          className={classNames('mt-6 mb-2 flex justify-between', className)}
 | 
			
		||||
        >
 | 
			
		||||
          <label className='pl-1' htmlFor={name}>
 | 
			
		||||
            {label}
 | 
			
		||||
@@ -46,7 +46,7 @@ export const Input: React.FC<InputProps> = (props) => {
 | 
			
		||||
          {type === 'password' && showForgotPassword ? (
 | 
			
		||||
            <Link href='/authentication/forgot-password'>
 | 
			
		||||
              <a
 | 
			
		||||
                className='font-headline text-center text-xs sm:text-sm text-green-800 dark:text-green-400 hover:underline'
 | 
			
		||||
                className='text-center font-headline text-xs text-green-800 hover:underline dark:text-green-400 sm:text-sm'
 | 
			
		||||
                data-testid='forgot-password-link'
 | 
			
		||||
              >
 | 
			
		||||
                {t('authentication:forgot-password')}
 | 
			
		||||
@@ -54,11 +54,11 @@ export const Input: React.FC<InputProps> = (props) => {
 | 
			
		||||
            </Link>
 | 
			
		||||
          ) : null}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='mt-0 relative'>
 | 
			
		||||
        <div className='relative mt-0'>
 | 
			
		||||
          <input
 | 
			
		||||
            data-testid='input'
 | 
			
		||||
            data-cy={`input-${name ?? 'name'}`}
 | 
			
		||||
            className='h-11 leading-10 px-3 rounded-lg bg-[#f1f1f1] text-[#2a2a2a] border border-transparent caret-green-600 font-paragraph w-full focus:border focus:outline-none focus:shadow-green'
 | 
			
		||||
            className='h-11 w-full rounded-lg border border-transparent bg-[#f1f1f1] px-3 font-paragraph leading-10 text-[#2a2a2a] caret-green-600 focus:border focus:shadow-green focus:outline-none'
 | 
			
		||||
            {...rest}
 | 
			
		||||
            id={name}
 | 
			
		||||
            name={name}
 | 
			
		||||
@@ -68,7 +68,7 @@ export const Input: React.FC<InputProps> = (props) => {
 | 
			
		||||
            <div
 | 
			
		||||
              data-testid='password-eye'
 | 
			
		||||
              onClick={handlePassword}
 | 
			
		||||
              className='password-eye absolute cursor-pointer bg-cover bg-[#f1f1f1]'
 | 
			
		||||
              className='password-eye absolute cursor-pointer bg-[#f1f1f1] bg-cover'
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          <FormState
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Loader } from './'
 | 
			
		||||
import { Loader } from '.'
 | 
			
		||||
 | 
			
		||||
describe('<Loader />', () => {
 | 
			
		||||
  it('should render with correct width and height', async () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { Main } from './'
 | 
			
		||||
import { Main } from '.'
 | 
			
		||||
 | 
			
		||||
describe('<Main />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ export const Main: React.FC<MainProps> = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <main
 | 
			
		||||
      className={classNames(
 | 
			
		||||
        'flex flex-1 flex-col justify-center items-center py-8',
 | 
			
		||||
        'flex flex-1 flex-col items-center justify-center py-8',
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
    >
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { render } from '@testing-library/react'
 | 
			
		||||
 | 
			
		||||
import { SocialMedia, SocialMediaButton } from './'
 | 
			
		||||
import { SocialMedia, SocialMediaButton } from '.'
 | 
			
		||||
 | 
			
		||||
describe('<SocialMediaButton />', () => {
 | 
			
		||||
  it('should render the social media', async () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,14 +11,14 @@ export const Textarea: React.FC<TextareaProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='flex flex-col'>
 | 
			
		||||
      <div className='flex justify-between mt-6 mb-2'>
 | 
			
		||||
      <div className='mt-6 mb-2 flex justify-between'>
 | 
			
		||||
        <label className='pl-1' htmlFor={id}>
 | 
			
		||||
          {label}
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className='mt-0 relative'>
 | 
			
		||||
      <div className='relative mt-0'>
 | 
			
		||||
        <TextareaAutosize
 | 
			
		||||
          className='p-3 rounded-lg bg-[#f1f1f1] text-[#2a2a2a] caret-green-600 font-paragraph w-full focus:border focus:outline-none resize-none focus:shadow-green overflow-hidden'
 | 
			
		||||
          className='w-full resize-none overflow-hidden rounded-lg bg-[#f1f1f1] p-3 font-paragraph text-[#2a2a2a] caret-green-600 focus:border focus:shadow-green focus:outline-none'
 | 
			
		||||
          wrap='soft'
 | 
			
		||||
          id={id}
 | 
			
		||||
          name={id}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
import { getUsersCurrentHandler } from './users/current/get'
 | 
			
		||||
import { postUsersRefreshTokenHandler } from './users/refresh-token/post'
 | 
			
		||||
 | 
			
		||||
export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'
 | 
			
		||||
 | 
			
		||||
export interface Handler {
 | 
			
		||||
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
 | 
			
		||||
  method: Method
 | 
			
		||||
  url: `/${string}`
 | 
			
		||||
  response: {
 | 
			
		||||
    isFile?: boolean
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								cypress/fixtures/users/[userId]/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cypress/fixtures/users/[userId]/get.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
import { Handler } from '../../handler'
 | 
			
		||||
import { userExample, userSettingsExample } from '../user'
 | 
			
		||||
 | 
			
		||||
export const getUserByIdHandler: Handler = {
 | 
			
		||||
  method: 'GET',
 | 
			
		||||
  url: `/users/${userExample.id}`,
 | 
			
		||||
  response: {
 | 
			
		||||
    statusCode: 200,
 | 
			
		||||
    body: {
 | 
			
		||||
      user: {
 | 
			
		||||
        ...userExample,
 | 
			
		||||
        settings: userSettingsExample
 | 
			
		||||
      },
 | 
			
		||||
      guilds: []
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,6 +5,7 @@ import { getGuildsHandler } from '../../../fixtures/guilds/get'
 | 
			
		||||
import { authenticationHandlers } from '../../../fixtures/handler'
 | 
			
		||||
import { getGuildMemberWithGuildIdHandler } from '../../../fixtures/guilds/[guildId]/get'
 | 
			
		||||
import { getChannelWithChannelIdHandler } from '../../../fixtures/channels/[channelId]/get'
 | 
			
		||||
import { getUserByIdHandler } from '../../../fixtures/users/[userId]/get'
 | 
			
		||||
 | 
			
		||||
const applicationPaths = [
 | 
			
		||||
  '/application',
 | 
			
		||||
@@ -32,7 +33,8 @@ describe('Common > application/authentication', () => {
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getGuildsHandler,
 | 
			
		||||
      getGuildMemberWithGuildIdHandler,
 | 
			
		||||
      getChannelWithChannelIdHandler
 | 
			
		||||
      getChannelWithChannelIdHandler,
 | 
			
		||||
      getUserByIdHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    for (const applicationPath of applicationPaths) {
 | 
			
		||||
      cy.visit(applicationPath)
 | 
			
		||||
 
 | 
			
		||||
@@ -203,14 +203,16 @@ describe('Pages > /application/[guildId]/[channelId]', () => {
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should redirect the user to `/application` if `guildId` or `channelId` are not numbers', () => {
 | 
			
		||||
  it('should redirect the user to `/404` if `guildId` or `channelId` are not numbers', () => {
 | 
			
		||||
    cy.task('startMockServer', authenticationHandlers).setCookie(
 | 
			
		||||
      'refreshToken',
 | 
			
		||||
      'refresh-token'
 | 
			
		||||
    )
 | 
			
		||||
    cy.visit('/application/abc/abc')
 | 
			
		||||
    cy.visit('/application/abc/abc', {
 | 
			
		||||
      failOnStatusCode: false
 | 
			
		||||
    })
 | 
			
		||||
      .location('pathname')
 | 
			
		||||
      .should('eq', '/application')
 | 
			
		||||
      .should('eq', '/404')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it("should redirect the user to `/404` if `guildId` doesn't exist", () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
import date from 'date-and-time'
 | 
			
		||||
 | 
			
		||||
import { userExample } from '../../../../../fixtures/users/user'
 | 
			
		||||
import { getUserByIdHandler } from '../../../../../fixtures/users/[userId]/get'
 | 
			
		||||
import { authenticationHandlers } from '../../../../../fixtures/handler'
 | 
			
		||||
 | 
			
		||||
describe('Pages > /application/users/[userId]', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    cy.task('stopMockServer')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it('should succeeds and display the public user profile correctly', () => {
 | 
			
		||||
    cy.task('startMockServer', [
 | 
			
		||||
      ...authenticationHandlers,
 | 
			
		||||
      getUserByIdHandler
 | 
			
		||||
    ]).setCookie('refreshToken', 'refresh-token')
 | 
			
		||||
    cy.visit(`/application/users/${userExample.id}`)
 | 
			
		||||
    cy.get('[data-cy=user-name]').should('have.text', userExample.name)
 | 
			
		||||
    cy.get('[data-cy=user-email]').should('have.text', userExample.email)
 | 
			
		||||
    cy.get('[data-cy=user-createdAt]').should(
 | 
			
		||||
      'have.text',
 | 
			
		||||
      date.format(new Date(userExample.createdAt), 'DD/MM/YYYY')
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  it("should redirect the user to `/404` if `userId` doesn't exist", () => {
 | 
			
		||||
    cy.task('startMockServer', [...authenticationHandlers]).setCookie(
 | 
			
		||||
      'refreshToken',
 | 
			
		||||
      'refresh-token'
 | 
			
		||||
    )
 | 
			
		||||
    cy.visit(`/application/users/123`, { failOnStatusCode: false })
 | 
			
		||||
      .location('pathname')
 | 
			
		||||
      .should('eq', '/404')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
@@ -6,6 +6,10 @@ import { API_DEFAULT_PORT } from '../../tools/api'
 | 
			
		||||
 | 
			
		||||
/// <reference types="cypress" />
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @typedef {import('../fixtures/handler').Method} Method
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/** @type {import('mockttp').Mockttp | null}  */
 | 
			
		||||
let server = null
 | 
			
		||||
 | 
			
		||||
@@ -31,13 +35,16 @@ module.exports = (on, config) => {
 | 
			
		||||
      await server.start(API_DEFAULT_PORT)
 | 
			
		||||
      for (const handler of handlers) {
 | 
			
		||||
        const { isFile = false } = handler.response
 | 
			
		||||
        const method = /** @type {Lowercase<Method>} */ (
 | 
			
		||||
          handler.method.toLowerCase()
 | 
			
		||||
        )
 | 
			
		||||
        if (isFile) {
 | 
			
		||||
          await server[handler.method.toLowerCase()](handler.url).thenFromFile(
 | 
			
		||||
          await server[method](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[method](handler.url).thenJson(
 | 
			
		||||
            handler.response.statusCode,
 | 
			
		||||
            handler.response.body
 | 
			
		||||
          )
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import { render } from '@testing-library/react'
 | 
			
		||||
import { {{ properCase name }} } from './{{ properCase name }}'
 | 
			
		||||
 | 
			
		||||
describe('<{{ properCase name }} />', () => {
 | 
			
		||||
  it('should render successfully', () => {
 | 
			
		||||
  it('should render', () => {
 | 
			
		||||
    const { baseElement } = render(<{{ properCase name }} />)
 | 
			
		||||
    expect(baseElement).toBeTruthy()
 | 
			
		||||
  })
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('plop').NodePlopAPI} plop
 | 
			
		||||
 * @returns {import('node-plop').PlopGeneratorConfig}
 | 
			
		||||
 */
 | 
			
		||||
exports.componentGenerator = () => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
declare module 'next-pwa'
 | 
			
		||||
declare module 'next/jest'
 | 
			
		||||
@@ -10,7 +10,7 @@ const errorObject: ErrorObject = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe('hooks/useForm/getErrorTranslationKey', () => {
 | 
			
		||||
  it('returns `errors:invalid` with unknown keyword', async () => {
 | 
			
		||||
  it('returns `errors:invalid` with unknown keyword', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      getErrorTranslationKey({
 | 
			
		||||
        ...errorObject,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								hooks/useForm/handleCheckboxBoolean.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								hooks/useForm/handleCheckboxBoolean.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
import { Type } from '@sinclair/typebox'
 | 
			
		||||
 | 
			
		||||
import { handleCheckboxBoolean } from './handleCheckboxBoolean'
 | 
			
		||||
 | 
			
		||||
const schema = Type.Object({
 | 
			
		||||
  myBoolean: Type.Boolean(),
 | 
			
		||||
  myString: Type.String()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
describe('hooks/useForm/handleCheckboxBoolean', () => {
 | 
			
		||||
  it('should convert all checkbox property to boolean', () => {
 | 
			
		||||
    const object = {
 | 
			
		||||
      myBoolean: 'on',
 | 
			
		||||
      myString: 'on'
 | 
			
		||||
    }
 | 
			
		||||
    const result = handleCheckboxBoolean(object, schema)
 | 
			
		||||
    expect(result).toEqual({
 | 
			
		||||
      myBoolean: true,
 | 
			
		||||
      myString: 'on'
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										25
									
								
								hooks/useForm/handleCheckboxBoolean.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								hooks/useForm/handleCheckboxBoolean.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
import type { TObject } from '@sinclair/typebox'
 | 
			
		||||
 | 
			
		||||
import type { ObjectAny } from '../../tools/types'
 | 
			
		||||
 | 
			
		||||
export const handleCheckboxBoolean = (
 | 
			
		||||
  object: ObjectAny,
 | 
			
		||||
  validateSchemaObject: TObject<ObjectAny>
 | 
			
		||||
): ObjectAny => {
 | 
			
		||||
  const booleanProperties: string[] = []
 | 
			
		||||
  for (const property in validateSchemaObject.properties) {
 | 
			
		||||
    const rule = validateSchemaObject.properties[property]
 | 
			
		||||
    if (rule.type === 'boolean') {
 | 
			
		||||
      booleanProperties.push(property)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  for (const booleanProperty of booleanProperties) {
 | 
			
		||||
    if (object[booleanProperty] == null) {
 | 
			
		||||
      object[booleanProperty] =
 | 
			
		||||
        validateSchemaObject.properties[booleanProperty].default
 | 
			
		||||
    } else {
 | 
			
		||||
      object[booleanProperty] = object[booleanProperty] === 'on'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return object
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								hooks/useForm/replaceEmptyStringInObjectToNull.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								hooks/useForm/replaceEmptyStringInObjectToNull.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
import { replaceEmptyStringInObjectToNull } from './replaceEmptyStringInObjectToNull'
 | 
			
		||||
 | 
			
		||||
describe('tools/utils/replaceEmptyStringInObjectToNull', () => {
 | 
			
		||||
  it('should replace empty string in object to null except for required properties', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      replaceEmptyStringInObjectToNull(
 | 
			
		||||
        {
 | 
			
		||||
          foo: '',
 | 
			
		||||
          bar: 'bar',
 | 
			
		||||
          baz: ''
 | 
			
		||||
        },
 | 
			
		||||
        ['baz']
 | 
			
		||||
      )
 | 
			
		||||
    ).toEqual({
 | 
			
		||||
      foo: null,
 | 
			
		||||
      bar: 'bar',
 | 
			
		||||
      baz: ''
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										19
									
								
								hooks/useForm/replaceEmptyStringInObjectToNull.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								hooks/useForm/replaceEmptyStringInObjectToNull.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
import type { ObjectAny } from '../../tools/types'
 | 
			
		||||
 | 
			
		||||
export const replaceEmptyStringInObjectToNull = (
 | 
			
		||||
  object: ObjectAny,
 | 
			
		||||
  required: string[] = []
 | 
			
		||||
): ObjectAny => {
 | 
			
		||||
  return Object.fromEntries(
 | 
			
		||||
    Object.entries(object).map(([key, value]) => {
 | 
			
		||||
      if (
 | 
			
		||||
        typeof value === 'string' &&
 | 
			
		||||
        value.length === 0 &&
 | 
			
		||||
        !required.includes(key)
 | 
			
		||||
      ) {
 | 
			
		||||
        return [key, null]
 | 
			
		||||
      }
 | 
			
		||||
      return [key, value]
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +1,15 @@
 | 
			
		||||
import { useMemo, useState } from 'react'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import { Type } from '@sinclair/typebox'
 | 
			
		||||
import type { FormDataObject, HandleForm } from 'react-component-form'
 | 
			
		||||
import type { HandleForm } from 'react-component-form'
 | 
			
		||||
import type { ErrorObject } from 'ajv'
 | 
			
		||||
 | 
			
		||||
import { FetchState, useFetchState } from '../useFetchState'
 | 
			
		||||
import { ajv } from '../../tools/ajv'
 | 
			
		||||
import { getErrorTranslationKey } from './getErrorTranslationKey'
 | 
			
		||||
import { replaceEmptyStringInObjectToNull } from './replaceEmptyStringInObjectToNull'
 | 
			
		||||
import type { ObjectAny } from '../../tools/types'
 | 
			
		||||
import { handleCheckboxBoolean } from './handleCheckboxBoolean'
 | 
			
		||||
 | 
			
		||||
interface Errors {
 | 
			
		||||
  [key: string]: ErrorObject<string, any> | null | undefined
 | 
			
		||||
@@ -21,7 +24,9 @@ const findError = (
 | 
			
		||||
export type GetErrorTranslation = (error?: ErrorObject | null) => string | null
 | 
			
		||||
 | 
			
		||||
export interface UseFormOptions {
 | 
			
		||||
  validateSchemaObject: { [key: string]: any }
 | 
			
		||||
  validateSchema: { [key: string]: any }
 | 
			
		||||
  replaceEmptyStringToNull?: boolean
 | 
			
		||||
  resetOnSuccess?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type HandleSubmit = (callback: HandleSubmitCallback) => HandleForm
 | 
			
		||||
@@ -32,20 +37,28 @@ interface Message {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type HandleSubmitCallback = (
 | 
			
		||||
  formData: FormDataObject,
 | 
			
		||||
  formData: ObjectAny,
 | 
			
		||||
  formElement: HTMLFormElement
 | 
			
		||||
) => Promise<Message | null>
 | 
			
		||||
 | 
			
		||||
export interface UseFormResult {
 | 
			
		||||
  message: string | null
 | 
			
		||||
  setMessageTranslationKey: React.Dispatch<
 | 
			
		||||
    React.SetStateAction<string | undefined>
 | 
			
		||||
  >
 | 
			
		||||
  fetchState: FetchState
 | 
			
		||||
  setFetchState: React.Dispatch<React.SetStateAction<FetchState>>
 | 
			
		||||
  getErrorTranslation: GetErrorTranslation
 | 
			
		||||
  handleSubmit: HandleSubmit
 | 
			
		||||
  errors: Errors
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useForm = (options: UseFormOptions): UseFormResult => {
 | 
			
		||||
  const { validateSchemaObject } = options
 | 
			
		||||
  const {
 | 
			
		||||
    validateSchema,
 | 
			
		||||
    replaceEmptyStringToNull = false,
 | 
			
		||||
    resetOnSuccess = false
 | 
			
		||||
  } = options
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const [fetchState, setFetchState] = useFetchState()
 | 
			
		||||
  const [messageTranslationKey, setMessageTranslationKey] = useState<
 | 
			
		||||
@@ -53,13 +66,13 @@ export const useForm = (options: UseFormOptions): UseFormResult => {
 | 
			
		||||
  >(undefined)
 | 
			
		||||
  const [errors, setErrors] = useState<Errors>({})
 | 
			
		||||
 | 
			
		||||
  const validateSchema = useMemo(() => {
 | 
			
		||||
    return Type.Object(validateSchemaObject)
 | 
			
		||||
  }, [validateSchemaObject])
 | 
			
		||||
  const validateSchemaObject = useMemo(() => {
 | 
			
		||||
    return Type.Object(validateSchema)
 | 
			
		||||
  }, [validateSchema])
 | 
			
		||||
 | 
			
		||||
  const validate = useMemo(() => {
 | 
			
		||||
    return ajv.compile(validateSchema)
 | 
			
		||||
  }, [validateSchema])
 | 
			
		||||
    return ajv.compile(validateSchemaObject)
 | 
			
		||||
  }, [validateSchemaObject])
 | 
			
		||||
 | 
			
		||||
  const getErrorTranslation = (error?: ErrorObject | null): string | null => {
 | 
			
		||||
    if (error != null) {
 | 
			
		||||
@@ -72,12 +85,19 @@ export const useForm = (options: UseFormOptions): UseFormResult => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit: HandleSubmit = (callback) => {
 | 
			
		||||
    return async (formData, formElement) => {
 | 
			
		||||
    return async (formData: ObjectAny, formElement) => {
 | 
			
		||||
      if (replaceEmptyStringToNull) {
 | 
			
		||||
        formData = replaceEmptyStringInObjectToNull(
 | 
			
		||||
          formData,
 | 
			
		||||
          validateSchemaObject.required
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      formData = handleCheckboxBoolean(formData, validateSchemaObject)
 | 
			
		||||
      const isValid = validate(formData)
 | 
			
		||||
      if (!isValid) {
 | 
			
		||||
        setFetchState('error')
 | 
			
		||||
        const errors: Errors = {}
 | 
			
		||||
        for (const property in validateSchema.properties) {
 | 
			
		||||
        for (const property in validateSchemaObject.properties) {
 | 
			
		||||
          errors[property] = validate.errors?.find(findError(`/${property}`))
 | 
			
		||||
        }
 | 
			
		||||
        setErrors(errors)
 | 
			
		||||
@@ -89,7 +109,9 @@ export const useForm = (options: UseFormOptions): UseFormResult => {
 | 
			
		||||
          setMessageTranslationKey(message.value)
 | 
			
		||||
          if (message.type === 'success') {
 | 
			
		||||
            setFetchState('success')
 | 
			
		||||
            if (resetOnSuccess) {
 | 
			
		||||
              formElement.reset()
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            setFetchState('error')
 | 
			
		||||
          }
 | 
			
		||||
@@ -102,7 +124,9 @@ export const useForm = (options: UseFormOptions): UseFormResult => {
 | 
			
		||||
    getErrorTranslation,
 | 
			
		||||
    errors,
 | 
			
		||||
    fetchState,
 | 
			
		||||
    setFetchState,
 | 
			
		||||
    handleSubmit,
 | 
			
		||||
    message: messageTranslationKey != null ? t(messageTranslationKey) : null
 | 
			
		||||
    message: messageTranslationKey != null ? t(messageTranslationKey) : null,
 | 
			
		||||
    setMessageTranslationKey
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
    "/authentication/signup": ["authentication", "errors"],
 | 
			
		||||
    "/authentication/signin": ["authentication", "errors"],
 | 
			
		||||
    "/application/users/[userId]": ["application", "errors"],
 | 
			
		||||
    "/application/users/[userId]/settings": ["application", "errors"],
 | 
			
		||||
    "/application/users/settings": ["application", "authentication", "errors"],
 | 
			
		||||
    "/application/guilds/create": ["application", "errors"],
 | 
			
		||||
    "/application/guilds/join": ["application", "errors"],
 | 
			
		||||
    "/application": ["application", "errors"],
 | 
			
		||||
 
 | 
			
		||||
@@ -12,5 +12,6 @@
 | 
			
		||||
  "success-signup": "You're almost there, please check your emails to confirm registration.",
 | 
			
		||||
  "success-forgot-password": "Password-reset request successful, please check your emails!",
 | 
			
		||||
  "wrong-credentials": "Invalid credentials. Please try again.",
 | 
			
		||||
  "alreadyUsed": "Name or Email already used."
 | 
			
		||||
  "already-used": "Name or Email already used.",
 | 
			
		||||
  "email-required-to-sign-in": "Email is required if it's your only way to sign in."
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,6 @@
 | 
			
		||||
  "write-a-message": "Écrire un message...",
 | 
			
		||||
  "biography": "Biographie",
 | 
			
		||||
  "label-checkbox-email": "Afficher votre email au public.",
 | 
			
		||||
  "label-checkbox-guilds": "Afficher la liste des guilds au public.",
 | 
			
		||||
  "private-user-guilds-list": "List of private guilds"
 | 
			
		||||
  "label-checkbox-guilds": "Afficher la liste des guildes au public.",
 | 
			
		||||
  "private-user-guilds-list": "Liste des guildes privées"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,5 +12,6 @@
 | 
			
		||||
  "success-signup": "Vous y êtes presque, veuillez vérifier vos emails pour confirmer votre inscription.",
 | 
			
		||||
  "success-forgot-password": "Demande de réinitialisation du mot de passe réussie, veuillez vérifier vos emails!",
 | 
			
		||||
  "wrong-credentials": "Informations d'identification invalides. Veuillez réessayer.",
 | 
			
		||||
  "alreadyUsed": "Nom ou Email déjà utilisé."
 | 
			
		||||
  "already-used": "Nom ou Email déjà utilisé.",
 | 
			
		||||
  "email-required-to-sign-in": "L'adresse e-mail est requise si c'est votre seul moyen de vous connecter."
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
const nextPWA = require('next-pwa')
 | 
			
		||||
/** @type {any} */
 | 
			
		||||
const nextTranslate = require('next-translate')
 | 
			
		||||
const { createSecureHeaders } = require('next-secure-headers')
 | 
			
		||||
 | 
			
		||||
@@ -16,7 +17,7 @@ module.exports = nextTranslate(
 | 
			
		||||
      disable: process.env.NODE_ENV !== 'production',
 | 
			
		||||
      dest: 'public'
 | 
			
		||||
    },
 | 
			
		||||
    async headers() {
 | 
			
		||||
    headers() {
 | 
			
		||||
      return [
 | 
			
		||||
        {
 | 
			
		||||
          source: '/:path*',
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15240
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15240
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										102
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										102
									
								
								package.json
									
									
									
									
									
								
							@@ -18,15 +18,15 @@
 | 
			
		||||
    "generate": "plop",
 | 
			
		||||
    "lint:commit": "commitlint",
 | 
			
		||||
    "lint:editorconfig": "editorconfig-checker",
 | 
			
		||||
    "lint:markdown": "markdownlint '**/*.md' --dot --ignore-path '.gitignore'",
 | 
			
		||||
    "lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'",
 | 
			
		||||
    "lint:prettier": "prettier '.' --check",
 | 
			
		||||
    "lint:markdown": "markdownlint \"**/*.md\" --dot --ignore-path \".gitignore\"",
 | 
			
		||||
    "lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\" --ignore-path \".gitignore\"",
 | 
			
		||||
    "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: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'",
 | 
			
		||||
    "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\"",
 | 
			
		||||
    "storybook": "start-storybook --port 6006",
 | 
			
		||||
    "storybook:build": "build-storybook",
 | 
			
		||||
    "storybook:serve": "serve -p 6006 storybook-static",
 | 
			
		||||
@@ -35,94 +35,96 @@
 | 
			
		||||
    "postinstall": "husky install"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@fontsource/montserrat": "4.5.1",
 | 
			
		||||
    "@fontsource/roboto": "4.5.1",
 | 
			
		||||
    "@fontsource/montserrat": "4.5.5",
 | 
			
		||||
    "@fontsource/roboto": "4.5.3",
 | 
			
		||||
    "@heroicons/react": "1.0.5",
 | 
			
		||||
    "@sinclair/typebox": "0.23.2",
 | 
			
		||||
    "ajv": "8.8.2",
 | 
			
		||||
    "@sinclair/typebox": "0.23.4",
 | 
			
		||||
    "ajv": "8.10.0",
 | 
			
		||||
    "ajv-formats": "2.1.1",
 | 
			
		||||
    "axios": "0.24.0",
 | 
			
		||||
    "axios": "0.26.0",
 | 
			
		||||
    "classnames": "2.3.1",
 | 
			
		||||
    "date-and-time": "2.1.0",
 | 
			
		||||
    "date-and-time": "2.1.2",
 | 
			
		||||
    "emoji-mart": "3.0.1",
 | 
			
		||||
    "katex": "0.15.2",
 | 
			
		||||
    "next": "12.0.7",
 | 
			
		||||
    "next": "12.1.0",
 | 
			
		||||
    "next-pwa": "5.4.4",
 | 
			
		||||
    "next-themes": "0.0.15",
 | 
			
		||||
    "next-translate": "1.2.0",
 | 
			
		||||
    "pretty-bytes": "5.6.0",
 | 
			
		||||
    "next-translate": "1.3.4",
 | 
			
		||||
    "pretty-bytes": "6.0.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.2",
 | 
			
		||||
    "react-markdown": "8.0.0",
 | 
			
		||||
    "react-responsive": "8.2.0",
 | 
			
		||||
    "react-swipeable": "6.2.0",
 | 
			
		||||
    "react-textarea-autosize": "8.3.3",
 | 
			
		||||
    "read-pkg": "7.0.0",
 | 
			
		||||
    "read-pkg": "7.1.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",
 | 
			
		||||
    "sharp": "0.30.1",
 | 
			
		||||
    "socket.io-client": "4.4.1",
 | 
			
		||||
    "unified": "10.1.1",
 | 
			
		||||
    "unist-util-visit": "4.1.0",
 | 
			
		||||
    "universal-cookie": "4.0.4"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@commitlint/cli": "16.0.2",
 | 
			
		||||
    "@commitlint/config-conventional": "16.0.0",
 | 
			
		||||
    "@lhci/cli": "0.8.2",
 | 
			
		||||
    "@saithodev/semantic-release-backmerge": "2.1.0",
 | 
			
		||||
    "@storybook/addon-essentials": "6.4.12",
 | 
			
		||||
    "@storybook/addon-links": "6.4.12",
 | 
			
		||||
    "@commitlint/cli": "16.2.1",
 | 
			
		||||
    "@commitlint/config-conventional": "16.2.1",
 | 
			
		||||
    "@lhci/cli": "0.9.0",
 | 
			
		||||
    "@saithodev/semantic-release-backmerge": "2.1.1",
 | 
			
		||||
    "@storybook/addon-essentials": "6.4.19",
 | 
			
		||||
    "@storybook/addon-links": "6.4.19",
 | 
			
		||||
    "@storybook/addon-postcss": "2.0.0",
 | 
			
		||||
    "@storybook/builder-webpack5": "6.4.12",
 | 
			
		||||
    "@storybook/manager-webpack5": "6.4.12",
 | 
			
		||||
    "@storybook/react": "6.4.12",
 | 
			
		||||
    "@testing-library/jest-dom": "5.16.1",
 | 
			
		||||
    "@testing-library/react": "12.1.2",
 | 
			
		||||
    "@storybook/builder-webpack5": "6.4.19",
 | 
			
		||||
    "@storybook/manager-webpack5": "6.4.19",
 | 
			
		||||
    "@storybook/react": "6.4.19",
 | 
			
		||||
    "@testing-library/jest-dom": "5.16.2",
 | 
			
		||||
    "@testing-library/react": "12.1.3",
 | 
			
		||||
    "@types/date-and-time": "0.13.0",
 | 
			
		||||
    "@types/emoji-mart": "3.0.9",
 | 
			
		||||
    "@types/hast": "2.3.4",
 | 
			
		||||
    "@types/jest": "27.4.0",
 | 
			
		||||
    "@types/katex": "0.11.1",
 | 
			
		||||
    "@types/node": "17.0.8",
 | 
			
		||||
    "@types/react": "17.0.38",
 | 
			
		||||
    "@types/node": "17.0.18",
 | 
			
		||||
    "@types/react": "17.0.39",
 | 
			
		||||
    "@types/react-responsive": "8.0.5",
 | 
			
		||||
    "@types/unist": "2.0.6",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "4.33.0",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "5.12.0",
 | 
			
		||||
    "@typescript-eslint/parser": "5.12.0",
 | 
			
		||||
    "autoprefixer": "10.4.2",
 | 
			
		||||
    "cypress": "9.2.1",
 | 
			
		||||
    "cypress": "9.5.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": "8.9.0",
 | 
			
		||||
    "eslint-config-conventions": "1.0.3",
 | 
			
		||||
    "eslint-config-next": "12.1.0",
 | 
			
		||||
    "eslint-config-prettier": "8.4.0",
 | 
			
		||||
    "eslint-plugin-cypress": "2.12.1",
 | 
			
		||||
    "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",
 | 
			
		||||
    "eslint-plugin-promise": "6.0.0",
 | 
			
		||||
    "eslint-plugin-storybook": "0.5.7",
 | 
			
		||||
    "eslint-plugin-unicorn": "41.0.0",
 | 
			
		||||
    "html-w3c-validator": "1.0.0",
 | 
			
		||||
    "husky": "7.0.4",
 | 
			
		||||
    "jest": "27.4.7",
 | 
			
		||||
    "lint-staged": "12.1.7",
 | 
			
		||||
    "markdownlint-cli": "0.30.0",
 | 
			
		||||
    "mockttp": "2.5.0",
 | 
			
		||||
    "jest": "27.5.1",
 | 
			
		||||
    "lint-staged": "12.3.4",
 | 
			
		||||
    "markdownlint-cli": "0.31.1",
 | 
			
		||||
    "mockttp": "2.6.0",
 | 
			
		||||
    "next-secure-headers": "2.2.0",
 | 
			
		||||
    "plop": "3.0.5",
 | 
			
		||||
    "postcss": "8.4.5",
 | 
			
		||||
    "postcss": "8.4.6",
 | 
			
		||||
    "prettier": "2.5.1",
 | 
			
		||||
    "semantic-release": "18.0.1",
 | 
			
		||||
    "prettier-plugin-tailwindcss": "0.1.7",
 | 
			
		||||
    "semantic-release": "19.0.2",
 | 
			
		||||
    "serve": "13.0.2",
 | 
			
		||||
    "start-server-and-test": "1.14.0",
 | 
			
		||||
    "storybook-tailwind-dark-mode": "1.0.11",
 | 
			
		||||
    "tailwindcss": "3.0.13",
 | 
			
		||||
    "tailwindcss": "3.0.23",
 | 
			
		||||
    "typescript": "4.4.4",
 | 
			
		||||
    "vercel": "23.1.2",
 | 
			
		||||
    "webpack": "5.66.0"
 | 
			
		||||
    "vercel": "24.0.0",
 | 
			
		||||
    "webpack": "5.69.1"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ const Error404: NextPage<FooterProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Head title='Divlo - 404' />
 | 
			
		||||
      <Head title='Thream | 404' />
 | 
			
		||||
 | 
			
		||||
      <Header />
 | 
			
		||||
      <main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ const Error500: NextPage<FooterProps> = (props) => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Head title='Divlo - 500' />
 | 
			
		||||
      <Head title='Thream | 500' />
 | 
			
		||||
 | 
			
		||||
      <Header />
 | 
			
		||||
      <main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ import { useEffect } from 'react'
 | 
			
		||||
import { AppProps } from 'next/app'
 | 
			
		||||
import { ThemeProvider } from 'next-themes'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
import UniversalCookie from 'universal-cookie'
 | 
			
		||||
 | 
			
		||||
import 'styles/global.css'
 | 
			
		||||
 | 
			
		||||
@@ -14,19 +13,13 @@ import '@fontsource/montserrat/700.css'
 | 
			
		||||
import '@fontsource/roboto/400.css'
 | 
			
		||||
import '@fontsource/roboto/700.css'
 | 
			
		||||
 | 
			
		||||
const universalCookie = new UniversalCookie()
 | 
			
		||||
 | 
			
		||||
/** how long in seconds, until the cookie expires (10 years) */
 | 
			
		||||
const COOKIE_MAX_AGE = 10 * 365.25 * 24 * 60 * 60
 | 
			
		||||
import { cookies } from 'tools/cookies'
 | 
			
		||||
 | 
			
		||||
const Application = ({ Component, pageProps }: AppProps): JSX.Element => {
 | 
			
		||||
  const { lang } = useTranslation()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    universalCookie.set('NEXT_LOCALE', lang, {
 | 
			
		||||
      path: '/',
 | 
			
		||||
      maxAge: COOKIE_MAX_AGE
 | 
			
		||||
    })
 | 
			
		||||
    cookies.set('NEXT_LOCALE', lang)
 | 
			
		||||
  }, [lang])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,7 @@ export const getServerSideProps = authenticationFromServerSide({
 | 
			
		||||
    if (isNaN(channelId) || isNaN(guildId)) {
 | 
			
		||||
      return {
 | 
			
		||||
        redirect: {
 | 
			
		||||
          destination: '/application',
 | 
			
		||||
          destination: '/404',
 | 
			
		||||
          permanent: false
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,17 +9,23 @@ import {
 | 
			
		||||
} from 'tools/authentication'
 | 
			
		||||
import { UserProfile } from 'components/Application/UserProfile'
 | 
			
		||||
import { GuildsProvider } from 'contexts/Guilds'
 | 
			
		||||
import { UserPublic } from 'models/User'
 | 
			
		||||
import { Guild } from 'models/Guild'
 | 
			
		||||
 | 
			
		||||
export interface UserProfilePageProps extends PagePropsWithAuthentication {
 | 
			
		||||
  user: UserPublic
 | 
			
		||||
  guilds: Guild[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const UserProfilePage: NextPage<UserProfilePageProps> = (props) => {
 | 
			
		||||
  const { user, guilds, authentication } = props
 | 
			
		||||
 | 
			
		||||
const UserProfilePage: NextPage<PagePropsWithAuthentication> = (props) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <AuthenticationProvider authentication={props.authentication}>
 | 
			
		||||
    <AuthenticationProvider authentication={authentication}>
 | 
			
		||||
      <GuildsProvider>
 | 
			
		||||
        <Head title={`Thream | ${props.authentication.user.name}`} />
 | 
			
		||||
        <Application
 | 
			
		||||
          path={`/application/users/${props.authentication.user.id}`}
 | 
			
		||||
          title={props.authentication.user.name}
 | 
			
		||||
        >
 | 
			
		||||
          <UserProfile user={props.authentication.user} />
 | 
			
		||||
        <Head title={`Thream | ${user.name}`} />
 | 
			
		||||
        <Application path={`/application/users/${user.id}`} title={user.name}>
 | 
			
		||||
          <UserProfile user={user} guilds={guilds} />
 | 
			
		||||
        </Application>
 | 
			
		||||
      </GuildsProvider>
 | 
			
		||||
    </AuthenticationProvider>
 | 
			
		||||
@@ -27,7 +33,23 @@ const UserProfilePage: NextPage<PagePropsWithAuthentication> = (props) => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getServerSideProps = authenticationFromServerSide({
 | 
			
		||||
  shouldBeAuthenticated: true
 | 
			
		||||
  shouldBeAuthenticated: true,
 | 
			
		||||
  fetchData: async (context, api) => {
 | 
			
		||||
    const userId = Number(context?.params?.userId)
 | 
			
		||||
    if (isNaN(userId)) {
 | 
			
		||||
      return {
 | 
			
		||||
        redirect: {
 | 
			
		||||
          destination: '/404',
 | 
			
		||||
          permanent: false
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const { data } = await api.get(`/users/${userId}`)
 | 
			
		||||
    return {
 | 
			
		||||
      user: data.user,
 | 
			
		||||
      guilds: data.guilds
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default UserProfilePage
 | 
			
		||||
 
 | 
			
		||||
@@ -15,11 +15,8 @@ const UserSettingsPage: NextPage<PagePropsWithAuthentication> = (props) => {
 | 
			
		||||
    <AuthenticationProvider authentication={props.authentication}>
 | 
			
		||||
      <GuildsProvider>
 | 
			
		||||
        <Head title='Thream | Settings' />
 | 
			
		||||
        <Application
 | 
			
		||||
          path={`/application/users/${props.authentication.user.id}/settings`}
 | 
			
		||||
          title='Settings'
 | 
			
		||||
        >
 | 
			
		||||
          <UserSettings user={props.authentication.user} />
 | 
			
		||||
        <Application path={`/application/users/settings`} title='Settings'>
 | 
			
		||||
          <UserSettings />
 | 
			
		||||
        </Application>
 | 
			
		||||
      </GuildsProvider>
 | 
			
		||||
    </AuthenticationProvider>
 | 
			
		||||
@@ -22,7 +22,10 @@ const ForgotPassword: NextPage<FooterProps> = (props) => {
 | 
			
		||||
  const { version } = props
 | 
			
		||||
 | 
			
		||||
  const { fetchState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({ validateSchemaObject: { email: userSchema.email } })
 | 
			
		||||
    useForm({
 | 
			
		||||
      validateSchema: { email: userSchema.email },
 | 
			
		||||
      resetOnSuccess: true
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
    try {
 | 
			
		||||
@@ -55,10 +58,10 @@ const ForgotPassword: NextPage<FooterProps> = (props) => {
 | 
			
		||||
      <Main>
 | 
			
		||||
        <AuthenticationForm onSubmit={handleSubmit(onSubmit)}>
 | 
			
		||||
          <Input type='email' placeholder='Email' name='email' label='Email' />
 | 
			
		||||
          <Button data-cy='submit' className='w-full mt-6' type='submit'>
 | 
			
		||||
          <Button data-cy='submit' className='mt-6 w-full' type='submit'>
 | 
			
		||||
            {t('authentication:submit')}
 | 
			
		||||
          </Button>
 | 
			
		||||
          <p className='mt-3 font-headline text-sm text-green-800 dark:text-green-400 hover:underline'>
 | 
			
		||||
          <p className='mt-3 font-headline text-sm text-green-800 hover:underline dark:text-green-400'>
 | 
			
		||||
            <Link href='/authentication/signin'>
 | 
			
		||||
              <a>{t('authentication:already-know-password')}</a>
 | 
			
		||||
            </Link>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,10 @@ const ResetPassword: NextPage<FooterProps> = (props) => {
 | 
			
		||||
  const { version } = props
 | 
			
		||||
 | 
			
		||||
  const { fetchState, message, errors, getErrorTranslation, handleSubmit } =
 | 
			
		||||
    useForm({ validateSchemaObject: { password: userSchema.password } })
 | 
			
		||||
    useForm({
 | 
			
		||||
      validateSchema: { password: userSchema.password },
 | 
			
		||||
      resetOnSuccess: true
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
  const onSubmit: HandleSubmitCallback = async (formData) => {
 | 
			
		||||
    try {
 | 
			
		||||
@@ -59,7 +62,7 @@ const ResetPassword: NextPage<FooterProps> = (props) => {
 | 
			
		||||
            name='password'
 | 
			
		||||
            label='Password'
 | 
			
		||||
          />
 | 
			
		||||
          <Button data-cy='submit' className='w-full mt-6' type='submit'>
 | 
			
		||||
          <Button data-cy='submit' className='mt-6 w-full' type='submit'>
 | 
			
		||||
            {t('authentication:submit')}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </AuthenticationForm>
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ const Home: NextPage<FooterProps> = (props) => {
 | 
			
		||||
      <Head />
 | 
			
		||||
      <Header />
 | 
			
		||||
      <Main>
 | 
			
		||||
        <div className='flex flex-col items-center w-4/5'>
 | 
			
		||||
        <div className='flex w-4/5 flex-col items-center'>
 | 
			
		||||
          <div className='max-w-xs'>
 | 
			
		||||
            <Link href='/authentication/signup'>
 | 
			
		||||
              <a>
 | 
			
		||||
@@ -35,24 +35,24 @@ const Home: NextPage<FooterProps> = (props) => {
 | 
			
		||||
            </Link>
 | 
			
		||||
          </div>
 | 
			
		||||
          <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 font-headline text-3xl font-medium text-green-800 dark:text-green-400'>
 | 
			
		||||
              Thream
 | 
			
		||||
            </h1>
 | 
			
		||||
            <div
 | 
			
		||||
              className='font-paragraph text-lg max-w-lg'
 | 
			
		||||
              className='max-w-lg font-paragraph text-lg'
 | 
			
		||||
              data-cy='main-description'
 | 
			
		||||
            >
 | 
			
		||||
              <Translation
 | 
			
		||||
                i18nKey='home:description'
 | 
			
		||||
                components={[
 | 
			
		||||
                  <strong
 | 
			
		||||
                    className='text-green-800 dark:text-green-400 font-bold'
 | 
			
		||||
                    className='font-bold text-green-800 dark:text-green-400'
 | 
			
		||||
                    key='bold'
 | 
			
		||||
                  />
 | 
			
		||||
                ]}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className='flex justify-center items-center text-center mt-8 space-x-4'>
 | 
			
		||||
            <div className='mt-8 flex items-center justify-center space-x-4 text-center'>
 | 
			
		||||
              <Link href='/authentication/signup' passHref>
 | 
			
		||||
                <ButtonLink data-cy='get-started'>
 | 
			
		||||
                  {t('home:get-started')}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,6 @@ module.exports = (
 | 
			
		||||
  /** @type {import('plop').NodePlopAPI} */
 | 
			
		||||
  plop
 | 
			
		||||
) => {
 | 
			
		||||
  plop.setGenerator('component', componentGenerator(plop))
 | 
			
		||||
  plop.setGenerator('component', componentGenerator())
 | 
			
		||||
  plop.setGenerator('language', languageGenerator(plop))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,12 @@
 | 
			
		||||
@tailwind utilities;
 | 
			
		||||
 | 
			
		||||
#__next {
 | 
			
		||||
  @apply flex flex-col h-screen;
 | 
			
		||||
  @apply flex h-screen flex-col;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body,
 | 
			
		||||
#preview-storybook {
 | 
			
		||||
  @apply bg-white dark:bg-black text-black dark:text-white font-headline;
 | 
			
		||||
  @apply bg-white font-headline text-black dark:bg-black dark:text-white;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,29 @@
 | 
			
		||||
import { createContext, useState, useEffect, useMemo, useContext } from 'react'
 | 
			
		||||
import { useTheme } from 'next-themes'
 | 
			
		||||
import setLanguage from 'next-translate/setLanguage'
 | 
			
		||||
import useTranslation from 'next-translate/useTranslation'
 | 
			
		||||
 | 
			
		||||
import { Authentication, PagePropsWithAuthentication } from '.'
 | 
			
		||||
import { UserCurrent } from '../../models/User'
 | 
			
		||||
import { Language, Theme } from '../../models/UserSettings'
 | 
			
		||||
 | 
			
		||||
export interface AuthenticationValue {
 | 
			
		||||
  authentication: Authentication
 | 
			
		||||
  user: UserCurrent
 | 
			
		||||
  setUser: React.Dispatch<React.SetStateAction<UserCurrent>>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultAnthenticationContext: AuthenticationValue = {} as any
 | 
			
		||||
const defaultAuthenticationContext: AuthenticationValue = {} as any
 | 
			
		||||
const AuthenticationContext = createContext<AuthenticationValue>(
 | 
			
		||||
  defaultAnthenticationContext
 | 
			
		||||
  defaultAuthenticationContext
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export const AuthenticationProvider: React.FC<PagePropsWithAuthentication> = (
 | 
			
		||||
  props
 | 
			
		||||
) => {
 | 
			
		||||
  const { setTheme } = useTheme()
 | 
			
		||||
  const [user] = useState<UserCurrent>(props.authentication.user)
 | 
			
		||||
  const { lang } = useTranslation()
 | 
			
		||||
  const { theme, setTheme } = useTheme()
 | 
			
		||||
  const [user, setUser] = useState<UserCurrent>(props.authentication.user)
 | 
			
		||||
 | 
			
		||||
  const authentication = useMemo(() => {
 | 
			
		||||
    return new Authentication(props.authentication.tokens)
 | 
			
		||||
@@ -28,12 +32,32 @@ export const AuthenticationProvider: React.FC<PagePropsWithAuthentication> = (
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setLanguage(user.settings.language).catch(() => {})
 | 
			
		||||
    setTheme(user.settings.theme)
 | 
			
		||||
  }, [setTheme, user.settings.language, user.settings.theme])
 | 
			
		||||
    setLanguage(props.authentication.user.settings.language).catch(() => {})
 | 
			
		||||
    setTheme(props.authentication.user.settings.theme)
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this effect once
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    authentication.api
 | 
			
		||||
      .put('/users/current/settings', { theme, language: lang })
 | 
			
		||||
      .then(() => {
 | 
			
		||||
        setUser((oldUser) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...oldUser,
 | 
			
		||||
            settings: {
 | 
			
		||||
              ...oldUser.settings,
 | 
			
		||||
              theme: theme as Theme,
 | 
			
		||||
              language: lang as Language
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
      .catch(() => {})
 | 
			
		||||
  }, [theme, lang, authentication.api])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AuthenticationContext.Provider value={{ authentication, user }}>
 | 
			
		||||
    <AuthenticationContext.Provider value={{ authentication, user, setUser }}>
 | 
			
		||||
      {props.children}
 | 
			
		||||
    </AuthenticationContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								tools/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								tools/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
export interface ObjectAny {
 | 
			
		||||
  [key: string]: any
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user