Compare commits

...

19 Commits

Author SHA1 Message Date
semantic-release-bot
d7eb9e80de
chore(release): 1.3.10 [skip ci] 2023-12-28 04:13:47 +00:00
0c5a86595e
chore: update semantic-release tooling 2023-12-28 05:12:38 +01:00
e94eccf2c5
fix: deprecation notice 2023-12-28 05:10:51 +01:00
71ea41695f
chore: better Prettier config for easier reviews 2023-10-23 23:33:39 +02:00
semantic-release-bot
a8781724d4
chore(release): 1.3.9 [skip ci] 2023-09-18 20:27:36 +00:00
c0e3f7cbc9
fix: update dependencies to latest 2023-09-18 22:21:59 +02:00
b1ef2cc41c
build: ignore ESLint errors for Production build
It improves performance when extra checking is not necessary.
2023-09-18 22:14:33 +02:00
d35416646b
build(deps): update latest 2023-08-24 22:24:29 +02:00
d82e65c3f0
chore: rename docker-compose.yml to compose.yaml
Ref: https://docs.docker.com/compose/compose-file/03-compose-file/
2023-07-28 12:27:00 +02:00
semantic-release-bot
ceb67bc16c
chore(release): 1.3.8 [skip ci] 2023-07-22 16:33:11 +00:00
6ae7ec75ef
fix: wrong file uploads api domain in config 2023-07-22 18:31:59 +02:00
c9045460e9
chore: correct .dockerignore 2023-07-22 18:10:52 +02:00
semantic-release-bot
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
semantic-release-bot
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
semantic-release-bot
7e76598f7f
chore(release): 1.3.5 [skip ci] 2023-05-13 18:52:40 +00:00
Divlo
fd138caa4b
fix: update API to v1.2.6 2023-05-13 20:51:19 +02:00
232 changed files with 7462 additions and 14468 deletions

View File

@ -1 +1 @@
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:18 FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:20

View File

@ -0,0 +1,9 @@
services:
workspace:
build:
context: "./"
dockerfile: "./Dockerfile"
volumes:
- "..:/workspace:cached"
command: "sleep infinity"
network_mode: "host"

View File

@ -1,12 +1,13 @@
{ {
"name": "@thream/website", "name": "@thream/website",
"dockerComposeFile": "./docker-compose.yml", "dockerComposeFile": "./compose.yaml",
"service": "workspace", "service": "workspace",
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
"customizations": { "customizations": {
"vscode": { "vscode": {
"settings": { "settings": {
"remote.autoForwardPorts": false "remote.autoForwardPorts": false,
"remote.localPortHost": "allInterfaces"
}, },
"extensions": [ "extensions": [
"editorconfig.editorconfig", "editorconfig.editorconfig",
@ -19,7 +20,5 @@
] ]
} }
}, },
"forwardPorts": [3000],
"postAttachCommand": ["npm", "install"],
"remoteUser": "node" "remoteUser": "node"
} }

View File

@ -1,8 +0,0 @@
services:
workspace:
build:
context: './'
dockerfile: './Dockerfile'
volumes:
- '..:/workspace:cached'
command: 'sleep infinity'

View File

@ -1,5 +1,4 @@
.*
!.npmrc
build build
.next
coverage coverage
node_modules node_modules

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,8 +1,8 @@
--- ---
name: '🐛 Bug Report' name: "🐛 Bug Report"
about: 'Report an unexpected problem or unintended behavior.' about: "Report an unexpected problem or unintended behavior."
title: '[Bug]' title: "[Bug]"
labels: 'bug' labels: "bug"
--- ---
<!-- <!--

View File

@ -1,8 +1,8 @@
--- ---
name: '📜 Documentation' name: "📜 Documentation"
about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).' about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)."
title: '[Documentation]' title: "[Documentation]"
labels: 'documentation' labels: "documentation"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '✨ Feature Request' name: "✨ Feature Request"
about: 'Suggest a new feature idea.' about: "Suggest a new feature idea."
title: '[Feature]' title: "[Feature]"
labels: 'feature request' labels: "feature request"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '🔧 Improvement' name: "🔧 Improvement"
about: 'Improve structure/format/performance/refactor/tests of the code.' about: "Improve structure/format/performance/refactor/tests of the code."
title: '[Improvement]' title: "[Improvement]"
labels: 'improvement' labels: "improvement"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '🙋 Question' name: "🙋 Question"
about: 'Further information is requested.' about: "Further information is requested."
title: '[Question]' title: "[Question]"
labels: 'question' labels: "question"
--- ---
### Question ### Question

View File

@ -1,4 +1,4 @@
name: 'Analyze' name: "Analyze"
on: on:
push: push:
@ -8,20 +8,20 @@ on:
jobs: jobs:
analyze: analyze:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: ['javascript'] language: ["javascript"]
steps: steps:
- uses: 'actions/checkout@v3.5.2' - uses: "actions/checkout@v4.0.0"
- name: 'Initialize CodeQL' - name: "Initialize CodeQL"
uses: 'github/codeql-action/init@v2' 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@v2' uses: "github/codeql-action/analyze@v2"

View File

@ -1,4 +1,4 @@
name: 'Build' name: "Build"
on: on:
push: push:
@ -8,18 +8,18 @@ on:
jobs: jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.2' - uses: "actions/checkout@v4.0.0"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: '18.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build' - name: "Build"
run: 'npm run build' run: "npm run build"

View File

@ -1,4 +1,4 @@
name: 'Lint' name: "Lint"
on: on:
push: push:
@ -8,35 +8,30 @@ on:
jobs: jobs:
lint: lint:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.2' - uses: "actions/checkout@v4.0.0"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: '18.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-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 }}"'
- name: 'lint:editorconfig' - name: "lint:editorconfig"
run: 'npm run lint:editorconfig' run: "npm run lint:editorconfig"
- name: 'lint:markdown' - name: "lint:markdown"
run: 'npm run lint:markdown' run: "npm run lint:markdown"
- name: 'lint:eslint' - name: "lint:eslint"
run: 'npm run lint:eslint' 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 }}

View File

@ -1,4 +1,4 @@
name: 'Release' name: "Release"
on: on:
push: push:
@ -6,39 +6,32 @@ on:
jobs: jobs:
release: release:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.2' - uses: "actions/checkout@v4.0.0"
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@v5.3.0' uses: "crazy-max/ghaction-import-gpg@v6.0.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: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: '18.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Release' - name: "Release"
run: 'npm run release' run: "npm run release"
env: env:
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

@ -1,4 +1,4 @@
name: 'Test' name: "Test"
on: on:
push: push:
@ -8,41 +8,41 @@ on:
jobs: jobs:
test-unit: test-unit:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.2' - uses: "actions/checkout@v4.0.0"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: '18.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Unit Test' - name: "Unit Test"
run: 'npm run test:unit' run: "npm run test:unit"
test-e2e: test-e2e:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.0' - uses: "actions/checkout@v4.0.0"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: '18.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build' - name: "Build"
run: 'npm run build' run: "npm run build"
- name: 'html-w3c-validator' - name: "html-w3c-validator"
run: 'npm run test:html-w3c-validator' run: "npm run test:html-w3c-validator"
- 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,6 +1,3 @@
{ {
"singleQuote": true, "semi": false
"jsxSingleQuote": true,
"semi": false,
"trailingComma": "none"
} }

View File

