22 Commits

Author SHA1 Message Date
3be7d9cc04 chore(release): 1.3.7 [skip ci] 2023-07-22 14:41:16 +00:00
5a411ade19 fix: update dependencies to latest + fix upload of files 2023-07-22 16:34:23 +02:00
41c2d95dde chore(release): 1.3.6 [skip ci] 2023-07-02 17:41:43 +00:00
2a6e3eca84 fix: update author - Théo LUDWIG 2023-07-02 19:36:23 +02:00
84662c7765 fix: update dependencies to latest 2023-07-02 19:34:20 +02:00
7e76598f7f chore(release): 1.3.5 [skip ci] 2023-05-13 18:52:40 +00:00
fd138caa4b fix: update API to v1.2.6 2023-05-13 20:51:19 +02:00
fa7b155ea4 chore(release): 1.3.4 [skip ci] 2023-05-13 18:30:07 +00:00
d210800855 fix: update dependencies to latest 2023-05-13 20:28:24 +02:00
07c574cb4a chore(release): 1.3.3 [skip ci] 2023-04-02 22:16:50 +00:00
ae953d6c1a fix: update dependencies to latest 2023-04-03 00:11:19 +02:00
c96385edd5 chore(release): 1.3.2 [skip ci] 2023-01-11 16:41:33 +00:00
cd1a477324 fix: update dependencies to latest 2023-01-11 17:39:09 +01:00
b5089f7f0b chore(release): 1.3.1 [skip ci] 2022-12-13 21:46:07 +00:00
67a1699102 chore: remove usage of styled-jsx 2022-12-13 22:31:32 +01:00
e8a9ce4e69 fix: disable lazy-loading for logo image in Header 2022-12-13 12:04:54 +01:00
8f5bc2fe21 docs: add explanations in CONTRIBUTING.md to add a translation with plop
fixes #24
2022-12-13 11:51:29 +01:00
109da1be71 fix: remove weird animation introduced in v1.3.0 2022-12-13 11:46:31 +01:00
1ea78821b0 fix: update API to v1.2.3 2022-12-13 11:41:42 +01:00
12ceefd650 chore: reduce docker image size 2022-12-13 11:38:24 +01:00
734357b396 build(deps): bump Next.js to v13 2022-12-13 11:38:07 +01:00
ccaf5234ed build(deps): update latest 2022-09-21 10:09:36 +02:00
99 changed files with 7493 additions and 28717 deletions

View File

@ -1,2 +1 @@
ARG VARIANT="16" FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:20
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}

View File

@ -3,21 +3,22 @@
"dockerComposeFile": "./docker-compose.yml", "dockerComposeFile": "./docker-compose.yml",
"service": "workspace", "service": "workspace",
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
"customizations": {
"vscode": {
"settings": { "settings": {
"remote.autoForwardPorts": false "remote.autoForwardPorts": false,
"remote.localPortHost": "allInterfaces"
}, },
"extensions": [ "extensions": [
"editorconfig.editorconfig", "editorconfig.editorconfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"divlo.vscode-styled-jsx-syntax",
"divlo.vscode-styled-jsx-languageserver",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"mikestead.dotenv", "mikestead.dotenv",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker" "ms-azuretools.vscode-docker"
], ]
"forwardPorts": [3000], }
"postAttachCommand": ["npm", "install"], },
"remoteUser": "node" "remoteUser": "node"
} }

View File

@ -6,3 +6,4 @@ services:
volumes: volumes:
- '..:/workspace:cached' - '..:/workspace:cached'
command: 'sleep infinity' command: 'sleep infinity'
network_mode: 'host'

View File

@ -1,12 +1,5 @@
.vscode .*
.git !.npmrc
.env
build build
.next
coverage coverage
node_modules node_modules
tmp
temp
.DS_Store
.lighthouseci
.vercel

View File

@ -1,3 +1,3 @@
COMPOSE_PROJECT_NAME=thream-website COMPOSE_PROJECT_NAME=thream-website
NEXT_PUBLIC_API_URL=http://localhost:8080 NEXT_PUBLIC_API_URL=http://127.0.0.1:8080
PORT=3000 PORT=3000

View File

@ -4,12 +4,14 @@
"parserOptions": { "parserOptions": {
"project": "./tsconfig.json" "project": "./tsconfig.json"
}, },
"env": {
"node": true,
"browser": true
},
"rules": { "rules": {
"prettier/prettier": "error", "prettier/prettier": "error",
"@next/next/no-img-element": "off" "@next/next/no-img-element": "off"
},
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"parser": "@typescript-eslint/parser"
} }
]
} }

View File

@ -1,6 +1,6 @@
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. --> <!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
## What changes this PR introduce? # What changes this PR introduce?
## List any relevant issue numbers ## List any relevant issue numbers

View File

@ -16,12 +16,12 @@ jobs:
language: ['javascript'] language: ['javascript']
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.3'
- name: 'Initialize CodeQL' - name: 'Initialize CodeQL'
uses: 'github/codeql-action/init@v1' uses: 'github/codeql-action/init@v2'
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: 'Perform CodeQL Analysis' - name: 'Perform CodeQL Analysis'
uses: 'github/codeql-action/analyze@v1' uses: 'github/codeql-action/analyze@v2'

View File

@ -10,16 +10,16 @@ jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.3'
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v3.0.0' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '16.x' node-version: '20.x'
cache: 'npm' cache: 'npm'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'Build' - name: 'Build'
run: 'npm run build' run: 'npm run build'

View File

@ -10,16 +10,16 @@ jobs:
lint: lint:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.3'
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v3.0.0' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '16.x' node-version: '20.x'
cache: 'npm' cache: 'npm'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'lint:commit' - name: 'lint:commit'
run: 'npm run lint:commit -- --to "${{ github.sha }}"' run: 'npm run lint:commit -- --to "${{ github.sha }}"'
@ -30,18 +30,8 @@ jobs:
- name: 'lint:markdown' - name: 'lint:markdown'
run: 'npm run lint:markdown' run: 'npm run lint:markdown'
- name: 'lint:typescript' - name: 'lint:eslint'
run: 'npm run lint:typescript' run: 'npm run lint:eslint'
- name: 'lint:prettier' - name: 'lint:prettier'
run: 'npm run lint:prettier' run: 'npm run lint:prettier'
- name: 'lint:dotenv'
uses: 'dotenv-linter/action-dotenv-linter@v2'
with:
github_token: ${{ secrets.github_token }}
- name: 'lint:docker'
uses: 'hadolint/hadolint-action@v1.6.0'
with:
dockerfile: './Dockerfile'

View File

@ -8,26 +8,26 @@ jobs:
release: release:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.3'
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: 'Import GPG key' - name: 'Import GPG key'
uses: 'crazy-max/ghaction-import-gpg@v4' uses: 'crazy-max/ghaction-import-gpg@v5.3.0'
with: with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
git_user_signingkey: true git_user_signingkey: true
git_commit_gpgsign: true git_commit_gpgsign: true
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v2.4.0' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '16.x' node-version: '20.x'
cache: 'npm' cache: 'npm'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'Release' - name: 'Release'
run: 'npm run release' run: 'npm run release'
@ -35,10 +35,3 @@ jobs:
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }} GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }} GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }}
- name: 'Deploy to Vercel'
run: 'npm run deploy -- --token="${VERCEL_TOKEN}" --prod'
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}

View File

@ -10,33 +10,33 @@ jobs:
test-unit: test-unit:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.3'
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v3.0.0' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '16.x' node-version: '20.x'
cache: 'npm' cache: 'npm'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'Unit Test' - name: 'Unit Test'
run: 'npm run test:unit' run: 'npm run test:unit'
test-lighthouse: test-e2e:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: 'actions/checkout@v3.5.3'
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v3.0.0' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '16.x' node-version: '20.x'
cache: 'npm' cache: 'npm'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'Build' - name: 'Build'
run: 'npm run build' run: 'npm run build'
@ -44,27 +44,5 @@ jobs:
- name: 'html-w3c-validator' - name: 'html-w3c-validator'
run: 'npm run test:html-w3c-validator' run: 'npm run test:html-w3c-validator'
- name: 'Lighthouse'
run: 'npm run test:lighthouse'
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
test-e2e:
runs-on: 'ubuntu-latest'
steps:
- uses: 'actions/checkout@v3.0.0'
- name: 'Use Node.js'
uses: 'actions/setup-node@v3.0.0'
with:
node-version: '16.x'
cache: 'npm'
- name: 'Install'
run: 'npm install'
- name: 'Build'
run: 'npm run build'
- name: 'End To End (e2e) Test' - name: 'End To End (e2e) Test'
run: 'npm run test:e2e' run: 'npm run test:e2e'

10
.gitignore vendored
View File

@ -18,10 +18,6 @@ cypress/screenshots
cypress/videos cypress/videos
cypress/downloads cypress/downloads
# PWA
**/workbox-*.js
**/sw.js
# envs # envs
.env .env
.env.production .env.production
@ -47,6 +43,8 @@ npm-debug.log*
# misc # misc
.DS_Store .DS_Store
.lighthouseci
.vercel
*.hbs *.hbs
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -1,9 +1,9 @@
{ {
"urls": [ "urls": [
"http://localhost:3000/", "http://127.0.0.1:3000/",
"http://localhost:3000/authentication/forgot-password", "http://127.0.0.1:3000/authentication/forgot-password",
"http://localhost:3000/authentication/reset-password", "http://127.0.0.1:3000/authentication/reset-password",
"http://localhost:3000/authentication/signin", "http://127.0.0.1:3000/authentication/signin",
"http://localhost:3000/authentication/signup" "http://127.0.0.1:3000/authentication/signup"
] ]
} }

View File

@ -1,31 +0,0 @@
{
"ci": {
"collect": {
"startServerCommand": "npm run start",
"startServerReadyPattern": "ready on",
"startServerReadyTimeout": 20000,
"url": [
"http://localhost:3000/",
"http://localhost:3000/authentication/forgot-password",
"http://localhost:3000/authentication/reset-password",
"http://localhost:3000/authentication/signin",
"http://localhost:3000/authentication/signup"
],
"numberOfRuns": 1
},
"assert": {
"preset": "lighthouse:recommended",
"assertions": {
"image-size-responsive": "warning",
"unsized-images": "warning",
"csp-xss": "warning",
"non-composited-animations": "warning",
"unused-javascript": "warning"
}
},
"upload": {
"target": "temporary-public-storage"
},
"server": {}
}
}

View File

@ -1,11 +1,11 @@
{ {
"config": { "config": {
"extends": "markdownlint/style/prettier",
"relative-links": true,
"default": true, "default": true,
"MD013": false, "MD033": false
"MD024": false,
"MD033": false,
"MD041": false
}, },
"globs": ["**/*.{md,mdx}"], "globs": ["**/*.{md,mdx}"],
"ignores": ["**/node_modules"] "ignores": ["**/node_modules"],
"customRules": ["markdownlint-rule-relative-links"]
} }

View File

@ -3,8 +3,6 @@
"editorconfig.editorconfig", "editorconfig.editorconfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"divlo.vscode-styled-jsx-syntax",
"divlo.vscode-styled-jsx-languageserver",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"mikestead.dotenv", "mikestead.dotenv",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint",

View File

