13 Commits

75 changed files with 6363 additions and 26381 deletions

View File

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

View File

@ -3,8 +3,11 @@
"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",
@ -14,8 +17,8 @@
"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

@ -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,7 +16,7 @@ jobs:
language: ['javascript'] language: ['javascript']
steps: steps:
- uses: 'actions/checkout@v3.1.0' - uses: 'actions/checkout@v3.5.3'
- name: 'Initialize CodeQL' - name: 'Initialize CodeQL'
uses: 'github/codeql-action/init@v2' uses: 'github/codeql-action/init@v2'

View File

@ -10,16 +10,16 @@ jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v3.1.0' - uses: 'actions/checkout@v3.5.3'
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v3.5.1' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '18.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.1.0' - uses: 'actions/checkout@v3.5.3'
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v3.5.1' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '18.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@v3.0.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.1.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@v3.5.1' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '18.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.1.0' - uses: 'actions/checkout@v3.5.3'
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v3.5.1' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '18.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.1.0' - uses: 'actions/checkout@v3.5.3'
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v3.5.1' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '18.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.1.0'
- name: 'Use Node.js'
uses: 'actions/setup-node@v3.5.1'
with:
node-version: '18.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'

6
.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,8 +43,6 @@ npm-debug.log*
# misc # misc
.DS_Store .DS_Store
.lighthouseci
.vercel
*.hbs *.hbs
# typescript # typescript

View File