@ -5,7 +5,7 @@
"editor.bracketPairColorization.enabled": true, "editor.bracketPairColorization.enabled": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true "source.fixAll": "explicit"
}, },
"eslint.options": { "eslint.options": {
"ignorePath": ".gitignore" "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

@ -47,6 +47,6 @@ Feel free to contribute to **Thream** and add new languages, we would appreciate
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,17 +1,19 @@
FROM node:18.16.0 AS builder-dependencies FROM node:20.9.0 AS builder-dependencies
WORKDIR /usr/src/application WORKDIR /usr/src/application
COPY ./.npmrc ./
COPY ./package*.json ./ COPY ./package*.json ./
RUN npm install RUN npm clean-install
FROM node:18.16.0 AS builder FROM node:20.9.0 AS builder
WORKDIR /usr/src/application WORKDIR /usr/src/application
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
COPY ./ ./ COPY ./ ./
RUN npm run build RUN npm run build
FROM gcr.io/distroless/nodejs18-debian11:latest AS runner FROM gcr.io/distroless/nodejs20-debian12:latest AS runner
WORKDIR /usr/src/application WORKDIR /usr/src/application
ENV NODE_ENV=production ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder /usr/src/application/.next/standalone ./ COPY --from=builder /usr/src/application/.next/standalone ./
COPY --from=builder /usr/src/application/.next/static ./.next/static COPY --from=builder /usr/src/application/.next/static ./.next/static

View File

@ -1,4 +1,8 @@
<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">
<strong>⚠️ This project is not maintained anymore, you can still use the code as you wish and fork it to maintain it yourself.</strong>
</p>
<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 +22,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.5](https://github.com/Thream/api/releases/tag/v1.2.5). It uses [Thream/api](https://github.com/Thream/api) [v1.2.10](https://github.com/Thream/api/releases/tag/v1.2.10).
## ⚙️ Getting Started ## ⚙️ Getting Started
@ -31,7 +35,7 @@ It uses [Thream/api](https://github.com/Thream/api) [v1.2.5](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 +44,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

View File

@ -1,18 +1,18 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from "react"
import Image from 'next/image' import Image from "next/image"
import { PlusIcon, MenuIcon, UsersIcon, XIcon } from '@heroicons/react/solid' import { PlusIcon, MenuIcon, UsersIcon, XIcon } from "@heroicons/react/solid"
import classNames from 'clsx' import classNames from "clsx"
import { useMediaQuery } from 'react-responsive' import { useMediaQuery } from "react-responsive"
import { useSwipeable } from 'react-swipeable' import { useSwipeable } from "react-swipeable"
import type { DirectionSidebar } from './Sidebar' import type { DirectionSidebar } from "./Sidebar"
import { Sidebar } from './Sidebar' import { Sidebar } from "./Sidebar"
import { IconButton } from '../design/IconButton' import { IconButton } from "../design/IconButton"
import { IconLink } from '../design/IconLink' import { IconLink } from "../design/IconLink"
import { Guilds } from './Guilds/Guilds' import { Guilds } from "./Guilds/Guilds"
import { Divider } from '../design/Divider' import { Divider } from "../design/Divider"
import { Members } from './Members' import { Members } from "./Members"
import { useAuthentication } from '../../tools/authentication' import { useAuthentication } from "../../tools/authentication"
export interface ChannelsPath { export interface ChannelsPath {
channelId: number channelId: number
@ -27,9 +27,9 @@ const isGuildsChannelsPath = (path: any): path is GuildsChannelsPath => {
} }
export type ApplicationPath = export type ApplicationPath =
| '/application' | "/application"
| '/application/guilds/join' | "/application/guilds/join"
| '/application/guilds/create' | "/application/guilds/create"
| `/application/users/${number}` | `/application/users/${number}`
| `/application/users/settings` | `/application/users/settings`
| GuildsChannelsPath | GuildsChannelsPath
@ -51,51 +51,51 @@ export const Application: React.FC<
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
const isMobile = useMediaQuery({ const isMobile = useMediaQuery({
query: '(max-width: 900px)' query: "(max-width: 900px)",
}) })
const [visibleSidebars, setVisibleSidebars] = useState({ const [visibleSidebars, setVisibleSidebars] = useState({
left: !isMobile, left: !isMobile,
right: false right: false,
}) })
const handleToggleSidebars = (direction: DirectionSidebar): void => { const handleToggleSidebars = (direction: DirectionSidebar): void => {
if (!isMobile) { if (!isMobile) {
if (direction === 'left') { if (direction === "left") {
return setVisibleSidebars({ return setVisibleSidebars({
...visibleSidebars, ...visibleSidebars,
left: !visibleSidebars.left left: !visibleSidebars.left,
}) })
} }
if (direction === 'right') { if (direction === "right") {
return setVisibleSidebars({ return setVisibleSidebars({
...visibleSidebars, ...visibleSidebars,
right: !visibleSidebars.right right: !visibleSidebars.right,
}) })
} }
} else { } else {
if (direction === 'right' && visibleSidebars.left) { if (direction === "right" && visibleSidebars.left) {
return setVisibleSidebars({ return setVisibleSidebars({
left: false, left: false,
right: true right: true,
}) })
} }
if (direction === 'left' && visibleSidebars.right) { if (direction === "left" && visibleSidebars.right) {
return setVisibleSidebars({ return setVisibleSidebars({
left: true, left: true,
right: false right: false,
}) })
} }
if (direction === 'left' && !visibleSidebars.right) { if (direction === "left" && !visibleSidebars.right) {
return setVisibleSidebars({ return setVisibleSidebars({
...visibleSidebars, ...visibleSidebars,
left: !visibleSidebars.left left: !visibleSidebars.left,
}) })
} }
if (direction === 'right' && !visibleSidebars.left) { if (direction === "right" && !visibleSidebars.left) {
return setVisibleSidebars({ return setVisibleSidebars({
...visibleSidebars, ...visibleSidebars,
right: !visibleSidebars.right right: !visibleSidebars.right,
}) })
} }
} }
@ -105,7 +105,7 @@ export const Application: React.FC<
if (isMobile && (visibleSidebars.left || visibleSidebars.right)) { if (isMobile && (visibleSidebars.left || visibleSidebars.right)) {
return setVisibleSidebars({ return setVisibleSidebars({
left: false, left: false,
right: false right: false,
}) })
} }
} }
@ -120,7 +120,7 @@ export const Application: React.FC<
} }
setVisibleSidebars({ setVisibleSidebars({
...visibleSidebars, ...visibleSidebars,
left: true left: true,
}) })
}, },
onSwipedLeft: () => { onSwipedLeft: () => {
@ -130,10 +130,10 @@ export const Application: React.FC<
} }
setVisibleSidebars({ setVisibleSidebars({
...visibleSidebars, ...visibleSidebars,
right: true right: true,
}) })
} }
} },
}) })
useEffect(() => { useEffect(() => {
@ -146,28 +146,28 @@ export const Application: React.FC<
return ( return (
<> <>
<header className='z-50 flex h-16 items-center justify-between bg-gray-200 px-2 py-3 shadow-lg dark:bg-gray-800'> <header className="z-50 flex h-16 items-center justify-between bg-gray-200 px-2 py-3 shadow-lg dark:bg-gray-800">
<IconButton <IconButton
className='h-10 w-10 p-2' className="h-10 w-10 p-2"
onClick={() => { onClick={() => {
return handleToggleSidebars('left') return handleToggleSidebars("left")
}} }}
> >
{!visibleSidebars.left ? <MenuIcon /> : <XIcon />} {!visibleSidebars.left ? <MenuIcon /> : <XIcon />}
</IconButton> </IconButton>
<div <div
data-cy='application-title' data-cy="application-title"
className='text-md font-semibold text-green-800 dark:text-green-400' className="text-md font-semibold text-green-800 dark:text-green-400"
> >
{title} {title}
</div> </div>
<div className='flex space-x-2'> <div className="flex space-x-2">
{title.startsWith('#') && ( {title.startsWith("#") && (
<IconButton <IconButton
data-cy='icon-button-right-sidebar-members' data-cy="icon-button-right-sidebar-members"
className='h-10 w-10 p-2' className="h-10 w-10 p-2"
onClick={() => { onClick={() => {
return handleToggleSidebars('right') return handleToggleSidebars("right")
}} }}
> >
{!visibleSidebars.right ? <UsersIcon /> : <XIcon />} {!visibleSidebars.right ? <UsersIcon /> : <XIcon />}
@ -177,26 +177,26 @@ export const Application: React.FC<
</header> </header>
<main <main
className='h-full-without-header relative flex overflow-hidden' className="h-full-without-header relative flex overflow-hidden"
{...swipeableHandlers} {...swipeableHandlers}
> >
<Sidebar <Sidebar
direction='left' direction="left"
visible={visibleSidebars.left} visible={visibleSidebars.left}
isMobile={isMobile} isMobile={isMobile}
> >
<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'> <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`}
title='Settings' title="Settings"
> >
<Image <Image
quality={100} quality={100}
className='rounded-full' className="rounded-full"
src={ src={
user.logo == null user.logo == null
? '/images/data/user-default.png' ? "/images/data/user-default.png"
: user.logo : user.logo
} }
alt={"Users's profil picture"} alt={"Users's profil picture"}
@ -206,11 +206,11 @@ export const Application: React.FC<
/> />
</IconLink> </IconLink>
<IconLink <IconLink
href='/application' href="/application"
selected={path === '/application'} selected={path === "/application"}
title='Join or create a Guild' title="Join or create a Guild"
> >
<PlusIcon className='h-12 w-12 text-green-800 dark:text-green-400' /> <PlusIcon className="h-12 w-12 text-green-800 dark:text-green-400" />
</IconLink> </IconLink>
<Divider /> <Divider />
<Guilds path={path} /> <Guilds path={path} />
@ -219,14 +219,14 @@ export const Application: React.FC<
</Sidebar> </Sidebar>
<div <div
id='application-page-content' id="application-page-content"
onClick={handleCloseSidebars} onClick={handleCloseSidebars}
className={classNames( className={classNames(
'h-full-without-header relative top-0 z-0 flex w-full flex-1 flex-col overflow-y-auto transition', "h-full-without-header relative top-0 z-0 flex w-full flex-1 flex-col overflow-y-auto transition",
{ {
'absolute opacity-20': "absolute opacity-20":
isMobile && (visibleSidebars.left || visibleSidebars.right) isMobile && (visibleSidebars.left || visibleSidebars.right),
} },
)} )}
> >
{children} {children}
@ -234,7 +234,7 @@ export const Application: React.FC<
{isGuildsChannelsPath(path) && ( {isGuildsChannelsPath(path) && (
<Sidebar <Sidebar
direction='right' direction="right"
visible={visibleSidebars.right} visible={visibleSidebars.right}
isMobile={isMobile} isMobile={isMobile}
> >

View File

@ -1,26 +1,26 @@
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import { useState } from 'react' import { useState } from "react"
import { Form, useForm } from 'react-component-form' import { Form, useForm } from "react-component-form"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import classNames from 'clsx' import classNames from "clsx"
import axios from 'axios' import axios from "axios"
import type { HandleUseFormCallback } from 'react-component-form' import type { HandleUseFormCallback } from "react-component-form"
import { FormState } from '../../design/FormState' import { FormState } from "../../design/FormState"
import { useGuildMember } from '../../../contexts/GuildMember' import { useGuildMember } from "../../../contexts/GuildMember"
import { Input } from '../../design/Input' import { Input } from "../../design/Input"
import { Button } from '../../design/Button' import { Button } from "../../design/Button"
import { useAuthentication } from '../../../tools/authentication' import { useAuthentication } from "../../../tools/authentication"
import type { import type {
Channel, Channel,
ChannelWithDefaultChannelId ChannelWithDefaultChannelId,
} from '../../../models/Channel' } from "../../../models/Channel"
import { channelSchema } from '../../../models/Channel' import { channelSchema } from "../../../models/Channel"
import { ConfirmPopup } from '../ConfirmPopup' import { ConfirmPopup } from "../ConfirmPopup"
import { useFormTranslation } from '../../../hooks/useFormTranslation' import { useFormTranslation } from "../../../hooks/useFormTranslation"
const schema = { const schema = {
name: channelSchema.name name: channelSchema.name,
} }
export interface ChannelSettingsProps { export interface ChannelSettingsProps {
@ -36,7 +36,7 @@ export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
const { channel } = props const { channel } = props
const [inputValues, setInputValues] = useState({ const [inputValues, setInputValues] = useState({
name: channel.name name: channel.name,
}) })
const [confirmation, setConfirmation] = useState(false) const [confirmation, setConfirmation] = useState(false)
@ -51,7 +51,7 @@ export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
message, message,
errors, errors,
setFetchState, setFetchState,
setMessage setMessage,
} = useForm(schema) } = useForm(schema)
const { getFirstErrorTranslation } = useFormTranslation() const { getFirstErrorTranslation } = useFormTranslation()
@ -63,8 +63,8 @@ export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
return null return null
} catch (error) { } catch (error) {
return { return {
type: 'error', type: "error",
message: 'errors:server-error' message: "errors:server-error",
} }
} }
} }
@ -75,7 +75,7 @@ export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
setInputValues((oldInputValues) => { setInputValues((oldInputValues) => {
return { return {
...oldInputValues, ...oldInputValues,
[event.target.name]: event.target.value [event.target.name]: event.target.value,
} }
}) })
} }
@ -84,15 +84,15 @@ export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
try { try {
const { data } = const { data } =
await authentication.api.delete<ChannelWithDefaultChannelId>( await authentication.api.delete<ChannelWithDefaultChannelId>(
`/channels/${channel.id}` `/channels/${channel.id}`,
) )
await router.push(`/application/${guild.id}/${data.defaultChannelId}`) await router.push(`/application/${guild.id}/${data.defaultChannelId}`)
} catch (error) { } catch (error) {
setFetchState('error') setFetchState("error")
if (axios.isAxiosError(error) && error.response?.status === 400) { if (axios.isAxiosError(error) && error.response?.status === 400) {
setMessage('application:delete-channel-only-one') setMessage("application:delete-channel-only-one")
} else { } else {
setMessage('errors:server-error') setMessage("errors:server-error")
} }
} }
} }
@ -101,37 +101,37 @@ export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
<> <>
<Form <Form
onSubmit={handleUseForm(onSubmit)} onSubmit={handleUseForm(onSubmit)}
className='my-auto flex flex-col items-center justify-center py-12' className="my-auto flex flex-col items-center justify-center py-12"
> >
<div className='flex w-full flex-col items-center justify-center sm:w-fit lg:flex-row'> <div className="flex w-full flex-col items-center justify-center sm:w-fit lg:flex-row">
<div className=' flex w-full flex-wrap items-center justify-center px-6 sm:w-max'> <div className=" flex w-full flex-wrap items-center justify-center px-6 sm:w-max">
<div className='mx-12 flex flex-col'> <div className="mx-12 flex flex-col">
<Input <Input
name='name' name="name"
label={t('common:name')} label={t("common:name")}
placeholder={t('common:name')} placeholder={t("common:name")}
className='!mt-0' className="!mt-0"
onChange={onChange} onChange={onChange}
value={inputValues.name} value={inputValues.name}
error={getFirstErrorTranslation(errors.name)} error={getFirstErrorTranslation(errors.name)}
data-cy='channel-name-input' data-cy="channel-name-input"
/> />
</div> </div>
</div> </div>
</div> </div>
<div className='mt-12 flex flex-col items-center justify-center sm:w-fit'> <div className="mt-12 flex flex-col items-center justify-center sm:w-fit">
<div className='space-x-6'> <div className="space-x-6">
<Button type='submit' data-cy='button-save-channel-settings'> <Button type="submit" data-cy="button-save-channel-settings">
{t('application:save')} {t("application:save")}
</Button> </Button>
<Button <Button
type='button' type="button"
color='red' color="red"
onClick={handleConfirmation} onClick={handleConfirmation}
data-cy='button-delete-channel-settings' data-cy="button-delete-channel-settings"
> >
{t('application:delete')} {t("application:delete")}
</Button> </Button>
</div> </div>
<FormState state={fetchState} message={message} /> <FormState state={fetchState} message={message} />
@ -139,17 +139,17 @@ export const ChannelSettings: React.FC<ChannelSettingsProps> = (props) => {
</Form> </Form>
<div <div
className={classNames( className={classNames(
'pointer-events-none invisible absolute z-50 flex h-full w-full items-center justify-center bg-black bg-opacity-90 opacity-0 backdrop-blur-md transition-all', "pointer-events-none invisible absolute z-50 flex h-full w-full items-center justify-center bg-black bg-opacity-90 opacity-0 backdrop-blur-md transition-all",
{ 'pointer-events-auto !visible !opacity-100': confirmation } { "pointer-events-auto !visible !opacity-100": confirmation },
)} )}
> >
<ConfirmPopup <ConfirmPopup
className={classNames('relative top-8 transition-all', { className={classNames("relative top-8 transition-all", {
'!top-0': confirmation "!top-0": confirmation,
})} })}
handleYes={handleDelete} handleYes={handleDelete}
handleNo={handleConfirmation} handleNo={handleConfirmation}
title={`${t('application:delete-the-channel')} ?`} title={`${t("application:delete-the-channel")} ?`}
/> />
</div> </div>
</> </>

View File

@ -1 +1 @@
export * from './ChannelSettings' export * from "./ChannelSettings"

View File

@ -1,13 +1,13 @@
import { memo } from 'react' import { memo } from "react"
import classNames from 'clsx' import classNames from "clsx"
import Link from 'next/link' import Link from "next/link"
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import { CogIcon } from '@heroicons/react/solid' import { CogIcon } from "@heroicons/react/solid"
import type { GuildsChannelsPath } from '../Application' import type { GuildsChannelsPath } from "../Application"
import type { Channel as ChannelType } from '../../../models/Channel' import type { Channel as ChannelType } from "../../../models/Channel"
import { useGuildMember } from '../../../contexts/GuildMember' import { useGuildMember } from "../../../contexts/GuildMember"
import { IconButton } from '../../design/IconButton' import { IconButton } from "../../design/IconButton"
export interface ChannelProps { export interface ChannelProps {
path: GuildsChannelsPath path: GuildsChannelsPath
@ -25,24 +25,24 @@ 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 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', "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 className='max-[315px] ml-2 mr-4 break-all' data-cy='channel-name'> <span className="max-[315px] ml-2 mr-4 break-all" data-cy="channel-name">
# {channel.name} # {channel.name}
</span> </span>
{member.isOwner && ( {member.isOwner && (
<IconButton <IconButton
onClick={async () => { onClick={async () => {
await router.push( await router.push(
`/application/${channel.guildId}/${channel.id}/settings` `/application/${channel.guildId}/${channel.id}/settings`,
) )
}} }}
className='bg-unherit absolute -right-10 h-full w-8 transition-all group-hover:right-0 group-hover:shadow-lg dark:group-hover:bg-gray-600' className="bg-unherit absolute -right-10 h-full w-8 transition-all group-hover:right-0 group-hover:shadow-lg dark:group-hover:bg-gray-600"
title='Settings' title="Settings"
> >
<CogIcon height={20} width={20} /> <CogIcon height={20} width={20} />
</IconButton> </IconButton>

View File

@ -1,9 +1,9 @@
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from "react-infinite-scroll-component"
import { useChannels } from '../../../contexts/Channels' import { useChannels } from "../../../contexts/Channels"
import type { GuildsChannelsPath } from '../Application' import type { GuildsChannelsPath } from "../Application"
import { Loader } from '../../design/Loader' import { Loader } from "../../design/Loader"
import { Channel } from './Channel' import { Channel } from "./Channel"
export interface ChannelsProps { export interface ChannelsProps {
path: GuildsChannelsPath path: GuildsChannelsPath
@ -16,12 +16,12 @@ export const Channels: React.FC<ChannelsProps> = (props) => {
return ( return (
<div <div
id='channels' id="channels"
className='scrollbar-firefox-support flex flex-1 flex-col overflow-y-auto' className="scrollbar-firefox-support flex flex-1 flex-col overflow-y-auto"
> >
<InfiniteScroll <InfiniteScroll
className='channels-list w-full' className="channels-list w-full"
scrollableTarget='channels' scrollableTarget="channels"
dataLength={channels.length} dataLength={channels.length}
next={nextPage} next={nextPage}
hasMore={hasMore} hasMore={hasMore}

View File

@ -1 +1 @@
export * from './Channels' export * from "./Channels"

View File

@ -1,9 +1,9 @@
import Image from 'next/image' import Image from "next/image"
import { useState } from 'react' import { useState } from "react"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import classNames from 'clsx' import classNames from "clsx"
import { Loader } from '../../design/Loader' import { Loader } from "../../design/Loader"
export interface ConfirmPopupProps { export interface ConfirmPopupProps {
className?: string className?: string
@ -27,43 +27,43 @@ export const ConfirmPopup: React.FC<ConfirmPopupProps> = ({ ...props }) => {
<div className={props.className}> <div className={props.className}>
<Loader <Loader
className={classNames( className={classNames(
'absolute left-1/2 top-1/2 scale-0 transition-all', "absolute left-1/2 top-1/2 scale-0 transition-all",
{ {
'scale-100': isLoading "scale-100": isLoading,
} },
)} )}
/> />
<div <div
className={classNames( className={classNames(
'visible flex flex-col items-center opacity-100 transition-all', "visible flex flex-col items-center opacity-100 transition-all",
{ {
'invisible opacity-0': isLoading "invisible opacity-0": isLoading,
} },
)} )}
> >
<Image <Image
quality={100} quality={100}
src='/images/svg/design/join-guild.svg' src="/images/svg/design/join-guild.svg"
alt='Illustration' alt="Illustration"
height={150} height={150}
width={150} width={150}
/> />
<div className='mt-8 flex flex-col'> <div className="mt-8 flex flex-col">
<h1 className='mb-6 text-center text-xl'>{props.title}</h1> <h1 className="mb-6 text-center text-xl">{props.title}</h1>
<div className='flex gap-7'> <div className="flex gap-7">
<button <button
className='rounded-3xl bg-success px-8 py-2 text-white transition hover:brightness-125 dark:text-black hover:dark:brightness-75' className="rounded-3xl bg-success px-8 py-2 text-white transition hover:brightness-125 dark:text-black hover:dark:brightness-75"
onClick={handleYesLoading} onClick={handleYesLoading}
data-cy='confirm-popup-yes-button' data-cy="confirm-popup-yes-button"
> >
{t('common:yes')} {t("common:yes")}
</button> </button>
<button <button
className='rounded-3xl bg-error px-8 py-2 text-white transition hover:brightness-125 dark:text-black hover:dark:brightness-75' className="rounded-3xl bg-error px-8 py-2 text-white transition hover:brightness-125 dark:text-black hover:dark:brightness-75"
onClick={props.handleNo} onClick={props.handleNo}
data-cy='confirm-popup-no-button' data-cy="confirm-popup-no-button"
> >
{t('common:no')} {t("common:no")}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1 +1 @@
export * from './ConfirmPopup' export * from "./ConfirmPopup"

View File

@ -1,20 +1,20 @@
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import { Form, useForm } from 'react-component-form' import { Form, useForm } from "react-component-form"
import type { HandleUseFormCallback } from 'react-component-form' import type { HandleUseFormCallback } from "react-component-form"
import { useAuthentication } from '../../../tools/authentication' import { useAuthentication } from "../../../tools/authentication"
import { Input } from '../../design/Input' import { Input } from "../../design/Input"
import { Main } from '../../design/Main' import { Main } from "../../design/Main"
import { Button } from '../../design/Button' import { Button } from "../../design/Button"
import { FormState } from '../../design/FormState' import { FormState } from "../../design/FormState"
import type { Channel } from '../../../models/Channel' import type { Channel } from "../../../models/Channel"
import { channelSchema } from '../../../models/Channel' import { channelSchema } from "../../../models/Channel"
import { useGuildMember } from '../../../contexts/GuildMember' import { useGuildMember } from "../../../contexts/GuildMember"
import { useFormTranslation } from '../../../hooks/useFormTranslation' import { useFormTranslation } from "../../../hooks/useFormTranslation"
const schema = { const schema = {
name: channelSchema.name name: channelSchema.name,
} }
export const CreateChannel: React.FC = () => { export const CreateChannel: React.FC = () => {
@ -31,38 +31,38 @@ export const CreateChannel: React.FC = () => {
try { try {
const { data: channel } = await authentication.api.post<Channel>( const { data: channel } = await authentication.api.post<Channel>(
`/guilds/${guild.id}/channels`, `/guilds/${guild.id}/channels`,
formData formData,
) )
await router.push(`/application/${guild.id}/${channel.id}`) await router.push(`/application/${guild.id}/${channel.id}`)
return null return null
} catch (error) { } catch (error) {
return { return {
type: 'error', type: "error",
message: 'errors:server-error' message: "errors:server-error",
} }
} }
} }
return ( return (
<Main> <Main>
<Form className='w-4/6 max-w-xs' onSubmit={handleUseForm(onSubmit)}> <Form className="w-4/6 max-w-xs" onSubmit={handleUseForm(onSubmit)}>
<Input <Input
type='text' type="text"
placeholder={t('common:name')} placeholder={t("common:name")}
name='name' name="name"
label={t('common:name')} label={t("common:name")}
error={getFirstErrorTranslation(errors.name)} error={getFirstErrorTranslation(errors.name)}
data-cy='channel-name-input' data-cy="channel-name-input"
/> />
<Button <Button
className='mt-6 w-full' className="mt-6 w-full"
type='submit' type="submit"
data-cy='button-create-channel' data-cy="button-create-channel"
> >
{t('application:create')} {t("application:create")}
</Button> </Button>
</Form> </Form>
<FormState id='message' state={fetchState} message={message} /> <FormState id="message" state={fetchState} message={message} />
</Main> </Main>
) )
} }

View File

@ -1 +1 @@
export * from './CreateChannel' export * from "./CreateChannel"

View File

@ -1,22 +1,22 @@
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import { Form, useForm } from 'react-component-form' import { Form, useForm } from "react-component-form"
import type { AxiosResponse } from 'axios' import type { AxiosResponse } from "axios"
import type { HandleUseFormCallback } from 'react-component-form' import type { HandleUseFormCallback } from "react-component-form"
import { useAuthentication } from '../../../tools/authentication' import { useAuthentication } from "../../../tools/authentication"
import type { GuildComplete } from '../../../models/Guild' import type { GuildComplete } from "../../../models/Guild"
import { guildSchema } from '../../../models/Guild' import { guildSchema } from "../../../models/Guild"
import { Input } from '../../design/Input' import { Input } from "../../design/Input"
import { Main } from '../../design/Main' import { Main } from "../../design/Main"
import { Button } from '../../design/Button' import { Button } from "../../design/Button"
import { FormState } from '../../design/FormState' import { FormState } from "../../design/FormState"
import { Textarea } from '../../design/Textarea' import { Textarea } from "../../design/Textarea"
import { useFormTranslation } from '../../../hooks/useFormTranslation' import { useFormTranslation } from "../../../hooks/useFormTranslation"
const schema = { const schema = {
name: guildSchema.name, name: guildSchema.name,
description: guildSchema.description description: guildSchema.description,
} }
export const CreateGuild: React.FC = () => { export const CreateGuild: React.FC = () => {
@ -33,44 +33,44 @@ export const CreateGuild: React.FC = () => {
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 channel = data.guild.channels[0] const channel = data.guild.channels[0]
if (channel == null) { if (channel == null) {
throw new Error('No channel found') throw new Error("No channel found")
} }
const channelId = channel.id 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",
message: 'errors:server-error' message: "errors:server-error",
} }
} }
} }
return ( return (
<Main> <Main>
<Form className='w-4/6 max-w-xs' onSubmit={handleUseForm(onSubmit)}> <Form className="w-4/6 max-w-xs" onSubmit={handleUseForm(onSubmit)}>
<Input <Input
type='text' type="text"
placeholder={t('common:name')} placeholder={t("common:name")}
name='name' name="name"
label={t('common:name')} label={t("common:name")}
error={getFirstErrorTranslation(errors.name)} error={getFirstErrorTranslation(errors.name)}
/> />
<Textarea <Textarea
label='Description' label="Description"
placeholder='Description' placeholder="Description"
id='description' id="description"
/> />
<Button className='mt-6 w-full' type='submit' data-cy='submit'> <Button className="mt-6 w-full" type="submit" data-cy="submit">
{t('application:create')} {t("application:create")}
</Button> </Button>
</Form> </Form>
<FormState <FormState
id='message' id="message"
state={fetchState} state={fetchState}
message={message != null ? t(message) : undefined} message={message != null ? t(message) : undefined}
/> />

View File

@ -1 +1 @@
export * from './CreateGuild' export * from "./CreateGuild"

View File

@ -1,11 +1,11 @@
import Link from 'next/link' import Link from "next/link"
import { CogIcon, PlusIcon } from '@heroicons/react/solid' import { CogIcon, PlusIcon } from "@heroicons/react/solid"
import { useGuildMember } from '../../../contexts/GuildMember' import { useGuildMember } from "../../../contexts/GuildMember"
import { Divider } from '../../design/Divider' import { Divider } from "../../design/Divider"
import { Channels } from '../Channels' import { Channels } from "../Channels"
import { IconButton } from '../../design/IconButton' import { IconButton } from "../../design/IconButton"
import type { GuildsChannelsPath } from '..' import type { GuildsChannelsPath } from ".."
export interface GuildLeftSidebarProps { export interface GuildLeftSidebarProps {
path: GuildsChannelsPath path: GuildsChannelsPath
@ -17,23 +17,23 @@ export const GuildLeftSidebar: React.FC<GuildLeftSidebarProps> = (props) => {
const { guild, member } = useGuildMember() const { guild, member } = useGuildMember()
return ( return (
<div className='mt-2 flex w-full flex-col justify-between'> <div className="mt-2 flex w-full flex-col justify-between">
<div className='mx-8 mt-2 p-2 text-center'> <div className="mx-8 mt-2 p-2 text-center">
<h2 data-cy='guild-left-sidebar-title' className='text-xl'> <h2 data-cy="guild-left-sidebar-title" className="text-xl">
{guild.name} {guild.name}
</h2> </h2>
</div> </div>
<Divider /> <Divider />
<Channels path={path} /> <Channels path={path} />
<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 <Link
href={`/application/${path.guildId}/channels/create`} href={`/application/${path.guildId}/channels/create`}
passHref passHref
data-cy='link-add-channel' 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>
</Link> </Link>
@ -41,9 +41,9 @@ export const GuildLeftSidebar: React.FC<GuildLeftSidebarProps> = (props) => {
<Link <Link
href={`/application/${path.guildId}/settings`} href={`/application/${path.guildId}/settings`}
passHref passHref
data-cy='link-settings-guild' 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>
</Link> </Link>

View File

@ -1 +1 @@
export * from './GuildLeftSidebar' export * from "./GuildLeftSidebar"

View File

@ -1,26 +1,26 @@
import Image from 'next/image' import Image from "next/image"
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import { useState } from 'react' import { useState } from "react"
import { Type } from '@sinclair/typebox' import { Type } from "@sinclair/typebox"
import { PhotographIcon } from '@heroicons/react/solid' import { PhotographIcon } from "@heroicons/react/solid"
import { Form, useForm } from 'react-component-form' import { Form, useForm } from "react-component-form"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import classNames from 'clsx' import classNames from "clsx"
import type { HandleUseFormCallback } from 'react-component-form' import type { HandleUseFormCallback } from "react-component-form"
import { guildSchema } from '../../../models/Guild' import { guildSchema } from "../../../models/Guild"
import { FormState } from '../../design/FormState' import { FormState } from "../../design/FormState"
import { useGuildMember } from '../../../contexts/GuildMember' import { useGuildMember } from "../../../contexts/GuildMember"
import { Textarea } from '../../design/Textarea' import { Textarea } from "../../design/Textarea"
import { Input } from '../../design/Input' import { Input } from "../../design/Input"
import { Button } from '../../design/Button' import { Button } from "../../design/Button"
import { useAuthentication } from '../../../tools/authentication' import { useAuthentication } from "../../../tools/authentication"
import { ConfirmPopup } from '../ConfirmPopup' import { ConfirmPopup } from "../ConfirmPopup"
import { useFormTranslation } from '../../../hooks/useFormTranslation' import { useFormTranslation } from "../../../hooks/useFormTranslation"
const schema = { const schema = {
name: guildSchema.name, name: guildSchema.name,
description: Type.Optional(guildSchema.description) description: Type.Optional(guildSchema.description),
} }
export const GuildSettings: React.FC = () => { export const GuildSettings: React.FC = () => {
@ -31,7 +31,7 @@ export const GuildSettings: React.FC = () => {
const [inputValues, setInputValues] = useState({ const [inputValues, setInputValues] = useState({
name: guild.name, name: guild.name,
description: guild.description description: guild.description,
}) })
const [confirmation, setConfirmation] = useState(false) const [confirmation, setConfirmation] = useState(false)
@ -46,7 +46,7 @@ export const GuildSettings: React.FC = () => {
message, message,
errors, errors,
setFetchState, setFetchState,
setMessage setMessage,
} = useForm(schema) } = useForm(schema)
const { getFirstErrorTranslation } = useFormTranslation() const { getFirstErrorTranslation } = useFormTranslation()
@ -55,13 +55,13 @@ export const GuildSettings: React.FC = () => {
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",
message: 'application:saved-information' message: "application:saved-information",
} }
} catch (error) { } catch (error) {
return { return {
type: 'error', type: "error",
message: 'errors:server-error' message: "errors:server-error",
} }
} }
} }
@ -72,26 +72,30 @@ export const GuildSettings: React.FC = () => {
setInputValues((oldInputValues) => { setInputValues((oldInputValues) => {
return { return {
...oldInputValues, ...oldInputValues,
[event.target.name]: event.target.value [event.target.name]: event.target.value,
} }
}) })
} }
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async ( const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async (
event event,
) => { ) => {
setFetchState('loading') setFetchState("loading")
const files = event?.target?.files const files = event?.target?.files
if (files != null && files.length === 1 && files[0] != null) { 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, {
setFetchState('idle') headers: {
"Content-Type": "multipart/form-data",
},
})
setFetchState("idle")
} catch (error) { } catch (error) {
setFetchState('error') setFetchState("error")
setMessage('errors:server-error') setMessage("errors:server-error")
} }
} }
} }
@ -100,18 +104,18 @@ export const GuildSettings: React.FC = () => {
try { try {
await authentication.api.delete(`/guilds/${guild.id}`) await authentication.api.delete(`/guilds/${guild.id}`)
} catch (error) { } catch (error) {
setFetchState('error') setFetchState("error")
setMessage('errors:server-error') setMessage("errors:server-error")
} }
} }
const handleLeave = async (): Promise<void> => { const handleLeave = async (): Promise<void> => {
try { try {
await authentication.api.delete(`/guilds/${guild.id}/members/leave`) await authentication.api.delete(`/guilds/${guild.id}/members/leave`)
await router.push('/application') await router.push("/application")
} catch (error) { } catch (error) {
setFetchState('error') setFetchState("error")
setMessage('errors:server-error') setMessage("errors:server-error")
} }
} }
@ -119,85 +123,85 @@ export const GuildSettings: React.FC = () => {
<> <>
<Form <Form
onSubmit={handleUseForm(onSubmit)} onSubmit={handleUseForm(onSubmit)}
className='my-auto flex flex-col items-center justify-center py-12' className="my-auto flex flex-col items-center justify-center py-12"
> >
{member.isOwner && ( {member.isOwner && (
<div className='flex w-full flex-col items-center justify-center sm:w-fit lg:flex-row'> <div className="flex w-full flex-col items-center justify-center sm:w-fit lg:flex-row">
<div className=' flex w-full flex-wrap items-center justify-center px-6 sm:w-max'> <div className=" flex w-full flex-wrap items-center justify-center px-6 sm:w-max">
<div className='relative'> <div className="relative">
<div className='absolute z-50 h-full w-full'> <div className="absolute z-50 h-full w-full">
<button className='relative flex h-full w-full items-center justify-center transition hover:scale-110'> <button className="relative flex h-full w-full items-center justify-center transition hover:scale-110">
<input <input
type='file' type="file"
className='absolute h-full w-full cursor-pointer opacity-0' className="absolute h-full w-full cursor-pointer opacity-0"
onChange={handleFileChange} onChange={handleFileChange}
/> />
<PhotographIcon color='white' className='h-8 w-8' /> <PhotographIcon color="white" className="h-8 w-8" />
</button> </button>
</div> </div>
<div className='flex items-center justify-center rounded-full bg-black shadow-xl'> <div className="flex items-center justify-center rounded-full bg-black shadow-xl">
<Image <Image
quality={100} quality={100}
className='rounded-full opacity-50' className="rounded-full opacity-50"
src={ src={
guild.icon == null guild.icon == null
? '/images/data/guild-default.png' ? "/images/data/guild-default.png"
: guild.icon : guild.icon
} }
alt='Profil Picture' alt="Profil Picture"
draggable='false' draggable="false"
height={125} height={125}
width={125} width={125}
/> />
</div> </div>
</div> </div>
<div className='mx-12 flex flex-col'> <div className="mx-12 flex flex-col">
<Input <Input
name='name' name="name"
label={t('common:name')} label={t("common:name")}
placeholder={t('common:name')} placeholder={t("common:name")}
className='!mt-0' className="!mt-0"
onChange={onChange} onChange={onChange}
value={inputValues.name} value={inputValues.name}
error={getFirstErrorTranslation(errors.name)} error={getFirstErrorTranslation(errors.name)}
data-cy='guild-name-input' data-cy="guild-name-input"
/> />
<Textarea <Textarea
name='description' name="description"
label={'Description'} label={"Description"}
placeholder={'Description'} placeholder={"Description"}
id='textarea-description' id="textarea-description"
onChange={onChange} onChange={onChange}
value={inputValues.description ?? ''} value={inputValues.description ?? ""}
data-cy='guild-description-input' data-cy="guild-description-input"
/> />
</div> </div>
</div> </div>
</div> </div>
)} )}
<div className='mt-12 flex flex-col items-center justify-center sm:w-fit'> <div className="mt-12 flex flex-col items-center justify-center sm:w-fit">
<div className='space-x-6'> <div className="space-x-6">
{member.isOwner ? ( {member.isOwner ? (
<> <>
<Button type='submit' data-cy='button-save-guild-settings'> <Button type="submit" data-cy="button-save-guild-settings">
{t('application:save')} {t("application:save")}
</Button> </Button>
<Button <Button
type='button' type="button"
color='red' color="red"
onClick={handleConfirmation} onClick={handleConfirmation}
data-cy='button-delete-guild-settings' data-cy="button-delete-guild-settings"
> >
{t('application:delete')} {t("application:delete")}
</Button> </Button>
</> </>
) : ( ) : (
<Button <Button
color='red' color="red"
onClick={handleLeave} onClick={handleLeave}
data-cy='button-leave-guild-settings' data-cy="button-leave-guild-settings"
> >
{t('application:leave')} {guild.name} {t("application:leave")} {guild.name}
</Button> </Button>
)} )}
</div> </div>
@ -213,17 +217,17 @@ export const GuildSettings: React.FC = () => {
</Form> </Form>
<div <div
className={classNames( className={classNames(
'pointer-events-none invisible absolute z-50 flex h-full w-full items-center justify-center bg-black bg-opacity-90 opacity-0 backdrop-blur-md transition-all', "pointer-events-none invisible absolute z-50 flex h-full w-full items-center justify-center bg-black bg-opacity-90 opacity-0 backdrop-blur-md transition-all",
{ 'pointer-events-auto !visible !opacity-100': confirmation } { "pointer-events-auto !visible !opacity-100": confirmation },
)} )}
> >
<ConfirmPopup <ConfirmPopup
className={classNames('relative top-8 transition-all', { className={classNames("relative top-8 transition-all", {
'!top-0': confirmation "!top-0": confirmation,
})} })}
handleYes={handleDelete} handleYes={handleDelete}
handleNo={handleConfirmation} handleNo={handleConfirmation}
title={`${t('application:delete-the-guild')} ?`} title={`${t("application:delete-the-guild")} ?`}
/> />
</div> </div>
</> </>

View File

@ -1 +1 @@
export * from './GuildSettings' export * from "./GuildSettings"

View File

@ -1,8 +1,8 @@
import { memo } from 'react' import { memo } from "react"
import Image from 'next/image' import Image from "next/image"
import type { GuildWithDefaultChannelId } from '../../../models/Guild' import type { GuildWithDefaultChannelId } from "../../../models/Guild"
import { IconLink } from '../../design/IconLink' import { IconLink } from "../../design/IconLink"
export interface GuildProps { export interface GuildProps {
guild: GuildWithDefaultChannelId guild: GuildWithDefaultChannelId
@ -14,19 +14,19 @@ const GuildMemo: React.FC<GuildProps> = (props) => {
return ( return (
<IconLink <IconLink
className='mt-2' className="mt-2"
href={`/application/${guild.id}/${guild.defaultChannelId}`} href={`/application/${guild.id}/${guild.defaultChannelId}`}
selected={selected} selected={selected}
title={guild.name} title={guild.name}
> >
<div className='pl-[6px]'> <div className="pl-[6px]">
<Image <Image
quality={100} quality={100}
className='rounded-full' className="rounded-full"
src={ src={
guild.icon != null ? guild.icon : '/images/data/guild-default.png' guild.icon != null ? guild.icon : "/images/data/guild-default.png"
} }
alt='logo' alt="logo"
width={48} width={48}
height={48} height={48}
draggable={false} draggable={false}

View File

@ -1,9 +1,9 @@
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from "react-infinite-scroll-component"
import { Loader } from '../../design/Loader' import { Loader } from "../../design/Loader"
import { useGuilds } from '../../../contexts/Guilds' import { useGuilds } from "../../../contexts/Guilds"
import type { GuildsPath } from '..' import type { GuildsPath } from ".."
import { Guild } from './Guild' import { Guild } from "./Guild"
export interface GuildsProps { export interface GuildsProps {
path: GuildsPath | string path: GuildsPath | string
@ -16,19 +16,19 @@ export const Guilds: React.FC<GuildsProps> = (props) => {
return ( return (
<div <div
id='guilds-list' id="guilds-list"
className='border-r-1 scrollbar-firefox-support mt-[130px] h-full min-w-[92px] space-y-2 overflow-y-auto border-gray-500 pt-2 dark:border-white/20' className="border-r-1 scrollbar-firefox-support mt-[130px] h-full min-w-[92px] space-y-2 overflow-y-auto border-gray-500 pt-2 dark:border-white/20"
> >
<InfiniteScroll <InfiniteScroll
className='guilds-list' className="guilds-list"
dataLength={guilds.length} dataLength={guilds.length}
next={nextPage} next={nextPage}
hasMore={hasMore} hasMore={hasMore}
scrollableTarget='guilds-list' scrollableTarget="guilds-list"
loader={<Loader />} loader={<Loader />}
> >
{guilds.map((guild) => { {guilds.map((guild) => {
const selected = typeof path !== 'string' && path.guildId === guild.id const selected = typeof path !== "string" && path.guildId === guild.id
return <Guild key={guild.id} guild={guild} selected={selected} /> return <Guild key={guild.id} guild={guild} selected={selected} />
})} })}
</InfiniteScroll> </InfiniteScroll>

View File

@ -1 +1 @@
export * from './Guilds' export * from "./Guilds"

View File

@ -1,17 +1,17 @@
import Image from 'next/image' import Image from "next/image"
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import { useState } from 'react' import { useState } from "react"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import classNames from 'clsx' import classNames from "clsx"
import axios from 'axios' import axios from "axios"
import { Emoji } from '../../Emoji' import { Emoji } from "../../Emoji"
import { ConfirmPopup } from '../ConfirmPopup' import { ConfirmPopup } from "../ConfirmPopup"
import type { import type {
GuildPublic as GuildPublicType, GuildPublic as GuildPublicType,
GuildWithDefaultChannelId GuildWithDefaultChannelId,
} from '../../../models/Guild' } from "../../../models/Guild"
import { useAuthentication } from '../../../tools/authentication' import { useAuthentication } from "../../../tools/authentication"
export interface GuildPublicProps { export interface GuildPublicProps {
guild: GuildPublicType guild: GuildPublicType
@ -36,70 +36,70 @@ export const GuildPublic: React.FC<GuildPublicProps> = (props) => {
guild: GuildWithDefaultChannelId guild: GuildWithDefaultChannelId
}>(`/guilds/${guild.id}/members/join`) }>(`/guilds/${guild.id}/members/join`)
await router.push( await router.push(
`/application/${guild.id}/${data.guild.defaultChannelId}` `/application/${guild.id}/${data.guild.defaultChannelId}`,
) )
} catch (error) { } catch (error) {
if ( if (
axios.isAxiosError(error) && axios.isAxiosError(error) &&
error.response?.status === 400 && error.response?.status === 400 &&
typeof error?.response?.data.defaultChannelId === 'number' typeof error?.response?.data.defaultChannelId === "number"
) { ) {
const defaultChannelId = error.response.data.defaultChannelId as number const defaultChannelId = error.response.data.defaultChannelId as number
await router.push(`/application/${guild.id}/${defaultChannelId}`) await router.push(`/application/${guild.id}/${defaultChannelId}`)
} else { } else {
await router.push('/application') await router.push("/application")
} }
} }
} }
return ( return (
<div className='relative h-80 overflow-hidden rounded border border-gray-500 shadow-lg transition duration-200 ease-in-out hover:-translate-y-2 hover:shadow-none dark:border-gray-700'> <div className="relative h-80 overflow-hidden rounded border border-gray-500 shadow-lg transition duration-200 ease-in-out hover:-translate-y-2 hover:shadow-none dark:border-gray-700">
<div <div
className={classNames( className={classNames(
'flex h-full cursor-pointer flex-col items-center justify-center p-4 pt-8 transition duration-200 ease-in-out', "flex h-full cursor-pointer flex-col items-center justify-center p-4 pt-8 transition duration-200 ease-in-out",
{ '-translate-x-full': isConfirmed } { "-translate-x-full": isConfirmed },
)} )}
onClick={handleIsConfirmed} onClick={handleIsConfirmed}
> >
<Image <Image
quality={100} quality={100}
className='rounded-full' className="rounded-full"
src={ src={
guild.icon != null ? guild.icon : '/images/data/guild-default.png' guild.icon != null ? guild.icon : "/images/data/guild-default.png"
} }
alt='logo' alt="logo"
width={80} width={80}
height={80} height={80}
/> />
<div className='m-2 mt-6 w-full px-4 text-center'> <div className="m-2 mt-6 w-full px-4 text-center">
<h3 <h3
data-cy='guild-name' data-cy="guild-name"
className='center mb-2 w-full truncate text-xl font-bold' className="center mb-2 w-full truncate text-xl font-bold"
> >
{guild.name} {guild.name}
</h3> </h3>
<p className='break-words'> <p className="break-words">
{guild.description != null ? ( {guild.description != null ? (
guild.description guild.description
) : ( ) : (
<span className='flex h-full items-center justify-center opacity-40 dark:opacity-20'> <span className="flex h-full items-center justify-center opacity-40 dark:opacity-20">
<Emoji value=':eyes:' size={25} /> <Emoji value=":eyes:" size={25} />
<span className='ml-2'>{t('application:nothing-here')}</span> <span className="ml-2">{t("application:nothing-here")}</span>
</span> </span>
)} )}
</p> </p>
</div> </div>
<p className='mt-auto flex flex-col text-green-800 dark:text-green-400'> <p className="mt-auto flex flex-col text-green-800 dark:text-green-400">
{guild.membersCount} {t('application:members')} {guild.membersCount} {t("application:members")}
</p> </p>
</div> </div>
<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 left-full top-1/2 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,
} },
)} )}
handleYes={handleYes} handleYes={handleYes}
handleNo={handleIsConfirmed} handleNo={handleIsConfirmed}

View File

@ -1,17 +1,17 @@
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import { useEffect, useState } from 'react' import { useEffect, useState } from "react"
import InfiniteScroll from 'react-infinite-scroll-component' import InfiniteScroll from "react-infinite-scroll-component"
import { useAuthentication } from '../../../tools/authentication' import { useAuthentication } from "../../../tools/authentication"
import type { GuildPublic as GuildPublicType } from '../../../models/Guild' import type { GuildPublic as GuildPublicType } from "../../../models/Guild"
import { Loader } from '../../design/Loader' import { Loader } from "../../design/Loader"
import { GuildPublic } from './GuildPublic' import { GuildPublic } from "./GuildPublic"
import { usePagination } from '../../../hooks/usePagination' import { usePagination } from "../../../hooks/usePagination"
import type { SocketData } from '../../../tools/handleSocketData' import type { SocketData } from "../../../tools/handleSocketData"
import { handleSocketData } from '../../../tools/handleSocketData' import { handleSocketData } from "../../../tools/handleSocketData"
export const JoinGuildsPublic: React.FC = () => { export const JoinGuildsPublic: React.FC = () => {
const [search, setSearch] = useState('') const [search, setSearch] = useState("")
const { authentication } = useAuthentication() const { authentication } = useAuthentication()
const { t } = useTranslation() const { t } = useTranslation()
@ -19,19 +19,19 @@ export const JoinGuildsPublic: React.FC = () => {
const { items, hasMore, nextPage, resetPagination, setItems } = const { items, hasMore, nextPage, resetPagination, setItems } =
usePagination<GuildPublicType>({ usePagination<GuildPublicType>({
api: authentication.api, api: authentication.api,
url: '/guilds/public' url: "/guilds/public",
}) })
useEffect(() => { useEffect(() => {
authentication?.socket?.on( authentication?.socket?.on(
'guilds', "guilds",
(data: SocketData<GuildPublicType>) => { (data: SocketData<GuildPublicType>) => {
handleSocketData({ data, setItems }) handleSocketData({ data, setItems })
} },
) )
return () => { return () => {
authentication?.socket?.off('guilds') authentication?.socket?.off("guilds")
} }
}, [authentication.socket, setItems]) }, [authentication.socket, setItems])
@ -45,23 +45,23 @@ export const JoinGuildsPublic: React.FC = () => {
} }
return ( return (
<div className='flex h-full w-full flex-col transition-all'> <div className="flex h-full w-full flex-col transition-all">
<input <input
data-cy='search-guild-input' data-cy="search-guild-input"
onChange={handleChange} onChange={handleChange}
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' 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")}...`}
/> />
<div className='w-full p-12'> <div className="w-full p-12">
<InfiniteScroll <InfiniteScroll
className='guilds-public-list relative mx-auto grid max-w-[1400px] grid-cols-[repeat(auto-fill,_minmax(20em,_1fr))] gap-8 !overflow-visible' className="guilds-public-list relative mx-auto grid max-w-[1400px] grid-cols-[repeat(auto-fill,_minmax(20em,_1fr))] gap-8 !overflow-visible"
dataLength={items.length} dataLength={items.length}
next={nextPage} next={nextPage}
scrollableTarget='application-page-content' scrollableTarget="application-page-content"
hasMore={hasMore} hasMore={hasMore}
loader={<Loader className='absolute left-1/2 -translate-x-1/2' />} loader={<Loader className="absolute left-1/2 -translate-x-1/2" />}
> >
{items.map((guild) => { {items.map((guild) => {
return <GuildPublic guild={guild} key={guild.id} /> return <GuildPublic guild={guild} key={guild.id} />

View File

@ -1 +1 @@
export * from './JoinGuildsPublic' export * from "./JoinGuildsPublic"

View File

@ -1,9 +1,9 @@
import { memo } from 'react' import { memo } from "react"
import Image from 'next/image' import Image from "next/image"
import Link from 'next/link' import Link from "next/link"
import type { MemberWithPublicUser } from '../../../models/Member' import type { MemberWithPublicUser } from "../../../models/Member"
import { Emoji } from '../../Emoji' import { Emoji } from "../../Emoji"
export interface MemberProps { export interface MemberProps {
member: MemberWithPublicUser member: MemberWithPublicUser
@ -14,27 +14,27 @@ 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 px-6 py-2 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={
member.user.logo == null member.user.logo == null
? '/images/data/user-default.png' ? "/images/data/user-default.png"
: member.user.logo : member.user.logo
} }
alt={"Users's profil picture"} alt={"Users's profil picture"}
height={50} height={50}
width={50} width={50}
draggable={false} draggable={false}
className='rounded-full' className="rounded-full"
/> />
</div> </div>
<div className='ml-5'> <div className="ml-5">
<p data-cy='member-user-name' className='flex truncate font-bold'> <p data-cy="member-user-name" className="flex truncate font-bold">
{member.user.name} {member.user.name}
{member.isOwner && ( {member.isOwner && (
<span className='ml-4'> <span className="ml-4">
<Emoji value=':crown:' size={18} /> <Emoji value=":crown:" size={18} />
</span> </span>
)} )}
</p> </p>

View File

@ -1,11 +1,11 @@
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 { Divider } from '../../design/Divider' import { Divider } from "../../design/Divider"
import { Loader } from '../../design/Loader' import { Loader } from "../../design/Loader"
import { useMembers } from '../../../contexts/Members' import { useMembers } from "../../../contexts/Members"
import { Member } from './Member' import { Member } from "./Member"
import { capitalize } from '../../../tools/utils/capitalize' import { capitalize } from "../../../tools/utils/capitalize"
export const Members: React.FC = () => { export const Members: React.FC = () => {
const { members, hasMore, nextPage } = useMembers() const { members, hasMore, nextPage } = useMembers()
@ -14,14 +14,14 @@ export const Members: React.FC = () => {
return ( return (
<> <>
<div className='mb-2'> <div className="mb-2">
<h1 data-cy='members-title' className='my-2 pt-2 text-center text-xl'> <h1 data-cy="members-title" className="my-2 pt-2 text-center text-xl">
{capitalize(t('application:members'))} {capitalize(t("application:members"))}
</h1> </h1>
<Divider /> <Divider />
</div> </div>
<InfiniteScroll <InfiniteScroll
className='members-list' className="members-list"
dataLength={members.length} dataLength={members.length}
next={nextPage} next={nextPage}
hasMore={hasMore} hasMore={hasMore}

View File

@ -1 +1 @@
export * from './Members' export * from "./Members"

View File

@ -1,7 +1,7 @@
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import TextareaAutosize from 'react-textarea-autosize' import TextareaAutosize from "react-textarea-autosize"
import type { MessageProps } from '../Message' import type { MessageProps } from "../Message"
export interface EditMessageProps extends MessageProps { export interface EditMessageProps extends MessageProps {
handleEdit: () => Promise<void> handleEdit: () => Promise<void>
@ -15,7 +15,7 @@ export const EditMessage: React.FC<
const { t } = useTranslation() const { t } = useTranslation()
const handleEditSubmit: React.FormEventHandler<HTMLFormElement> = async ( const handleEditSubmit: React.FormEventHandler<HTMLFormElement> = async (
event event,
) => { ) => {
event.preventDefault() event.preventDefault()
await handleEdit() await handleEdit()
@ -23,23 +23,23 @@ export const EditMessage: React.FC<
return ( return (
<form <form
className='flex h-full w-full items-center' className="flex h-full w-full items-center"
onSubmit={handleEditSubmit} onSubmit={handleEditSubmit}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
<TextareaAutosize <TextareaAutosize
className='scrollbar-firefox-support w-full resize-none bg-transparent p-2 tracking-wide outline-none' className="scrollbar-firefox-support w-full resize-none bg-transparent p-2 tracking-wide outline-none"
placeholder={t('application:write-a-message')} placeholder={t("application:write-a-message")}
wrap='soft' wrap="soft"
maxRows={6} maxRows={6}
name='message' name="message"
defaultValue={message.value} defaultValue={message.value}
ref={textareaRef} ref={textareaRef}
autoFocus autoFocus
onFocus={(event) => { onFocus={(event) => {
event.currentTarget.setSelectionRange( event.currentTarget.setSelectionRange(
event.currentTarget.value.length, event.currentTarget.value.length,
event.currentTarget.value.length event.currentTarget.value.length,
) )
}} }}
/> />

View File

@ -1 +1 @@
export * from './EditMessage' export * from "./EditMessage"

View File

@ -1,15 +1,15 @@
import { useState, useRef } from 'react' 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 { MessageWithMember } from '../../../../models/Message' import type { MessageWithMember } from "../../../../models/Message"
import { MessageText } from './MessageText' import { MessageText } from "./MessageText"
import { Loader } from '../../../design/Loader' import { Loader } from "../../../design/Loader"
import { MessageFile } from './MessageFile' import { MessageFile } from "./MessageFile"
import { useAuthentication } from '../../../../tools/authentication' import { useAuthentication } from "../../../../tools/authentication"
import { MessageOptions } from './MessageOptions' import { MessageOptions } from "./MessageOptions"
import { EditMessage } from './EditMessage' import { EditMessage } from "./EditMessage"
export interface MessageProps { export interface MessageProps {
message: MessageWithMember message: MessageWithMember
@ -23,12 +23,12 @@ export const Message: React.FC<MessageProps> = (props) => {
const { authentication, user } = useAuthentication() const { authentication, user } = useAuthentication()
const handleTextareaKeyDown: React.KeyboardEventHandler<HTMLFormElement> = ( const handleTextareaKeyDown: React.KeyboardEventHandler<HTMLFormElement> = (
event event,
) => { ) => {
if (event.key === 'Enter' && !event.shiftKey) { if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault() event.preventDefault()
event.currentTarget.dispatchEvent( event.currentTarget.dispatchEvent(
new Event('submit', { cancelable: true, bubbles: true }) new Event("submit", { cancelable: true, bubbles: true }),
) )
} }
} }
@ -36,13 +36,13 @@ export const Message: React.FC<MessageProps> = (props) => {
const handleEdit = async (): Promise<void> => { const handleEdit = async (): Promise<void> => {
const newMessage = textareaReference.current?.value ?? message.value const newMessage = textareaReference.current?.value ?? message.value
if ( if (
typeof newMessage === 'string' && typeof newMessage === "string" &&
newMessage.length > 0 && newMessage.length > 0 &&
newMessage !== message.value newMessage !== message.value
) { ) {
try { try {
await authentication.api.put(`/messages/${message.id}`, { await authentication.api.put(`/messages/${message.id}`, {
value: newMessage value: newMessage,
}) })
} catch {} } catch {}
} }
@ -58,17 +58,17 @@ export const Message: React.FC<MessageProps> = (props) => {
return ( return (
<div <div
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"
> >
<Link href={`/application/users/${message.member.user.id}`}> <Link href={`/application/users/${message.member.user.id}`}>
<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
quality={100} quality={100}
className='rounded-full' className="rounded-full"
src={ src={
message.member.user.logo == null message.member.user.logo == null
? '/images/data/user-default.png' ? "/images/data/user-default.png"
: message.member.user.logo : message.member.user.logo
} }
alt={"Users's profil picture"} alt={"Users's profil picture"}
@ -79,33 +79,33 @@ export const Message: React.FC<MessageProps> = (props) => {
</div> </div>
</div> </div>
</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}`}>
<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>
</Link> </Link>
<span <span
data-cy='message-date' data-cy="message-date"
className='ml-4 select-none text-xs text-gray-500 dark:text-gray-200' className="ml-4 select-none text-xs text-gray-500 dark:text-gray-200"
> >
{date.format(new Date(message.createdAt), 'DD/MM/YYYY - HH:mm:ss')} {date.format(new Date(message.createdAt), "DD/MM/YYYY - HH:mm:ss")}
</span> </span>
</div> </div>
{message.member.userId === user.id && ( {message.member.userId === user.id && (
<MessageOptions <MessageOptions
message={message} message={message}
editMode={isEditing ? ':white_check_mark:' : ':pencil2:'} editMode={isEditing ? ":white_check_mark:" : ":pencil2:"}
handleEdit={isEditing ? handleEdit : handleEditMode} handleEdit={isEditing ? handleEdit : handleEditMode}
/> />
)} )}
{message.type === 'text' ? ( {message.type === "text" ? (
<> <>
{isEditing ? ( {isEditing ? (
<EditMessage <EditMessage
@ -118,7 +118,7 @@ export const Message: React.FC<MessageProps> = (props) => {
<MessageText message={message} /> <MessageText message={message} />
)} )}
</> </>
) : message.type === 'file' ? ( ) : message.type === "file" ? (
<MessageFile message={message} /> <MessageFile message={message} />
) : ( ) : (
<Loader /> <Loader />

View File

@ -1,14 +1,14 @@
export const FileIcon: React.FC = () => { export const FileIcon: React.FC = () => {
return ( return (
<svg <svg
className='fill-current text-black dark:text-white' className="fill-current text-black dark:text-white"
width='21' width="21"
height='26' height="26"
viewBox='0 0 21 26' viewBox="0 0 21 26"
fill='none' fill="none"
xmlns='http://www.w3.org/2000/svg' xmlns="http://www.w3.org/2000/svg"
> >
<path d='M2.625 0C1.92881 0 1.26113 0.273928 0.768845 0.761522C0.276562 1.24912 0 1.91044 0 2.6V23.4C0 24.0896 0.276562 24.7509 0.768845 25.2385C1.26113 25.7261 1.92881 26 2.625 26H18.375C19.0712 26 19.7389 25.7261 20.2312 25.2385C20.7234 24.7509 21 24.0896 21 23.4V7.8L13.125 0H2.625ZM13.125 9.1H11.8125V2.6L18.375 9.1H13.125Z' /> <path d="M2.625 0C1.92881 0 1.26113 0.273928 0.768845 0.761522C0.276562 1.24912 0 1.91044 0 2.6V23.4C0 24.0896 0.276562 24.7509 0.768845 25.2385C1.26113 25.7261 1.92881 26 2.625 26H18.375C19.0712 26 19.7389 25.7261 20.2312 25.2385C20.7234 24.7509 21 24.0896 21 23.4V7.8L13.125 0H2.625ZM13.125 9.1H11.8125V2.6L18.375 9.1H13.125Z" />
</svg> </svg>
) )
} }

View File

@ -1,18 +1,18 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from "react"
import axios from 'axios' import axios from "axios"
import prettyBytes from 'pretty-bytes' import prettyBytes from "pretty-bytes"
import { DownloadIcon } from '@heroicons/react/solid' import { DownloadIcon } from "@heroicons/react/solid"
import type { MessageWithMember } from '../../../../../models/Message' import type { MessageWithMember } from "../../../../../models/Message"
import { Loader } from '../../../../design/Loader' import { Loader } from "../../../../design/Loader"
import { FileIcon } from './FileIcon' import { FileIcon } from "./FileIcon"
import { api } from '../../../../../tools/api' import { api } from "../../../../../tools/api"
const supportedImageMimetype = [ const supportedImageMimetype = [
'image/png', "image/png",
'image/jpg', "image/jpg",
'image/jpeg', "image/jpeg",
'image/gif' "image/gif",
] ]
export interface FileData { export interface FileData {
@ -34,8 +34,8 @@ export const MessageFile: React.FC<MessageContentProps> = (props) => {
const fetchData = async (): Promise<void> => { const fetchData = async (): Promise<void> => {
const { data } = await api.get(message.value, { const { data } = await api.get(message.value, {
responseType: 'blob', responseType: "blob",
cancelToken: ourRequest.token cancelToken: ourRequest.token,
}) })
const fileURL = URL.createObjectURL(data) const fileURL = URL.createObjectURL(data)
setFile({ blob: data, url: fileURL }) setFile({ blob: data, url: fileURL })
@ -52,27 +52,27 @@ export const MessageFile: React.FC<MessageContentProps> = (props) => {
} }
if (supportedImageMimetype.includes(message.mimetype)) { if (supportedImageMimetype.includes(message.mimetype)) {
return ( return (
<a href={file.url} target='_blank' rel='noreferrer'> <a href={file.url} target="_blank" rel="noreferrer">
<img <img
data-cy={`message-file-image-${message.id}`} data-cy={`message-file-image-${message.id}`}
className='max-h-80 sm:max-w-xs' className="max-h-80 sm:max-w-xs"
src={file.url} src={file.url}
alt={message.value} alt={message.value}
/> />
</a> </a>
) )
} }
if (message.mimetype.startsWith('audio/')) { if (message.mimetype.startsWith("audio/")) {
return ( return (
<audio controls data-cy={`message-file-audio-${message.id}`}> <audio controls data-cy={`message-file-audio-${message.id}`}>
<source src={file.url} type={message.mimetype} /> <source src={file.url} type={message.mimetype} />
</audio> </audio>
) )
} }
if (message.mimetype.startsWith('video/')) { if (message.mimetype.startsWith("video/")) {
return ( return (
<video <video
className='max-h-80 max-w-xs' className="max-h-80 max-w-xs"
controls controls
data-cy={`message-file-video-${message.id}`} data-cy={`message-file-video-${message.id}`}
> >
@ -82,17 +82,17 @@ export const MessageFile: React.FC<MessageContentProps> = (props) => {
} }
return ( return (
<a href={file.url} download data-cy={`message-file-download-${message.id}`}> <a href={file.url} download data-cy={`message-file-download-${message.id}`}>
<div className='flex items-center'> <div className="flex items-center">
<div className='flex items-center'> <div className="flex items-center">
<div> <div>
<FileIcon /> <FileIcon />
</div> </div>
<div className='ml-4'> <div className="ml-4">
<p>{file.blob.type}</p> <p>{file.blob.type}</p>
<p className='mt-1'>{prettyBytes(file.blob.size)}</p> <p className="mt-1">{prettyBytes(file.blob.size)}</p>
</div> </div>
</div> </div>
<DownloadIcon className='ml-4 h-8 w-8' /> <DownloadIcon className="ml-4 h-8 w-8" />
</div> </div>
</a> </a>
) )

View File

@ -1 +1 @@
export * from './MessageFile' export * from "./MessageFile"

View File

@ -1,12 +1,12 @@
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import { useAuthentication } from '../../../../../tools/authentication' import { useAuthentication } from "../../../../../tools/authentication"
import { Emoji } from '../../../../Emoji' import { Emoji } from "../../../../Emoji"
import type { MessageProps } from '../Message' import type { MessageProps } from "../Message"
interface MessageOptionsProps extends MessageProps { interface MessageOptionsProps extends MessageProps {
handleEdit: () => void handleEdit: () => void
editMode: ':white_check_mark:' | ':pencil2:' editMode: ":white_check_mark:" | ":pencil2:"
} }
export const MessageOptions: React.FC< export const MessageOptions: React.FC<
@ -22,22 +22,22 @@ export const MessageOptions: React.FC<
} }
return ( return (
<div className='absolute -top-8 right-6 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"
title={t('application:edit')} title={t("application:edit")}
onClick={handleEdit} onClick={handleEdit}
> >
<Emoji value={editMode} size={18} /> <Emoji value={editMode} size={18} />
</div> </div>
)} )}
<div <div
className='message-options rounded-r-lg border-r-slate-600' className="message-options rounded-r-lg border-r-slate-600"
title={t('application:delete')} title={t("application:delete")}
onClick={handleDeleteMessage} onClick={handleDeleteMessage}
> >
<Emoji value=':wastebasket:' size={18} /> <Emoji value=":wastebasket:" size={18} />
</div> </div>
</div> </div>
) )

View File

@ -1 +1 @@
export * from './MessageOptions' export * from "./MessageOptions"

View File

@ -1,16 +1,16 @@
import { useMemo } from 'react' import { useMemo } from "react"
import ReactMarkdown from 'react-markdown' import ReactMarkdown from "react-markdown"
import gfm from 'remark-gfm' import gfm from "remark-gfm"
import remarkBreaks from 'remark-breaks' import remarkBreaks from "remark-breaks"
import remarkMath from 'remark-math' import remarkMath from "remark-math"
import rehypeKatex from 'rehype-katex' import rehypeKatex from "rehype-katex"
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism' import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism"
import 'katex/dist/katex.min.css' import "katex/dist/katex.min.css"
import { Emoji, emojiPlugin, isStringWithOnlyOneEmoji } from '../../../../Emoji' import { Emoji, emojiPlugin, isStringWithOnlyOneEmoji } from "../../../../Emoji"
import type { MessageWithMember } from '../../../../../models/Message' import type { MessageWithMember } from "../../../../../models/Message"
export interface MessageContentProps { export interface MessageContentProps {
message: MessageWithMember message: MessageWithMember
@ -35,41 +35,42 @@ export const MessageText: React.FC<MessageContentProps> = (props) => {
return ( return (
<ReactMarkdown <ReactMarkdown
disallowedElements={['table']} disallowedElements={["table"]}
unwrapDisallowed unwrapDisallowed
remarkPlugins={[[gfm], [remarkBreaks], [remarkMath]]} remarkPlugins={[[gfm], [remarkBreaks], [remarkMath]]}
rehypePlugins={[[emojiPlugin], [rehypeKatex]]} rehypePlugins={[[emojiPlugin], [rehypeKatex]]}
linkTarget='_blank' linkTarget="_blank"
components={{ components={{
a: (props) => { a: (props) => {
return ( return (
<a <a
className='text-green-800 hover:underline dark:text-green-400' className="text-green-800 hover:underline dark:text-green-400"
{...props} {...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
const match = /language-(\w+)/.exec(className ?? '') const match = /language-(\w+)/.exec(className ?? "")
return !(inline as boolean) && match != null ? ( return !(inline as boolean) && match != null ? (
<SyntaxHighlighter <SyntaxHighlighter
style={vscDarkPlus as any} style={vscDarkPlus as any}
language={match[1]} language={match[1]}
PreTag='div' PreTag="div"
{...props} {...props}
> >
{String(children).replace(/\n$/, '')} {String(children).replace(/\n$/, "")}
</SyntaxHighlighter> </SyntaxHighlighter>
) : ( ) : (
<code className={className} {...props}> <code className={className} {...props}>
{children} {children}
</code> </code>
) )
} },
}} }}
> >
{message.value} {message.value}

View File

@ -1 +1 @@
export * from './MessageText' export * from "./MessageText"

View File

@ -1 +1 @@
export * from './Message' export * from "./Message"

View File

@ -1,10 +1,10 @@
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 { Loader } from '../../design/Loader' import { Loader } from "../../design/Loader"
import { Message } from './Message' import { Message } from "./Message"
import { useMessages } from '../../../contexts/Messages' import { useMessages } from "../../../contexts/Messages"
import { Emoji } from '../../Emoji' import { Emoji } from "../../Emoji"
export const Messages: React.FC = () => { export const Messages: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -13,25 +13,25 @@ export const Messages: React.FC = () => {
if (messages.length === 0) { if (messages.length === 0) {
return ( return (
<div <div
id='messages' id="messages"
className='scrollbar-firefox-support mt-8 flex w-full flex-1 flex-col overflow-y-auto text-center text-lg transition-all' className="scrollbar-firefox-support mt-8 flex w-full flex-1 flex-col overflow-y-auto text-center text-lg transition-all"
> >
<p> <p>
{t('application:nothing-here')} <Emoji value=':ghost:' size={20} /> {t("application:nothing-here")} <Emoji value=":ghost:" size={20} />
</p> </p>
<p>{t('application:start-chatting-kill-ghost')}</p> <p>{t("application:start-chatting-kill-ghost")}</p>
</div> </div>
) )
} }
return ( return (
<div <div
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"
> >
<InfiniteScroll <InfiniteScroll
scrollableTarget='messages' scrollableTarget="messages"
className='messages-list !overflow-x-hidden' className="messages-list !overflow-x-hidden"
dataLength={messages.length} dataLength={messages.length}
next={nextPage} next={nextPage}
inverse inverse

View File

@ -1 +1 @@
export * from './Messages' export * from "./Messages"

View File

@ -1,9 +1,9 @@
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import { PlusSmIcon, ArrowDownIcon } from '@heroicons/react/solid' import { PlusSmIcon, ArrowDownIcon } from "@heroicons/react/solid"
import classNames from 'clsx' import classNames from "clsx"
import Image from 'next/image' import Image from "next/image"
import { PopupGuildCard } from './PopupGuildCard' import { PopupGuildCard } from "./PopupGuildCard"
export interface PopupGuildProps { export interface PopupGuildProps {
className?: string className?: string
@ -18,43 +18,43 @@ export const PopupGuild: React.FC<PopupGuildProps> = (props) => {
<div <div
className={classNames( className={classNames(
className, className,
'h-full-without-header flex min-w-full flex-wrap items-center justify-center overflow-y-auto p-8' "h-full-without-header flex min-w-full flex-wrap items-center justify-center overflow-y-auto p-8",
)} )}
> >
<PopupGuildCard <PopupGuildCard
image={ image={
<Image <Image
quality={100} quality={100}
src='/images/svg/design/create-guild.svg' src="/images/svg/design/create-guild.svg"
alt={t('application:create-a-guild')} alt={t("application:create-a-guild")}
draggable='false' draggable="false"
width={230} width={230}
height={230} height={230}
/> />
} }
description={t('application:create-a-guild-description')} description={t("application:create-a-guild-description")}
link={{ link={{
icon: <PlusSmIcon className='mr-2 h-8 w-8' />, icon: <PlusSmIcon className="mr-2 h-8 w-8" />,
text: t('application:create-a-guild'), text: t("application:create-a-guild"),
href: '/application/guilds/create' href: "/application/guilds/create",
}} }}
/> />
<PopupGuildCard <PopupGuildCard
image={ image={
<Image <Image
quality={100} quality={100}
src='/images/svg/design/join-guild.svg' src="/images/svg/design/join-guild.svg"
alt={t('application:join-a-guild')} alt={t("application:join-a-guild")}
draggable='false' draggable="false"
width={200} width={200}
height={200} height={200}
/> />
} }
description={t('application:join-a-guild-description')} description={t("application:join-a-guild-description")}
link={{ link={{
icon: <ArrowDownIcon className='mr-2 h-6 w-6' />, icon: <ArrowDownIcon className="mr-2 h-6 w-6" />,
text: t('application:join-a-guild'), text: t("application:join-a-guild"),
href: '/application/guilds/join' href: "/application/guilds/join",
}} }}
/> />
</div> </div>

View File

@ -1,5 +1,5 @@
import React from 'react' import React from "react"
import Link from 'next/link' import Link from "next/link"
export interface PopupGuildCardProps { export interface PopupGuildCardProps {
image: JSX.Element image: JSX.Element
@ -15,17 +15,17 @@ export const PopupGuildCard: React.FC<PopupGuildCardProps> = (props) => {
const { image, description, link } = props const { image, description, link } = props
return ( return (
<div className='m-8 h-96 w-80 rounded-2xl bg-gray-800'> <div className="m-8 h-96 w-80 rounded-2xl bg-gray-800">
<div className='flex h-1/2 w-full items-center justify-center'> <div className="flex h-1/2 w-full items-center justify-center">
{image} {image}
</div> </div>
<div className='mt-2 flex h-1/2 w-full flex-col justify-between rounded-b-2xl bg-gray-700 shadow-sm'> <div className="mt-2 flex h-1/2 w-full flex-col justify-between rounded-b-2xl bg-gray-700 shadow-sm">
<p className='mt-6 px-8 text-center text-sm text-gray-200'> <p className="mt-6 px-8 text-center text-sm text-gray-200">
{description} {description}
</p> </p>
<Link <Link
href={link.href} 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' 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}

View File

@ -1 +1 @@
export * from './PopupGuild' export * from "./PopupGuild"

View File

@ -1,12 +1,12 @@
import { useState, useRef } from 'react' import { useState, useRef } from "react"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import TextareaAutosize from 'react-textarea-autosize' import TextareaAutosize from "react-textarea-autosize"
import classNames from 'clsx' import classNames from "clsx"
import type { GuildsChannelsPath } from '..' import type { GuildsChannelsPath } from ".."
import { useAuthentication } from '../../../tools/authentication' import { useAuthentication } from "../../../tools/authentication"
import type { EmojiPickerOnClick } from '../../Emoji' import type { EmojiPickerOnClick } from "../../Emoji"
import { EmojiPicker } from '../../Emoji' import { EmojiPicker } from "../../Emoji"
export interface SendMessageProps { export interface SendMessageProps {
path: GuildsChannelsPath path: GuildsChannelsPath
@ -18,49 +18,54 @@ export const SendMessage: React.FC<SendMessageProps> = (props) => {
const { authentication } = useAuthentication() const { authentication } = useAuthentication()
const [isVisibleEmojiPicker, setIsVisibleEmojiPicker] = useState(false) const [isVisibleEmojiPicker, setIsVisibleEmojiPicker] = useState(false)
const [message, setMessage] = useState('') const [message, setMessage] = useState("")
const textareaReference = useRef<HTMLTextAreaElement>(null) const textareaReference = useRef<HTMLTextAreaElement>(null)
const handleTextareaKeyDown: React.KeyboardEventHandler<HTMLFormElement> = ( const handleTextareaKeyDown: React.KeyboardEventHandler<HTMLFormElement> = (
event event,
) => { ) => {
if (event.key === 'Enter' && !event.shiftKey) { if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault() event.preventDefault()
event.currentTarget.dispatchEvent( event.currentTarget.dispatchEvent(
new Event('submit', { cancelable: true, bubbles: true }) new Event("submit", { cancelable: true, bubbles: true }),
) )
} }
} }
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async ( const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (
event event,
) => { ) => {
event.preventDefault() event.preventDefault()
if (typeof message === 'string' && message.length > 0) { if (typeof message === "string" && message.length > 0) {
await authentication.api.post(`/channels/${path.channelId}/messages`, { await authentication.api.post(`/channels/${path.channelId}/messages`, {
value: message value: message,
}) })
setMessage('') setMessage("")
} }
} }
const handleTextareaChange: React.ChangeEventHandler<HTMLTextAreaElement> = ( const handleTextareaChange: React.ChangeEventHandler<HTMLTextAreaElement> = (
event event,
) => { ) => {
setMessage(event.target.value) setMessage(event.target.value)
} }
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async ( const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async (
event event,
) => { ) => {
const files = event?.target?.files const files = event?.target?.files
if (files != null && files.length === 1 && files[0] != null) { 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",
},
},
) )
} }
} }
@ -72,7 +77,7 @@ export const SendMessage: React.FC<SendMessageProps> = (props) => {
} }
const handleEmojiPicker: EmojiPickerOnClick = (emoji) => { const handleEmojiPicker: EmojiPickerOnClick = (emoji) => {
const emojiColons = emoji.colons ?? '' const emojiColons = emoji.colons ?? ""
setMessage((oldMessage) => { setMessage((oldMessage) => {
return oldMessage + emojiColons return oldMessage + emojiColons
}) })
@ -81,51 +86,51 @@ export const SendMessage: React.FC<SendMessageProps> = (props) => {
} }
return ( return (
<div className='relative p-6 pb-4'> <div className="relative p-6 pb-4">
<div <div
className={classNames( className={classNames(
'absolute bottom-24 right-20 z-50 w-fit transition-all duration-100', "absolute bottom-24 right-20 z-50 w-fit transition-all duration-100",
{ {
'invisible translate-y-5 opacity-0': !isVisibleEmojiPicker "invisible translate-y-5 opacity-0": !isVisibleEmojiPicker,
} },
)} )}
> >
<EmojiPicker onClick={handleEmojiPicker} /> <EmojiPicker onClick={handleEmojiPicker} />
</div> </div>
<div className='flex h-full w-full rounded-lg bg-gray-200 py-1 text-gray-600 dark:bg-gray-800 dark:text-gray-200'> <div className="flex h-full w-full rounded-lg bg-gray-200 py-1 text-gray-600 dark:bg-gray-800 dark:text-gray-200">
<form <form
className='flex h-full w-full items-center' className="flex h-full w-full items-center"
onSubmit={handleSubmit} onSubmit={handleSubmit}
onKeyDown={handleTextareaKeyDown} onKeyDown={handleTextareaKeyDown}
> >
<TextareaAutosize <TextareaAutosize
className='scrollbar-firefox-support my-2 w-full resize-none bg-transparent p-2 px-6 tracking-wide outline-none' className="scrollbar-firefox-support my-2 w-full resize-none bg-transparent p-2 px-6 tracking-wide outline-none"
placeholder={t('application:write-a-message')} placeholder={t("application:write-a-message")}
wrap='soft' wrap="soft"
maxRows={6} maxRows={6}
name='message' name="message"
onChange={handleTextareaChange} onChange={handleTextareaChange}
value={message} value={message}
ref={textareaReference} ref={textareaReference}
/> />
</form> </form>
<div className='flex h-full items-center justify-around pr-6'> <div className="flex h-full items-center justify-around pr-6">
<button <button
className='flex h-full w-full items-center justify-center p-1 text-2xl transition hover:-translate-y-1' className="flex h-full w-full items-center justify-center p-1 text-2xl transition hover:-translate-y-1"
onClick={handleVisibleEmojiPicker} onClick={handleVisibleEmojiPicker}
> >
🙂 🙂
</button> </button>
<button className='relative flex h-full w-full cursor-pointer items-center justify-center p-1 text-green-800 transition hover:-translate-y-1 dark:text-green-400'> <button className="relative flex h-full w-full cursor-pointer items-center justify-center p-1 text-green-800 transition hover:-translate-y-1 dark:text-green-400">
<input <input
type='file' type="file"
className='absolute h-full w-full cursor-pointer opacity-0' className="absolute h-full w-full cursor-pointer opacity-0"
onChange={handleFileChange} onChange={handleFileChange}
/> />
<svg width='25' height='25' viewBox='0 0 22 22'> <svg width="25" height="25" viewBox="0 0 22 22">
<path <path
d='M11 0C4.925 0 0 4.925 0 11C0 17.075 4.925 22 11 22C17.075 22 22 17.075 22 11C22 4.925 17.075 0 11 0ZM12 15C12 15.2652 11.8946 15.5196 11.7071 15.7071C11.5196 15.8946 11.2652 16 11 16C10.7348 16 10.4804 15.8946 10.2929 15.7071C10.1054 15.5196 10 15.2652 10 15V12H7C6.73478 12 6.48043 11.8946 6.29289 11.7071C6.10536 11.5196 6 11.2652 6 11C6 10.7348 6.10536 10.4804 6.29289 10.2929C6.48043 10.1054 6.73478 10 7 10H10V7C10 6.73478 10.1054 6.48043 10.2929 6.29289C10.4804 6.10536 10.7348 6 11 6C11.2652 6 11.5196 6.10536 11.7071 6.29289C11.8946 6.48043 12 6.73478 12 7V10H15C15.2652 10 15.5196 10.1054 15.7071 10.2929C15.8946 10.4804 16 10.7348 16 11C16 11.2652 15.8946 11.5196 15.7071 11.7071C15.5196 11.8946 15.2652 12 15 12H12V15Z' d="M11 0C4.925 0 0 4.925 0 11C0 17.075 4.925 22 11 22C17.075 22 22 17.075 22 11C22 4.925 17.075 0 11 0ZM12 15C12 15.2652 11.8946 15.5196 11.7071 15.7071C11.5196 15.8946 11.2652 16 11 16C10.7348 16 10.4804 15.8946 10.2929 15.7071C10.1054 15.5196 10 15.2652 10 15V12H7C6.73478 12 6.48043 11.8946 6.29289 11.7071C6.10536 11.5196 6 11.2652 6 11C6 10.7348 6.10536 10.4804 6.29289 10.2929C6.48043 10.1054 6.73478 10 7 10H10V7C10 6.73478 10.1054 6.48043 10.2929 6.29289C10.4804 6.10536 10.7348 6 11 6C11.2652 6 11.5196 6.10536 11.7071 6.29289C11.8946 6.48043 12 6.73478 12 7V10H15C15.2652 10 15.5196 10.1054 15.7071 10.2929C15.8946 10.4804 16 10.7348 16 11C16 11.2652 15.8946 11.5196 15.7071 11.7071C15.5196 11.8946 15.2652 12 15 12H12V15Z"
fill='currentColor' fill="currentColor"
/> />
</svg> </svg>
</button> </button>

View File

@ -1 +1 @@
export * from './SendMessage' export * from "./SendMessage"

View File

@ -1,8 +1,8 @@
import classNames from 'clsx' import classNames from "clsx"
import type { ApplicationProps } from '..' import type { ApplicationProps } from ".."
export type DirectionSidebar = 'left' | 'right' export type DirectionSidebar = "left" | "right"
export interface SidebarProps { export interface SidebarProps {
direction: DirectionSidebar direction: DirectionSidebar
@ -12,24 +12,24 @@ export interface SidebarProps {
} }
export const Sidebar: React.FC<React.PropsWithChildren<SidebarProps>> = ( export const Sidebar: React.FC<React.PropsWithChildren<SidebarProps>> = (
props props,
) => { ) => {
const { direction, visible, children, path, isMobile } = props const { direction, visible, children, path, isMobile } = props
return ( return (
<nav <nav
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 right-0 top-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",
'right-0 top-0': direction === 'right' && isMobile, "right-0 top-0": direction === "right" && isMobile,
absolute: isMobile absolute: isMobile,
} },
)} )}
> >
{children} {children}

View File

@ -1 +1 @@
export * from './Sidebar' export * from "./Sidebar"

View File

@ -1,9 +1,9 @@
import Image from 'next/image' import Image from "next/image"
import date from 'date-and-time' import date from "date-and-time"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import type { UserPublic } from '../../../models/User' import type { UserPublic } from "../../../models/User"
import type { Guild } from '../../../models/Guild' import type { Guild } from "../../../models/Guild"
export interface UserProfileProps { export interface UserProfileProps {
className?: string className?: string
@ -16,73 +16,73 @@ 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">
<div className='flex items-center justify-between'> <div className="flex items-center justify-between">
<div className='flex w-max flex-col items-center gap-7 md:flex-row'> <div className="flex w-max flex-col items-center gap-7 md:flex-row">
<div className='relative flex items-center justify-center overflow-hidden rounded-full shadow-lg transition-all'> <div className="relative flex items-center justify-center overflow-hidden rounded-full shadow-lg transition-all">
<Image <Image
quality={100} quality={100}
className='rounded-full' className="rounded-full"
src={ src={
user.logo != null user.logo != null
? user.logo ? user.logo
: '/images/data/user-default.png' : "/images/data/user-default.png"
} }
alt='Profil Picture' alt="Profil Picture"
draggable='false' draggable="false"
height={125} height={125}
width={125} width={125}
/> />
</div> </div>
<div className='ml-10 flex flex-col'> <div className="ml-10 flex flex-col">
<div className='mb-2 flex items-center'> <div className="mb-2 flex items-center">
<p <p
className='space text-dark text-3xl font-bold tracking-wide dark:text-white' className="space text-dark text-3xl font-bold tracking-wide dark:text-white"
data-cy='user-name' data-cy="user-name"
> >
{user.name} {user.name}
</p> </p>
<p <p
className='ml-8 select-none text-sm tracking-widest text-white opacity-40' className="ml-8 select-none text-sm tracking-widest text-white opacity-40"
data-cy='user-createdAt' data-cy="user-createdAt"
> >
{date.format(new Date(user.createdAt), 'DD/MM/YYYY')} {date.format(new Date(user.createdAt), "DD/MM/YYYY")}
</p> </p>
</div> </div>
<div className='my-2 text-left'> <div className="my-2 text-left">
{user.email != null && ( {user.email != null && (
<p className='font-bold'> <p className="font-bold">
Email:{' '} Email:{" "}
<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: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' 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"
> >
{user.email} {user.email}
</a> </a>
</p> </p>
)} )}
{user.website != null && ( {user.website != null && (
<p className='font-bold'> <p className="font-bold">
{t('application:website')}:{' '} {t("application:website")}:{" "}
<a <a
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: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' 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>
</p> </p>
)} )}
{user.status != null && ( {user.status != null && (
<p className='flex font-bold'> <p className="flex font-bold">
{t('application:status')}:{' '} {t("application:status")}:{" "}
<span className='ml-2 font-normal tracking-wide'> <span className="ml-2 font-normal tracking-wide">
{user.status} {user.status}
</span> </span>
</p> </p>
@ -92,7 +92,7 @@ export const UserProfile: React.FC<UserProfileProps> = (props) => {
</div> </div>
</div> </div>
{user.biography != null && ( {user.biography != null && (
<div className='mt-7 text-center'> <div className="mt-7 text-center">
<p>{user.biography}</p> <p>{user.biography}</p>
</div> </div>
)} )}

View File

@ -1 +1 @@
export * from './UserProfile' export * from "./UserProfile"

View File

@ -1,27 +1,27 @@
import Image from 'next/image' import Image from "next/image"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import { useState, useMemo } from 'react' import { useState, useMemo } from "react"
import { Form, useForm } from 'react-component-form' import { Form, useForm } from "react-component-form"
import { EyeIcon, PhotographIcon } from '@heroicons/react/solid' import { EyeIcon, PhotographIcon } from "@heroicons/react/solid"
import { Type } from '@sinclair/typebox' import { Type } from "@sinclair/typebox"
import axios from 'axios' import axios from "axios"
import Link from 'next/link' import Link from "next/link"
import type { HandleUseFormCallback } from 'react-component-form' import type { HandleUseFormCallback } from "react-component-form"
import { Input } from '../../design/Input' import { Input } from "../../design/Input"
import { Checkbox } from '../../design/Checkbox' import { Checkbox } from "../../design/Checkbox"
import { Textarea } from '../../design/Textarea' import { Textarea } from "../../design/Textarea"
import { SocialMediaButton } from '../../design/SocialMediaButton' import { SocialMediaButton } from "../../design/SocialMediaButton"
import { SwitchTheme } from '../../Header/SwitchTheme' import { SwitchTheme } from "../../Header/SwitchTheme"
import { Language } from '../../Header/Language' import { Language } from "../../Header/Language"
import { useAuthentication } from '../../../tools/authentication' import { useAuthentication } from "../../../tools/authentication"
import { Button } from '../../design/Button' import { Button } from "../../design/Button"
import { FormState } from '../../design/FormState' import { FormState } from "../../design/FormState"
import { userSchema } from '../../../models/User' import { userSchema } from "../../../models/User"
import { userSettingsSchema } from '../../../models/UserSettings' import { userSettingsSchema } from "../../../models/UserSettings"
import type { ProviderOAuth } from '../../../models/OAuth' import type { ProviderOAuth } from "../../../models/OAuth"
import { providers } from '../../../models/OAuth' import { providers } from "../../../models/OAuth"
import { useFormTranslation } from '../../../hooks/useFormTranslation' import { useFormTranslation } from "../../../hooks/useFormTranslation"
const schema = { const schema = {
name: userSchema.name, name: userSchema.name,
@ -30,7 +30,7 @@ const schema = {
website: Type.Optional(userSchema.website), website: Type.Optional(userSchema.website),
biography: Type.Optional(userSchema.biography), biography: Type.Optional(userSchema.biography),
isPublicGuilds: userSettingsSchema.isPublicGuilds, isPublicGuilds: userSettingsSchema.isPublicGuilds,
isPublicEmail: userSettingsSchema.isPublicEmail isPublicEmail: userSettingsSchema.isPublicEmail,
} }
export const UserSettings: React.FC = () => { export const UserSettings: React.FC = () => {
@ -43,7 +43,7 @@ export const UserSettings: React.FC = () => {
website: user.website, website: user.website,
biography: user.biography, biography: user.biography,
isPublicGuilds: user.settings.isPublicGuilds, isPublicGuilds: user.settings.isPublicGuilds,
isPublicEmail: user.settings.isPublicEmail isPublicEmail: user.settings.isPublicEmail,
}) })
const { const {
@ -52,7 +52,7 @@ export const UserSettings: React.FC = () => {
setFetchState, setFetchState,
message, message,
setMessage, setMessage,
errors errors,
} = useForm(schema) } = useForm(schema)
const { getFirstErrorTranslation } = useFormTranslation() const { getFirstErrorTranslation } = useFormTranslation()
@ -68,19 +68,19 @@ export const UserSettings: React.FC = () => {
const userSettings = { isPublicEmail, isPublicGuilds } const userSettings = { isPublicEmail, isPublicGuilds }
const { data: userCurrentData } = await authentication.api.put( const { data: userCurrentData } = await authentication.api.put(
`/users/current?redirectURI=${window.location.origin}/authentication/signin`, `/users/current?redirectURI=${window.location.origin}/authentication/signin`,
userData userData,
) )
setInputValues(formData as unknown as any) setInputValues(formData as unknown as any)
const hasEmailChanged = user.email !== userCurrentData.user.email const hasEmailChanged = user.email !== userCurrentData.user.email
if (hasEmailChanged) { if (hasEmailChanged) {
return { return {
type: 'success', type: "success",
message: 'application:success-email-changed' message: "application:success-email-changed",
} }
} }
const { data: userCurrentSettings } = await authentication.api.put( const { data: userCurrentSettings } = await authentication.api.put(
'/users/current/settings', "/users/current/settings",
userSettings userSettings,
) )
setUser((oldUser) => { setUser((oldUser) => {
return { return {
@ -88,36 +88,36 @@ export const UserSettings: React.FC = () => {
...userCurrentData, ...userCurrentData,
settings: { settings: {
...oldUser.settings, ...oldUser.settings,
...userCurrentSettings.settings ...userCurrentSettings.settings,
} },
} }
}) })
return { return {
type: 'success', type: "success",
message: '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) {
const message = error.response.data.message as string const message = error.response.data.message as string
if (message.endsWith('already taken.')) { if (message.endsWith("already taken.")) {
return { return {
type: 'error', type: "error",
message: '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",
message: 'authentication:email-required-to-sign-in' message: "authentication:email-required-to-sign-in",
} }
} }
return { return {
type: 'error', type: "error",
message: 'errors:server-error' message: "errors:server-error",
} }
} }
return { return {
type: 'error', type: "error",
message: 'errors:server-error' message: "errors:server-error",
} }
} }
} }
@ -128,90 +128,95 @@ export const UserSettings: React.FC = () => {
setInputValues((oldInputValues) => { setInputValues((oldInputValues) => {
return { return {
...oldInputValues, ...oldInputValues,
[event.target.name]: event.target.value [event.target.name]: event.target.value,
} }
}) })
} }
const onChangeCheckbox: React.ChangeEventHandler<HTMLInputElement> = ( const onChangeCheckbox: React.ChangeEventHandler<HTMLInputElement> = (
event event,
) => { ) => {
setInputValues((oldInputValues) => { setInputValues((oldInputValues) => {
return { return {
...oldInputValues, ...oldInputValues,
[event.target.name]: event.target.checked [event.target.name]: event.target.checked,
} }
}) })
} }
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async ( const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async (
event event,
) => { ) => {
setFetchState('loading') setFetchState("loading")
const files = event?.target?.files const files = event?.target?.files
if (files != null && files.length === 1 && files[0] != null) { 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 {
...oldUser, ...oldUser,
logo: data.user.logo logo: data.user.logo,
} }
}) })
setFetchState('idle') setFetchState("idle")
} catch (error) { } catch (error) {
setFetchState('error') setFetchState("error")
setMessage('errors:server-error') setMessage("errors:server-error")
} }
} }
} }
const handleSignout = async (): Promise<void> => { const handleSignout = async (): Promise<void> => {
setFetchState('loading') setFetchState("loading")
await authentication.signoutServerSide() await authentication.signoutServerSide()
} }
const handleSignoutAllDevices = async (): Promise<void> => { const handleSignoutAllDevices = async (): Promise<void> => {
setFetchState('loading') setFetchState("loading")
await authentication.signoutAllDevicesServerSide() await authentication.signoutAllDevicesServerSide()
} }
const handleDeletionProvider = ( const handleDeletionProvider = (
provider: ProviderOAuth provider: ProviderOAuth,
): (() => Promise<void>) => { ): (() => Promise<void>) => {
return async () => { return async () => {
try { try {
setFetchState('loading') setFetchState("loading")
await authentication.api.delete(`/users/oauth2/${provider}`) await authentication.api.delete(`/users/oauth2/${provider}`)
setUser((oldUser) => { setUser((oldUser) => {
return { return {
...oldUser, ...oldUser,
strategies: oldUser.strategies.filter((strategy) => { strategies: oldUser.strategies.filter((strategy) => {
return strategy !== provider return strategy !== provider
}) }),
} }
}) })
setMessage('application:success-deleted-provider') setMessage("application:success-deleted-provider")
} catch (error) { } catch (error) {
setFetchState('error') setFetchState("error")
setMessage('errors:server-error') setMessage("errors:server-error")
} }
} }
} }
const handleAddProvider = ( const handleAddProvider = (
provider: ProviderOAuth provider: ProviderOAuth,
): (() => Promise<void>) => { ): (() => Promise<void>) => {
return async () => { return async () => {
const redirect = window.location.href.replace(location.search, '') const redirect = window.location.href.replace(location.search, "")
const { data: url } = await authentication.api.get( const { data: url } = await authentication.api.get(
`/users/oauth2/${provider.toLowerCase()}/add-strategy?redirectURI=${redirect}` `/users/oauth2/${provider.toLowerCase()}/add-strategy?redirectURI=${redirect}`,
) )
window.location.href = url window.location.href = url
} }
@ -220,116 +225,116 @@ export const UserSettings: React.FC = () => {
return ( return (
<Form <Form
onSubmit={handleUseForm(onSubmit)} onSubmit={handleUseForm(onSubmit)}
className='my-auto flex flex-col items-center justify-center py-12 lg:min-w-[875px]' className="my-auto flex flex-col items-center justify-center py-12 lg:min-w-[875px]"
> >
<div className='flex w-full flex-col items-center justify-center sm:w-fit lg:flex-row'> <div className="flex w-full flex-col items-center justify-center sm:w-fit lg:flex-row">
<div className=' flex w-full flex-wrap items-center justify-center px-6 sm:w-max'> <div className=" flex w-full flex-wrap items-center justify-center px-6 sm:w-max">
<div className='relative'> <div className="relative">
<div className='absolute z-50 h-full w-full'> <div className="absolute z-50 h-full w-full">
<button className='relative flex h-full w-full items-center justify-center transition hover:scale-110'> <button className="relative flex h-full w-full items-center justify-center transition hover:scale-110">
<input <input
type='file' type="file"
className='absolute h-full w-full cursor-pointer opacity-0' className="absolute h-full w-full cursor-pointer opacity-0"
onChange={handleFileChange} onChange={handleFileChange}
/> />
<PhotographIcon color='white' className='h-8 w-8' /> <PhotographIcon color="white" className="h-8 w-8" />
</button> </button>
</div> </div>
<div className='flex items-center justify-center rounded-full bg-black shadow-xl'> <div className="flex items-center justify-center rounded-full bg-black shadow-xl">
<Image <Image
quality={100} quality={100}
className='rounded-full opacity-50' className="rounded-full opacity-50"
src={ src={
user.logo != null user.logo != null
? user.logo ? user.logo
: '/images/data/user-default.png' : "/images/data/user-default.png"
} }
alt='Profil Picture' alt="Profil Picture"
draggable='false' draggable="false"
height={125} height={125}
width={125} width={125}
/> />
</div> </div>
</div> </div>
<div className='mx-12 flex flex-col'> <div className="mx-12 flex flex-col">
<Input <Input
name='name' name="name"
label={t('common:name')} label={t("common:name")}
placeholder={t('common:name')} placeholder={t("common:name")}
className='!mt-0' className="!mt-0"
onChange={onChange} onChange={onChange}
value={inputValues.name ?? ''} value={inputValues.name ?? ""}
error={getFirstErrorTranslation(errors.name)} error={getFirstErrorTranslation(errors.name)}
/> />
<Input <Input
name='status' name="status"
label={t('application:status')} label={t("application:status")}
placeholder={t('application:status')} placeholder={t("application:status")}
className='!mt-4' className="!mt-4"
onChange={onChange} onChange={onChange}
value={inputValues.status ?? ''} value={inputValues.status ?? ""}
error={getFirstErrorTranslation(errors.status)} error={getFirstErrorTranslation(errors.status)}
/> />
</div> </div>
</div> </div>
</div> </div>
<div className='mt-12 flex w-full flex-col items-center justify-between sm:w-fit lg:flex-row'> <div className="mt-12 flex w-full flex-col items-center justify-between sm:w-fit lg:flex-row">
<div className='w-4/5 pr-0 sm:w-[450px] lg:border-r-[1px] lg:border-neutral-700 lg:pr-12'> <div className="w-4/5 pr-0 sm:w-[450px] lg:border-r-[1px] lg:border-neutral-700 lg:pr-12">
<Input <Input
name='email' name="email"
label='Email' label="Email"
placeholder='Email' placeholder="Email"
onChange={onChange} onChange={onChange}
value={inputValues.email ?? ''} value={inputValues.email ?? ""}
error={getFirstErrorTranslation(errors.email)} error={getFirstErrorTranslation(errors.email)}
/> />
<Checkbox <Checkbox
name='isPublicEmail' name="isPublicEmail"
label={t('application:label-checkbox-email')} label={t("application:label-checkbox-email")}
id='checkbox-email-visibility' id="checkbox-email-visibility"
onChange={onChangeCheckbox} onChange={onChangeCheckbox}
checked={inputValues.isPublicEmail} checked={inputValues.isPublicEmail}
/> />
<Input <Input
name='website' name="website"
label={t('application:website')} label={t("application:website")}
placeholder={t('application:website')} placeholder={t("application:website")}
onChange={onChange} onChange={onChange}
value={inputValues.website ?? ''} value={inputValues.website ?? ""}
error={getFirstErrorTranslation(errors.website)} error={getFirstErrorTranslation(errors.website)}
/> />
<Textarea <Textarea
name='biography' name="biography"
label={t('application:biography')} label={t("application:biography")}
placeholder={t('application:biography')} placeholder={t("application:biography")}
id='textarea-biography' id="textarea-biography"
onChange={onChange} onChange={onChange}
value={inputValues.biography ?? ''} value={inputValues.biography ?? ""}
/> />
</div> </div>
<div className='flex h-full w-4/5 flex-col items-center justify-between pr-0 sm:w-[415px] lg:pl-12'> <div className="flex h-full w-4/5 flex-col items-center justify-between pr-0 sm:w-[415px] lg:pl-12">
<div className='flex w-full items-center pt-10 lg:pt-0'> <div className="flex w-full items-center pt-10 lg:pt-0">
<Language className='!top-12' /> <Language className="!top-12" />
<div className='ml-auto flex'> <div className="ml-auto flex">
<SwitchTheme /> <SwitchTheme />
<Link <Link
href={`/application/users/${user.id}`} 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"
> >
<EyeIcon <EyeIcon
height={20} height={20}
className='opacity-50 transition-opacity group-hover:opacity-100' className="opacity-50 transition-opacity group-hover:opacity-100"
/> />
</Link> </Link>
</div> </div>
</div> </div>
<div className='mt-14 flex w-full flex-col gap-4'> <div className="mt-14 flex w-full flex-col gap-4">
{!hasAllProviders ? ( {!hasAllProviders ? (
<div className='flex w-full flex-col gap-4'> <div className="flex w-full flex-col gap-4">
<h3 className='text-center'> <h3 className="text-center">
{t('application:signin-with-an-account')} {t("application:signin-with-an-account")}
</h3> </h3>
{providers.map((provider, index) => { {providers.map((provider, index) => {
if (!user.strategies.includes(provider)) { if (!user.strategies.includes(provider)) {
@ -337,7 +342,7 @@ export const UserSettings: React.FC = () => {
<SocialMediaButton <SocialMediaButton
key={index} key={index}
socialMedia={provider} socialMedia={provider}
className='w-full justify-center' className="w-full justify-center"
onClick={handleAddProvider(provider)} onClick={handleAddProvider(provider)}
/> />
) )
@ -347,9 +352,9 @@ export const UserSettings: React.FC = () => {
</div> </div>
) : null} ) : null}
{user.strategies.length !== 1 && ( {user.strategies.length !== 1 && (
<div className='mt-4 flex w-full flex-col gap-4'> <div className="mt-4 flex w-full flex-col gap-4">
<h3 className='text-center'> <h3 className="text-center">
{t('application:signout-with-an-account')} {t("application:signout-with-an-account")}
</h3> </h3>
{providers.map((provider, index) => { {providers.map((provider, index) => {
if (user.strategies.includes(provider)) { if (user.strategies.includes(provider)) {
@ -357,7 +362,7 @@ export const UserSettings: React.FC = () => {
<SocialMediaButton <SocialMediaButton
key={index} key={index}
socialMedia={provider} socialMedia={provider}
className='w-full justify-center' className="w-full justify-center"
onClick={handleDeletionProvider(provider)} onClick={handleDeletionProvider(provider)}
/> />
) )
@ -370,16 +375,16 @@ export const UserSettings: React.FC = () => {
</div> </div>
</div> </div>
<div className='mt-12 flex flex-col items-center justify-center sm:w-fit'> <div className="mt-12 flex flex-col items-center justify-center sm:w-fit">
<div className='space-x-6'> <div className="space-x-6">
<Button type='submit'>{t('application:save')}</Button> <Button type="submit">{t("application:save")}</Button>
<Button type='button' color='red' onClick={handleSignout}> <Button type="button" color="red" onClick={handleSignout}>
{t('application:signout')} {t("application:signout")}
</Button> </Button>
</div> </div>
<div className='mt-4'> <div className="mt-4">
<Button type='button' color='red' onClick={handleSignoutAllDevices}> <Button type="button" color="red" onClick={handleSignoutAllDevices}>
{t('application:signout-all-devices')} {t("application:signout-all-devices")}
</Button> </Button>
</div> </div>
<FormState <FormState

View File

@ -1 +1 @@
export * from './UserSettings' export * from "./UserSettings"

View File

@ -1 +1 @@
export * from './Application' export * from "./Application"

View File

@ -1,26 +1,26 @@
import { useMemo } from 'react' import { useMemo } from "react"
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import Link from 'next/link' import Link from "next/link"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import { useTheme } from 'next-themes' import { useTheme } from "next-themes"
import axios from 'axios' import axios from "axios"
import { useForm } from 'react-component-form' import { useForm } from "react-component-form"
import type { HandleUseFormCallback } from 'react-component-form' import type { HandleUseFormCallback } from "react-component-form"
import { Main } from '../design/Main' import { Main } from "../design/Main"
import { Input } from '../design/Input' import { Input } from "../design/Input"
import { Button } from '../design/Button' import { Button } from "../design/Button"
import { FormState } from '../design/FormState' import { FormState } from "../design/FormState"
import { AuthenticationForm } from '.' import { AuthenticationForm } from "."
import { userSchema } from '../../models/User' import { userSchema } from "../../models/User"
import { api } from '../../tools/api' import { api } from "../../tools/api"
import type { Tokens } from '../../tools/authentication' import type { Tokens } from "../../tools/authentication"
import { Authentication as AuthenticationClass } from '../../tools/authentication' import { Authentication as AuthenticationClass } from "../../tools/authentication"
import { AuthenticationSocialMedia } from './AuthenticationSocialMedia' import { AuthenticationSocialMedia } from "./AuthenticationSocialMedia"
import { useFormTranslation } from '../../hooks/useFormTranslation' import { useFormTranslation } from "../../hooks/useFormTranslation"
export interface AuthenticationProps { export interface AuthenticationProps {
mode: 'signup' | 'signin' mode: "signup" | "signin"
} }
export const Authentication: React.FC<AuthenticationProps> = (props) => { export const Authentication: React.FC<AuthenticationProps> = (props) => {
@ -32,9 +32,9 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
const schema = useMemo(() => { const schema = useMemo(() => {
return { return {
...(mode === 'signup' && { name: userSchema.name }), ...(mode === "signup" && { name: userSchema.name }),
email: userSchema.email, email: userSchema.email,
password: userSchema.password password: userSchema.password,
} }
}, [mode]) }, [mode])
@ -43,55 +43,55 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
const onSubmit: HandleUseFormCallback<typeof schema> = async ( const onSubmit: HandleUseFormCallback<typeof schema> = async (
formData, formData,
formElement formElement,
) => { ) => {
if (mode === 'signup') { if (mode === "signup") {
try { try {
await api.post( await api.post(
`/users/signup?redirectURI=${window.location.origin}/authentication/signin`, `/users/signup?redirectURI=${window.location.origin}/authentication/signin`,
{ ...formData, language: lang, theme } { ...formData, language: lang, theme },
) )
formElement.reset() formElement.reset()
return { return {
type: 'success', type: "success",
message: '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) {
const message = error.response.data.message as string const message = error.response.data.message as string
if (message.endsWith('already taken.')) { if (message.endsWith("already taken.")) {
return { return {
type: 'error', type: "error",
message: 'authentication:already-used' message: "authentication:already-used",
} }
} }
return { return {
type: 'error', type: "error",
message: 'errors:server-error' message: "errors:server-error",
} }
} }
return { return {
type: 'error', type: "error",
message: 'errors:server-error' message: "errors:server-error",
} }
} }
} else { } else {
try { try {
const { data } = await api.post<Tokens>('/users/signin', formData) const { data } = await api.post<Tokens>("/users/signin", formData)
const authentication = new AuthenticationClass(data, true) const authentication = new AuthenticationClass(data, true)
authentication.signin() authentication.signin()
await router.push('/application') await router.push("/application")
return null return null
} 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",
message: 'authentication:wrong-credentials' message: "authentication:wrong-credentials",
} }
} }
return { return {
type: 'error', type: "error",
message: 'errors:server-error' message: "errors:server-error",
} }
} }
} }
@ -100,53 +100,53 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
return ( return (
<Main> <Main>
<AuthenticationSocialMedia /> <AuthenticationSocialMedia />
<div className='pt-8 text-center font-paragraph text-lg'> <div className="pt-8 text-center font-paragraph text-lg">
{t('authentication:or')} {t("authentication:or")}
</div> </div>
<AuthenticationForm onSubmit={handleUseForm(onSubmit)}> <AuthenticationForm onSubmit={handleUseForm(onSubmit)}>
{mode === 'signup' && ( {mode === "signup" && (
<Input <Input
type='text' type="text"
placeholder={t('common:name')} placeholder={t("common:name")}
name='name' name="name"
label={t('common:name')} label={t("common:name")}
error={getFirstErrorTranslation(errors.name)} error={getFirstErrorTranslation(errors.name)}
/> />
)} )}
<Input <Input
type='email' type="email"
placeholder='Email' placeholder="Email"
name='email' name="email"
label='Email' label="Email"
error={getFirstErrorTranslation(errors.email)} error={getFirstErrorTranslation(errors.email)}
/> />
<Input <Input
type='password' type="password"
placeholder={t('authentication:password')} placeholder={t("authentication:password")}
name='password' name="password"
label={t('authentication:password')} label={t("authentication:password")}
showForgotPassword={mode === 'signin'} showForgotPassword={mode === "signin"}
error={getFirstErrorTranslation(errors.password)} error={getFirstErrorTranslation(errors.password)}
/> />
<Button data-cy='submit' className='mt-6 w-full' type='submit'> <Button data-cy="submit" className="mt-6 w-full" type="submit">
{t('authentication:submit')} {t("authentication:submit")}
</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 <Link
href={ href={
mode === 'signup' mode === "signup"
? '/authentication/signin' ? "/authentication/signin"
: '/authentication/signup' : "/authentication/signup"
} }
> >
{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")}
</Link> </Link>
</p> </p>
</AuthenticationForm> </AuthenticationForm>
<FormState <FormState
id='message' id="message"
state={fetchState} state={fetchState}
message={message != null ? t(message) : undefined} message={message != null ? t(message) : undefined}
/> />

View File

@ -1,13 +1,13 @@
import classNames from 'clsx' import classNames from "clsx"
import type { FormProps } from 'react-component-form' import type { FormProps } from "react-component-form"
import { Form } from 'react-component-form' import { Form } from "react-component-form"
export const AuthenticationForm: React.FC<FormProps> = (props) => { export const AuthenticationForm: React.FC<FormProps> = (props) => {
const { className, children, ...rest } = props const { className, children, ...rest } = props
return ( return (
<Form <Form
className={classNames('w-4/6 max-w-xs', className)} className={classNames("w-4/6 max-w-xs", className)}
noValidate noValidate
{...rest} {...rest}
> >

View File

@ -1,22 +1,22 @@
import { useEffect } from 'react' import { useEffect } from "react"
import { useRouter } from 'next/router' import { useRouter } from "next/router"
import { api } from '../../tools/api' import { api } from "../../tools/api"
import { Authentication, isTokens } from '../../tools/authentication' import { Authentication, isTokens } from "../../tools/authentication"
import type { SocialMedia } from '../design/SocialMediaButton' import type { SocialMedia } from "../design/SocialMediaButton"
import { SocialMediaButton } from '../design/SocialMediaButton' import { SocialMediaButton } from "../design/SocialMediaButton"
import { providers } from '../../models/OAuth' import { providers } from "../../models/OAuth"
export const AuthenticationSocialMedia: React.FC = () => { export const AuthenticationSocialMedia: React.FC = () => {
const router = useRouter() const router = useRouter()
const handleAuthentication = ( const handleAuthentication = (
socialMedia: SocialMedia socialMedia: SocialMedia,
): (() => Promise<void>) => { ): (() => Promise<void>) => {
return async () => { return async () => {
const redirect = window.location.href.replace(location.search, '') const redirect = window.location.href.replace(location.search, "")
const { data: url } = await api.get( const { data: url } = await api.get(
`/users/oauth2/${socialMedia.toLowerCase()}/signin?redirectURI=${redirect}` `/users/oauth2/${socialMedia.toLowerCase()}/signin?redirectURI=${redirect}`,
) )
window.location.href = url window.location.href = url
} }
@ -27,13 +27,13 @@ export const AuthenticationSocialMedia: React.FC = () => {
if (isTokens(data)) { if (isTokens(data)) {
const authentication = new Authentication(data, true) const authentication = new Authentication(data, true)
authentication.signin() authentication.signin()
router.push('/application').catch(() => {}) router.push("/application").catch(() => {})
} }
}, [router]) }, [router])
return ( return (
<div className='flex flex-col sm:w-full sm:items-center'> <div className="flex flex-col sm:w-full sm:items-center">
<div className='flex flex-col items-center justify-center space-y-6 sm:w-4/6 sm:flex-row sm:space-x-6 sm:space-y-0'> <div className="flex flex-col items-center justify-center space-y-6 sm:w-4/6 sm:flex-row sm:space-x-6 sm:space-y-0">
{providers.map((provider, index) => { {providers.map((provider, index) => {
return ( return (
<SocialMediaButton <SocialMediaButton

View File

@ -1,2 +1,2 @@
export * from './Authentication' export * from "./Authentication"
export * from './AuthenticationForm' export * from "./AuthenticationForm"

View File

@ -1,6 +1,6 @@
import { Emoji as EmojiMart } from 'emoji-mart' import { Emoji as EmojiMart } from "emoji-mart"
import { EMOJI_SET } from './emojiPlugin' import { EMOJI_SET } from "./emojiPlugin"
export interface EmojiProps { export interface EmojiProps {
value: string value: string

View File

@ -1,13 +1,13 @@
import 'emoji-mart/css/emoji-mart.css' import "emoji-mart/css/emoji-mart.css"
import type { EmojiData } from 'emoji-mart' import type { EmojiData } from "emoji-mart"
import { Picker } from 'emoji-mart' import { Picker } from "emoji-mart"
import { useTheme } from 'next-themes' import { useTheme } from "next-themes"
import { EMOJI_SET } from '../emojiPlugin' import { EMOJI_SET } from "../emojiPlugin"
export type EmojiPickerOnClick = ( export type EmojiPickerOnClick = (
emoji: EmojiData, emoji: EmojiData,
event: React.MouseEvent<HTMLElement, MouseEvent> event: React.MouseEvent<HTMLElement, MouseEvent>,
) => void ) => void
export interface EmojiPickerProps { export interface EmojiPickerProps {
@ -20,7 +20,7 @@ export const EmojiPicker: React.FC<EmojiPickerProps> = (props) => {
return ( return (
<Picker <Picker
set={EMOJI_SET} set={EMOJI_SET}
theme={theme as 'light' | 'dark' | 'auto'} theme={theme as "light" | "dark" | "auto"}
onClick={props.onClick} onClick={props.onClick}
showPreview={false} showPreview={false}
showSkinTones={false} showSkinTones={false}

View File

@ -1 +1 @@
export * from './EmojiPicker' export * from "./EmojiPicker"

View File

@ -1,21 +1,21 @@
import { visit } from 'unist-util-visit' import { visit } from "unist-util-visit"
import type { Plugin, Transformer } from 'unified' import type { Plugin, Transformer } from "unified"
import type { Literal, Parent } from 'unist' import type { Literal, Parent } from "unist"
import type { ElementContent } from 'hast' import type { ElementContent } from "hast"
import type { EmojiSet } from 'emoji-mart' import type { EmojiSet } from "emoji-mart"
import { emojiRegex } from './isStringWithOnlyOneEmoji' import { emojiRegex } from "./isStringWithOnlyOneEmoji"
export const EMOJI_SET: EmojiSet = 'twitter' export const EMOJI_SET: EmojiSet = "twitter"
const extractText = ( const extractText = (
string: string, string: string,
start: number, start: number,
end: number end: number,
): ElementContent => { ): ElementContent => {
return { return {
type: 'text', type: "text",
value: string.slice(start, end) value: string.slice(start, end),
} }
} }
@ -23,9 +23,9 @@ export const emojiPlugin: Plugin<[], Literal<string>> = () => {
const transformer: Transformer<Literal<string>> = (tree) => { const transformer: Transformer<Literal<string>> = (tree) => {
visit<Literal<string>, string>( visit<Literal<string>, string>(
tree, tree,
'text', "text",
(node, position, parent: Parent<ElementContent> | null) => { (node, position, parent: Parent<ElementContent> | null) => {
if (typeof node.value !== 'string') { if (typeof node.value !== "string") {
return return
} }
position = position ?? 0 position = position ?? 0
@ -38,15 +38,15 @@ export const emojiPlugin: Plugin<[], Literal<string>> = () => {
definition.push(extractText(node.value, lastIndex, match.index)) definition.push(extractText(node.value, lastIndex, match.index))
} }
definition.push({ definition.push({
type: 'element', type: "element",
tagName: 'emoji', tagName: "emoji",
properties: { value }, properties: { value },
children: [] children: [],
}) })
lastIndex = match.index + value.length lastIndex = match.index + value.length
if (lastIndex !== node.value.length) { if (lastIndex !== node.value.length) {
definition.push( definition.push(
extractText(node.value, lastIndex, node.value.length) extractText(node.value, lastIndex, node.value.length),
) )
} }
if (parent != null) { if (parent != null) {
@ -56,7 +56,7 @@ export const emojiPlugin: Plugin<[], Literal<string>> = () => {
parent.children = parent.children.concat(last) parent.children = parent.children.concat(last)
} }
} }
} },
) )
} }
return transformer return transformer

View File

@ -1,4 +1,4 @@
export * from './Emoji' export * from "./Emoji"
export * from './EmojiPicker' export * from "./EmojiPicker"
export * from './emojiPlugin' export * from "./emojiPlugin"
export * from './isStringWithOnlyOneEmoji' export * from "./isStringWithOnlyOneEmoji"

View File

@ -1,9 +1,9 @@
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import Link from 'next/link' import Link from "next/link"
import type { FooterProps } from './Footer' import type { FooterProps } from "./Footer"
import { Footer } from './Footer' import { Footer } from "./Footer"
import { Header } from './Header' import { Header } from "./Header"
export interface ErrorPageProps extends FooterProps { export interface ErrorPageProps extends FooterProps {
statusCode: number statusCode: number
@ -16,25 +16,25 @@ export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
return ( return (
<> <>
<div className='flex h-screen flex-col pt-0'> <div className="flex h-screen flex-col pt-0">
<Header /> <Header />
<main className='flex min-w-full flex-1 flex-col items-center justify-center'> <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
className='text-green-800 dark:text-green-400' className="text-green-800 dark:text-green-400"
data-cy='status-code' data-cy="status-code"
> >
{statusCode} {statusCode}
</span> </span>
</h1> </h1>
<p className='text-center text-lg'> <p className="text-center text-lg">
{message}{' '} {message}{" "}
<Link <Link
href='/' href="/"
className='text-green-800 hover:underline dark:text-green-400' className="text-green-800 hover:underline dark:text-green-400"
> >
{t('errors:return-to-home-page')} {t("errors:return-to-home-page")}
</Link> </Link>
</p> </p>
</main> </main>

View File

@ -1,8 +1,8 @@
import Link from 'next/link' import Link from "next/link"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import { API_VERSION } from '../../tools/api' import { API_VERSION } from "../../tools/api"
import { VersionLink } from './VersionLink' import { VersionLink } from "./VersionLink"
export interface FooterProps { export interface FooterProps {
version: string version: string
@ -13,19 +13,19 @@ export const Footer: React.FC<FooterProps> = (props) => {
const { version } = props const { version } = 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 <Link
href='/' href="/"
className='text-green-800 hover:underline dark:text-green-400' className="text-green-800 hover:underline dark:text-green-400"
> >
Thream Thream
</Link>{' '} </Link>{" "}
| {t('common:all-rights-reserved')} | {t("common:all-rights-reserved")}
</p> </p>
<p className='mt-1'> <p className="mt-1">
<VersionLink repository='website' version={version} /> |{' '} <VersionLink repository="website" version={version} /> |{" "}
<VersionLink repository='api' version={API_VERSION} /> <VersionLink repository="api" version={API_VERSION} />
</p> </p>
</footer> </footer>
) )

View File

@ -1,6 +1,6 @@
export interface VersionLinkProps { export interface VersionLinkProps {
version: string version: string
repository: 'website' | 'api' repository: "website" | "api"
} }
export const VersionLink: React.FC<VersionLinkProps> = (props) => { export const VersionLink: React.FC<VersionLinkProps> = (props) => {
@ -9,10 +9,10 @@ export const VersionLink: React.FC<VersionLinkProps> = (props) => {
return ( return (
<a <a
data-cy={`version-link-${repository}`} data-cy={`version-link-${repository}`}
className='text-green-800 hover:underline dark:text-green-400' className="text-green-800 hover:underline dark:text-green-400"
href={`https://github.com/Thream/${repository}/releases/tag/v${version}`} href={`https://github.com/Thream/${repository}/releases/tag/v${version}`}
target='_blank' target="_blank"
rel='noopener noreferrer' rel="noopener noreferrer"
> >
{repository} v{version} {repository} v{version}
</a> </a>

View File

@ -1 +1 @@
export * from './Footer' export * from "./Footer"

View File

@ -1,5 +1,5 @@
import NextHead from 'next/head' import NextHead from "next/head"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
interface HeadProps { interface HeadProps {
title?: string title?: string
@ -12,43 +12,37 @@ export const Head: React.FC<HeadProps> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
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 (
<NextHead> <NextHead>
<title>{title}</title> <title>{title}</title>
<link rel='icon' type='image/png' href={image} /> <link rel="icon" type="image/png" href={image} />
{/* 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 */}
<meta property='og:title' content={title} /> <meta property="og:title" content={title} />
<meta property='og:type' content='website' /> <meta property="og:type" content="website" />
<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 */}
<meta name='twitter:card' content='summary' /> <meta name="twitter:card" content="summary" />
<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

@ -1,29 +1,29 @@
import Link from 'next/link' import Link from "next/link"
import Image from 'next/image' import Image from "next/image"
import { Language } from './Language' import { Language } from "./Language"
import { SwitchTheme } from './SwitchTheme' import { SwitchTheme } from "./SwitchTheme"
export const Header: React.FC = () => { export const Header: React.FC = () => {
return ( return (
<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="/">
<div className='flex items-center justify-center'> <div className="flex items-center justify-center">
<Image <Image
priority 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>
</Link> </Link>
<div className='flex justify-between'> <div className="flex justify-between">
<Language /> <Language />
<SwitchTheme /> <SwitchTheme />
</div> </div>

View File

@ -1,15 +1,15 @@
export const Arrow: React.FC = () => { export const Arrow: React.FC = () => {
return ( return (
<svg <svg
width='12' width="12"
height='8' height="8"
viewBox='0 0 12 8' viewBox="0 0 12 8"
fill='none' fill="none"
xmlns='http://www.w3.org/2000/svg' xmlns="http://www.w3.org/2000/svg"
> >
<path <path
className='fill-current text-black dark:text-white' className="fill-current text-black dark:text-white"
d='M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z' d="M9.8024 0.292969L5.61855 4.58597L1.43469 0.292969L0.0566406 1.70697L5.61855 7.41397L11.1805 1.70697L9.8024 0.292969Z"
/> />
</svg> </svg>
) )

View File

@ -1,12 +1,12 @@
import { useCallback, useState, useRef } from 'react' import { useCallback, useState, useRef } from "react"
import useTranslation from 'next-translate/useTranslation' import useTranslation from "next-translate/useTranslation"
import setLanguage from 'next-translate/setLanguage' import setLanguage from "next-translate/setLanguage"
import classNames from 'clsx' import classNames from "clsx"
import i18n from '../../../i18n.json' import i18n from "../../../i18n.json"
import { Arrow } from './Arrow' import { Arrow } from "./Arrow"
import { LanguageFlag } from './LanguageFlag' import { LanguageFlag } from "./LanguageFlag"
import { useClickOutsideAlerter } from '../../../hooks/useClickOutsideAlerter' import { useClickOutsideAlerter } from "../../../hooks/useClickOutsideAlerter"
export interface LanguageProps { export interface LanguageProps {
className?: string className?: string
@ -35,12 +35,12 @@ export const Language: React.FC<LanguageProps> = (props) => {
return ( return (
<div <div
className='relative flex cursor-pointer flex-col items-center justify-center' className="relative flex cursor-pointer flex-col items-center justify-center"
ref={languageClickRef} ref={languageClickRef}
> >
<div <div
data-cy='language-click' data-cy="language-click"
className='mr-5 flex items-center' className="mr-5 flex items-center"
onClick={handleHiddenMenu} onClick={handleHiddenMenu}
> >
<LanguageFlag language={currentLanguage} /> <LanguageFlag language={currentLanguage} />
@ -48,11 +48,11 @@ export const Language: React.FC<LanguageProps> = (props) => {
</div> </div>
<ul <ul
data-cy='languages-list' data-cy="languages-list"
className={classNames( className={classNames(
className, className,
'absolute top-16 z-10 mr-4 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag', "absolute top-16 z-10 mr-4 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag",
{ hidden: hiddenMenu } { hidden: hiddenMenu },
)} )}
> >
{i18n.locales.map((language, index) => { {i18n.locales.map((language, index) => {
@ -62,7 +62,7 @@ export const Language: React.FC<LanguageProps> = (props) => {
return ( return (
<li <li
key={index} key={index}
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20' className="flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20"
onClick={async () => { onClick={async () => {
return await handleLanguage(language) return await handleLanguage(language)
}} }}

View File

@ -1,4 +1,4 @@
import Image from 'next/image' import Image from "next/image"
export interface LanguageFlagProps { export interface LanguageFlagProps {
language: string language: string
@ -16,7 +16,7 @@ export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
src={`/images/svg/languages/${language}.svg`} src={`/images/svg/languages/${language}.svg`}
alt={language} alt={language}
/> />
<p data-cy='language-flag-text' className='mx-2 text-base'> <p data-cy="language-flag-text" className="mx-2 text-base">
{language.toUpperCase()} {language.toUpperCase()}
</p> </p>
</> </>

View File

@ -1 +1 @@
export * from './Language' export * from "./Language"

View File

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

View File

@ -1 +1 @@
export * from './Header' export * from "./Header"

View File

@ -1,18 +1,18 @@
import { forwardRef } from 'react' import { forwardRef } from "react"
import classNames from 'clsx' import classNames from "clsx"
const className = const className =
'py-2 px-6 font-paragraph rounded-lg bg-transparent border hover:text-white dark:hover:text-black fill-current stroke-current transform transition-colors duration-300 ease-in-out focus:outline-none focus:text-white dark:focus:text-black' "py-2 px-6 font-paragraph rounded-lg bg-transparent border hover:text-white dark:hover:text-black fill-current stroke-current transform transition-colors duration-300 ease-in-out focus:outline-none focus:text-white dark:focus:text-black"
const classNameGreen = const classNameGreen =
'border-green-800 dark:border-green-400 text-green-800 dark:text-green-400 hover:bg-green-800 focus:bg-green-800 dark:focus:bg-green-400 dark:hover:bg-green-400' "border-green-800 dark:border-green-400 text-green-800 dark:text-green-400 hover:bg-green-800 focus:bg-green-800 dark:focus:bg-green-400 dark:hover:bg-green-400"
const classNameRed = const classNameRed =
'border-red-800 dark:border-red-400 text-red-800 dark:text-red-400 hover:bg-red-800 focus:bg-red-800 dark:focus:bg-red-400 dark:hover:bg-red-400' "border-red-800 dark:border-red-400 text-red-800 dark:text-red-400 hover:bg-red-800 focus:bg-red-800 dark:focus:bg-red-400 dark:hover:bg-red-400"
export type ButtonColor = 'green' | 'red' export type ButtonColor = "green" | "red"
export interface ButtonLinkProps extends React.ComponentPropsWithRef<'a'> { export interface ButtonLinkProps extends React.ComponentPropsWithRef<"a"> {
color?: ButtonColor color?: ButtonColor
} }
@ -21,7 +21,7 @@ export const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(
const { const {
children, children,
className: givenClassName, className: givenClassName,
color = 'green', color = "green",
...rest ...rest
} = props } = props
@ -31,22 +31,22 @@ export const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(
className={classNames( className={classNames(
className, className,
{ {
[classNameGreen]: color === 'green', [classNameGreen]: color === "green",
[classNameRed]: color === 'red' [classNameRed]: color === "red",
}, },
givenClassName givenClassName,
)} )}
{...rest} {...rest}
> >
{children} {children}
</a> </a>
) )
} },
) )
ButtonLink.displayName = 'ButtonLink' ButtonLink.displayName = "ButtonLink"
export interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> { export interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
color?: ButtonColor color?: ButtonColor
} }
@ -54,7 +54,7 @@ export const Button: React.FC<ButtonProps> = (props) => {
const { const {
children, children,
className: givenClassName, className: givenClassName,
color = 'green', color = "green",
...rest ...rest
} = props } = props
@ -63,10 +63,10 @@ export const Button: React.FC<ButtonProps> = (props) => {
className={classNames( className={classNames(
className, className,
{ {
[classNameGreen]: color === 'green', [classNameGreen]: color === "green",
[classNameRed]: color === 'red' [classNameRed]: color === "red",
}, },
givenClassName givenClassName,
)} )}
{...rest} {...rest}
> >

View File

@ -1 +1 @@
export * from './Button' export * from "./Button"

View File

@ -1,6 +1,6 @@
import classNames from 'clsx' import classNames from "clsx"
export interface CheckboxProps extends React.ComponentPropsWithRef<'input'> { export interface CheckboxProps extends React.ComponentPropsWithRef<"input"> {
className?: string className?: string
label: string label: string
} }
@ -8,15 +8,15 @@ export interface CheckboxProps extends React.ComponentPropsWithRef<'input'> {
export const Checkbox: React.FC<CheckboxProps> = (props) => { export const Checkbox: React.FC<CheckboxProps> = (props) => {
const { label, id, className } = props const { label, id, className } = props
return ( return (
<div className={classNames('mt-4 flex items-center', className)}> <div className={classNames("mt-4 flex items-center", className)}>
<input <input
{...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: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' 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 "
htmlFor={id} htmlFor={id}
> >
{label} {label}

View File

@ -1 +1 @@
export * from './Checkbox' export * from "./Checkbox"

View File

@ -1,7 +1,7 @@
export const Divider: React.FC = () => { export const Divider: React.FC = () => {
return ( return (
<div className='relative mb-3 flex h-[2px] w-full justify-center'> <div className="relative mb-3 flex h-[2px] w-full justify-center">
<div className='absolute h-[2px] w-8/12 rounded-full bg-gray-600 dark:bg-white/20'></div> <div className="absolute h-[2px] w-8/12 rounded-full bg-gray-600 dark:bg-white/20"></div>
</div> </div>
) )
} }

View File

@ -1 +1 @@
export * from './Divider' export * from "./Divider"

Some files were not shown because too many files have changed in this diff Show More