@ -6,5 +6,9 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true "source.fixAll": true
} },
"eslint.options": {
"ignorePath": ".gitignore"
},
"prettier.ignorePath": ".gitignore"
} }

View File

@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at reported to the community leaders responsible for enforcement at
contact@divlo.fr. <contact@theoludwig.fr>.
All complaints will be reviewed and investigated promptly and fairly. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the All community leaders are obligated to respect the privacy and security of the

View File

@ -16,6 +16,7 @@ All work on **Thream/website** happens directly on [GitHub](https://github.com/T
- Suggest a new feature idea. - Suggest a new feature idea.
- Correct spelling errors, improvements or additions to documentation files. - Correct spelling errors, improvements or additions to documentation files.
- Improve structure/format/performance/refactor/tests of the code. - Improve structure/format/performance/refactor/tests of the code.
- [Add translations](#add-a-translation).
## Pull Requests ## Pull Requests
@ -29,31 +30,7 @@ If you're adding new features to **Thream/website**, please include tests.
## Commits ## Commits
The commit message guidelines respect The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases.
[@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional)
and [Semantic Versioning](https://semver.org/) for releases.
### Types
Types define which kind of changes you made to the project.
| Types | Description |
| -------- | ------------------------------------------------------------------------------------------------------------ |
| feat | A new feature. |
| fix | A bug fix. |
| docs | Documentation only changes. |
| style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). |
| refactor | A code change that neither fixes a bug nor adds a feature. |
| perf | A code change that improves performance. |
| test | Adding missing tests or correcting existing tests. |
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). |
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs). |
| chore | Other changes that don't modify src or test files. |
| revert | Reverts a previous commit. |
### Scopes
Scopes define what part of the code changed.
### Examples ### Examples
@ -61,3 +38,15 @@ Scopes define what part of the code changed.
git commit -m "feat(components): add Button" git commit -m "feat(components): add Button"
git commit -m "docs(readme): update installation process" git commit -m "docs(readme): update installation process"
``` ```
## Add a translation
[Reference issue](https://github.com/Thream/website/issues/24)
Feel free to contribute to **Thream** and add new languages, we would appreciate your help!
To add a new language:
- `npm clean-install`
- `npm run generate`
- Start editing JSON files with the translation in `locales/{{locale}}` (e.g: `locales/en`)

View File

@ -1,23 +1,22 @@
FROM node:16.17.0 AS dependencies FROM node:20.5.0 AS builder-dependencies
WORKDIR /usr/src/app WORKDIR /usr/src/application
COPY ./.npmrc ./
COPY ./package*.json ./ COPY ./package*.json ./
RUN npm install RUN npm clean-install
FROM node:16.17.0 AS builder FROM node:20.5.0 AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/application
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
COPY ./ ./ COPY ./ ./
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
RUN npm run build RUN npm run build
FROM node:16.17.0 AS runner FROM gcr.io/distroless/nodejs20-debian11:latest AS runner
WORKDIR /usr/src/app WORKDIR /usr/src/application
ENV NODE_ENV=production ENV NODE_ENV=production
COPY --from=builder /usr/src/app/next.config.js ./next.config.js ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder /usr/src/app/public ./public COPY --from=builder /usr/src/application/.next/standalone ./
COPY --from=builder /usr/src/app/.next ./.next COPY --from=builder /usr/src/application/.next/static ./.next/static
COPY --from=builder /usr/src/app/i18n.json ./i18n.json COPY --from=builder /usr/src/application/public ./public
COPY --from=builder /usr/src/app/locales ./locales COPY --from=builder /usr/src/application/locales ./locales
COPY --from=builder /usr/src/app/pages ./pages COPY --from=builder /usr/src/application/next.config.js ./next.config.js
COPY --from=builder /usr/src/app/node_modules ./node_modules CMD ["./server.js"]
RUN npx next telemetry disable
CMD ["node_modules/.bin/next", "start", "--port", "${PORT}"]

View File

@ -1,4 +1,4 @@
<h1 align="center"><a href="https://thream.divlo.fr/">Thream/website</a></h1> <h1 align="center"><a href="https://thream.theoludwig.fr/">Thream/website</a></h1>
<p align="center"> <p align="center">
<a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a> <a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a>
@ -18,7 +18,7 @@
Thream's website to stay close with your friends and communities. Thream's website to stay close with your friends and communities.
It uses [Thream/api](https://github.com/Thream/api) [v1.2.0](https://github.com/Thream/api/releases/tag/v1.2.0). It uses [Thream/api](https://github.com/Thream/api) [v1.2.8](https://github.com/Thream/api/releases/tag/v1.2.8).
## ⚙️ Getting Started ## ⚙️ Getting Started
@ -31,7 +31,7 @@ It uses [Thream/api](https://github.com/Thream/api) [v1.2.0](https://github.com/
```sh ```sh
# Clone the repository # Clone the repository
git clone https://github.com/Thream/website.git git clone git@github.com:Thream/website.git
# Go to the project root # Go to the project root
cd website cd website
@ -40,7 +40,7 @@ cd website
cp .env.example .env cp .env.example .env
# Install # Install
npm install npm clean-install
``` ```
You will need to configure the environment variables by creating an `.env` file at You will need to configure the environment variables by creating an `.env` file at
@ -61,7 +61,7 @@ docker compose up --build
#### Services started #### Services started
- website : `http://localhost:3000` - `website`: <http://127.0.0.1:3000>
## 💡 Contributing ## 💡 Contributing

View File

@ -185,7 +185,7 @@ export const Application: React.FC<
visible={visibleSidebars.left} visible={visibleSidebars.left}
isMobile={isMobile} isMobile={isMobile}
> >
<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'> <div className='left-0 top-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 <IconLink
href={`/application/users/settings`} href={`/application/users/settings`}
selected={path === `/application/users/settings`} selected={path === `/application/users/settings`}

View File

@ -64,7 +64,7 @@ export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
} catch (error) { } catch (error) {
return { return {
type: 'error', type: 'error',
value: 'errors:server-error' message: 'errors:server-error'
} }
} }
} }

View File

@ -22,19 +22,16 @@ const ChannelMemo: React.FC<ChannelProps> = (props) => {
const { member } = useGuildMember() const { member } = useGuildMember()
return ( return (
<Link href={`/application/${path.guildId}/${channel.id}`}> <Link
<a href={`/application/${path.guildId}/${channel.id}`}
className={classNames( className={classNames(
'group relative my-3 mx-3 flex items-center justify-between overflow-hidden rounded-lg py-2 text-sm transition-all duration-200 hover:bg-gray-100 dark:hover:bg-gray-600', 'group relative mx-3 my-3 flex items-center justify-between overflow-hidden rounded-lg py-2 text-sm transition-all duration-200 hover:bg-gray-100 dark:hover:bg-gray-600',
{ {
'font-semibold text-green-800 dark:text-green-400': selected 'font-semibold text-green-800 dark:text-green-400': selected
} }
)} )}
> >
<span <span className='max-[315px] ml-2 mr-4 break-all' data-cy='channel-name'>
className='max-[315px] ml-2 mr-4 break-all'
data-cy='channel-name'
>
# {channel.name} # {channel.name}
</span> </span>
{member.isOwner && ( {member.isOwner && (
@ -50,7 +47,6 @@ const ChannelMemo: React.FC<ChannelProps> = (props) => {
<CogIcon height={20} width={20} /> <CogIcon height={20} width={20} />
</IconButton> </IconButton>
)} )}
</a>
</Link> </Link>
) )
} }

View File