@ -1,31 +0,0 @@
{
"ci": {
"collect": {
"startServerCommand": "npm run start",
"startServerReadyPattern": "ready on",
"startServerReadyTimeout": 20000,
"url": [
"http://127.0.0.1:3000/",
"http://127.0.0.1:3000/authentication/forgot-password",
"http://127.0.0.1:3000/authentication/reset-password",
"http://127.0.0.1:3000/authentication/signin",
"http://127.0.0.1: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

@ -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

@ -30,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
@ -67,10 +43,10 @@ git commit -m "docs(readme): update installation process"
[Reference issue](https://github.com/Thream/website/issues/24) [Reference issue](https://github.com/Thream/website/issues/24)
Feel free to contribute to Thream and add new languages, we would appreciate your help! Feel free to contribute to **Thream** and add new languages, we would appreciate your help!
To add a new language: To add a new language:
- `npm install` - `npm clean-install`
- `npm run generate` - `npm run generate`
- Start editing JSON files with the translation in `locales/{{locale}}` (e.g: `locales/en`) - Start editing JSON files with the translation in `locales/{{locale}}` (e.g: `locales/en`)

View File

@ -1,21 +1,22 @@
FROM node:18.12.1 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:18.12.1 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:18.12.1 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
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder /usr/src/app/.next/standalone ./ COPY --from=builder /usr/src/application/.next/standalone ./
COPY --from=builder /usr/src/app/.next/static ./.next/static COPY --from=builder /usr/src/application/.next/static ./.next/static
COPY --from=builder /usr/src/app/public ./public 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/next.config.js ./next.config.js COPY --from=builder /usr/src/application/next.config.js ./next.config.js
CMD ["node", "server.js"] CMD ["./server.js"]

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.3](https://github.com/Thream/api/releases/tag/v1.2.3). 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.3](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://127.0.0.1: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

@ -25,7 +25,7 @@ const ChannelMemo: React.FC<ChannelProps> = (props) => {
<Link <Link
href={`/application/${path.guildId}/${channel.id}`} 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
} }

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

@ -35,13 +35,17 @@ export const CreateGuild: React.FC = () => {
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

@ -56,12 +56,12 @@ export const GuildSettings: React.FC = () => {
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')

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,7 +14,7 @@ const MemberMemo: React.FC<MemberProps> = (props) => {
return ( return (
<Link href={`/application/users/${member.user.id}`}> <Link href={`/application/users/${member.user.id}`}>
<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 cursor-pointer items-center overflow-hidden px-6 py-2 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={

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

@ -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

@ -58,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'
> >
@ -73,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>

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 {

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'
} }
} }
} }

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

@ -15,7 +15,7 @@ export const Header: React.FC = () => {
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'>

View File

@ -29,7 +29,7 @@ export const SwitchTheme: React.FC = () => {
<div <div
data-cy='switch-theme-dark' data-cy='switch-theme-dark'
className={classNames( className={classNames(
'absolute top-0 bottom-0 left-[8px] mt-auto mb-auto h-[10px] w-[14px] leading-[0] transition-opacity duration-[250ms] ease-in-out', '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-100': theme === 'dark',
'opacity-0': theme === 'light' 'opacity-0': theme === 'light'
@ -43,7 +43,7 @@ export const SwitchTheme: React.FC = () => {
<div <div
data-cy='switch-theme-light' data-cy='switch-theme-light'
className={classNames( className={classNames(
'absolute right-[10px] top-0 bottom-0 mt-auto mb-auto h-[10px] w-[10px] leading-[0]', 'absolute bottom-0 right-[10px] top-0 mb-auto mt-auto h-[10px] w-[10px] leading-[0]',
{ {
'opacity-100': theme === 'light', 'opacity-100': theme === 'light',
'opacity-0': theme === 'dark' 'opacity-0': theme === 'dark'

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

@ -36,7 +36,7 @@ export const Input: React.FC<InputProps> = (props) => {
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
<div className={classNames('mt-6 mb-2 flex justify-between', className)}> <div className={classNames('mb-2 mt-6 flex justify-between', className)}>
<label className='pl-1' htmlFor={name}> <label className='pl-1' htmlFor={name}>
{label} {label}
</label> </label>
@ -66,7 +66,7 @@ export const Input: React.FC<InputProps> = (props) => {
style={{ style={{
backgroundImage: `url('/images/svg/icons/input/${inputType}.svg')` backgroundImage: `url('/images/svg/icons/input/${inputType}.svg')`
}} }}
className='absolute top-3 right-4 z-10 h-5 w-5 cursor-pointer bg-[#f1f1f1] bg-cover' className='absolute right-4 top-3 z-10 h-5 w-5 cursor-pointer bg-[#f1f1f1] bg-cover'
/> />
)} )}
<FormState <FormState

View File

@ -7,18 +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 <div
data-cy='progress-spinner' data-cy='progress-spinner'
className='relative my-0 mx-auto before:block before:pt-[100%] before:content-none' className='relative mx-auto my-0 before:block before:pt-[100%] before:content-none'
style={{ width: `${width}px`, height: `${height}px` }} style={{ width: `${width}px`, height: `${height}px` }}
> >
<svg className={styles.progressSpinnerSvg} viewBox='25 25 50 50'> <svg className={styles['progressSpinnerSvg']} viewBox='25 25 50 50'>
<circle <circle
className={styles.progressSpinnerCircle} className={styles['progressSpinnerCircle']}
cx='50' cx='50'
cy='50' cy='50'
r='20' r='20'

View File

@ -58,10 +58,10 @@ export const SocialMediaButton: React.FC<SocialMediaButtonProps> = (props) => {
style={{ background: socialMediaColor }} style={{ background: socialMediaColor }}
className={classNames( className={classNames(
className, className,
styles.button, styles['button'],
{ {
[styles.buttonGoogle]: socialMedia === 'Google', [styles['buttonGoogle'] as string]: socialMedia === 'Google',
[styles.buttonMedia]: socialMedia !== 'Google' [styles['buttonMedia'] as string]: socialMedia !== 'Google'
}, },
givenClassName givenClassName
)} )}
@ -88,10 +88,10 @@ export const SocialMediaLink: React.FC<SocialMediaLinkProps> = (props) => {
style={{ background: socialMediaColor }} style={{ background: socialMediaColor }}
className={classNames( className={classNames(
className, className,
styles.button, styles['button'],
{ {
[styles.buttonGoogle]: socialMedia === 'Google', [styles['buttonGoogle'] as string]: socialMedia === 'Google',
[styles.buttonMedia]: socialMedia !== 'Google' [styles['buttonMedia'] as string]: socialMedia !== 'Google'
}, },
givenClassName givenClassName
)} )}

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(),
@ -26,25 +26,35 @@ export default defineConfig({
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
}, },

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-3000}:${PORT-3000}'
environment: environment:
PORT: ${PORT-3000} 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,

View File

@ -1,8 +1,4 @@
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} */
const nextConfig = { const nextConfig = {
@ -10,11 +6,11 @@ const nextConfig = {
output: 'standalone', output: 'standalone',
images: { images: {
domains: [ domains: [
'api.thream.divlo.fr', 'api.thream.theoludwig.fr',
'file-uploads-api.thream.divlo.fr', 'file-uploads-api.theoludwig.fr',
...(process.env.NODE_ENV !== 'production' ? ['127.0.0.1'] : []) ...(process.env.NODE_ENV !== 'production' ? ['127.0.0.1'] : [])
] ]
} }
} }
module.exports = nextTranslate(nextPWA(nextConfig)) module.exports = nextTranslate(nextConfig)

31985
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.1", "version": "1.3.7",
"private": true, "private": true,
"repository": { "repository": {
"type": "git", "type": "git",
@ -14,98 +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://127.0.0.1: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://127.0.0.1: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://127.0.0.1:3000\" \"cypress open\"",
"release": "semantic-release", "release": "semantic-release",
"deploy": "vercel",
"postinstall": "husky install" "postinstall": "husky install"
}, },
"dependencies": { "dependencies": {
"@fontsource/montserrat": "4.5.13", "@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.25.13", "@sinclair/typebox": "0.29.6",
"axios": "1.2.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",
"katex": "0.16.4", "katex": "0.16.8",
"next": "13.0.6", "next": "13.4.7",
"next-pwa": "5.6.0",
"next-themes": "0.2.1", "next-themes": "0.2.1",
"next-translate": "1.6.0", "next-translate": "2.4.4",
"pretty-bytes": "6.0.0", "pretty-bytes": "6.1.1",
"react": "18.2.0", "react": "18.2.0",
"react-component-form": "3.1.1", "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.4", "react-markdown": "8.0.7",
"react-responsive": "9.0.2", "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.4.0", "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.31.2", "sharp": "0.32.4",
"socket.io-client": "4.5.4", "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.3.0", "@commitlint/cli": "17.6.7",
"@commitlint/config-conventional": "17.3.0", "@commitlint/config-conventional": "17.6.7",
"@lhci/cli": "0.10.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.11.14", "@types/node": "20.4.4",
"@types/react": "18.0.26", "@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.46.1", "@typescript-eslint/eslint-plugin": "6.1.0",
"@typescript-eslint/parser": "5.46.1", "@typescript-eslint/parser": "6.1.0",
"autoprefixer": "10.4.13", "autoprefixer": "10.4.14",
"cypress": "12.1.0", "cypress": "12.17.2",
"editorconfig-checker": "4.0.2", "editorconfig-checker": "5.1.1",
"eslint": "8.29.0", "eslint": "8.45.0",
"eslint-config-conventions": "6.0.0", "eslint-config-conventions": "11.0.1",
"eslint-config-next": "13.0.6", "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.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-unicorn": "45.0.2", "eslint-plugin-unicorn": "48.0.0",
"html-w3c-validator": "1.2.1", "html-w3c-validator": "1.4.0",
"husky": "8.0.2", "husky": "8.0.3",
"lint-staged": "13.1.0", "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.20", "next-translate-plugin": "2.4.4",
"prettier": "2.8.1", "plop": "3.1.2",
"prettier-plugin-tailwindcss": "0.2.1", "postcss": "8.4.27",
"semantic-release": "19.0.5", "prettier": "3.0.0",
"start-server-and-test": "1.15.2", "prettier-plugin-tailwindcss": "0.4.1",
"tailwindcss": "3.2.4", "semantic-release": "21.0.7",
"typescript": "4.9.4", "start-server-and-test": "2.0.0",
"vercel": "28.8.0" "tailwindcss": "3.3.3",
"typescript": "5.1.6"
} }
} }

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

@ -42,18 +42,18 @@ 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'
} }
} }
} }

View File

@ -35,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
@ -43,12 +43,12 @@ 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'
} }
} }
} }

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

@ -1,12 +1,12 @@
import axios from 'axios' import axios from 'axios'
export const API_VERSION = '1.2.3' 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://127.0.0.1:${API_DEFAULT_PORT}` : `http://127.0.0.1:${API_DEFAULT_PORT}`
export const api = axios.create({ export const api = axios.create({

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,6 +0,0 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"github": {
"enabled": false
}
}