@ -27,7 +27,7 @@ export const ConfirmPopup: React.FC<ConfirmPopupProps> = ({ ...props }) => {
<div className={props.className}> <div className={props.className}>
<Loader <Loader
className={classNames( className={classNames(
'absolute top-1/2 left-1/2 scale-0 transition-all', 'absolute left-1/2 top-1/2 scale-0 transition-all',
{ {
'scale-100': isLoading 'scale-100': isLoading
} }

View File

@ -38,7 +38,7 @@ export const CreateChannel: React.FC = () => {
} catch (error) { } catch (error) {
return { return {
type: 'error', type: 'error',
value: 'errors:server-error' message: 'errors:server-error'
} }
} }
} }

View File

@ -23,25 +23,29 @@ export const CreateGuild: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const { handleUseForm, fetchState, message, errors } = useForm(schema as any) const { handleUseForm, fetchState, message, errors } = useForm(schema)
const { getFirstErrorTranslation } = useFormTranslation() const { getFirstErrorTranslation } = useFormTranslation()
const { authentication } = useAuthentication() const { authentication } = useAuthentication()
const onSubmit: HandleUseFormCallback<any> = async (formData) => { const onSubmit: HandleUseFormCallback<typeof schema> = async (formData) => {
try { try {
const { data } = await authentication.api.post< const { data } = await authentication.api.post<
any, any,
AxiosResponse<{ guild: GuildComplete }> AxiosResponse<{ guild: GuildComplete }>
>('/guilds', { name: formData.name, description: formData.description }) >('/guilds', { name: formData.name, description: formData.description })
const guildId = data.guild.id const guildId = data.guild.id
const channelId = data.guild.channels[0].id const channel = data.guild.channels[0]
if (channel == null) {
throw new Error('No channel found')
}
const channelId = channel.id
await router.push(`/application/${guildId}/${channelId}`) await router.push(`/application/${guildId}/${channelId}`)
return null return null
} catch (error) { } catch (error) {
return { return {
type: 'error', type: 'error',
value: 'errors:server-error' message: 'errors:server-error'
} }
} }
} }

View File

@ -28,20 +28,24 @@ export const GuildLeftSidebar: React.FC<GuildLeftSidebarProps> = (props) => {
<Divider /> <Divider />
<div className='mb-1 flex items-center justify-center space-x-6 p-2'> <div className='mb-1 flex items-center justify-center space-x-6 p-2'>
{member.isOwner && ( {member.isOwner && (
<Link href={`/application/${path.guildId}/channels/create`} passHref> <Link
<a data-cy='link-add-channel'> href={`/application/${path.guildId}/channels/create`}
passHref
data-cy='link-add-channel'
>
<IconButton className='h-10 w-10' title='Add a Channel'> <IconButton className='h-10 w-10' title='Add a Channel'>
<PlusIcon /> <PlusIcon />
</IconButton> </IconButton>
</a>
</Link> </Link>
)} )}
<Link href={`/application/${path.guildId}/settings`} passHref> <Link
<a data-cy='link-settings-guild'> href={`/application/${path.guildId}/settings`}
passHref
data-cy='link-settings-guild'
>
<IconButton className='h-7 w-7' title='Settings'> <IconButton className='h-7 w-7' title='Settings'>
<CogIcon /> <CogIcon />
</IconButton> </IconButton>
</a>
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -47,21 +47,21 @@ export const GuildSettings: React.FC = () => {
errors, errors,
setFetchState, setFetchState,
setMessage setMessage
} = useForm(schema as any) } = useForm(schema)
const { getFirstErrorTranslation } = useFormTranslation() const { getFirstErrorTranslation } = useFormTranslation()
const onSubmit: HandleUseFormCallback<any> = async (formData) => { const onSubmit: HandleUseFormCallback<typeof schema> = async (formData) => {
try { try {
await authentication.api.put(`/guilds/${guild.id}`, formData) await authentication.api.put(`/guilds/${guild.id}`, formData)
setInputValues(formData as unknown as any) setInputValues(formData as unknown as any)
return { return {
type: 'success', type: 'success',
value: 'application:saved-information' message: 'application:saved-information'
} }
} catch (error) { } catch (error) {
return { return {
type: 'error', type: 'error',
value: 'errors:server-error' message: 'errors:server-error'
} }
} }
} }
@ -82,12 +82,16 @@ export const GuildSettings: React.FC = () => {
) => { ) => {
setFetchState('loading') setFetchState('loading')
const files = event?.target?.files const files = event?.target?.files
if (files != null && files.length === 1) { if (files != null && files.length === 1 && files[0] != null) {
const file = files[0] const file = files[0]
const formData = new FormData() const formData = new FormData()
formData.append('icon', file) formData.append('icon', file)
try { try {
await authentication.api.put(`/guilds/${guild.id}/icon`, formData) await authentication.api.put(`/guilds/${guild.id}/icon`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
setFetchState('idle') setFetchState('idle')
} catch (error) { } catch (error) {
setFetchState('error') setFetchState('error')
@ -206,7 +210,7 @@ export const GuildSettings: React.FC = () => {
message={ message={
message != null message != null
? t(message) ? t(message)
: getFirstErrorTranslation(errors.email) : getFirstErrorTranslation(errors.name)
} }
/> />
</div> </div>

View File

@ -96,7 +96,7 @@ export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
<ConfirmPopup <ConfirmPopup
title={`${t('application:join-the-guild')} ?`} title={`${t('application:join-the-guild')} ?`}
className={classNames( className={classNames(
'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', 'w-ful h-ful translate-x- absolute left-full top-1/2 flex h-full w-full -translate-y-1/2 flex-col items-center justify-center rounded-2xl transition-all',
{ {
'!left-0': isConfirmed '!left-0': isConfirmed
} }

View File

@ -49,7 +49,7 @@ export const JoinGuildsPublic: React.FC = () => {
<input <input
data-cy='search-guild-input' data-cy='search-guild-input'
onChange={handleChange} onChange={handleChange}
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' className='mx-auto my-6 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' type='search'
name='search-guild' name='search-guild'
placeholder={`🔎 ${t('application:search')}...`} placeholder={`🔎 ${t('application:search')}...`}

View File

@ -14,8 +14,7 @@ const MemberMemo: React.FC<MemberProps> = (props) => {
return ( return (
<Link href={`/application/users/${member.user.id}`}> <Link href={`/application/users/${member.user.id}`}>
<a> <div className='flex cursor-pointer items-center overflow-hidden px-6 py-2 pr-10 hover:bg-gray-300 dark:hover:bg-gray-900'>
<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'> <div className='flex min-w-[50px] rounded-full'>
<Image <Image
src={ src={
@ -42,7 +41,6 @@ const MemberMemo: React.FC<MemberProps> = (props) => {
{member.user.status != null && member.user.status} {member.user.status != null && member.user.status}
</div> </div>
</div> </div>
</a>
</Link> </Link>
) )
} }

View File

@ -2,8 +2,6 @@ import { useState, useRef } from 'react'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import date from 'date-and-time' import date from 'date-and-time'
import type { MotionProps } from 'framer-motion'
import { motion } from 'framer-motion'
import type { MessageWithMember } from '../../../../models/Message' import type { MessageWithMember } from '../../../../models/Message'
import { MessageText } from './MessageText' import { MessageText } from './MessageText'
@ -13,7 +11,7 @@ import { useAuthentication } from '../../../../tools/authentication'
import { MessageOptions } from './MessageOptions' import { MessageOptions } from './MessageOptions'
import { EditMessage } from './EditMessage' import { EditMessage } from './EditMessage'
export interface MessageProps extends MotionProps { export interface MessageProps {
message: MessageWithMember message: MessageWithMember
} }
@ -58,22 +56,11 @@ export const Message: React.FC<MessageProps> = (props) => {
} }
return ( return (
<motion.div <div
layout
initial='initial'
animate='animate'
exit='exit'
data-cy={`message-${message.id}`} data-cy={`message-${message.id}`}
className='group flex w-full p-4 transition hover:bg-gray-200 dark:hover:bg-gray-900' className='group flex w-full p-4 transition hover:bg-gray-200 dark:hover:bg-gray-900'
transition={{ type: 'spring', stiffness: 500, damping: 60 }}
variants={{
initial: { x: -100, opacity: 0 },
animate: { x: 0, opacity: 1 },
exit: { opacity: 0 }
}}
> >
<Link href={`/application/users/${message.member.user.id}`}> <Link href={`/application/users/${message.member.user.id}`}>
<a>
<div className='mr-4 flex h-12 w-12 flex-shrink-0 items-center justify-center'> <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'> <div className='h-10 w-10 drop-shadow-md'>
<Image <Image
@ -91,19 +78,16 @@ export const Message: React.FC<MessageProps> = (props) => {
/> />
</div> </div>
</div> </div>
</a>
</Link> </Link>
<div className='relative w-full whitespace-pre-wrap break-words break-all'> <div className='relative w-full whitespace-pre-wrap break-words break-all'>
<div className='flex w-max items-center'> <div className='flex w-max items-center'>
<Link href={`/application/users/${message.member.user.id}`}> <Link href={`/application/users/${message.member.user.id}`}>
<a>
<span <span
data-cy='message-member-user-name' data-cy='message-member-user-name'
className='font-bold text-gray-900 dark:text-gray-200' className='font-bold text-gray-900 dark:text-gray-200'
> >
{message.member.user.name} {message.member.user.name}
</span> </span>
</a>
</Link> </Link>
<span <span
data-cy='message-date' data-cy='message-date'
@ -140,6 +124,6 @@ export const Message: React.FC<MessageProps> = (props) => {
<Loader /> <Loader />
)} )}
</div> </div>
</motion.div> </div>
) )
} }

View File

@ -22,7 +22,7 @@ export const MessageOptions: React.FC<
} }
return ( return (
<div className='absolute right-6 -top-8 flex opacity-0 transition-opacity group-hover:opacity-100'> <div className='absolute -top-8 right-6 flex opacity-0 transition-opacity group-hover:opacity-100'>
{message.type === 'text' && ( {message.type === 'text' && (
<div <div
className='message-options rounded-l-lg border-l-slate-600' className='message-options rounded-l-lg border-l-slate-600'

View File

@ -50,7 +50,8 @@ export const MessageText: React.FC<MessageContentProps> = (props) => {
) )
}, },
emoji: (props) => { emoji: (props) => {
return <Emoji value={props.value} size={20} tooltip /> const { value } = props
return <Emoji value={value} size={20} tooltip />
}, },
code: (properties) => { code: (properties) => {
const { inline, className, children, ...props } = properties const { inline, className, children, ...props } = properties

View File

@ -1,6 +1,5 @@
import useTranslation from 'next-translate/useTranslation' import useTranslation from 'next-translate/useTranslation'
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from 'react-infinite-scroll-component'
import { AnimatePresence } from 'framer-motion'
import { Loader } from '../../design/Loader' import { Loader } from '../../design/Loader'
import { Message } from './Message' import { Message } from './Message'
@ -30,7 +29,6 @@ export const Messages: React.FC = () => {
id='messages' id='messages'
className='scrollbar-firefox-support flex w-full flex-1 flex-col-reverse overflow-y-auto transition-all' className='scrollbar-firefox-support flex w-full flex-1 flex-col-reverse overflow-y-auto transition-all'
> >
<AnimatePresence>
<InfiniteScroll <InfiniteScroll
scrollableTarget='messages' scrollableTarget='messages'
className='messages-list !overflow-x-hidden' className='messages-list !overflow-x-hidden'
@ -44,7 +42,6 @@ export const Messages: React.FC = () => {
return <Message key={message.id} message={message} /> return <Message key={message.id} message={message} />
})} })}
</InfiniteScroll> </InfiniteScroll>
</AnimatePresence>
</div> </div>
) )
} }

View File

@ -23,11 +23,12 @@ export const PopupGuildCard: React.FC<PopupGuildCardProps> = (props) => {
<p className='mt-6 px-8 text-center text-sm text-gray-200'> <p className='mt-6 px-8 text-center text-sm text-gray-200'>
{description} {description}
</p> </p>
<Link href={link.href}> <Link
<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'> href={link.href}
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.icon}
{link.text} {link.text}
</a>
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -54,13 +54,18 @@ export const SendMessage: React.FC<SendMessageProps> = (props) => {
event event
) => { ) => {
const files = event?.target?.files const files = event?.target?.files
if (files != null && files.length === 1) { if (files != null && files.length === 1 && files[0] != null) {
const file = files[0] const file = files[0]
const formData = new FormData() const formData = new FormData()
formData.append('file', file) formData.append('file', file)
await authentication.api.post( await authentication.api.post(
`/channels/${path.channelId}/messages/uploads`, `/channels/${path.channelId}/messages/uploads`,
formData formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
) )
} }
} }

View File

@ -21,13 +21,13 @@ export const Sidebar: React.FC<React.PropsWithChildren<SidebarProps>> = (
className={classNames( className={classNames(
'h-full-without-header visible z-50 flex bg-gray-200 drop-shadow-2xl transition-all dark:bg-gray-800', 'h-full-without-header visible z-50 flex bg-gray-200 drop-shadow-2xl transition-all dark:bg-gray-800',
{ {
'scrollbar-firefox-support top-0 right-0 flex-col space-y-1 overflow-y-auto': 'scrollbar-firefox-support right-0 top-0 flex-col space-y-1 overflow-y-auto':
direction === 'right', direction === 'right',
'w-72': direction === 'right' && visible, 'w-72': direction === 'right' && visible,
'invisible w-0 opacity-0': !visible, 'invisible w-0 opacity-0': !visible,
'w-80': direction === 'left' && visible, 'w-80': direction === 'left' && visible,
'max-w-max': typeof path !== 'string' && direction === 'left', 'max-w-max': typeof path !== 'string' && direction === 'left',
'top-0 right-0': direction === 'right' && isMobile, 'right-0 top-0': direction === 'right' && isMobile,
absolute: isMobile absolute: isMobile
} }
)} )}

View File

@ -16,7 +16,6 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<>
<div className='relative flex h-full flex-col items-center justify-center'> <div className='relative flex h-full flex-col items-center justify-center'>
<div className='transition'> <div className='transition'>
<div className='max-w-[1000px] px-12'> <div className='max-w-[1000px] px-12'>
@ -59,7 +58,7 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
<a <a
href={`mailto:${user.email}`} href={`mailto:${user.email}`}
target='_blank' target='_blank'
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' className='relative ml-2 font-normal tracking-wide no-underline opacity-80 transition-all after:absolute after:bottom-[-1px] after:left-0 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' rel='noreferrer'
data-cy='user-email' data-cy='user-email'
> >
@ -74,7 +73,7 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
target='_blank' target='_blank'
rel='noreferrer' rel='noreferrer'
href={user.website} href={user.website}
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' className='relative ml-2 font-normal tracking-wide no-underline opacity-80 transition-all after:absolute after:bottom-[-2px] after:left-0 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} {user.website}
</a> </a>
@ -100,12 +99,5 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
</div> </div>
</div> </div>
</div> </div>
<style jsx global>{`
#application-page-content {
overflow-x: hidden;
}
`}</style>
</>
) )
} }

View File

@ -75,7 +75,7 @@ export const UserSettings: React.FC = () => {
if (hasEmailChanged) { if (hasEmailChanged) {
return { return {
type: 'success', type: 'success',
value: 'application:success-email-changed' message: 'application:success-email-changed'
} }
} }
const { data: userCurrentSettings } = await authentication.api.put( const { data: userCurrentSettings } = await authentication.api.put(
@ -94,7 +94,7 @@ export const UserSettings: React.FC = () => {
}) })
return { return {
type: 'success', type: 'success',
value: 'application:saved-information' message: 'application:saved-information'
} }
} catch (error) { } catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 400) { if (axios.isAxiosError(error) && error.response?.status === 400) {
@ -102,22 +102,22 @@ export const UserSettings: React.FC = () => {
if (message.endsWith('already taken.')) { if (message.endsWith('already taken.')) {
return { return {
type: 'error', type: 'error',
value: 'authentication:already-used' message: 'authentication:already-used'
} }
} else if (message.endsWith('email to sign in.')) { } else if (message.endsWith('email to sign in.')) {
return { return {
type: 'error', type: 'error',
value: 'authentication:email-required-to-sign-in' message: 'authentication:email-required-to-sign-in'
} }
} }
return { return {
type: 'error', type: 'error',
value: 'errors:server-error' message: 'errors:server-error'
} }
} }
return { return {
type: 'error', type: 'error',
value: 'errors:server-error' message: 'errors:server-error'
} }
} }
} }
@ -149,14 +149,19 @@ export const UserSettings: React.FC = () => {
) => { ) => {
setFetchState('loading') setFetchState('loading')
const files = event?.target?.files const files = event?.target?.files
if (files != null && files.length === 1) { if (files != null && files.length === 1 && files[0] != null) {
const file = files[0] const file = files[0]
const formData = new FormData() const formData = new FormData()
formData.append('logo', file) formData.append('logo', file)
try { try {
const { data } = await authentication.api.put( const { data } = await authentication.api.put(
`/users/current/logo`, `/users/current/logo`,
formData formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
) )
setUser((oldUser) => { setUser((oldUser) => {
return { return {
@ -312,8 +317,8 @@ export const UserSettings: React.FC = () => {
<Language className='!top-12' /> <Language className='!top-12' />
<div className='ml-auto flex'> <div className='ml-auto flex'>
<SwitchTheme /> <SwitchTheme />
<Link href={`/application/users/${user.id}`}> <Link
<a href={`/application/users/${user.id}`}
className='group ml-3 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-slate-200 transition-colors hover:bg-slate-300 dark:bg-slate-700 hover:dark:bg-slate-800' className='group ml-3 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-slate-200 transition-colors hover:bg-slate-300 dark:bg-slate-700 hover:dark:bg-slate-800'
title='Preview Public Profile' title='Preview Public Profile'
> >
@ -321,7 +326,6 @@ export const UserSettings: React.FC = () => {
height={20} height={20}
className='opacity-50 transition-opacity group-hover:opacity-100' className='opacity-50 transition-opacity group-hover:opacity-100'
/> />
</a>
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -54,7 +54,7 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
formElement.reset() formElement.reset()
return { return {
type: 'success', type: 'success',
value: 'authentication:success-signup' message: 'authentication:success-signup'
} }
} catch (error) { } catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 400) { if (axios.isAxiosError(error) && error.response?.status === 400) {
@ -62,17 +62,17 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
if (message.endsWith('already taken.')) { if (message.endsWith('already taken.')) {
return { return {
type: 'error', type: 'error',
value: 'authentication:already-used' message: 'authentication:already-used'
} }
} }
return { return {
type: 'error', type: 'error',
value: 'errors:server-error' message: 'errors:server-error'
} }
} }
return { return {
type: 'error', type: 'error',
value: 'errors:server-error' message: 'errors:server-error'
} }
} }
} else { } else {
@ -86,12 +86,12 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
if (axios.isAxiosError(error) && error.response?.status === 400) { if (axios.isAxiosError(error) && error.response?.status === 400) {
return { return {
type: 'error', type: 'error',
value: 'authentication:wrong-credentials' message: 'authentication:wrong-credentials'
} }
} }
return { return {
type: 'error', type: 'error',
value: 'errors:server-error' message: 'errors:server-error'
} }
} }
} }
@ -139,11 +139,9 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
: '/authentication/signup' : '/authentication/signup'
} }
> >
<a>
{mode === 'signup' {mode === 'signup'
? t('authentication:already-have-an-account') ? t('authentication:already-have-an-account')
: t('authentication:dont-have-an-account')} : t('authentication:dont-have-an-account')}
</a>
</Link> </Link>
</p> </p>
</AuthenticationForm> </AuthenticationForm>

View File

@ -1,17 +1,24 @@
import useTranslation from 'next-translate/useTranslation' import useTranslation from 'next-translate/useTranslation'
import Link from 'next/link' import Link from 'next/link'
export interface ErrorPageProps { import type { FooterProps } from './Footer'
import { Footer } from './Footer'
import { Header } from './Header'
export interface ErrorPageProps extends FooterProps {
statusCode: number statusCode: number
message: string message: string
} }
export const ErrorPage: React.FC<ErrorPageProps> = (props) => { export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
const { message, statusCode } = props const { message, statusCode, version } = props
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<> <>
<div className='flex h-screen flex-col pt-0'>
<Header />
<main className='flex min-w-full flex-1 flex-col items-center justify-center'>
<h1 className='my-6 text-4xl font-semibold'> <h1 className='my-6 text-4xl font-semibold'>
{t('errors:error')}{' '} {t('errors:error')}{' '}
<span <span
@ -23,31 +30,16 @@ export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
</h1> </h1>
<p className='text-center text-lg'> <p className='text-center text-lg'>
{message}{' '} {message}{' '}
<Link href='/'> <Link
<a className='text-green-800 hover:underline dark:text-green-400'> href='/'
className='text-green-800 hover:underline dark:text-green-400'
>
{t('errors:return-to-home-page')} {t('errors:return-to-home-page')}
</a>
</Link> </Link>
</p> </p>
</main>
<style jsx global> <Footer version={version} />
{` </div>
main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-width: 100vw;
flex: 1;
}
#__next {
display: flex;
flex-direction: column;
padding-top: 0;
height: 100vh;
}
`}
</style>
</> </>
) )
} }

View File

@ -15,10 +15,11 @@ export const Footer: React.FC<FooterProps> = (props) => {
return ( return (
<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'> <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> <p>
<Link href='/'> <Link
<a className='text-green-800 hover:underline dark:text-green-400'> href='/'
className='text-green-800 hover:underline dark:text-green-400'
>
Thream Thream
</a>
</Link>{' '} </Link>{' '}
| {t('common:all-rights-reserved')} | {t('common:all-rights-reserved')}
</p> </p>

View File

@ -13,9 +13,9 @@ export const Head: React.FC<HeadProps> = (props) => {
const { const {
title = 'Thream', title = 'Thream',
image = 'https://thream.divlo.fr/images/icons/128x128.png', image = 'https://thream.theoludwig.fr/images/icon-128x128.png',
description = t('common:description'), description = t('common:description'),
url = 'https://thream.divlo.fr/' url = 'https://thream.theoludwig.fr/'
} = props } = props
return ( return (
@ -26,7 +26,7 @@ export const Head: React.FC<HeadProps> = (props) => {
{/* Meta Tag */} {/* Meta Tag */}
<meta name='viewport' content='width=device-width, initial-scale=1.0' /> <meta name='viewport' content='width=device-width, initial-scale=1.0' />
<meta name='description' content={description} /> <meta name='description' content={description} />
<meta name='Language' content='fr, en' /> <meta name='Language' content='fr-FR, en-US' />
<meta name='theme-color' content='#27B05E' /> <meta name='theme-color' content='#27B05E' />
{/* Open Graph Metadata */} {/* Open Graph Metadata */}
@ -35,7 +35,7 @@ export const Head: React.FC<HeadProps> = (props) => {
<meta property='og:url' content={url} /> <meta property='og:url' content={url} />
<meta property='og:image' content={image} /> <meta property='og:image' content={image} />
<meta property='og:description' content={description} /> <meta property='og:description' content={description} />
<meta property='og:locale' content='fr_FR, en_US' /> <meta property='og:locale' content='fr-FR, en-US' />
<meta property='og:site_name' content={title} /> <meta property='og:site_name' content={title} />
{/* Twitter card Metadata */} {/* Twitter card Metadata */}
@ -43,12 +43,6 @@ export const Head: React.FC<HeadProps> = (props) => {
<meta name='twitter:description' content={description} /> <meta name='twitter:description' content={description} />
<meta name='twitter:title' content={title} /> <meta name='twitter:title' content={title} />
<meta name='twitter:image' content={image} /> <meta name='twitter:image' content={image} />
{/* PWA Data */}
<link rel='manifest' href='/manifest.json' />
<meta name='apple-mobile-web-app-capable' content='yes' />
<meta name='mobile-web-app-capable' content='yes' />
<link rel='apple-touch-icon' href={image} />
</NextHead> </NextHead>
) )
} }

View File

@ -9,20 +9,19 @@ export const Header: React.FC = () => {
<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'> <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'> <div className='container flex justify-between'>
<Link href='/'> <Link href='/'>
<a>
<div className='flex items-center justify-center'> <div className='flex items-center justify-center'>
<Image <Image
priority
quality={100} quality={100}
width={60} width={60}
height={60} height={60}
src='/images/icons/Thream.png' src='/images/Thream.png'
alt='Thream' alt='Thream'
/> />
<span className='ml-1 hidden font-headline font-medium text-green-800 dark:text-green-400 xs:block'> <span className='ml-1 hidden font-headline font-medium text-green-800 dark:text-green-400 xs:block'>
Thream Thream
</span> </span>
</div> </div>
</a>
</Link> </Link>
<div className='flex justify-between'> <div className='flex justify-between'>
<Language /> <Language />

View File

@ -0,0 +1,78 @@
import { useEffect, useState } from 'react'
import classNames from 'clsx'
import { useTheme } from 'next-themes'
export const SwitchTheme: React.FC = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
const handleClick = (): void => {
setTheme(theme === 'dark' ? 'light' : 'dark')
}
return (
<div
className='flex items-center'
data-cy='switch-theme-click'
onClick={handleClick}
>
<div className='relative inline-block cursor-pointer touch-pan-x select-none border-0 bg-transparent p-0'>
<div className='h-[24px] w-[50px] rounded-[30px] bg-[#4d4d4d] p-0 text-white transition-all duration-200 ease-in-out'>
<div
data-cy='switch-theme-dark'
className={classNames(
'absolute bottom-0 left-[8px] top-0 mb-auto mt-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out',
{
'opacity-100': theme === 'dark',
'opacity-0': theme === 'light'
}
)}
>
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
🌜
</span>
</div>
<div
data-cy='switch-theme-light'
className={classNames(
'absolute bottom-0 right-[10px] top-0 mb-auto mt-auto h-[10px] w-[10px] leading-[0]',
{
'opacity-100': theme === 'light',
'opacity-0': theme === 'dark'
}
)}
>
<span className='relative flex h-[10px] w-[10px] items-center justify-center'>
🌞
</span>
</div>
</div>
<div
className={classNames(
'absolute top-[1px] box-border h-[22px] w-[22px] rounded-[50%] bg-[#fafafa] text-white transition-all duration-[250ms] ease-in-out',
{
'left-[27px]': theme === 'dark',
'left-0': theme === 'light'
}
)}
style={{ border: '1px solid #4d4d4d' }}
/>
<input
data-cy='switch-theme-input'
type='checkbox'
aria-label='Dark mode toggle'
className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0'
defaultChecked
/>
</div>
</div>
)
}

View File

@ -1,126 +0,0 @@
import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
export const SwitchTheme: React.FC = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
const handleClick = (): void => {
setTheme(theme === 'dark' ? 'light' : 'dark')
}
return (
<>
<div
className='flex items-center'
data-cy='switch-theme-click'
onClick={handleClick}
>
<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 relative flex items-center justify-center'>
🌜
</span>
</div>
<div
data-cy='switch-theme-light'
className='toggle-track-x absolute'
>
<span className='toggle_Light relative flex items-center justify-center'>
🌞
</span>
</div>
</div>
<div className='toggle-thumb absolute' />
<input
data-cy='switch-theme-input'
type='checkbox'
aria-label='Dark mode toggle'
className='toggle-screenreader-only absolute overflow-hidden'
defaultChecked
/>
</div>
</div>
<style jsx>
{`
.toggle-theme-button {
touch-action: pan-x;
border: 0;
padding: 0;
user-select: none;
}
.toggle-track {
width: 50px;
height: 24px;
padding: 0;
border-radius: 30px;
background-color: #4d4d4d;
transition: all 0.2s ease;
color: #fff;
}
.toggle-track-check {
width: 14px;
height: 10px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
left: 8px;
opacity: ${theme === 'dark' ? 1 : 0};
transition: opacity 0.25s ease;
}
.toggle-track-x {
width: 10px;
height: 10px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
right: 10px;
opacity: ${theme === 'dark' ? 0 : 1};
}
.toggle_Dark,
.toggle_Light {
height: 10px;
width: 10px;
}
.toggle-thumb {
left: ${theme === 'dark' ? '27px' : '0px'};
width: 22px;
height: 22px;
border: 1px solid #4d4d4d;
border-radius: 50%;
background-color: #fafafa;
box-sizing: border-box;
transition: all 0.25s ease;
top: 1px;
color: #fff;
}
.toggle-screenreader-only {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
padding: 0;
width: 1px;
}
`}
</style>
</>
)
}

View File

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

View File

@ -1,20 +0,0 @@
export const ScrollableBody: React.FC<React.PropsWithChildren<{}>> = (
props
) => {
const { children } = props
return (
<>
{children}
<style jsx global>{`
body {
scrollbar-width: thin;
scrollbar-color: var(--scroll-bar-color) var(--scroll-bar-bg-color);
z-index: 1000;
height: calc(100vh - 64px);
overflow-y: auto;
}
`}</style>
</>
)
}

View File

@ -13,7 +13,7 @@ export const Checkbox: React.FC<CheckboxProps> = (props) => {
{...props} {...props}
type='checkbox' type='checkbox'
id={id} id={id}
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' 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:left-[59%] before:top-[50%] 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:left-[36%] after:top-[62.5%] 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 <label
className='duration-400 cursor-pointer select-none opacity-80 transition hover:opacity-100 ' className='duration-400 cursor-pointer select-none opacity-80 transition hover:opacity-100 '

View File

@ -14,8 +14,11 @@ export const IconLink: React.FC<React.PropsWithChildren<IconLinkProps>> = (
const { children, selected, href, title, className } = props const { children, selected, href, title, className } = props
return ( return (
<Link href={href}> <Link
<a className='group relative flex w-full justify-center' title={title}> href={href}
className='group relative flex w-full justify-center'
title={title}
>
<div <div
className={classNames('group flex w-full justify-center', className)} className={classNames('group flex w-full justify-center', className)}
> >
@ -31,7 +34,6 @@ export const IconLink: React.FC<React.PropsWithChildren<IconLinkProps>> = (
></span> ></span>
</div> </div>
</div> </div>
</a>
</Link> </Link>
) )
} }

View File

@ -35,22 +35,18 @@ export const Input: React.FC<InputProps> = (props) => {
} }
return ( return (
<>
<div className='flex flex-col'> <div className='flex flex-col'>
<div <div className={classNames('mb-2 mt-6 flex justify-between', className)}>
className={classNames('mt-6 mb-2 flex justify-between', className)}
>
<label className='pl-1' htmlFor={name}> <label className='pl-1' htmlFor={name}>
{label} {label}
</label> </label>
{type === 'password' && showForgotPassword ? ( {type === 'password' && showForgotPassword ? (
<Link href='/authentication/forgot-password'> <Link
<a href='/authentication/forgot-password'
className='text-center font-headline text-xs text-green-800 hover:underline dark:text-green-400 sm:text-sm' className='text-center font-headline text-xs text-green-800 hover:underline dark:text-green-400 sm:text-sm'
data-cy='forgot-password-link' data-cy='forgot-password-link'
> >
{t('authentication:forgot-password')} {t('authentication:forgot-password')}
</a>
</Link> </Link>
) : null} ) : null}
</div> </div>
@ -67,7 +63,10 @@ export const Input: React.FC<InputProps> = (props) => {
<div <div
data-cy='password-eye' data-cy='password-eye'
onClick={handlePassword} onClick={handlePassword}
className='password-eye absolute cursor-pointer bg-[#f1f1f1] bg-cover' style={{
backgroundImage: `url('/images/svg/icons/input/${inputType}.svg')`
}}
className='absolute right-4 top-3 z-10 h-5 w-5 cursor-pointer bg-[#f1f1f1] bg-cover'
/> />
)} )}
<FormState <FormState
@ -77,19 +76,5 @@ export const Input: React.FC<InputProps> = (props) => {
/> />
</div> </div>
</div> </div>
<style jsx>
{`
.password-eye {
top: 12px;
right: 16px;
z-index: 1;
width: 20px;
height: 20px;
background-image: url('/images/svg/icons/input/${inputType}.svg');
}
`}
</style>
</>
) )
} }

View File

@ -0,0 +1,39 @@
@keyframes progressSpinnerRotate {
100% {
transform: rotate(360deg);
}
}
@keyframes progressSpinnerDash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35px;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -124px;
}
}
.progressSpinnerSvg {
animation: progressSpinnerRotate 2s linear infinite;
height: 100%;
transform-origin: center center;
width: 100%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
.progressSpinnerCircle {
stroke-dasharray: 89, 200;
stroke-dashoffset: 0;
stroke: #27b05e;
animation: progressSpinnerDash 1.5s ease-in-out infinite;
stroke-linecap: round;
}

View File

@ -1,3 +1,5 @@
import styles from './Loader.module.css'
export interface LoaderProps { export interface LoaderProps {
width?: number width?: number
height?: number height?: number
@ -5,14 +7,18 @@ export interface LoaderProps {
} }
export const Loader: React.FC<LoaderProps> = (props) => { export const Loader: React.FC<LoaderProps> = (props) => {
const { width = 50, height = 50 } = props const { width = 50, height = 50, className } = props
return ( return (
<div className={props.className}> <div className={className}>
<div data-cy='progress-spinner' className='progress-spinner'> <div
<svg className='progress-spinner-svg' viewBox='25 25 50 50'> data-cy='progress-spinner'
className='relative mx-auto my-0 before:block before:pt-[100%] before:content-none'
style={{ width: `${width}px`, height: `${height}px` }}
>
<svg className={styles['progressSpinnerSvg']} viewBox='25 25 50 50'>
<circle <circle
className='progress-spinner-circle' className={styles['progressSpinnerCircle']}
cx='50' cx='50'
cy='50' cy='50'
r='20' r='20'
@ -22,60 +28,6 @@ export const Loader: React.FC<LoaderProps> = (props) => {
/> />
</svg> </svg>
</div> </div>
<style jsx>
{`
.progress-spinner {
position: relative;
margin: 0 auto;
width: ${width}px;
height: ${height}px;
}
.progress-spinner::before {
content: '';
display: block;
padding-top: 100%;
}
.progress-spinner-svg {
animation: progress-spinner-rotate 2s linear infinite;
height: 100%;
transform-origin: center center;
width: 100%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
.progress-spinner-circle {
stroke-dasharray: 89, 200;
stroke-dashoffset: 0;
stroke: #27b05e;
animation: progress-spinner-dash 1.5s ease-in-out infinite;
stroke-linecap: round;
}
@keyframes progress-spinner-rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes progress-spinner-dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35px;
}
100% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -124px;
}
}
`}
</style>
</div> </div>
) )
} }

View File

@ -0,0 +1,11 @@
.buttonGoogle {
color: #000;
border: 1px solid #000;
}
.buttonMedia {
color: #fff;
border: none;
}
.button:focus {
box-shadow: 0 0 0 2px #27b05e;
}

View File

@ -3,6 +3,7 @@ import Image from 'next/image'
import classNames from 'clsx' import classNames from 'clsx'
import type { ProviderOAuth } from '../../../models/OAuth' import type { ProviderOAuth } from '../../../models/OAuth'
import styles from './SocialMediaButton.module.css'
export type SocialMedia = ProviderOAuth export type SocialMedia = ProviderOAuth
@ -52,27 +53,21 @@ export const SocialMediaButton: React.FC<SocialMediaButtonProps> = (props) => {
}, [socialMedia]) }, [socialMedia])
return ( return (
<>
<button <button
{...rest} {...rest}
className={classNames(className, 'button', givenClassName)} style={{ background: socialMediaColor }}
className={classNames(
className,
styles['button'],
{
[styles['buttonGoogle'] as string]: socialMedia === 'Google',
[styles['buttonMedia'] as string]: socialMedia !== 'Google'
},
givenClassName
)}
> >
<SocialMediaChildren socialMedia={socialMedia} /> <SocialMediaChildren socialMedia={socialMedia} />
</button> </button>
<style jsx>
{`
.button {
background: ${socialMediaColor};
color: ${socialMedia === 'Google' ? '#000' : '#fff'};
border: ${socialMedia === 'Google' ? '1px solid #000' : 'none'};
}
.button:focus {
box-shadow: 0 0 0 2px #27b05e;
}
`}
</style>
</>
) )
} }
@ -88,23 +83,20 @@ export const SocialMediaLink: React.FC<SocialMediaLinkProps> = (props) => {
}, [socialMedia]) }, [socialMedia])
return ( return (
<> <a
<a {...rest} className={classNames(className, 'link', givenClassName)}> {...rest}
style={{ background: socialMediaColor }}
className={classNames(
className,
styles['button'],
{
[styles['buttonGoogle'] as string]: socialMedia === 'Google',
[styles['buttonMedia'] as string]: socialMedia !== 'Google'
},
givenClassName
)}
>
<SocialMediaChildren socialMedia={socialMedia} /> <SocialMediaChildren socialMedia={socialMedia} />
</a> </a>
<style jsx>
{`
.link {
background: ${socialMediaColor};
color: ${socialMedia === 'Google' ? '#000' : '#fff'};
border: ${socialMedia === 'Google' ? '1px solid #000' : 'none'};
}
.link:focus {
box-shadow: 0 0 0 2px #27b05e;
}
`}
</style>
</>
) )
} }

View File

@ -10,7 +10,7 @@ export const Textarea: React.FC<TextareaProps> = (props) => {
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
<div className='mt-6 mb-2 flex justify-between'> <div className='mb-2 mt-6 flex justify-between'>
<label className='pl-1' htmlFor={id}> <label className='pl-1' htmlFor={id}>
{label} {label}
</label> </label>

View File

@ -17,9 +17,7 @@ export interface Guilds {
const defaultGuildsContext = {} as any const defaultGuildsContext = {} as any
const GuildsContext = createContext<Guilds>(defaultGuildsContext) const GuildsContext = createContext<Guilds>(defaultGuildsContext)
export const GuildsProvider: React.FC<React.PropsWithChildren<{}>> = ( export const GuildsProvider: React.FC<React.PropsWithChildren> = (props) => {
props
) => {
const { children } = props const { children } = props
const { authentication } = useAuthentication() const { authentication } = useAuthentication()

View File

@ -5,7 +5,7 @@ import { getLocal } from 'mockttp'
import type { Mockttp } from 'mockttp' import type { Mockttp } from 'mockttp'
import { API_DEFAULT_PORT } from './tools/api' import { API_DEFAULT_PORT } from './tools/api'
import type { Handlers, Method } from './cypress/fixtures/handler' import type { Handlers } from './cypress/fixtures/handler'
const UPLOADS_FIXTURES_DIRECTORY = path.join( const UPLOADS_FIXTURES_DIRECTORY = path.join(
process.cwd(), process.cwd(),
@ -19,34 +19,42 @@ let server: Mockttp | null = null
export default defineConfig({ export default defineConfig({
fixturesFolder: false, fixturesFolder: false,
video: false, video: false,
downloadsFolder: undefined,
screenshotOnRunFailure: false, screenshotOnRunFailure: false,
e2e: { e2e: {
baseUrl: 'http://localhost:3000', baseUrl: 'http://127.0.0.1:3000',
supportFile: false, supportFile: false,
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
on('task', { on('task', {
async startMockServer(handlers: Handlers): Promise<null> { async startMockServer(handlers: Handlers): Promise<null> {
server = getLocal({ server = getLocal({ cors: true })
cors: true
})
await server.start(API_DEFAULT_PORT) await server.start(API_DEFAULT_PORT)
for (const handler of handlers) { for (const handler of handlers) {
const { isFile = false } = handler.response const { isFile = false, statusCode, body } = handler.response
const method = handler.method.toLowerCase() as Lowercase<Method> let requestBuilder = server.forGet(handler.url)
switch (handler.method) {
case 'GET':
requestBuilder = server.forGet(handler.url)
break
case 'POST':
requestBuilder = server.forPost(handler.url)
break
case 'PUT':
requestBuilder = server.forPut(handler.url)
break
case 'DELETE':
requestBuilder = server.forDelete(handler.url)
break
}
if (isFile) { if (isFile) {
await server[method](handler.url).thenFromFile( await requestBuilder.thenFromFile(
handler.response.statusCode, statusCode,
path.join(UPLOADS_FIXTURES_DIRECTORY, ...handler.response.body) path.join(UPLOADS_FIXTURES_DIRECTORY, ...body)
) )
} else { } else {
await server[method](handler.url).thenJson( await requestBuilder.thenJson(statusCode, body)
handler.response.statusCode,
handler.response.body
)
} }
} }
return null return null
}, },
@ -62,7 +70,6 @@ export default defineConfig({
return config return config
} }
}, },
component: { component: {
devServer: { devServer: {
framework: 'next', framework: 'next',

View File

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />

View File

@ -2,10 +2,10 @@ services:
thream-website: thream-website:
container_name: ${COMPOSE_PROJECT_NAME} container_name: ${COMPOSE_PROJECT_NAME}
image: 'thream-website' image: 'thream-website'
restart: 'unless-stopped'
build: build:
context: './' context: './'
ports: network_mode: 'host'
- '${PORT}:${PORT}'
environment: environment:
PORT: ${PORT} PORT: ${PORT-3000}
env_file: './.env' env_file: '.env'

View File

@ -13,7 +13,7 @@ const getErrorTranslationKey = (error: Error): string => {
return 'errors:required' return 'errors:required'
} }
if (error.keyword === 'format') { if (error.keyword === 'format') {
if (error.params.format === 'email') { if (error.params['format'] === 'email') {
return 'errors:invalid-email' return 'errors:invalid-email'
} }
return 'errors:invalid' return 'errors:invalid'
@ -43,7 +43,7 @@ export const useFormTranslation = (): UseFormTranslationResult => {
if (error != null) { if (error != null) {
return t(getErrorTranslationKey(error)).replace( return t(getErrorTranslationKey(error)).replace(
'{expected}', '{expected}',
error?.params?.limit error?.params?.['limit']
) )
} }
return undefined return undefined

View File

@ -57,10 +57,13 @@ export const usePagination = <T extends PaginationItem>(
`${url}?${searchParameters.toString()}` `${url}?${searchParameters.toString()}`
) )
if (!inverse) { if (!inverse) {
const endIndex = newItems.length - 1
const lastItem = newItems[endIndex]
afterId.current = afterId.current =
newItems.length > 0 ? newItems[newItems.length - 1].id : null newItems.length > 0 && lastItem != null ? lastItem.id : null
} else { } else {
afterId.current = newItems.length > 0 ? newItems[0].id : null afterId.current =
newItems.length > 0 && newItems[0] != null ? newItems[0].id : null
} }
setItems((oldItems) => { setItems((oldItems) => {
const updatedItems = inverse const updatedItems = inverse
@ -110,10 +113,13 @@ export const usePagination = <T extends PaginationItem>(
const newItems = getPaginationCache<T>(cacheKey) const newItems = getPaginationCache<T>(cacheKey)
setItems(newItems) setItems(newItems)
if (!inverse) { if (!inverse) {
const endIndex = newItems.length - 1
const lastItem = newItems[endIndex]
afterId.current = afterId.current =
newItems.length > 0 ? newItems[newItems.length - 1].id : null newItems.length > 0 && lastItem != null ? lastItem.id : null
} else { } else {
afterId.current = newItems.length > 0 ? newItems[0].id : null afterId.current =
newItems.length > 0 && newItems[0] != null ? newItems[0].id : null
} }
fetchState.current = 'idle' fetchState.current = 'idle'
} }

View File

@ -12,8 +12,8 @@ export const providersTypebox = providers.map((provider) => {
return Type.Literal(provider) return Type.Literal(provider)
}) })
export type ProviderOAuth = typeof providers[number] export type ProviderOAuth = (typeof providers)[number]
export type AuthenticationStrategy = typeof strategies[number] export type AuthenticationStrategy = (typeof strategies)[number]
export const oauthSchema = { export const oauthSchema = {
id, id,

5
next-env.d.ts vendored
View File

@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -1,20 +1,16 @@
const nextPWA = require('next-pwa')({ const nextTranslate = require('next-translate-plugin')
disable: process.env.NODE_ENV !== 'production',
dest: 'public'
})
const nextTranslate = require('next-translate')
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
module.exports = nextTranslate( const nextConfig = {
nextPWA({
reactStrictMode: false, reactStrictMode: false,
output: 'standalone',
images: { images: {
domains: [ domains: [
'api.thream.divlo.fr', 'api.thream.theoludwig.fr',
'thream-api.herokuapp.com', 'file-uploads-api.theoludwig.fr',
'file-uploads-api.thream.divlo.fr', ...(process.env.NODE_ENV !== 'production' ? ['127.0.0.1'] : [])
...(process.env.NODE_ENV !== 'production' ? ['localhost'] : [])
] ]
} }
}) }
)
module.exports = nextTranslate(nextConfig)

34219
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@thream/website", "name": "@thream/website",
"version": "1.3.0", "version": "1.3.7",
"private": true, "private": true,
"repository": { "repository": {
"type": "git", "type": "git",
@ -14,100 +14,96 @@
"dev": "next dev", "dev": "next dev",
"start": "next start", "start": "next start",
"build": "next build", "build": "next build",
"export": "next export",
"generate": "plop", "generate": "plop",
"lint:commit": "commitlint", "lint:commit": "commitlint",
"lint:editorconfig": "editorconfig-checker", "lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint-cli2", "lint:markdown": "markdownlint-cli2",
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\" --ignore-path \".gitignore\"", "lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore",
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"", "lint:prettier": "prettier . --check",
"lint:staged": "lint-staged", "lint:staged": "lint-staged",
"test:unit": "cypress run --component", "test:unit": "cypress run --component",
"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://127.0.0.1:3000\" \"html-w3c-validator\"",
"test:lighthouse": "lhci autorun", "test:e2e": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"cypress run\"",
"test:e2e": "start-server-and-test \"start\" \"http://localhost:3000\" \"cypress run\"", "test:dev": "start-server-and-test \"dev\" \"http://127.0.0.1:3000\" \"cypress open\"",
"test:dev": "start-server-and-test \"dev\" \"http://localhost:3000\" \"cypress open\"",
"release": "semantic-release", "release": "semantic-release",
"deploy": "vercel",
"postinstall": "husky install" "postinstall": "husky install"
}, },
"dependencies": { "dependencies": {
"@fontsource/montserrat": "4.5.12", "@fontsource/montserrat": "5.0.5",
"@fontsource/roboto": "4.5.8", "@fontsource/roboto": "5.0.5",
"@heroicons/react": "1.0.6", "@heroicons/react": "1.0.6",
"@sinclair/typebox": "0.24.34", "@sinclair/typebox": "0.29.6",
"axios": "0.26.1", "@thream/socketio-jwt": "3.1.2",
"clsx": "1.2.1", "axios": "1.4.0",
"date-and-time": "2.4.1", "clsx": "2.0.0",
"date-and-time": "3.0.2",
"emoji-mart": "3.0.1", "emoji-mart": "3.0.1",
"framer-motion": "7.2.1", "katex": "0.16.8",
"katex": "0.16.2", "next": "13.4.7",
"next": "12.2.5", "next-themes": "0.2.1",
"next-pwa": "5.6.0", "next-translate": "2.4.4",
"next-themes": "0.2.0", "pretty-bytes": "6.1.1",
"next-translate": "1.5.0",
"pretty-bytes": "6.0.0",
"react": "18.2.0", "react": "18.2.0",
"react-component-form": "3.1.0", "react-component-form": "4.1.1",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-infinite-scroll-component": "6.1.0", "react-infinite-scroll-component": "6.1.0",
"react-markdown": "8.0.3", "react-markdown": "8.0.7",
"react-responsive": "8.2.0", "react-responsive": "9.0.2",
"react-swipeable": "7.0.0", "react-swipeable": "7.0.1",
"react-syntax-highlighter": "15.5.0", "react-syntax-highlighter": "15.5.0",
"react-textarea-autosize": "8.3.4", "react-textarea-autosize": "8.5.2",
"read-pkg": "7.1.0", "read-pkg": "8.0.0",
"rehype-katex": "6.0.2", "rehype-katex": "6.0.3",
"remark-breaks": "3.0.2", "remark-breaks": "3.0.3",
"remark-gfm": "3.0.1", "remark-gfm": "3.0.1",
"remark-math": "5.1.1", "remark-math": "5.1.1",
"sharp": "0.30.7", "sharp": "0.32.4",
"socket.io-client": "4.5.1", "socket.io-client": "4.7.1",
"unified": "10.1.2", "unified": "10.1.2",
"unist-util-visit": "4.1.1", "unist-util-visit": "4.1.2",
"universal-cookie": "4.0.4" "universal-cookie": "4.0.4"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "17.1.2", "@commitlint/cli": "17.6.7",
"@commitlint/config-conventional": "17.1.0", "@commitlint/config-conventional": "17.6.7",
"@lhci/cli": "0.9.0", "@saithodev/semantic-release-backmerge": "3.2.0",
"@saithodev/semantic-release-backmerge": "2.1.2",
"@semantic-release/git": "10.0.1", "@semantic-release/git": "10.0.1",
"@tsconfig/strictest": "2.0.1",
"@types/emoji-mart": "3.0.9", "@types/emoji-mart": "3.0.9",
"@types/hast": "2.3.4", "@types/hast": "2.3.4",
"@types/katex": "0.14.0", "@types/katex": "0.16.1",
"@types/node": "18.7.14", "@types/node": "20.4.4",
"@types/react": "18.0.18", "@types/react": "18.2.15",
"@types/react-responsive": "8.0.5", "@types/react-responsive": "8.0.5",
"@types/react-syntax-highlighter": "15.5.5", "@types/react-syntax-highlighter": "15.5.7",
"@types/unist": "2.0.6", "@types/unist": "2.0.6",
"@typescript-eslint/eslint-plugin": "5.36.1", "@typescript-eslint/eslint-plugin": "6.1.0",
"@typescript-eslint/parser": "5.36.1", "@typescript-eslint/parser": "6.1.0",
"autoprefixer": "10.4.8", "autoprefixer": "10.4.14",
"cypress": "10.7.0", "cypress": "12.17.2",
"editorconfig-checker": "4.0.2", "editorconfig-checker": "5.1.1",
"eslint": "8.23.0", "eslint": "8.45.0",
"eslint-config-conventions": "4.0.0", "eslint-config-conventions": "11.0.1",
"eslint-config-next": "12.2.5", "eslint-config-next": "13.4.7",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.8.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.27.5",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "5.0.0",
"eslint-plugin-promise": "6.0.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-unicorn": "43.0.2", "eslint-plugin-unicorn": "48.0.0",
"html-w3c-validator": "1.2.0", "html-w3c-validator": "1.4.0",
"husky": "8.0.1", "husky": "8.0.3",
"lint-staged": "13.0.3", "lint-staged": "13.2.3",
"markdownlint-cli2": "0.5.1", "markdownlint-cli2": "0.8.1",
"mockttp": "2.7.0", "markdownlint-rule-relative-links": "2.1.0",
"plop": "3.1.1", "mockttp": "3.9.1",
"postcss": "8.4.16", "next-translate-plugin": "2.4.4",
"prettier": "2.7.1", "plop": "3.1.2",
"prettier-plugin-tailwindcss": "0.1.13", "postcss": "8.4.27",
"semantic-release": "19.0.5", "prettier": "3.0.0",
"serve": "14.0.1", "prettier-plugin-tailwindcss": "0.4.1",
"start-server-and-test": "1.14.0", "semantic-release": "21.0.7",
"tailwindcss": "3.1.8", "start-server-and-test": "2.0.0",
"typescript": "4.8.2", "tailwindcss": "3.3.3",
"vercel": "28.2.0" "typescript": "5.1.6"
} }
} }

View File

@ -3,9 +3,7 @@ import useTranslation from 'next-translate/useTranslation'
import { ErrorPage } from '../components/ErrorPage' import { ErrorPage } from '../components/ErrorPage'
import { Head } from '../components/Head' import { Head } from '../components/Head'
import { Header } from '../components/Header'
import type { FooterProps } from '../components/Footer' import type { FooterProps } from '../components/Footer'
import { Footer } from '../components/Footer'
const Error404: NextPage<FooterProps> = (props) => { const Error404: NextPage<FooterProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -14,12 +12,11 @@ const Error404: NextPage<FooterProps> = (props) => {
return ( return (
<> <>
<Head title='Thream | 404' /> <Head title='Thream | 404' />
<ErrorPage
<Header /> statusCode={404}
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'> message={t('errors:page-not-found')}
<ErrorPage statusCode={404} message={t('errors:page-not-found')} /> version={version}
</main> />
<Footer version={version} />
</> </>
) )
} }

View File

@ -5,7 +5,6 @@ import { ErrorPage } from '../components/ErrorPage'
import { Head } from '../components/Head' import { Head } from '../components/Head'
import { Header } from '../components/Header' import { Header } from '../components/Header'
import type { FooterProps } from '../components/Footer' import type { FooterProps } from '../components/Footer'
import { Footer } from '../components/Footer'
const Error500: NextPage<FooterProps> = (props) => { const Error500: NextPage<FooterProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -16,10 +15,11 @@ const Error500: NextPage<FooterProps> = (props) => {
<Head title='Thream | 500' /> <Head title='Thream | 500' />
<Header /> <Header />
<main className='flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl'> <ErrorPage
<ErrorPage statusCode={500} message={t('errors:server-error')} /> statusCode={500}
</main> message={t('errors:server-error')}
<Footer version={version} /> version={version}
/>
</> </>
) )
} }

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import type { AppProps } from 'next/app' import type { AppType } from 'next/app'
import { ThemeProvider } from 'next-themes' import { ThemeProvider } from 'next-themes'
import useTranslation from 'next-translate/useTranslation' import useTranslation from 'next-translate/useTranslation'
@ -15,7 +15,7 @@ import '@fontsource/roboto/700.css'
import { cookies } from '../tools/cookies' import { cookies } from '../tools/cookies'
const Application = ({ Component, pageProps }: AppProps): JSX.Element => { const Application: AppType = ({ Component, pageProps }) => {
const { lang } = useTranslation() const { lang } = useTranslation()
useEffect(() => { useEffect(() => {

View File

@ -62,9 +62,9 @@ const ChannelPage: NextPage<ChannelPageProps> = (props) => {
export const getServerSideProps = authenticationFromServerSide({ export const getServerSideProps = authenticationFromServerSide({
shouldBeAuthenticated: true, shouldBeAuthenticated: true,
fetchData: async (context, api) => { fetchData: async (context, api) => {
const channelId = Number(context?.params?.channelId) const channelId = Number(context?.params?.['channelId'])
const guildId = Number(context?.params?.guildId) const guildId = Number(context?.params?.['guildId'])
if (isNaN(channelId) || isNaN(guildId)) { if (Number.isNaN(channelId) || Number.isNaN(guildId)) {
return { return {
notFound: true notFound: true
} }

View File

@ -57,9 +57,9 @@ const ChannelSettingsPage: NextPage<ChannelSettingsPageProps> = (props) => {
export const getServerSideProps = authenticationFromServerSide({ export const getServerSideProps = authenticationFromServerSide({
shouldBeAuthenticated: true, shouldBeAuthenticated: true,
fetchData: async (context, api) => { fetchData: async (context, api) => {
const channelId = Number(context?.params?.channelId) const channelId = Number(context?.params?.['channelId'])
const guildId = Number(context?.params?.guildId) const guildId = Number(context?.params?.['guildId'])
if (isNaN(channelId) || isNaN(guildId)) { if (Number.isNaN(channelId) || Number.isNaN(guildId)) {
return { return {
notFound: true notFound: true
} }

View File

@ -44,8 +44,8 @@ const CreateChannelPage: NextPage<CreateChannelPageProps> = (props) => {
export const getServerSideProps = authenticationFromServerSide({ export const getServerSideProps = authenticationFromServerSide({
shouldBeAuthenticated: true, shouldBeAuthenticated: true,
fetchData: async (context, api) => { fetchData: async (context, api) => {
const guildId = Number(context?.params?.guildId) const guildId = Number(context?.params?.['guildId'])
if (isNaN(guildId)) { if (Number.isNaN(guildId)) {
return { return {
notFound: true notFound: true
} }

View File

@ -41,8 +41,8 @@ const GuildSettingsPage: NextPage<GuildSettingsPageProps> = (props) => {
export const getServerSideProps = authenticationFromServerSide({ export const getServerSideProps = authenticationFromServerSide({
shouldBeAuthenticated: true, shouldBeAuthenticated: true,
fetchData: async (context, api) => { fetchData: async (context, api) => {
const guildId = Number(context?.params?.guildId) const guildId = Number(context?.params?.['guildId'])
if (isNaN(guildId)) { if (Number.isNaN(guildId)) {
return { return {
notFound: true notFound: true
} }

View File

@ -35,8 +35,8 @@ const UserProfilePage: NextPage<UserProfilePageProps> = (props) => {
export const getServerSideProps = authenticationFromServerSide({ export const getServerSideProps = authenticationFromServerSide({
shouldBeAuthenticated: true, shouldBeAuthenticated: true,
fetchData: async (context, api) => { fetchData: async (context, api) => {
const userId = Number(context?.params?.userId) const userId = Number(context?.params?.['userId'])
if (isNaN(userId)) { if (Number.isNaN(userId)) {
return { return {
notFound: true notFound: true
} }

View File

@ -15,7 +15,6 @@ import { Input } from '../../components/design/Input'
import { Button } from '../../components/design/Button' import { Button } from '../../components/design/Button'
import { FormState } from '../../components/design/FormState' import { FormState } from '../../components/design/FormState'
import { authenticationFromServerSide } from '../../tools/authentication' import { authenticationFromServerSide } from '../../tools/authentication'
import { ScrollableBody } from '../../components/ScrollableBody'
import { userSchema } from '../../models/User' import { userSchema } from '../../models/User'
import { api } from '../../tools/api' import { api } from '../../tools/api'
import { useFormTranslation } from '../../hooks/useFormTranslation' import { useFormTranslation } from '../../hooks/useFormTranslation'
@ -43,24 +42,24 @@ const ForgotPassword: NextPage<FooterProps> = (props) => {
formElement.reset() formElement.reset()
return { return {
type: 'success', type: 'success',
value: 'authentication:success-forgot-password' message: 'authentication:success-forgot-password'
} }
} catch (error) { } catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 400) { if (axios.isAxiosError(error) && error.response?.status === 400) {
return { return {
type: 'error', type: 'error',
value: 'errors:invalid-email' message: 'errors:invalid-email'
} }
} }
return { return {
type: 'error', type: 'error',
value: 'errors:server-error' message: 'errors:server-error'
} }
} }
} }
return ( return (
<ScrollableBody> <>
<Head title={`Thream | ${t('authentication:forgot-password')}`} /> <Head title={`Thream | ${t('authentication:forgot-password')}`} />
<Header /> <Header />
<Main> <Main>
@ -71,7 +70,7 @@ const ForgotPassword: NextPage<FooterProps> = (props) => {
</Button> </Button>
<p className='mt-3 font-headline text-sm text-green-800 hover:underline dark:text-green-400'> <p className='mt-3 font-headline text-sm text-green-800 hover:underline dark:text-green-400'>
<Link href='/authentication/signin'> <Link href='/authentication/signin'>
<a>{t('authentication:already-know-password')}</a> {t('authentication:already-know-password')}
</Link> </Link>
</p> </p>
</AuthenticationForm> </AuthenticationForm>
@ -86,7 +85,7 @@ const ForgotPassword: NextPage<FooterProps> = (props) => {
/> />
</Main> </Main>
<Footer version={version} /> <Footer version={version} />
</ScrollableBody> </>
) )
} }

View File

@ -15,7 +15,6 @@ import { Input } from '../../components/design/Input'
import { Button } from '../../components/design/Button' import { Button } from '../../components/design/Button'
import { authenticationFromServerSide } from '../../tools/authentication' import { authenticationFromServerSide } from '../../tools/authentication'
import { AuthenticationForm } from '../../components/Authentication' import { AuthenticationForm } from '../../components/Authentication'
import { ScrollableBody } from '../../components/ScrollableBody'
import { api } from '../../tools/api' import { api } from '../../tools/api'
import { userSchema } from '../../models/User' import { userSchema } from '../../models/User'
import { useFormTranslation } from '../../hooks/useFormTranslation' import { useFormTranslation } from '../../hooks/useFormTranslation'
@ -36,7 +35,7 @@ const ResetPassword: NextPage<FooterProps> = (props) => {
try { try {
await api.put(`/users/reset-password`, { await api.put(`/users/reset-password`, {
...formData, ...formData,
temporaryToken: router.query.temporaryToken temporaryToken: router.query['temporaryToken']
}) })
await router.push('/authentication/signin') await router.push('/authentication/signin')
return null return null
@ -44,18 +43,18 @@ const ResetPassword: NextPage<FooterProps> = (props) => {
if (axios.isAxiosError(error) && error.response?.status === 400) { if (axios.isAxiosError(error) && error.response?.status === 400) {
return { return {
type: 'error', type: 'error',
value: 'errors:invalid' message: 'errors:invalid'
} }
} }
return { return {
type: 'error', type: 'error',
value: 'errors:server-error' message: 'errors:server-error'
} }
} }
} }
return ( return (
<ScrollableBody> <>
<Head title={`Thream | ${t('authentication:reset-password')}`} /> <Head title={`Thream | ${t('authentication:reset-password')}`} />
<Header /> <Header />
<Main> <Main>
@ -81,7 +80,7 @@ const ResetPassword: NextPage<FooterProps> = (props) => {
/> />
</Main> </Main>
<Footer version={version} /> <Footer version={version} />
</ScrollableBody> </>
) )
} }

View File

@ -7,19 +7,18 @@ import { Header } from '../../components/Header'
import type { FooterProps } from '../../components/Footer' import type { FooterProps } from '../../components/Footer'
import { Footer } from '../../components/Footer' import { Footer } from '../../components/Footer'
import { authenticationFromServerSide } from '../../tools/authentication' import { authenticationFromServerSide } from '../../tools/authentication'
import { ScrollableBody } from '../../components/ScrollableBody'
const Signin: NextPage<FooterProps> = (props) => { const Signin: NextPage<FooterProps> = (props) => {
const { version } = props const { version } = props
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<ScrollableBody> <>
<Head title={`Thream | ${t('authentication:signin')}`} /> <Head title={`Thream | ${t('authentication:signin')}`} />
<Header /> <Header />
<Authentication mode='signin' /> <Authentication mode='signin' />
<Footer version={version} /> <Footer version={version} />
</ScrollableBody> </>
) )
} }

View File

@ -7,19 +7,18 @@ import { Header } from '../../components/Header'
import type { FooterProps } from '../../components/Footer' import type { FooterProps } from '../../components/Footer'
import { Footer } from '../../components/Footer' import { Footer } from '../../components/Footer'
import { authenticationFromServerSide } from '../../tools/authentication' import { authenticationFromServerSide } from '../../tools/authentication'
import { ScrollableBody } from '../../components/ScrollableBody'
const Signup: NextPage<FooterProps> = (props) => { const Signup: NextPage<FooterProps> = (props) => {
const { version } = props const { version } = props
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<ScrollableBody> <>
<Head title={`Thream | ${t('authentication:signup')}`} /> <Head title={`Thream | ${t('authentication:signup')}`} />
<Header /> <Header />
<Authentication mode='signup' /> <Authentication mode='signup' />
<Footer version={version} /> <Footer version={version} />
</ScrollableBody> </>
) )
} }

View File

@ -11,29 +11,27 @@ import type { FooterProps } from '../components/Footer'
import { Footer } from '../components/Footer' import { Footer } from '../components/Footer'
import { SocialMediaLink } from '../components/design/SocialMediaButton' import { SocialMediaLink } from '../components/design/SocialMediaButton'
import { ButtonLink } from '../components/design/Button' import { ButtonLink } from '../components/design/Button'
import { ScrollableBody } from '../components/ScrollableBody'
const Home: NextPage<FooterProps> = (props) => { const Home: NextPage<FooterProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
const { version } = props const { version } = props
return ( return (
<ScrollableBody> <>
<Head /> <Head />
<Header /> <Header />
<Main> <Main>
<div className='flex w-4/5 flex-col items-center'> <div className='flex w-4/5 flex-col items-center'>
<div className='max-w-xs'> <div className='max-w-xs'>
<Link href='/authentication/signup'> <Link href='/authentication/signup'>
<a>
<Image <Image
quality={100} quality={100}
width={351} width={351}
height={341} height={341}
src='/images/svg/design/home.svg' src='/images/svg/design/home.svg'
alt={"Thream's chat app"} alt={"Thream's chat app"}
priority
/> />
</a>
</Link> </Link>
</div> </div>
<div className='text-center'> <div className='text-center'>
@ -55,7 +53,7 @@ const Home: NextPage<FooterProps> = (props) => {
/> />
</div> </div>
<div className='mt-8 flex items-center justify-center space-x-4 text-center'> <div className='mt-8 flex items-center justify-center space-x-4 text-center'>
<Link href='/authentication/signup' passHref> <Link href='/authentication/signup' passHref legacyBehavior>
<ButtonLink data-cy='get-started'> <ButtonLink data-cy='get-started'>
{t('home:get-started')} {t('home:get-started')}
</ButtonLink> </ButtonLink>
@ -72,7 +70,7 @@ const Home: NextPage<FooterProps> = (props) => {
</div> </div>
</Main> </Main>
<Footer version={version} /> <Footer version={version} />
</ScrollableBody> </>
) )
} }

View File

Before

Width:  |  Height:  |  Size: 397 KiB

After

Width:  |  Height:  |  Size: 397 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -1,51 +0,0 @@
{
"name": "Thream",
"short_name": "Thream",
"theme_color": "#27B05E",
"background_color": "#262B3F",
"start_url": "/",
"display": "standalone",
"icons": [
{
"src": "images/icons/72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "images/icons/96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "images/icons/128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "images/icons/144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "images/icons/152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "images/icons/192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "images/icons/384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "images/icons/512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@ -12,9 +12,18 @@
@apply flex h-screen flex-col; @apply flex h-screen flex-col;
} }
#application-page-content {
overflow-x: hidden;
}
body { body {
@apply bg-white font-headline text-black dark:bg-black dark:text-white; @apply bg-white font-headline text-black dark:bg-black dark:text-white;
overflow: hidden; overflow: hidden;
scrollbar-width: thin;
scrollbar-color: var(--scroll-bar-color) var(--scroll-bar-bg-color);
z-index: 1000;
height: calc(100vh - 64px);
overflow-y: auto;
} }
.h-full-without-header { .h-full-without-header {

View File

@ -1,4 +1,5 @@
module.exports = { /** @type {import('tailwindcss').Config} */
const tailwindConfig = {
content: [ content: [
'./pages/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}' './components/**/*.{js,ts,jsx,tsx}'
@ -31,3 +32,5 @@ module.exports = {
}, },
plugins: [] plugins: []
} }
module.exports = tailwindConfig

View File

@ -1,13 +1,13 @@
import axios from 'axios' import axios from 'axios'
export const API_VERSION = '1.2.0' export const API_VERSION = '1.2.8'
export const API_DEFAULT_PORT = 8080 export const API_DEFAULT_PORT = 8080
export const API_URL = export const API_URL =
process.env.NEXT_PUBLIC_API_URL != null process.env['NEXT_PUBLIC_API_URL'] != null
? process.env.NEXT_PUBLIC_API_URL ? process.env['NEXT_PUBLIC_API_URL']
: `http://localhost:${API_DEFAULT_PORT}` : `http://127.0.0.1:${API_DEFAULT_PORT}`
export const api = axios.create({ export const api = axios.create({
baseURL: API_URL, baseURL: API_URL,

View File

@ -2,6 +2,7 @@ import type { AxiosInstance } from 'axios'
import axios from 'axios' import axios from 'axios'
import type { Socket } from 'socket.io-client' import type { Socket } from 'socket.io-client'
import { io } from 'socket.io-client' import { io } from 'socket.io-client'
import { isUnauthorizedError } from '@thream/socketio-jwt'
import { API_URL } from '../api' import { API_URL } from '../api'
import { cookies } from '../cookies' import { cookies } from '../cookies'
@ -30,7 +31,7 @@ export class Authentication {
) )
}) })
this.socket.on('connect_error', (error) => { this.socket.on('connect_error', (error) => {
if (error.message.startsWith('Unauthorized')) { if (isUnauthorizedError(error)) {
fetchRefreshToken(this.tokens.refreshToken) fetchRefreshToken(this.tokens.refreshToken)
.then(({ accessToken }) => { .then(({ accessToken }) => {
this.setAccessToken(accessToken) this.setAccessToken(accessToken)
@ -58,7 +59,6 @@ export class Authentication {
) )
this.setAccessToken(accessToken) this.setAccessToken(accessToken)
} }
config.headers = config.headers == null ? {} : config.headers
config.headers.Authorization = `${this.tokens.type} ${this.tokens.accessToken}` config.headers.Authorization = `${this.tokens.type} ${this.tokens.accessToken}`
return config return config
}, },
@ -96,7 +96,7 @@ export class Authentication {
this.accessTokenAge = Date.now() this.accessTokenAge = Date.now()
const token = `${this.tokens.type} ${this.tokens.accessToken}` const token = `${this.tokens.type} ${this.tokens.accessToken}`
if (typeof this?.socket?.auth !== 'function' && this.socket != null) { if (typeof this?.socket?.auth !== 'function' && this.socket != null) {
this.socket.auth.token = token this.socket.auth['token'] = token
} }
} }

View File

@ -1,24 +1,24 @@
{ {
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "node", "lib": ["dom", "dom.iterable", "ESNext"],
"allowJs": true, "allowJs": true,
"checkJs": true, "baseUrl": ".",
"jsx": "preserve", "paths": {
"sourceMap": true, "@/*": ["./*"]
"removeComments": true,
"noEmit": true,
"strict": true,
"types": ["cypress"],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"lib": ["dom", "dom.iterable", "esnext"],
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true
}, },
"exclude": ["dist", ".next", "out", "next.config.js"], "types": ["cypress"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"] "noEmit": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"jsx": "preserve",
"incremental": true,
"exactOptionalPropertyTypes": false,
"verbatimModuleSyntax": false,
"isolatedModules": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
} }

View File

@ -1,5 +0,0 @@
{
"github": {
"enabled": false
}
}