refactor: apply prettier conf and cleanup

This commit is contained in:
Sonny
2023-11-26 02:04:09 +01:00
parent c0af440302
commit 9a5f661df4
120 changed files with 1229 additions and 9101 deletions

3
.gitignore vendored
View File

@@ -35,6 +35,3 @@ yarn-error.log*
# vercel # vercel
.vercel .vercel
# env file
.env

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm test

View File

@@ -13,7 +13,7 @@
"vueIndentScriptAndStyle": false, "vueIndentScriptAndStyle": false,
"proseWrap": "preserve", "proseWrap": "preserve",
"insertPragma": false, "insertPragma": false,
"printWidth": 100, "printWidth": 80,
"requirePragma": false, "requirePragma": false,
"tabWidth": 2, "tabWidth": 2,
"useTabs": false, "useTabs": false,

View File

@@ -25,7 +25,7 @@ npm run dev
## Prod ## Prod
If you want to use your own database leave, the `DATABASE_URL` property filled in `docker/docker-compose.yml` with your databse credentials, otherwise you'll have to delete it. If you want to use your own database leave, the `DATABASE_URL` property filled in `docker/docker-compose.yml` with your database credentials, otherwise you'll have to delete it.
```shell ```shell
cd docker cd docker
@@ -33,7 +33,7 @@ make build
make start-prod make start-prod
``` ```
## Github Actions ## GitHub Actions
Env var to define : Env var to define :

View File

@@ -1,3 +0,0 @@
export const config = {
siteName: "My Links",
};

View File

@@ -1,4 +1,4 @@
version: "3.8" version: '3.8'
services: services:
my-links-dev-db: my-links-dev-db:
@@ -8,4 +8,4 @@ services:
env_file: env_file:
- .env - .env
ports: ports:
- 3306:3306 - '3306:3306'

View File

@@ -1,4 +1,4 @@
version: "3.8" version: '3.8'
networks: networks:
mylinks_app: mylinks_app:
@@ -11,7 +11,7 @@ services:
build: build:
context: . context: .
ports: ports:
- "127.0.0.1:5000:3000" - '127.0.0.1:5000:3000'
env_file: env_file:
- .env - .env
networks: networks:

View File

@@ -1,40 +1,40 @@
import acceptLanguage from "accept-language"; import acceptLanguage from 'accept-language';
import { NextResponse } from "next/server"; import { NextResponse } from 'next/server';
import { i18n } from "./next-i18next.config"; import { i18n } from './next-i18next.config';
acceptLanguage.languages(i18n.locales); acceptLanguage.languages(i18n.locales);
export const config = { export const config = {
matcher: ["/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)"], matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)'],
}; };
const cookieName = "i18next"; const cookieName = 'i18next';
// Source : https://github.com/i18next/next-app-dir-i18next-example/blob/3d653a46ae33f46abc011b6186f7a4595b84129f/middleware.js // Source : https://github.com/i18next/next-app-dir-i18next-example/blob/3d653a46ae33f46abc011b6186f7a4595b84129f/middleware.js
export function middleware(req) { export function middleware(req) {
if ( if (
req.nextUrl.pathname.indexOf("icon") > -1 || req.nextUrl.pathname.indexOf('icon') > -1 ||
req.nextUrl.pathname.indexOf("chrome") > -1 req.nextUrl.pathname.indexOf('chrome') > -1
) )
return NextResponse.next(); return NextResponse.next();
let lng; let lng;
if (req.cookies.has(cookieName)) if (req.cookies.has(cookieName))
lng = acceptLanguage.get(req.cookies.get(cookieName).value); lng = acceptLanguage.get(req.cookies.get(cookieName).value);
if (!lng) lng = acceptLanguage.get(req.headers.get("Accept-Language")); if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'));
if (!lng) lng = i18n.defaultLocale; if (!lng) lng = i18n.defaultLocale;
// Redirect if lng in path is not supported // Redirect if lng in path is not supported
if ( if (
!i18n.locales.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) && !i18n.locales.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
!req.nextUrl.pathname.startsWith("/_next") !req.nextUrl.pathname.startsWith('/_next')
) { ) {
return NextResponse.redirect( return NextResponse.redirect(
new URL(`/${lng}${req.nextUrl.pathname}`, req.url), new URL(`/${lng}${req.nextUrl.pathname}`, req.url),
); );
} }
if (req.headers.has("referer")) { if (req.headers.has('referer')) {
const refererUrl = new URL(req.headers.get("referer")); const refererUrl = new URL(req.headers.get('referer'));
const lngInReferer = i18n.locales.find((l) => const lngInReferer = i18n.locales.find((l) =>
refererUrl.pathname.startsWith(`/${l}`), refererUrl.pathname.startsWith(`/${l}`),
); );

View File

@@ -2,9 +2,9 @@
module.exports = { module.exports = {
// debug: process.env.NODE_ENV === "development", // debug: process.env.NODE_ENV === "development",
i18n: { i18n: {
defaultLocale: "en", defaultLocale: 'en',
locales: ["en", "fr"], locales: ['en', 'fr'],
}, },
reloadOnPrerender: process.env.NODE_ENV === "development", reloadOnPrerender: process.env.NODE_ENV === 'development',
returnNull: false, returnNull: false,
}; };

View File

@@ -1,4 +1,4 @@
const { i18n } = require("./next-i18next.config"); const { i18n } = require('./next-i18next.config');
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const config = { const config = {
@@ -6,22 +6,22 @@ const config = {
webpack(config) { webpack(config) {
config.module.rules.push({ config.module.rules.push({
test: /\.svg$/, test: /\.svg$/,
use: ["@svgr/webpack"], use: ['@svgr/webpack'],
}); });
return config; return config;
}, },
images: { images: {
remotePatterns: [ remotePatterns: [
{ hostname: "localhost" }, { hostname: 'localhost' },
{ hostname: "t3.gstatic.com" }, { hostname: 't3.gstatic.com' },
{ hostname: "lh3.googleusercontent.com" }, { hostname: 'lh3.googleusercontent.com' },
{ hostname: "www.mylinks.app" }, { hostname: 'www.mylinks.app' },
], ],
formats: ["image/webp"], formats: ['image/webp'],
}, },
reactStrictMode: false, reactStrictMode: false,
output: "standalone", output: 'standalone',
}; };
module.exports = config; module.exports = config;

7986
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,8 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^5.6.0", "@prisma/client": "^5.6.0",
@@ -40,6 +41,7 @@
"eslint": "^8.54.0", "eslint": "^8.54.0",
"eslint-config-next": "14.0.3", "eslint-config-next": "14.0.3",
"prisma": "^5.6.0", "prisma": "^5.6.0",
"typescript": "5.3.2" "typescript": "5.3.2",
"husky": "^8.0.0"
} }
} }

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com"> <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<path d="M 106.813 73.354 L 86.501 73.355 C 86.501 84.421 95.594 93.398 106.813 93.398 C 124.952 93.398 139.996 108.476 139.996 126.656 L 140 126.656 C 140 144.835 124.956 159.913 106.816 159.913 L 0 159.913 L 62.001 200 L 62.001 189.977 C 62.001 184.441 66.476 179.956 72 179.956 L 106.816 179.956 L 106.816 179.962 C 136.14 179.962 159.999 156.048 159.999 126.656 C 159.999 97.266 136.14 73.354 106.813 73.354 Z" fill="#3f88c5" data-fill-palette-color="accent" style="" bx:origin="0.624436 0.21322"/> <path d="M 106.813 73.354 L 86.501 73.355 C 86.501 84.421 95.594 93.398 106.813 93.398 C 124.952 93.398 139.996 108.476 139.996 126.656 L 140 126.656 C 140 144.835 124.956 159.913 106.816 159.913 L 0 159.913 L 62.001 200 L 62.001 189.977 C 62.001 184.441 66.476 179.956 72 179.956 L 106.816 179.956 L 106.816 179.962 C 136.14 179.962 159.999 156.048 159.999 126.656 C 159.999 97.266 136.14 73.354 106.813 73.354 Z" fill="#3f88c5" />
<path d="M 93.184 126.646 L 113.498 126.645 C 113.498 115.579 104.402 106.603 93.184 106.603 C 75.042 106.603 60 91.524 60 73.344 C 60 55.165 75.044 40.09 93.184 40.09 L 200 40.09 L 137.999 0 L 137.999 10.023 C 137.999 15.559 133.524 20.044 128 20.044 L 93.184 20.044 L 93.184 20.039 C 63.86 20.039 40.001 43.951 40.001 73.344 L 39.997 73.344 C 39.997 102.735 63.855 126.646 93.184 126.646 Z" fill="#3f88c5" data-fill-palette-color="accent" style="" bx:origin="0.374445 0.792419"/> <path d="M 93.184 126.646 L 113.498 126.645 C 113.498 115.579 104.402 106.603 93.184 106.603 C 75.042 106.603 60 91.524 60 73.344 C 60 55.165 75.044 40.09 93.184 40.09 L 200 40.09 L 137.999 0 L 137.999 10.023 C 137.999 15.559 133.524 20.044 128 20.044 L 93.184 20.044 L 93.184 20.039 C 63.86 20.039 40.001 43.951 40.001 73.344 L 39.997 73.344 C 39.997 102.735 63.855 126.646 93.184 126.646 Z" fill="#3f88c5" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 957 B

View File

@@ -1,5 +1,5 @@
{ {
"select-categorie": "Please select a category", "select-category": "Please select a category",
"or-create-one": "or create one", "or-create-one": "or create one",
"no-link": "No link for <b>{{name}}</b>", "no-link": "No link for <b>{{name}}</b>",
"footer": { "footer": {

View File

@@ -30,7 +30,7 @@
"title": "4. User Rights", "title": "4. User Rights",
"description": "The user has the right to retrieve all their data at any time and/or request the complete deletion of their data." "description": "The user has the right to retrieve all their data at any time and/or request the complete deletion of their data."
}, },
"rgpd": { "gdpr": {
"title": "5. GDPR Compliance", "title": "5. GDPR Compliance",
"description": "MyLinks complies with the General Data Protection Regulation (GDPR) of the European Union." "description": "MyLinks complies with the General Data Protection Regulation (GDPR) of the European Union."
}, },

View File

@@ -19,7 +19,7 @@
"name": "Nom de la catégorie", "name": "Nom de la catégorie",
"create": "Créer une catégorie", "create": "Créer une catégorie",
"edit": "Modifier une catégorie", "edit": "Modifier une catégorie",
"remove": "Supprimer une categorie", "remove": "Supprimer une catégorie",
"remove-confirm": "Confirmer la suppression ?", "remove-confirm": "Confirmer la suppression ?",
"remove-description": "Vous devez supprimer tous les liens de cette catégorie avant de pouvoir supprimer cette catégorie" "remove-description": "Vous devez supprimer tous les liens de cette catégorie avant de pouvoir supprimer cette catégorie"
}, },

View File

@@ -1,5 +1,5 @@
{ {
"select-categorie": "Veuillez séléctionner une categorie", "select-category": "Veuillez sélectionner une categories",
"or-create-one": "ou en créer une", "or-create-one": "ou en créer une",
"no-link": "Aucun lien pour <b>{{name}}</b>", "no-link": "Aucun lien pour <b>{{name}}</b>",
"footer": { "footer": {

View File

@@ -36,7 +36,7 @@
"title": "4. Droits de l'utilisateur", "title": "4. Droits de l'utilisateur",
"description": "L'utilisateur a le droit de récupérer l'ensemble de ses données à tout moment et/ou de demander la suppression complète de ses données." "description": "L'utilisateur a le droit de récupérer l'ensemble de ses données à tout moment et/ou de demander la suppression complète de ses données."
}, },
"rgpd": { "gdpr": {
"title": "5. Conformité au RGPD", "title": "5. Conformité au RGPD",
"description": "MyLinks est conforme au Règlement Général sur la Protection des Données (RGPD) de l'Union européenne." "description": "MyLinks est conforme au Règlement Général sur la Protection des Données (RGPD) de l'Union européenne."
}, },

View File

@@ -1,25 +1,25 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 500 165" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com"> <svg viewBox="0 0 500 165" xmlns="http://www.w3.org/2000/svg">
<g id="tight-bounds" transform="matrix(1.264159, 0, 0, 1.265787, 0, 0)" style="" bx:origin="0.499856 0.5"> <g id="tight-bounds" transform="matrix(1.264159, 0, 0, 1.265787, 0, 0)">
<svg viewBox="0 0 395.52 130.3536553275838" height="130.3536553275838" width="395.52"> <svg viewBox="0 0 395.52 130.3536553275838" height="130.3536553275838" width="395.52">
<g> <g>
<svg viewBox="0 0 609.8756479073379 200.99999999999997" height="130.3536553275838" width="395.52" transform="matrix(1.0990717554193026, 0, 0, 1.0219297409057617, -1464.3746338148903, -341.48333740234375)" bx:origin="-0.010232 -0.035228"> <svg viewBox="0 0 609.8756479073379 200.99999999999997" height="130.3536553275838" width="395.52">
<g> <g>
<rect width="8.33878379307346" height="200.99999999999997" x="222.27426506352492" y="0" stroke-width="0" fill-opacity="1" class="rect-o-0" data-fill-palette-color="primary" rx="1%" id="o-0" data-palette-color="#005aa5" style="fill: rgb(72, 162, 255);" stroke="transparent"/> <rect width="8.33878379307346" height="200.99999999999997" x="222.27426506352492" y="0" stroke-width="0" fill-opacity="1" class="rect-o-0" rx="1%" id="o-0" style="fill: rgb(72, 162, 255);" stroke="transparent"/>
</g> </g>
<g transform="matrix(1,0,0,1,252.44232580181685,0.4999999999999858)"> <g transform="matrix(1,0,0,1,252.44232580181685,0.4999999999999858)">
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103"> <svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
<g id="textblocktransform"> <g>
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103" id="textblock"> <svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
<g> <g>
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103"> <svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
<g transform="matrix(1,0,0,1,0,0)"> <g transform="matrix(1,0,0,1,0,0)">
<svg width="357.43332210552103" viewBox="3.7404773235321045 -36.008331298828125 142.35955810546875 76.29547882080078" height="200" data-palette-color="#005aa5"> <svg width="357.43332210552103" viewBox="3.7404773235321045 -36.008331298828125 142.35955810546875 76.29547882080078" height="200" data-palette-color="#005aa5">
<svg/> <svg/>
<svg/> <svg/>
<g class="wordmark-text-0" data-fill-palette-color="primary" id="text-0"> <g>
<path xmlns="http://www.w3.org/2000/svg" d="M39.3-3.35v0c-0.033 0.967-0.4 1.783-1.1 2.45-0.7 0.667-1.533 0.983-2.5 0.95-0.967-0.033-1.783-0.4-2.45-1.1-0.667-0.7-0.983-1.533-0.95-2.5v0c0.067-2.167 0.033-8.567-0.1-19.2v0c-2.467 3.1-4.9 6.417-7.3 9.95v0c-0.567 0.8-1.317 1.29-2.25 1.47-0.933 0.187-1.8 0.013-2.6-0.52v0c-0.1-0.067-0.183-0.133-0.25-0.2v0c-0.367-0.233-0.683-0.517-0.95-0.85v0l-7.7-9.7c0.033 0.067 0.043 3.257 0.03 9.57-0.02 6.32-0.03 9.497-0.03 9.53v0c0 0.967-0.34 1.79-1.02 2.47-0.687 0.687-1.513 1.03-2.48 1.03-0.967 0-1.79-0.343-2.47-1.03-0.687-0.68-1.03-1.503-1.03-2.47v0c0-1.4 0.007-3.75 0.02-7.05 0.02-3.3 0.03-5.85 0.03-7.65 0-1.8-0.033-4.033-0.1-6.7v0c-0.067-2.367-0.183-4.817-0.35-7.35v0c-0.067-0.967 0.217-1.817 0.85-2.55 0.633-0.733 1.433-1.133 2.4-1.2v0c0.5-0.033 0.967 0.033 1.4 0.2v0c0.933 0.133 1.683 0.583 2.25 1.35v0l11.1 13.95c3.6-5 7.267-9.5 11-13.5v0c0.7-1.2 1.733-1.783 3.1-1.75v0c0.967 0.033 1.783 0.4 2.45 1.1 0.667 0.7 0.983 1.533 0.95 2.5v0c-0.1 3.167-0.1 8.007 0 14.52 0.1 6.52 0.117 11.28 0.05 14.28zM54.6-3.3v0c0-0.833-0.007-1.977-0.02-3.43-0.02-1.447-0.03-2.587-0.03-3.42v0-2.85l-5-8.3c-2.467-4.033-4.3-7.183-5.5-9.45v0c-0.467-0.833-0.567-1.71-0.3-2.63 0.267-0.913 0.827-1.603 1.68-2.07 0.847-0.467 1.73-0.567 2.65-0.3 0.913 0.267 1.603 0.833 2.07 1.7v0c1.1 1.967 3.6 6.183 7.5 12.65v0l2.7-4.2 5.7-8.6c0.5-0.8 1.217-1.317 2.15-1.55 0.933-0.233 1.81-0.093 2.63 0.42 0.813 0.52 1.337 1.247 1.57 2.18 0.233 0.933 0.083 1.817-0.45 2.65v0c-0.2 0.333-2.133 3.267-5.8 8.8v0c-1.967 2.967-3.517 5.45-4.65 7.45v0c0.033 0.767 0.05 2.45 0.05 5.05 0 2.6 0.017 4.5 0.05 5.7v0c0.033 0.967-0.283 1.8-0.95 2.5-0.667 0.7-1.483 1.067-2.45 1.1-0.967 0.033-1.8-0.283-2.5-0.95-0.7-0.667-1.067-1.483-1.1-2.45z" fill-rule="nonzero" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal; fill: rgb(72, 162, 255);" data-fill-palette-color="primary" stroke="none"/> <path xmlns="http://www.w3.org/2000/svg" d="M39.3-3.35v0c-0.033 0.967-0.4 1.783-1.1 2.45-0.7 0.667-1.533 0.983-2.5 0.95-0.967-0.033-1.783-0.4-2.45-1.1-0.667-0.7-0.983-1.533-0.95-2.5v0c0.067-2.167 0.033-8.567-0.1-19.2v0c-2.467 3.1-4.9 6.417-7.3 9.95v0c-0.567 0.8-1.317 1.29-2.25 1.47-0.933 0.187-1.8 0.013-2.6-0.52v0c-0.1-0.067-0.183-0.133-0.25-0.2v0c-0.367-0.233-0.683-0.517-0.95-0.85v0l-7.7-9.7c0.033 0.067 0.043 3.257 0.03 9.57-0.02 6.32-0.03 9.497-0.03 9.53v0c0 0.967-0.34 1.79-1.02 2.47-0.687 0.687-1.513 1.03-2.48 1.03-0.967 0-1.79-0.343-2.47-1.03-0.687-0.68-1.03-1.503-1.03-2.47v0c0-1.4 0.007-3.75 0.02-7.05 0.02-3.3 0.03-5.85 0.03-7.65 0-1.8-0.033-4.033-0.1-6.7v0c-0.067-2.367-0.183-4.817-0.35-7.35v0c-0.067-0.967 0.217-1.817 0.85-2.55 0.633-0.733 1.433-1.133 2.4-1.2v0c0.5-0.033 0.967 0.033 1.4 0.2v0c0.933 0.133 1.683 0.583 2.25 1.35v0l11.1 13.95c3.6-5 7.267-9.5 11-13.5v0c0.7-1.2 1.733-1.783 3.1-1.75v0c0.967 0.033 1.783 0.4 2.45 1.1 0.667 0.7 0.983 1.533 0.95 2.5v0c-0.1 3.167-0.1 8.007 0 14.52 0.1 6.52 0.117 11.28 0.05 14.28zM54.6-3.3v0c0-0.833-0.007-1.977-0.02-3.43-0.02-1.447-0.03-2.587-0.03-3.42v0-2.85l-5-8.3c-2.467-4.033-4.3-7.183-5.5-9.45v0c-0.467-0.833-0.567-1.71-0.3-2.63 0.267-0.913 0.827-1.603 1.68-2.07 0.847-0.467 1.73-0.567 2.65-0.3 0.913 0.267 1.603 0.833 2.07 1.7v0c1.1 1.967 3.6 6.183 7.5 12.65v0l2.7-4.2 5.7-8.6c0.5-0.8 1.217-1.317 2.15-1.55 0.933-0.233 1.81-0.093 2.63 0.42 0.813 0.52 1.337 1.247 1.57 2.18 0.233 0.933 0.083 1.817-0.45 2.65v0c-0.2 0.333-2.133 3.267-5.8 8.8v0c-1.967 2.967-3.517 5.45-4.65 7.45v0c0.033 0.767 0.05 2.45 0.05 5.05 0 2.6 0.017 4.5 0.05 5.7v0c0.033 0.967-0.283 1.8-0.95 2.5-0.667 0.7-1.483 1.067-2.45 1.1-0.967 0.033-1.8-0.283-2.5-0.95-0.7-0.667-1.067-1.483-1.1-2.45z" fill-rule="nonzero" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" style="mix-blend-mode: normal; fill: rgb(72, 162, 255);" stroke="none"/>
<path xmlns="http://www.w3.org/2000/svg" d="M7.9 39.93v0c-1.133-0.033-2.05-0.5-2.75-1.4v0c-0.667-0.667-1.017-1.483-1.05-2.45v0c-0.033-3.4-0.1-8.217-0.2-14.45v0l-0.15-14.05c0-0.967 0.333-1.793 1-2.48 0.667-0.68 1.483-1.027 2.45-1.04 0.967-0.02 1.793 0.303 2.48 0.97 0.68 0.667 1.037 1.483 1.07 2.45v0l0.15 13.1 0.15 12.6c1.633 0 4.01-0.017 7.13-0.05 3.113-0.033 5.403-0.05 6.87-0.05v0c0.967 0 1.793 0.323 2.48 0.97 0.68 0.653 1.02 1.447 1.02 2.38 0 0.933-0.34 1.74-1.02 2.42-0.687 0.687-1.513 1.03-2.48 1.03v0c-1.1 0-3.95 0.017-8.55 0.05v0c-4.633 0.033-7.5 0.033-8.6 0zM34.5 39.98v0c-0.967 0-1.8-0.327-2.5-0.98-0.7-0.647-1.067-1.453-1.1-2.42v0c0-0.867 0.277-1.643 0.83-2.33 0.547-0.68 1.237-1.087 2.07-1.22v0c0.033-5.8 0.017-13.15-0.05-22.05v0c-0.8-0.167-1.467-0.567-2-1.2-0.533-0.633-0.8-1.383-0.8-2.25v0c0-0.967 0.35-1.783 1.05-2.45 0.7-0.667 1.533-1 2.5-1v0l5.65 0.05c0.967 0 1.783 0.35 2.45 1.05 0.667 0.7 1 1.533 1 2.5v0c0 0.833-0.273 1.567-0.82 2.2-0.553 0.633-1.247 1.033-2.08 1.2v0c0.1 8.767 0.133 16.033 0.1 21.8v0c0.833 0.133 1.527 0.517 2.08 1.15 0.547 0.633 0.837 1.367 0.87 2.2v0c0 0.967-0.323 1.8-0.97 2.5-0.653 0.7-1.463 1.067-2.43 1.1v0zM76.15 39.63v0c-1.3 0.233-2.383-0.167-3.25-1.2v0c-2.233-2.633-5.05-6.533-8.45-11.7v0c-3.7-5.233-6.317-8.783-7.85-10.65v0c0.033 1.333 0.117 4.29 0.25 8.87 0.133 4.587 0.2 8.397 0.2 11.43v0c0 0.967-0.34 1.79-1.02 2.47-0.687 0.687-1.513 1.03-2.48 1.03-0.967 0-1.79-0.343-2.47-1.03-0.687-0.68-1.03-1.503-1.03-2.47v0c0-2.933-0.093-7.677-0.28-14.23-0.18-6.547-0.203-11.503-0.07-14.87v0c0.033-0.967 0.4-1.783 1.1-2.45 0.7-0.667 1.533-0.983 2.5-0.95v0c0.467 0.033 0.917 0.133 1.35 0.3v0c0.633 0.2 1.183 0.567 1.65 1.1v0c0.567 0.7 1.483 1.807 2.75 3.32 1.267 1.52 2.217 2.663 2.85 3.43v0c1.867 1.933 4.633 5.5 8.3 10.7v0l2.4 3.45c0.1-7.733 0.117-14.017 0.05-18.85v0c0-0.967 0.333-1.793 1-2.48 0.667-0.68 1.483-1.027 2.45-1.04 0.967-0.02 1.793 0.303 2.48 0.97 0.68 0.667 1.037 1.483 1.07 2.45v0c0.033 4.533-0.017 14.183-0.15 28.95v0c-0.033 0.933-0.367 1.723-1 2.37-0.633 0.653-1.417 1.013-2.35 1.08zM109.85 38.83l-11.25-14.45-4.2 3.45c0.1 2.7 0.217 5.617 0.35 8.75v0c0.033 0.933-0.273 1.757-0.92 2.47-0.653 0.72-1.453 1.107-2.4 1.16-0.953 0.047-1.787-0.257-2.5-0.91-0.72-0.647-1.113-1.453-1.18-2.42v0c-0.5-10.267-0.683-20.267-0.55-30v0c0.033-0.967 0.393-1.783 1.08-2.45 0.68-0.667 1.503-0.993 2.47-0.98 0.967 0.02 1.783 0.37 2.45 1.05 0.667 0.687 1 1.513 1 2.48v0c-0.067 4.233-0.067 8.217 0 11.95v0c5.233-4.467 10.083-9.217 14.55-14.25v0c0.667-0.7 1.477-1.067 2.43-1.1 0.947-0.033 1.78 0.283 2.5 0.95 0.713 0.667 1.087 1.473 1.12 2.42 0.033 0.953-0.283 1.797-0.95 2.53v0c-3.267 3.733-6.6 7.15-10 10.25v0l11.5 14.8c0.6 0.733 0.833 1.583 0.7 2.55v0c-0.1 0.967-0.523 1.75-1.27 2.35-0.753 0.6-1.613 0.843-2.58 0.73-0.967-0.12-1.75-0.563-2.35-1.33zM132.85 40.28v0c-2.567 0.067-4.883-0.333-6.95-1.2-2.067-0.867-3.7-2.133-4.9-3.8-1.2-1.667-1.8-3.6-1.8-5.8v0c0-1.333 0.383-2.377 1.15-3.13 0.767-0.747 1.633-1.12 2.6-1.12v0c0.967 0 1.75 0.34 2.35 1.02 0.6 0.687 0.9 1.447 0.9 2.28v0c0 0.833 0.05 1.527 0.15 2.08 0.1 0.547 0.35 1.053 0.75 1.52v0c0.767 0.933 2.717 1.4 5.85 1.4v0c1.7 0 3.15-0.35 4.35-1.05 1.2-0.7 1.8-1.7 1.8-3v0c0-0.867-0.7-1.7-2.1-2.5v0c-0.967-0.533-3.117-1.367-6.45-2.5v0c-3.633-1.2-6.367-2.593-8.2-4.18-1.833-1.58-2.75-3.737-2.75-6.47v0c0-2.1 0.6-3.933 1.8-5.5 1.2-1.567 2.777-2.767 4.73-3.6 1.947-0.833 4.02-1.25 6.22-1.25v0c2.267-0.067 4.45 0.3 6.55 1.1 2.1 0.8 3.793 1.94 5.08 3.42 1.28 1.487 1.92 3.163 1.92 5.03v0c0 1-0.367 1.857-1.1 2.57-0.733 0.72-1.583 1.08-2.55 1.08v0c-0.667 0-1.317-0.2-1.95-0.6-0.633-0.4-1.033-0.9-1.2-1.5v0c-0.2-0.8-0.49-1.417-0.87-1.85-0.387-0.433-0.897-0.833-1.53-1.2v0c-0.833-0.467-1.583-0.8-2.25-1-0.667-0.2-1.617-0.3-2.85-0.3v0c-1.467 0-2.673 0.273-3.62 0.82-0.953 0.553-1.43 1.263-1.43 2.13v0c0 1.1 0.543 2 1.63 2.7 1.08 0.7 2.603 1.367 4.57 2v0c3.8 1.267 6.333 2.25 7.6 2.95v0c2.1 1.167 3.583 2.4 4.45 3.7 0.867 1.3 1.3 2.933 1.3 4.9v0c0 2.1-0.633 3.973-1.9 5.62-1.267 1.653-2.923 2.937-4.97 3.85-2.053 0.92-4.18 1.38-6.38 1.38z" fill-rule="nonzero" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal; fill: rgb(72, 162, 255);" data-fill-palette-color="primary" stroke="none"/> <path xmlns="http://www.w3.org/2000/svg" d="M7.9 39.93v0c-1.133-0.033-2.05-0.5-2.75-1.4v0c-0.667-0.667-1.017-1.483-1.05-2.45v0c-0.033-3.4-0.1-8.217-0.2-14.45v0l-0.15-14.05c0-0.967 0.333-1.793 1-2.48 0.667-0.68 1.483-1.027 2.45-1.04 0.967-0.02 1.793 0.303 2.48 0.97 0.68 0.667 1.037 1.483 1.07 2.45v0l0.15 13.1 0.15 12.6c1.633 0 4.01-0.017 7.13-0.05 3.113-0.033 5.403-0.05 6.87-0.05v0c0.967 0 1.793 0.323 2.48 0.97 0.68 0.653 1.02 1.447 1.02 2.38 0 0.933-0.34 1.74-1.02 2.42-0.687 0.687-1.513 1.03-2.48 1.03v0c-1.1 0-3.95 0.017-8.55 0.05v0c-4.633 0.033-7.5 0.033-8.6 0zM34.5 39.98v0c-0.967 0-1.8-0.327-2.5-0.98-0.7-0.647-1.067-1.453-1.1-2.42v0c0-0.867 0.277-1.643 0.83-2.33 0.547-0.68 1.237-1.087 2.07-1.22v0c0.033-5.8 0.017-13.15-0.05-22.05v0c-0.8-0.167-1.467-0.567-2-1.2-0.533-0.633-0.8-1.383-0.8-2.25v0c0-0.967 0.35-1.783 1.05-2.45 0.7-0.667 1.533-1 2.5-1v0l5.65 0.05c0.967 0 1.783 0.35 2.45 1.05 0.667 0.7 1 1.533 1 2.5v0c0 0.833-0.273 1.567-0.82 2.2-0.553 0.633-1.247 1.033-2.08 1.2v0c0.1 8.767 0.133 16.033 0.1 21.8v0c0.833 0.133 1.527 0.517 2.08 1.15 0.547 0.633 0.837 1.367 0.87 2.2v0c0 0.967-0.323 1.8-0.97 2.5-0.653 0.7-1.463 1.067-2.43 1.1v0zM76.15 39.63v0c-1.3 0.233-2.383-0.167-3.25-1.2v0c-2.233-2.633-5.05-6.533-8.45-11.7v0c-3.7-5.233-6.317-8.783-7.85-10.65v0c0.033 1.333 0.117 4.29 0.25 8.87 0.133 4.587 0.2 8.397 0.2 11.43v0c0 0.967-0.34 1.79-1.02 2.47-0.687 0.687-1.513 1.03-2.48 1.03-0.967 0-1.79-0.343-2.47-1.03-0.687-0.68-1.03-1.503-1.03-2.47v0c0-2.933-0.093-7.677-0.28-14.23-0.18-6.547-0.203-11.503-0.07-14.87v0c0.033-0.967 0.4-1.783 1.1-2.45 0.7-0.667 1.533-0.983 2.5-0.95v0c0.467 0.033 0.917 0.133 1.35 0.3v0c0.633 0.2 1.183 0.567 1.65 1.1v0c0.567 0.7 1.483 1.807 2.75 3.32 1.267 1.52 2.217 2.663 2.85 3.43v0c1.867 1.933 4.633 5.5 8.3 10.7v0l2.4 3.45c0.1-7.733 0.117-14.017 0.05-18.85v0c0-0.967 0.333-1.793 1-2.48 0.667-0.68 1.483-1.027 2.45-1.04 0.967-0.02 1.793 0.303 2.48 0.97 0.68 0.667 1.037 1.483 1.07 2.45v0c0.033 4.533-0.017 14.183-0.15 28.95v0c-0.033 0.933-0.367 1.723-1 2.37-0.633 0.653-1.417 1.013-2.35 1.08zM109.85 38.83l-11.25-14.45-4.2 3.45c0.1 2.7 0.217 5.617 0.35 8.75v0c0.033 0.933-0.273 1.757-0.92 2.47-0.653 0.72-1.453 1.107-2.4 1.16-0.953 0.047-1.787-0.257-2.5-0.91-0.72-0.647-1.113-1.453-1.18-2.42v0c-0.5-10.267-0.683-20.267-0.55-30v0c0.033-0.967 0.393-1.783 1.08-2.45 0.68-0.667 1.503-0.993 2.47-0.98 0.967 0.02 1.783 0.37 2.45 1.05 0.667 0.687 1 1.513 1 2.48v0c-0.067 4.233-0.067 8.217 0 11.95v0c5.233-4.467 10.083-9.217 14.55-14.25v0c0.667-0.7 1.477-1.067 2.43-1.1 0.947-0.033 1.78 0.283 2.5 0.95 0.713 0.667 1.087 1.473 1.12 2.42 0.033 0.953-0.283 1.797-0.95 2.53v0c-3.267 3.733-6.6 7.15-10 10.25v0l11.5 14.8c0.6 0.733 0.833 1.583 0.7 2.55v0c-0.1 0.967-0.523 1.75-1.27 2.35-0.753 0.6-1.613 0.843-2.58 0.73-0.967-0.12-1.75-0.563-2.35-1.33zM132.85 40.28v0c-2.567 0.067-4.883-0.333-6.95-1.2-2.067-0.867-3.7-2.133-4.9-3.8-1.2-1.667-1.8-3.6-1.8-5.8v0c0-1.333 0.383-2.377 1.15-3.13 0.767-0.747 1.633-1.12 2.6-1.12v0c0.967 0 1.75 0.34 2.35 1.02 0.6 0.687 0.9 1.447 0.9 2.28v0c0 0.833 0.05 1.527 0.15 2.08 0.1 0.547 0.35 1.053 0.75 1.52v0c0.767 0.933 2.717 1.4 5.85 1.4v0c1.7 0 3.15-0.35 4.35-1.05 1.2-0.7 1.8-1.7 1.8-3v0c0-0.867-0.7-1.7-2.1-2.5v0c-0.967-0.533-3.117-1.367-6.45-2.5v0c-3.633-1.2-6.367-2.593-8.2-4.18-1.833-1.58-2.75-3.737-2.75-6.47v0c0-2.1 0.6-3.933 1.8-5.5 1.2-1.567 2.777-2.767 4.73-3.6 1.947-0.833 4.02-1.25 6.22-1.25v0c2.267-0.067 4.45 0.3 6.55 1.1 2.1 0.8 3.793 1.94 5.08 3.42 1.28 1.487 1.92 3.163 1.92 5.03v0c0 1-0.367 1.857-1.1 2.57-0.733 0.72-1.583 1.08-2.55 1.08v0c-0.667 0-1.317-0.2-1.95-0.6-0.633-0.4-1.033-0.9-1.2-1.5v0c-0.2-0.8-0.49-1.417-0.87-1.85-0.387-0.433-0.897-0.833-1.53-1.2v0c-0.833-0.467-1.583-0.8-2.25-1-0.667-0.2-1.617-0.3-2.85-0.3v0c-1.467 0-2.673 0.273-3.62 0.82-0.953 0.553-1.43 1.263-1.43 2.13v0c0 1.1 0.543 2 1.63 2.7 1.08 0.7 2.603 1.367 4.57 2v0c3.8 1.267 6.333 2.25 7.6 2.95v0c2.1 1.167 3.583 2.4 4.45 3.7 0.867 1.3 1.3 2.933 1.3 4.9v0c0 2.1-0.633 3.973-1.9 5.62-1.267 1.653-2.923 2.937-4.97 3.85-2.053 0.92-4.18 1.38-6.38 1.38z" fill-rule="nonzero" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" style="mix-blend-mode: normal; fill: rgb(72, 162, 255);" stroke="none"/>
</g> </g>
</svg> </svg>
</g> </g>
@@ -33,8 +33,8 @@
<svg viewBox="0 0 200.44498811830644 199.99999999999997" height="199.99999999999997" width="200.44498811830644"> <svg viewBox="0 0 200.44498811830644 199.99999999999997" height="199.99999999999997" width="200.44498811830644">
<g> <g>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0.11100006103515625 100 99.77799987792969" enable-background="new 0 0 100 100" height="199.99999999999997" width="200.44498811830644" class="icon-icon-0" data-fill-palette-color="accent" id="icon-0"> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0.11100006103515625 100 99.77799987792969" enable-background="new 0 0 100 100" height="199.99999999999997" width="200.44498811830644" class="icon-icon-0" data-fill-palette-color="accent" id="icon-0">
<path d="M53.406 36.706L43.25 36.707c0 5.521 4.547 9.999 10.156 9.999 9.07 0 16.592 7.522 16.592 16.592H70c0 9.07-7.522 16.592-16.592 16.592H0l31 19.999v-5c0-2.762 2.238-5 5-5h17.408v0.003C68.07 89.892 80 77.962 80 63.298 80 48.636 68.07 36.706 53.406 36.706z" data-fill-palette-color="accent" style="fill: rgb(72, 162, 255);"/> <path d="M53.406 36.706L43.25 36.707c0 5.521 4.547 9.999 10.156 9.999 9.07 0 16.592 7.522 16.592 16.592H70c0 9.07-7.522 16.592-16.592 16.592H0l31 19.999v-5c0-2.762 2.238-5 5-5h17.408v0.003C68.07 89.892 80 77.962 80 63.298 80 48.636 68.07 36.706 53.406 36.706z" style="fill: rgb(72, 162, 255);"/>
<path d="M46.592 63.294l10.157-0.001c0-5.521-4.548-9.999-10.157-9.999C37.521 53.294 30 45.771 30 36.702c0-9.07 7.522-16.591 16.592-16.591H100l-31-20v5c0 2.762-2.238 5-5 5H46.592v-0.003C31.93 10.108 20 22.038 20 36.702h-0.002C19.998 51.364 31.928 63.294 46.592 63.294z" data-fill-palette-color="accent" style="fill: rgb(72, 162, 255);"/> <path d="M46.592 63.294l10.157-0.001c0-5.521-4.548-9.999-10.157-9.999C37.521 53.294 30 45.771 30 36.702c0-9.07 7.522-16.591 16.592-16.591H100l-31-20v5c0 2.762-2.238 5-5 5H46.592v-0.003C31.93 10.108 20 22.038 20 36.702h-0.002C19.998 51.364 31.928 63.294 46.592 63.294z" style="fill: rgb(72, 162, 255);"/>
</svg> </svg>
</g> </g>
</svg> </svg>
@@ -44,4 +44,4 @@
</svg> </svg>
<rect width="395.52" height="130.3536553275838" fill="none" stroke="none" visibility="hidden"/> <rect width="395.52" height="130.3536553275838" fill="none" stroke="none" visibility="hidden"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -1,6 +1,6 @@
import { CSSProperties, ReactNode } from "react"; import { CSSProperties, ReactNode } from 'react';
import styles from "./block-wrapper.module.scss"; import styles from './block-wrapper.module.scss';
interface BlockWrapperProps { interface BlockWrapperProps {
children: ReactNode; children: ReactNode;
@@ -9,7 +9,10 @@ interface BlockWrapperProps {
export default function BlockWrapper({ children, style }: BlockWrapperProps) { export default function BlockWrapper({ children, style }: BlockWrapperProps) {
return ( return (
<section className={styles["block-wrapper"]} style={style}> <section
className={styles['block-wrapper']}
style={style}
>
{children} {children}
</section> </section>
); );

View File

@@ -1,5 +1,5 @@
@import "styles/keyframes.scss"; @import 'styles/keyframes.scss';
@import "styles/colors.scss"; @import 'styles/colors.scss';
.block-wrapper { .block-wrapper {
height: auto; height: auto;

View File

@@ -1,12 +1,12 @@
import Link from "next/link"; import Link from 'next/link';
import { CSSProperties, ReactNode } from "react"; import { CSSProperties, ReactNode } from 'react';
export default function ButtonLink({ export default function ButtonLink({
href = "#", href = '#',
onClick, onClick,
children, children,
style = {}, style = {},
className = "", className = '',
}: { }: {
href?: string; href?: string;
onClick?: (...args: any) => any; onClick?: (...args: any) => any;
@@ -15,13 +15,18 @@ export default function ButtonLink({
className?: string; className?: string;
}) { }) {
const handleClick = (event) => { const handleClick = (event) => {
if (!href || href === "#") { if (!href || href === '#') {
event.preventDefault(); event.preventDefault();
} }
onClick && onClick(); onClick && onClick();
}; };
return ( return (
<Link href={href} onClick={handleClick} style={style} className={className}> <Link
href={href}
onClick={handleClick}
style={style}
className={className}
>
{children} {children}
</Link> </Link>
); );

View File

@@ -1,4 +1,4 @@
import { MutableRefObject, useState } from "react"; import { MutableRefObject, useState } from 'react';
interface SelectorProps { interface SelectorProps {
name: string; name: string;
@@ -18,8 +18,8 @@ export default function Checkbox({
labelComponent, labelComponent,
disabled = false, disabled = false,
innerRef = null, innerRef = null,
fieldClass = "", fieldClass = '',
placeholder = "Type something...", placeholder = 'Type something...',
isChecked, isChecked,
onChangeCallback, onChangeCallback,
}: SelectorProps): JSX.Element { }: SelectorProps): JSX.Element {
@@ -35,17 +35,23 @@ export default function Checkbox({
return ( return (
<div className={`checkbox-field ${fieldClass}`}> <div className={`checkbox-field ${fieldClass}`}>
{label && ( {label && (
<label htmlFor={name} title={`${name} field`}> <label
htmlFor={name}
title={`${name} field`}
>
{label} {label}
</label> </label>
)} )}
{labelComponent && ( {labelComponent && (
<label htmlFor={name} title={`${name} field`}> <label
htmlFor={name}
title={`${name} field`}
>
{labelComponent} {labelComponent}
</label> </label>
)} )}
<input <input
type="checkbox" type='checkbox'
id={name} id={name}
name={name} name={name}
onChange={onChange} onChange={onChange}

View File

@@ -1,7 +1,7 @@
import { withTranslation } from "next-i18next"; import { withTranslation } from 'next-i18next';
import React from "react"; import React from 'react';
import LangSelector from "../LangSelector"; import LangSelector from '../LangSelector';
import styles from "./error-boundary.module.scss"; import styles from './error-boundary.module.scss';
class ErrorBoundary extends React.Component { class ErrorBoundary extends React.Component {
constructor(props) { constructor(props) {
@@ -21,22 +21,22 @@ class ErrorBoundary extends React.Component {
if (!this.state.errorInfo) return this.props.children; if (!this.state.errorInfo) return this.props.children;
return ( return (
<div className={styles["error-boundary"]}> <div className={styles['error-boundary']}>
<div className={styles["boundary-content"]}> <div className={styles['boundary-content']}>
<h1>{this.props.t("common:generic-error")}</h1> <h1>{this.props.t('common:generic-error')}</h1>
<p <p
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: this.props.t("common:generic-error-description"), __html: this.props.t('common:generic-error-description'),
}} }}
/> />
<button onClick={() => window.location.reload()}> <button onClick={() => window.location.reload()}>
{this.props.t("common:retry")} {this.props.t('common:retry')}
</button> </button>
<details> <details>
<summary>{this.state.error && this.state.error.toString()}</summary> <summary>{this.state.error && this.state.error.toString()}</summary>
<code>{this.state.errorInfo.componentStack}</code> <code>{this.state.errorInfo.componentStack}</code>
</details> </details>
<div className="lang-selector"> <div className='lang-selector'>
<LangSelector /> <LangSelector />
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
import MessageManager from "components/MessageManager/MessageManager"; import MessageManager from 'components/MessageManager/MessageManager';
import { i18n, useTranslation } from "next-i18next"; import { i18n, useTranslation } from 'next-i18next';
import { NextSeo } from "next-seo"; import { NextSeo } from 'next-seo';
import Link from "next/link"; import Link from 'next/link';
interface FormProps { interface FormProps {
title: string; title: string;
@@ -29,8 +29,8 @@ export default function Form({
infoMessage, infoMessage,
canSubmit, canSubmit,
handleSubmit, handleSubmit,
textBtnConfirm = i18n.t("common:confirm"), textBtnConfirm = i18n.t('common:confirm'),
classBtnConfirm = "", classBtnConfirm = '',
children, children,
disableHomeLink = false, disableHomeLink = false,
}: FormProps) { }: FormProps) {
@@ -42,13 +42,17 @@ export default function Form({
<h2>{title}</h2> <h2>{title}</h2>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
{children} {children}
<button type="submit" className={classBtnConfirm} disabled={!canSubmit}> <button
type='submit'
className={classBtnConfirm}
disabled={!canSubmit}
>
{textBtnConfirm} {textBtnConfirm}
</button> </button>
</form> </form>
{!disableHomeLink && ( {!disableHomeLink && (
<Link href={categoryId ? `/?categoryId=${categoryId}` : "/"}> <Link href={categoryId ? `/?categoryId=${categoryId}` : '/'}>
{t("common:back-home")} {t('common:back-home')}
</Link> </Link>
)} )}
<MessageManager <MessageManager

View File

@@ -1,5 +1,5 @@
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import { useRouter } from "next/router"; import { useRouter } from 'next/router';
export default function LangSelector() { export default function LangSelector() {
const router = useRouter(); const router = useRouter();
@@ -10,20 +10,23 @@ export default function LangSelector() {
i18n.changeLanguage(newLocale); i18n.changeLanguage(newLocale);
router.push({ pathname, query }, asPath, { locale: newLocale }); router.push({ pathname, query }, asPath, { locale: newLocale });
}; };
const languages = ["en", "fr"]; const languages = ['en', 'fr'];
return ( return (
<select <select
name="lng-select" name='lng-select'
id="lng-select" id='lng-select'
onChange={(event) => { onChange={(event) => {
onToggleLanguageClick(event.target.value); onToggleLanguageClick(event.target.value);
}} }}
value={i18n.language} value={i18n.language}
> >
{languages.map((lang) => ( {languages.map((lang) => (
<option key={lang} value={lang}> <option
{t(`common:language.${lang as "fr" | "en"}`)} key={lang}
value={lang}
>
{t(`common:language.${lang as 'fr' | 'en'}`)}
</option> </option>
))} ))}
</select> </select>

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 { TbLoader3 } from "react-icons/tb"; import { TbLoader3 } from 'react-icons/tb';
import { TfiWorld } from "react-icons/tfi"; import { TfiWorld } from 'react-icons/tfi';
import styles from "./links.module.scss"; import styles from './links.module.scss';
interface LinkFaviconProps { interface LinkFaviconProps {
url: string; url: string;
@@ -22,9 +22,9 @@ export default function LinkFavicon({
const [isLoading, setLoading] = useState<boolean>(true); const [isLoading, setLoading] = useState<boolean>(true);
const baseUrlApi = const baseUrlApi =
process.env.NEXT_PUBLIC_API_URL || process.env.NEXT_PUBLIC_API_URL ||
(typeof window !== "undefined" && window)?.location?.origin + "/api"; (typeof window !== 'undefined' && window)?.location?.origin + '/api';
if (!baseUrlApi) { if (!baseUrlApi) {
console.warn("Missing API URL"); console.warn('Missing API URL');
} }
const setFallbackFavicon = () => setFailed(true); const setFallbackFavicon = () => setFailed(true);
@@ -32,8 +32,8 @@ export default function LinkFavicon({
return ( return (
<div <div
className={styles["favicon"]} className={styles['favicon']}
style={{ marginRight: !noMargin ? "1em" : "0" }} style={{ marginRight: !noMargin ? '1em' : '0' }}
> >
{!isFailed && baseUrlApi ? ( {!isFailed && baseUrlApi ? (
<Image <Image
@@ -45,14 +45,14 @@ export default function LinkFavicon({
onLoad={handleStopLoading} onLoad={handleStopLoading}
height={size} height={size}
width={size} width={size}
alt="icon" alt='icon'
/> />
) : ( ) : (
<TfiWorld size={size} /> <TfiWorld size={size} />
)} )}
{isLoading && ( {isLoading && (
<span <span
className={styles["favicon-loader"]} className={styles['favicon-loader']}
style={{ height: `${size}px`, width: `${size}px` }} style={{ height: `${size}px`, width: `${size}px` }}
> >
<TbLoader3 size={size} /> <TbLoader3 size={size} />

View File

@@ -1,14 +1,14 @@
import { motion } from "framer-motion"; import { motion } from 'framer-motion';
import LinkTag from "next/link"; import LinkTag from 'next/link';
import { AiFillStar } from "react-icons/ai"; import { AiFillStar } from 'react-icons/ai';
import PATHS from "constants/paths"; import PATHS from 'constants/paths';
import { Link } from "types"; import { Link } from 'types';
import EditItem from "components/QuickActions/EditItem"; import EditItem from 'components/QuickActions/EditItem';
import FavoriteItem from "components/QuickActions/FavoriteItem"; import FavoriteItem from 'components/QuickActions/FavoriteItem';
import RemoveItem from "components/QuickActions/RemoveItem"; import RemoveItem from 'components/QuickActions/RemoveItem';
import LinkFavicon from "./LinkFavicon"; import LinkFavicon from './LinkFavicon';
import styles from "./links.module.scss"; import styles from './links.module.scss';
import { makeRequest } from "lib/request"; import { makeRequest } from 'lib/request';
export default function LinkItem({ export default function LinkItem({
link, link,
@@ -16,14 +16,14 @@ export default function LinkItem({
index, index,
}: { }: {
link: Link; link: Link;
toggleFavorite: (linkId: Link["id"]) => void; toggleFavorite: (linkId: Link['id']) => void;
index: number; index: number;
}) { }) {
const { id, name, url, favorite } = link; const { id, name, url, favorite } = link;
const onFavorite = () => { const onFavorite = () => {
makeRequest({ makeRequest({
url: `${PATHS.API.LINK}/${link.id}`, url: `${PATHS.API.LINK}/${link.id}`,
method: "PUT", method: 'PUT',
body: { body: {
name, name,
url, url,
@@ -37,57 +37,70 @@ export default function LinkItem({
return ( return (
<motion.li <motion.li
className={styles["link"]} className={styles['link']}
key={id} key={id}
initial={{ x: -30, opacity: 0 }} initial={{ x: -30, opacity: 0 }}
animate={{ x: 0, opacity: 1 }} animate={{ x: 0, opacity: 1 }}
transition={{ transition={{
type: "spring", type: 'spring',
stiffness: 260, stiffness: 260,
damping: 20, damping: 20,
delay: index * 0.05, delay: index * 0.05,
}} }}
> >
<LinkFavicon url={url} /> <LinkFavicon url={url} />
<LinkTag href={url} target={"_blank"} rel="noreferrer"> <LinkTag
<span className={styles["link-name"]}> href={url}
{name} {favorite && <AiFillStar color="#ffc107" />} target={'_blank'}
rel='noreferrer'
>
<span className={styles['link-name']}>
{name} {favorite && <AiFillStar color='#ffc107' />}
</span> </span>
<LinkItemURL url={url} /> <LinkItemURL url={url} />
</LinkTag> </LinkTag>
<div className={styles["controls"]}> <div className={styles['controls']}>
<FavoriteItem isFavorite={favorite} onClick={onFavorite} /> <FavoriteItem
<EditItem type="link" id={id} /> isFavorite={favorite}
<RemoveItem type="link" id={id} /> onClick={onFavorite}
/>
<EditItem
type='link'
id={id}
/>
<RemoveItem
type='link'
id={id}
/>
</div> </div>
</motion.li> </motion.li>
); );
} }
function LinkItemURL({ url }: { url: Link["url"] }) { function LinkItemURL({ url }: { url: Link['url'] }) {
try { try {
const { origin, pathname, search } = new URL(url); const { origin, pathname, search } = new URL(url);
let text = ""; let text = '';
if (pathname !== "/") { if (pathname !== '/') {
text += pathname; text += pathname;
} }
if (search !== "") { if (search !== '') {
if (text === "") { if (text === '') {
text += "/"; text += '/';
} }
text += search; text += search;
} }
return ( return (
<span className={styles["link-url"]}> <span className={styles['link-url']}>
{origin} {origin}
<span className={styles["url-pathname"]}>{text}</span> <span className={styles['url-pathname']}>{text}</span>
</span> </span>
); );
} catch (error) { } catch (error) {
console.error("error", error); console.error('error', error);
return <span className={styles["link-url"]}>{url}</span>; return <span className={styles['link-url']}>{url}</span>;
} }
} }

View File

@@ -1,18 +1,18 @@
import ButtonLink from "components/ButtonLink"; import ButtonLink from 'components/ButtonLink';
import CreateItem from "components/QuickActions/CreateItem"; import CreateItem from 'components/QuickActions/CreateItem';
import EditItem from "components/QuickActions/EditItem"; import EditItem from 'components/QuickActions/EditItem';
import RemoveItem from "components/QuickActions/RemoveItem"; import RemoveItem from 'components/QuickActions/RemoveItem';
import QuickActionSearch from "components/QuickActions/Search"; import QuickActionSearch from 'components/QuickActions/Search';
import { motion } from "framer-motion"; import { motion } from 'framer-motion';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import LinkTag from "next/link"; import LinkTag from 'next/link';
import { RxHamburgerMenu } from "react-icons/rx"; import { RxHamburgerMenu } from 'react-icons/rx';
import { Category, Link } from "types"; import { Category, Link } from 'types';
import { TFunctionParam } from "types/i18next"; import { TFunctionParam } from 'types/i18next';
import LinkItem from "./LinkItem"; import LinkItem from './LinkItem';
import styles from "./links.module.scss"; import styles from './links.module.scss';
import clsx from "clsx"; import clsx from 'clsx';
import PATHS from "constants/paths"; import PATHS from 'constants/paths';
export default function Links({ export default function Links({
category, category,
@@ -22,51 +22,63 @@ export default function Links({
openSearchModal, openSearchModal,
}: { }: {
category: Category; category: Category;
toggleFavorite: (linkId: Link["id"]) => void; toggleFavorite: (linkId: Link['id']) => void;
isMobile: boolean; isMobile: boolean;
openMobileModal: () => void; openMobileModal: () => void;
openSearchModal: () => void; openSearchModal: () => void;
}) { }) {
const { t } = useTranslation("home"); const { t } = useTranslation('home');
if (category === null) { if (category === null) {
return ( return (
<div className={styles["no-category"]}> <div className={styles['no-category']}>
<p>{t("home:select-categorie")}</p> <p>{t('home:select-category')}</p>
<LinkTag href="/category/create">{t("home:or-create-one")}</LinkTag> <LinkTag href='/category/create'>{t('home:or-create-one')}</LinkTag>
</div> </div>
); );
} }
const { id, name, links } = category; const { id, name, links } = category;
return ( return (
<div className={styles["links-wrapper"]}> <div className={styles['links-wrapper']}>
<h2 className={styles["category-header"]}> <h2 className={styles['category-header']}>
{isMobile && ( {isMobile && (
<ButtonLink <ButtonLink
style={{ style={{
display: "flex", display: 'flex',
}} }}
onClick={openMobileModal} onClick={openMobileModal}
> >
<RxHamburgerMenu size={"1.5em"} style={{ marginRight: ".5em" }} /> <RxHamburgerMenu
size={'1.5em'}
style={{ marginRight: '.5em' }}
/>
</ButtonLink> </ButtonLink>
)} )}
<span className={styles["category-name"]}> <span className={styles['category-name']}>
{name} {name}
{links.length > 0 && ( {links.length > 0 && (
<span className={styles["links-count"]}> {links.length}</span> <span className={styles['links-count']}> {links.length}</span>
)} )}
</span> </span>
<span className={styles["category-controls"]}> <span className={styles['category-controls']}>
<QuickActionSearch openSearchModal={openSearchModal} /> <QuickActionSearch openSearchModal={openSearchModal} />
<CreateItem type="link" categoryId={category.id} /> <CreateItem
<EditItem type="category" id={id} /> type='link'
<RemoveItem type="category" id={id} /> categoryId={category.id}
/>
<EditItem
type='category'
id={id}
/>
<RemoveItem
type='category'
id={id}
/>
</span> </span>
</h2> </h2>
{links.length !== 0 ? ( {links.length !== 0 ? (
<ul className={clsx(styles["links"], "reset")}> <ul className={clsx(styles['links'], 'reset')}>
{links.map((link, index) => ( {links.map((link, index) => (
<LinkItem <LinkItem
link={link} link={link}
@@ -77,45 +89,54 @@ export default function Links({
))} ))}
</ul> </ul>
) : ( ) : (
<div className={styles["no-link"]}> <div className={styles['no-link']}>
<motion.p <motion.p
key={Math.random()} key={Math.random()}
initial={{ opacity: 0, scale: 0.85 }} initial={{ opacity: 0, scale: 0.85 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ transition={{
type: "spring", type: 'spring',
stiffness: 260, stiffness: 260,
damping: 20, damping: 20,
duration: 0.01, duration: 0.01,
}} }}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: t("home:no-link", { name } as TFunctionParam, { __html: t('home:no-link', { name } as TFunctionParam, {
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
}), }),
}} }}
/> />
<LinkTag href={`/link/create?categoryId=${id}`}> <LinkTag href={`/link/create?categoryId=${id}`}>
{t("common:link.create")} {t('common:link.create')}
</LinkTag> </LinkTag>
</div> </div>
)} )}
<footer className={styles["footer"]}> <footer className={styles['footer']}>
<div className="top"> <div className='top'>
<LinkTag href={PATHS.PRIVACY}>{t("common:privacy")}</LinkTag> <LinkTag href={PATHS.PRIVACY}>{t('common:privacy')}</LinkTag>
{""} {''}
<LinkTag href={PATHS.TERMS}>{t("common:terms")}</LinkTag> <LinkTag href={PATHS.TERMS}>{t('common:terms')}</LinkTag>
</div> </div>
<div className="bottom"> <div className='bottom'>
{t("home:footer.made_by")}{" "} {t('home:footer.made_by')}{' '}
<LinkTag href={PATHS.AUTHOR} target="_blank"> <LinkTag
href={PATHS.AUTHOR}
target='_blank'
>
Sonny Sonny
</LinkTag> </LinkTag>
{""} {''}
<LinkTag href={PATHS.REPO_GITHUB} target="_blank"> <LinkTag
href={PATHS.REPO_GITHUB}
target='_blank'
>
Github Github
</LinkTag> </LinkTag>
{""} {''}
<LinkTag href={PATHS.EXTENSION} target="_blank"> <LinkTag
href={PATHS.EXTENSION}
target='_blank'
>
Extension Extension
</LinkTag> </LinkTag>
</div> </div>

View File

@@ -1,5 +1,5 @@
@import "styles/keyframes.scss"; @import 'styles/keyframes.scss';
@import "styles/colors.scss"; @import 'styles/colors.scss';
.no-link, .no-link,
.no-category { .no-category {
@@ -22,10 +22,9 @@
} }
.links-wrapper { .links-wrapper {
height: calc(100%); // FIXME: eurk height: 100%;
min-width: 0; min-width: 0;
padding: 0.5em; padding: 0.5em 0.5em 0;
padding-bottom: 0;
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;

View File

@@ -1,6 +1,6 @@
import { useRouter } from "next/dist/client/router"; import { useRouter } from 'next/dist/client/router';
import styles from "./message-manager.module.scss"; import styles from './message-manager.module.scss';
interface MessageManagerProps { interface MessageManagerProps {
error?: string; error?: string;
@@ -18,14 +18,14 @@ export default function MessageManager({
return ( return (
<> <>
{info && <div className={styles["info-msg"]}>{info}</div>} {info && <div className={styles['info-msg']}>{info}</div>}
{infoUrl && <div className={styles["info-msg"]}>{infoUrl}</div>} {infoUrl && <div className={styles['info-msg']}>{infoUrl}</div>}
{error && <div className={styles["error-msg"]}>{error}</div>} {error && <div className={styles['error-msg']}>{error}</div>}
{errorUrl && <div className={styles["error-msg"]}>{errorUrl}</div>} {errorUrl && <div className={styles['error-msg']}>{errorUrl}</div>}
{success && <div className={styles["success-msg"]}>{success}</div>} {success && <div className={styles['success-msg']}>{success}</div>}
{successUrl && <div className={styles["success-msg"]}>{successUrl}</div>} {successUrl && <div className={styles['success-msg']}>{successUrl}</div>}
</> </>
); );
} }

View File

@@ -1,4 +1,4 @@
@import "styles/colors.scss"; @import 'styles/colors.scss';
.info-msg { .info-msg {
height: fit-content; height: fit-content;

View File

@@ -1,8 +1,8 @@
import { ReactNode } from "react"; import { ReactNode } from 'react';
import { createPortal } from "react-dom"; import { createPortal } from 'react-dom';
import { GrClose } from "react-icons/gr"; import { GrClose } from 'react-icons/gr';
import { motion } from "framer-motion"; import { motion } from 'framer-motion';
import styles from "./modal.module.scss"; import styles from './modal.module.scss';
interface ModalProps { interface ModalProps {
close?: (...args: any) => void | Promise<void>; close?: (...args: any) => void | Promise<void>;
@@ -21,41 +21,41 @@ export default function Modal({
children, children,
showCloseBtn = true, showCloseBtn = true,
noHeader = false, noHeader = false,
padding = "1em 1.5em", padding = '1em 1.5em',
}: ModalProps) { }: ModalProps) {
const handleWrapperClick = (event) => const handleWrapperClick = (event) =>
event.target.classList?.[0] === styles["modal-wrapper"] && close && close(); event.target.classList?.[0] === styles['modal-wrapper'] && close && close();
return createPortal( return createPortal(
<motion.div <motion.div
className={styles["modal-wrapper"]} className={styles['modal-wrapper']}
onClick={handleWrapperClick} onClick={handleWrapperClick}
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0.1, delay: 0.1 } }} exit={{ opacity: 0, transition: { duration: 0.1, delay: 0.1 } }}
> >
<motion.div <motion.div
className={styles["modal-container"]} className={styles['modal-container']}
style={{ padding }} style={{ padding }}
initial={{ opacity: 0, y: "-6em" }} initial={{ opacity: 0, y: '-6em' }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: "-6em", transition: { duration: 0.1 } }} exit={{ opacity: 0, y: '-6em', transition: { duration: 0.1 } }}
transition={{ type: "tween" }} transition={{ type: 'tween' }}
> >
{!noHeader && ( {!noHeader && (
<div className={styles["modal-header"]}> <div className={styles['modal-header']}>
<h3>{title}</h3> <h3>{title}</h3>
{showCloseBtn && ( {showCloseBtn && (
<button <button
onClick={close} onClick={close}
className={`${styles["btn-close"]} reset`} className={`${styles['btn-close']} reset`}
> >
<GrClose /> <GrClose />
</button> </button>
)} )}
</div> </div>
)} )}
<div className={styles["modal-body"]}>{children}</div> <div className={styles['modal-body']}>{children}</div>
</motion.div> </motion.div>
</motion.div>, </motion.div>,
document.body, document.body,

View File

@@ -1,4 +1,4 @@
@import "styles/colors.scss"; @import 'styles/colors.scss';
.modal-wrapper { .modal-wrapper {
z-index: 9999; z-index: 9999;
@@ -23,7 +23,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
box-shadow: 0 0 1em 0px rgba($black, 0.25); box-shadow: 0 0 1em 0 rgba($black, 0.25);
} }
.modal-header { .modal-header {

View File

@@ -1,33 +1,33 @@
import LinkTag from "next/link"; import LinkTag from 'next/link';
import { useSession } from "next-auth/react"; import { useSession } from 'next-auth/react';
import PATHS from "constants/paths"; import PATHS from 'constants/paths';
import styles from "./navbar.module.scss"; import styles from './navbar.module.scss';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import Image from "next/image"; import Image from 'next/image';
import { TFunctionParam } from "types/i18next"; import { TFunctionParam } from 'types/i18next';
export default function Navbar() { export default function Navbar() {
const { data, status } = useSession(); const { data, status } = useSession();
const { t } = useTranslation(); const { t } = useTranslation();
const avatarLabel = t("common:avatar", { const avatarLabel = t('common:avatar', {
name: data?.user?.name, name: data?.user?.name,
} as TFunctionParam); } as TFunctionParam);
return ( return (
<nav className={styles["navbar"]}> <nav className={styles['navbar']}>
<ul className="reset"> <ul className='reset'>
<li> <li>
<LinkTag href={PATHS.HOME}>MyLinks</LinkTag> <LinkTag href={PATHS.HOME}>MyLinks</LinkTag>
</li> </li>
<li> <li>
<LinkTag href={PATHS.PRIVACY}>{t("common:privacy")}</LinkTag> <LinkTag href={PATHS.PRIVACY}>{t('common:privacy')}</LinkTag>
</li> </li>
<li> <li>
<LinkTag href={PATHS.TERMS}>{t("common:terms")}</LinkTag> <LinkTag href={PATHS.TERMS}>{t('common:terms')}</LinkTag>
</li> </li>
{status === "authenticated" ? ( {status === 'authenticated' ? (
<> <>
<li className={styles["user"]}> <li className={styles['user']}>
<Image <Image
src={data.user.image} src={data.user.image}
width={24} width={24}
@@ -38,12 +38,12 @@ export default function Navbar() {
{data.user.name} {data.user.name}
</li> </li>
<li> <li>
<LinkTag href={PATHS.LOGOUT}>{t("common:logout")}</LinkTag> <LinkTag href={PATHS.LOGOUT}>{t('common:logout')}</LinkTag>
</li> </li>
</> </>
) : ( ) : (
<li> <li>
<LinkTag href={PATHS.LOGIN}>{t("common:login")}</LinkTag> <LinkTag href={PATHS.LOGIN}>{t('common:login')}</LinkTag>
</li> </li>
)} )}
</ul> </ul>

View File

@@ -1,14 +1,14 @@
import LinkTag from "next/link"; import LinkTag from 'next/link';
import { useSession } from "next-auth/react"; import { useSession } from 'next-auth/react';
import PATHS from "constants/paths"; import PATHS from 'constants/paths';
import styles from "./navbar.module.scss"; import styles from './navbar.module.scss';
export default function NavbarUntranslated() { export default function NavbarUntranslated() {
const { status } = useSession(); const { status } = useSession();
return ( return (
<nav className={styles["navbar"]}> <nav className={styles['navbar']}>
<ul className="reset"> <ul className='reset'>
<li> <li>
<LinkTag href={PATHS.HOME}>MyLinks</LinkTag> <LinkTag href={PATHS.HOME}>MyLinks</LinkTag>
</li> </li>
@@ -18,7 +18,7 @@ export default function NavbarUntranslated() {
<li> <li>
<LinkTag href={PATHS.TERMS}>Terms of use</LinkTag> <LinkTag href={PATHS.TERMS}>Terms of use</LinkTag>
</li> </li>
{status === "authenticated" ? ( {status === 'authenticated' ? (
<li> <li>
<LinkTag href={PATHS.LOGOUT}>Logout</LinkTag> <LinkTag href={PATHS.LOGOUT}>Logout</LinkTag>
</li> </li>

View File

@@ -1,7 +1,7 @@
import { motion } from "framer-motion"; import { motion } from 'framer-motion';
import useIsMobile from "hooks/useIsMobile"; import useIsMobile from 'hooks/useIsMobile';
import { CSSProperties, ReactNode } from "react"; import { CSSProperties, ReactNode } from 'react';
import LangSelector from "./LangSelector"; import LangSelector from './LangSelector';
export default function PageTransition({ export default function PageTransition({
className, className,
@@ -21,12 +21,12 @@ export default function PageTransition({
className={className} className={className}
initial={{ opacity: 0, scale: 0.95 }} initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ type: "tween" }} transition={{ type: 'tween' }}
style={style} style={style}
> >
{children} {children}
{!hideLangageSelector && !isMobile && ( {!hideLangageSelector && !isMobile && (
<div className="lang-selector"> <div className='lang-selector'>
<LangSelector /> <LangSelector />
</div> </div>
)} )}

View File

@@ -1,25 +1,25 @@
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import LinkTag from "next/link"; import LinkTag from 'next/link';
import { IoAddOutline } from "react-icons/io5"; import { IoAddOutline } from 'react-icons/io5';
import { Category } from "types"; import { Category } from 'types';
import styles from "./quickactions.module.scss"; import styles from './quickactions.module.scss';
export default function CreateItem({ export default function CreateItem({
type, type,
categoryId, categoryId,
onClick, onClick,
}: { }: {
type: "category" | "link"; type: 'category' | 'link';
categoryId?: Category["id"]; categoryId?: Category['id'];
onClick?: (event: any) => void; onClick?: (event: any) => void;
}) { }) {
const { t } = useTranslation("home"); const { t } = useTranslation('home');
return ( return (
<LinkTag <LinkTag
href={`/${type}/create${categoryId && `?categoryId=${categoryId}`}`} href={`/${type}/create${categoryId && `?categoryId=${categoryId}`}`}
title={t(`common:${type}.create`)} title={t(`common:${type}.create`)}
className={styles["action"]} className={styles['action']}
onClick={onClick && onClick} onClick={onClick && onClick}
> >
<IoAddOutline /> <IoAddOutline />

View File

@@ -1,27 +1,27 @@
import LinkTag from "next/link"; import LinkTag from 'next/link';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import { AiOutlineEdit } from "react-icons/ai"; import { AiOutlineEdit } from 'react-icons/ai';
import { Category, Link } from "types"; import { Category, Link } from 'types';
import styles from "./quickactions.module.scss"; import styles from './quickactions.module.scss';
export default function EditItem({ export default function EditItem({
type, type,
id, id,
onClick, onClick,
className = "", className = '',
}: { }: {
type: "category" | "link"; type: 'category' | 'link';
id: Link["id"] | Category["id"]; id: Link['id'] | Category['id'];
onClick?: (event: any) => void; onClick?: (event: any) => void;
className?: string; className?: string;
}) { }) {
const { t } = useTranslation("home"); const { t } = useTranslation('home');
return ( return (
<LinkTag <LinkTag
href={`/${type}/edit/${id}`} href={`/${type}/edit/${id}`}
title={t(`common:${type}.edit`)} title={t(`common:${type}.edit`)}
className={`${styles["action"]} ${className ? className : ""}`} className={`${styles['action']} ${className ? className : ''}`}
onClick={onClick && onClick} onClick={onClick && onClick}
> >
<AiOutlineEdit /> <AiOutlineEdit />

View File

@@ -1,5 +1,5 @@
import { ReactNode } from "react"; import { ReactNode } from 'react';
import { AiFillStar, AiOutlineStar } from "react-icons/ai"; import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
export default function FavoriteItem({ export default function FavoriteItem({
isFavorite, isFavorite,
@@ -10,7 +10,7 @@ export default function FavoriteItem({
children?: ReactNode; children?: ReactNode;
onClick: () => void; onClick: () => void;
}) { }) {
const starColor = "#ffc107"; const starColor = '#ffc107';
return ( return (
<div onClick={onClick}> <div onClick={onClick}>
{isFavorite ? ( {isFavorite ? (

View File

@@ -1,28 +1,28 @@
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import LinkTag from "next/link"; import LinkTag from 'next/link';
import { CgTrashEmpty } from "react-icons/cg"; import { CgTrashEmpty } from 'react-icons/cg';
import { Category, Link } from "types"; import { Category, Link } from 'types';
import styles from "./quickactions.module.scss"; import styles from './quickactions.module.scss';
export default function RemoveItem({ export default function RemoveItem({
type, type,
id, id,
onClick, onClick,
}: { }: {
type: "category" | "link"; type: 'category' | 'link';
id: Link["id"] | Category["id"]; id: Link['id'] | Category['id'];
onClick?: (event: any) => void; onClick?: (event: any) => void;
}) { }) {
const { t } = useTranslation("home"); const { t } = useTranslation('home');
return ( return (
<LinkTag <LinkTag
href={`/${type}/remove/${id}`} href={`/${type}/remove/${id}`}
title={t(`common:${type}.remove`)} title={t(`common:${type}.remove`)}
className={styles["action"]} className={styles['action']}
onClick={onClick && onClick} onClick={onClick && onClick}
> >
<CgTrashEmpty color="red" /> <CgTrashEmpty color='red' />
</LinkTag> </LinkTag>
); );
} }

View File

@@ -1,6 +1,6 @@
import ButtonLink from "components/ButtonLink"; import ButtonLink from 'components/ButtonLink';
import { BiSearchAlt } from "react-icons/bi"; import { BiSearchAlt } from 'react-icons/bi';
import styles from "./quickactions.module.scss"; import styles from './quickactions.module.scss';
export default function QuickActionSearch({ export default function QuickActionSearch({
openSearchModal, openSearchModal,
@@ -8,7 +8,10 @@ export default function QuickActionSearch({
openSearchModal: () => void; openSearchModal: () => void;
}) { }) {
return ( return (
<ButtonLink className={styles["action"]} onClick={openSearchModal}> <ButtonLink
className={styles['action']}
onClick={openSearchModal}
>
<BiSearchAlt /> <BiSearchAlt />
</ButtonLink> </ButtonLink>
); );

View File

@@ -1,11 +1,11 @@
import { FcGoogle } from "react-icons/fc"; import { FcGoogle } from 'react-icons/fc';
import styles from "./search.module.scss"; import styles from './search.module.scss';
export default function LabelSearchWithGoogle() { export default function LabelSearchWithGoogle() {
return ( return (
<i className={styles["search-with-google"]}> <i className={styles['search-with-google']}>
Recherche avec{" "} Recherche avec{' '}
<span> <span>
<FcGoogle size={24} /> <FcGoogle size={24} />
oogle oogle

View File

@@ -1,12 +1,12 @@
import * as Keys from "constants/keys"; import * as Keys from 'constants/keys';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import { ReactNode, useEffect, useMemo } from "react"; import { ReactNode, useEffect, useMemo } from 'react';
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from 'react-hotkeys-hook';
import { SearchItem } from "types"; import { SearchItem } from 'types';
import { groupItemBy } from "utils/array"; import { groupItemBy } from 'utils/array';
import SearchListItem from "./SearchListItem"; import SearchListItem from './SearchListItem';
import styles from "./search.module.scss"; import styles from './search.module.scss';
import clsx from "clsx"; import clsx from 'clsx';
const isActiveItem = (item: SearchItem, otherItem: SearchItem) => const isActiveItem = (item: SearchItem, otherItem: SearchItem) =>
item?.id === otherItem?.id && item?.type === otherItem?.type; item?.id === otherItem?.id && item?.type === otherItem?.type;
@@ -24,7 +24,7 @@ export default function SearchList({
closeModal: () => void; closeModal: () => void;
}) { }) {
const searchItemsGrouped = useMemo( const searchItemsGrouped = useMemo(
() => groupItemBy(items, "category.name"), () => groupItemBy(items, 'category.name'),
[items], [items],
); );
const groupedItems = useMemo<any>( const groupedItems = useMemo<any>(
@@ -41,7 +41,7 @@ export default function SearchList({
Keys.ARROW_UP, Keys.ARROW_UP,
() => setSelectedItem(items[selectedItemIndex - 1]), () => setSelectedItem(items[selectedItemIndex - 1]),
{ {
enableOnFormTags: ["INPUT"], enableOnFormTags: ['INPUT'],
enabled: items.length > 1 && selectedItemIndex !== 0, enabled: items.length > 1 && selectedItemIndex !== 0,
preventDefault: true, preventDefault: true,
}, },
@@ -50,7 +50,7 @@ export default function SearchList({
Keys.ARROW_DOWN, Keys.ARROW_DOWN,
() => setSelectedItem(items[selectedItemIndex + 1]), () => setSelectedItem(items[selectedItemIndex + 1]),
{ {
enableOnFormTags: ["INPUT"], enableOnFormTags: ['INPUT'],
enabled: items.length > 1 && selectedItemIndex !== items.length - 1, enabled: items.length > 1 && selectedItemIndex !== items.length - 1,
preventDefault: true, preventDefault: true,
}, },
@@ -61,12 +61,12 @@ export default function SearchList({
}, [items, setSelectedItem]); }, [items, setSelectedItem]);
return ( return (
<ul className={clsx(styles["search-list"], "reset")}> <ul className={clsx(styles['search-list'], 'reset')}>
{groupedItems.length > 0 ? ( {groupedItems.length > 0 ? (
groupedItems.map(([key, items]) => ( groupedItems.map(([key, items]) => (
<li key={`${key}-${key}`}> <li key={`${key}-${key}`}>
<span>{typeof key === "undefined" ? "-" : key}</span> <span>{typeof key === 'undefined' ? '-' : key}</span>
<ul className="reset"> <ul className='reset'>
{items.map((item) => ( {items.map((item) => (
<SearchListItem <SearchListItem
item={item} item={item}
@@ -88,6 +88,6 @@ export default function SearchList({
} }
function LabelNoItem() { function LabelNoItem() {
const { t } = useTranslation("home"); const { t } = useTranslation('home');
return <i className={styles["no-item"]}>{t("common:no-item-found")}</i>; return <i className={styles['no-item']}>{t('common:no-item-found')}</i>;
} }

View File

@@ -1,11 +1,11 @@
import LinkTag from "next/link"; import LinkTag from 'next/link';
import { AiOutlineFolder } from "react-icons/ai"; import { AiOutlineFolder } from 'react-icons/ai';
import LinkFavicon from "components/Links/LinkFavicon"; import LinkFavicon from 'components/Links/LinkFavicon';
import { SearchItem } from "types"; import { SearchItem } from 'types';
import { useEffect, useId, useRef } from "react"; import { useEffect, useId, useRef } from 'react';
import styles from "./search.module.scss"; import styles from './search.module.scss';
export default function SearchListItem({ export default function SearchListItem({
item, item,
@@ -23,14 +23,14 @@ export default function SearchListItem({
useEffect(() => { useEffect(() => {
if (selected) { if (selected) {
ref.current?.scrollIntoView({ behavior: "smooth", block: "center" }); ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
} }
}, [selected]); }, [selected]);
return ( return (
<li <li
className={ className={
styles["search-item"] + (selected ? ` ${styles["selected"]}` : "") styles['search-item'] + (selected ? ` ${styles['selected']}` : '')
} }
ref={ref} ref={ref}
key={id} key={id}
@@ -38,12 +38,16 @@ export default function SearchListItem({
> >
<LinkTag <LinkTag
href={url} href={url}
target="_blank" target='_blank'
rel="no-referrer" rel='no-referrer'
onClick={closeModal} onClick={closeModal}
> >
{type === "link" ? ( {type === 'link' ? (
<LinkFavicon url={item.url} noMargin size={24} /> <LinkFavicon
url={item.url}
noMargin
size={24}
/>
) : ( ) : (
<AiOutlineFolder size={24} /> <AiOutlineFolder size={24} />
)} )}

View File

@@ -1,15 +1,15 @@
import Modal from "components/Modal/Modal"; import Modal from 'components/Modal/Modal';
import TextBox from "components/TextBox"; import TextBox from 'components/TextBox';
import { GOOGLE_SEARCH_URL } from "constants/search-urls"; import { GOOGLE_SEARCH_URL } from 'constants/search-urls';
import useAutoFocus from "hooks/useAutoFocus"; import useAutoFocus from 'hooks/useAutoFocus';
import { useLocalStorage } from "hooks/useLocalStorage"; import { useLocalStorage } from 'hooks/useLocalStorage';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import { FormEvent, useCallback, useMemo, useState } from "react"; import { FormEvent, useCallback, useMemo, useState } from 'react';
import { BsSearch } from "react-icons/bs"; import { BsSearch } from 'react-icons/bs';
import { Category, SearchItem } from "types"; import { Category, SearchItem } from 'types';
import LabelSearchWithGoogle from "./LabelSearchWithGoogle"; import LabelSearchWithGoogle from './LabelSearchWithGoogle';
import SearchList from "./SearchList"; import SearchList from './SearchList';
import styles from "./search.module.scss"; import styles from './search.module.scss';
export default function SearchModal({ export default function SearchModal({
close, close,
@@ -28,15 +28,15 @@ export default function SearchModal({
const autoFocusRef = useAutoFocus(); const autoFocusRef = useAutoFocus();
const [canSearchLink, setCanSearchLink] = useLocalStorage( const [canSearchLink, setCanSearchLink] = useLocalStorage(
"search-link", 'search-link',
true, true,
); );
const [canSearchCategory, setCanSearchCategory] = useLocalStorage( const [canSearchCategory, setCanSearchCategory] = useLocalStorage(
"search-category", 'search-category',
false, false,
); );
const [search, setSearch] = useState<string>(""); const [search, setSearch] = useState<string>('');
const [selectedItem, setSelectedItem] = useState<SearchItem>(items[0]); const [selectedItem, setSelectedItem] = useState<SearchItem>(items[0]);
const canSubmit = useMemo<boolean>(() => search.length > 0, [search]); const canSubmit = useMemo<boolean>(() => search.length > 0, [search]);
@@ -48,8 +48,8 @@ export default function SearchModal({
? [] ? []
: items.filter( : items.filter(
(item) => (item) =>
((item.type === "category" && canSearchCategory) || ((item.type === 'category' && canSearchCategory) ||
(item.type === "link" && canSearchLink)) && (item.type === 'link' && canSearchLink)) &&
item.name item.name
.toLocaleLowerCase() .toLocaleLowerCase()
.includes(search.toLocaleLowerCase().trim()), .includes(search.toLocaleLowerCase().trim()),
@@ -58,7 +58,7 @@ export default function SearchModal({
); );
const resetForm = useCallback(() => { const resetForm = useCallback(() => {
setSearch(""); setSearch('');
close(); close();
}, [close]); }, [close]);
@@ -81,7 +81,7 @@ export default function SearchModal({
} }
const category = categories.find((c) => c.id === selectedItem.id); const category = categories.find((c) => c.id === selectedItem.id);
if (selectedItem.type === "category" && category) { if (selectedItem.type === 'category' && category) {
return handleSelectCategory(category); return handleSelectCategory(category);
} }
@@ -98,20 +98,27 @@ export default function SearchModal({
); );
return ( return (
<Modal close={close} noHeader={noHeader} padding={"0"}> <Modal
<form onSubmit={handleSubmit} className={styles["search-form"]}> close={close}
<div className={styles["search-input-wrapper"]}> noHeader={noHeader}
<label htmlFor="search"> padding={'0'}
>
<form
onSubmit={handleSubmit}
className={styles['search-form']}
>
<div className={styles['search-input-wrapper']}>
<label htmlFor='search'>
<BsSearch size={24} /> <BsSearch size={24} />
</label> </label>
<TextBox <TextBox
name="search" name='search'
onChangeCallback={handleSearchInputChange} onChangeCallback={handleSearchInputChange}
value={search} value={search}
placeholder={t("common:search")} placeholder={t('common:search')}
innerRef={autoFocusRef} innerRef={autoFocusRef}
fieldClass={styles["search-input-field"]} fieldClass={styles['search-input-field']}
inputClass={"reset"} inputClass={'reset'}
/> />
</div> </div>
<SearchFilter <SearchFilter
@@ -129,8 +136,12 @@ export default function SearchModal({
closeModal={close} closeModal={close}
/> />
)} )}
<button type="submit" disabled={!canSubmit} style={{ display: "none" }}> <button
{t("common:confirm")} type='submit'
disabled={!canSubmit}
style={{ display: 'none' }}
>
{t('common:confirm')}
</button> </button>
</form> </form>
</Modal> </Modal>
@@ -153,33 +164,33 @@ function SearchFilter({
return ( return (
<div <div
style={{ style={{
display: "flex", display: 'flex',
gap: "1em", gap: '1em',
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
marginBottom: "1em", marginBottom: '1em',
}} }}
> >
<div style={{ display: "flex", gap: ".25em" }}> <div style={{ display: 'flex', gap: '.25em' }}>
<input <input
type="checkbox" type='checkbox'
name="filter-link" name='filter-link'
id="filter-link" id='filter-link'
onChange={({ target }) => setCanSearchLink(target.checked)} onChange={({ target }) => setCanSearchLink(target.checked)}
checked={canSearchLink} checked={canSearchLink}
/> />
<label htmlFor="filter-link">{t("common:link.links")}</label> <label htmlFor='filter-link'>{t('common:link.links')}</label>
</div> </div>
<div style={{ display: "flex", gap: ".25em" }}> <div style={{ display: 'flex', gap: '.25em' }}>
<input <input
type="checkbox" type='checkbox'
name="filter-category" name='filter-category'
id="filter-category" id='filter-category'
onChange={({ target }) => setCanSearchCategory(target.checked)} onChange={({ target }) => setCanSearchCategory(target.checked)}
checked={canSearchCategory} checked={canSearchCategory}
/> />
<label htmlFor="filter-category"> <label htmlFor='filter-category'>
{t("common:category.categories")} {t('common:category.categories')}
</label> </label>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
@import "styles/colors.scss"; @import 'styles/colors.scss';
form.search-form { form.search-form {
width: 100%; width: 100%;
@@ -38,7 +38,7 @@ form.search-form {
& > span { & > span {
display: flex; display: flex;
align-items: "center"; align-items: center;
} }
} }

View File

@@ -1,5 +1,5 @@
import { MutableRefObject, useEffect, useState } from "react"; import { MutableRefObject, useEffect, useState } from 'react';
import Select, { OptionsOrGroups, GroupBase } from "react-select"; import Select, { OptionsOrGroups, GroupBase } from 'react-select';
type Option = { label: string | number; value: string | number }; type Option = { label: string | number; value: string | number };
@@ -22,7 +22,7 @@ export default function Selector({
label, label,
labelComponent, labelComponent,
innerRef = null, innerRef = null,
fieldClass = "", fieldClass = '',
value, value,
options = [], options = [],
onChangeCallback, onChangeCallback,
@@ -49,12 +49,18 @@ export default function Selector({
return ( return (
<div className={`input-field ${fieldClass}`}> <div className={`input-field ${fieldClass}`}>
{label && ( {label && (
<label htmlFor={name} title={`${name} field`}> <label
htmlFor={name}
title={`${name} field`}
>
{label} {label}
</label> </label>
)} )}
{labelComponent && ( {labelComponent && (
<label htmlFor={name} title={`${name} field`}> <label
htmlFor={name}
title={`${name} field`}
>
{labelComponent} {labelComponent}
</label> </label>
)} )}

View File

@@ -1,26 +1,33 @@
import { AnimatePresence } from "framer-motion"; import { AnimatePresence } from 'framer-motion';
import useModal from "hooks/useModal"; import useModal from 'hooks/useModal';
import Modal from "../Modal/Modal"; import Modal from '../Modal/Modal';
import * as Keys from "constants/keys"; import * as Keys from 'constants/keys';
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from 'react-hotkeys-hook';
import { IoSettingsOutline } from "react-icons/io5"; import { IoSettingsOutline } from 'react-icons/io5';
import LangSelector from "../LangSelector"; import LangSelector from '../LangSelector';
export default function SettingsModal() { export default function SettingsModal() {
const modal = useModal(); const modal = useModal();
useHotkeys(Keys.CLOSE_SEARCH_KEY, modal.close, { useHotkeys(Keys.CLOSE_SEARCH_KEY, modal.close, {
enabled: modal.isShowing, enabled: modal.isShowing,
enableOnFormTags: ["INPUT"], enableOnFormTags: ['INPUT'],
}); });
return ( return (
<> <>
<button onClick={modal.open} className="reset" title="Settings"> <button
onClick={modal.open}
className='reset'
title='Settings'
>
<IoSettingsOutline size={24} /> <IoSettingsOutline size={24} />
</button> </button>
<AnimatePresence> <AnimatePresence>
{modal.isShowing && ( {modal.isShowing && (
<Modal title="Settings" close={modal.close}> <Modal
title='Settings'
close={modal.close}
>
<LangSelector /> <LangSelector />
<p>about tab with all links related to MyLinks</p> <p>about tab with all links related to MyLinks</p>
<button>disconnect</button> <button>disconnect</button>

View File

@@ -1,9 +1,9 @@
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import { useMemo } from "react"; import { useMemo } from 'react';
import { Category } from "types"; import { Category } from 'types';
import CategoryItem from "./CategoryItem"; import CategoryItem from './CategoryItem';
import styles from "./categories.module.scss"; import styles from './categories.module.scss';
import clsx from "clsx"; import clsx from 'clsx';
interface CategoriesProps { interface CategoriesProps {
categories: Category[]; categories: Category[];
@@ -23,11 +23,11 @@ export default function Categories({
); );
return ( return (
<div className={styles["categories"]}> <div className={styles['categories']}>
<h4> <h4>
{t("common:category.categories")} {linksCount} {t('common:category.categories')} {linksCount}
</h4> </h4>
<ul className={clsx(styles["items"], "reset")}> <ul className={clsx(styles['items'], 'reset')}>
{categories.map((category, index) => ( {categories.map((category, index) => (
<CategoryItem <CategoryItem
category={category} category={category}

View File

@@ -1,8 +1,8 @@
import { motion } from "framer-motion"; import { motion } from 'framer-motion';
import { useEffect, useRef } from "react"; import { useEffect, useRef } from 'react';
import { AiFillFolderOpen, AiOutlineFolder } from "react-icons/ai"; import { AiFillFolderOpen, AiOutlineFolder } from 'react-icons/ai';
import { Category } from "types"; import { Category } from 'types';
import styles from "./categories.module.scss"; import styles from './categories.module.scss';
interface CategoryItemProps { interface CategoryItemProps {
category: Category; category: Category;
@@ -18,14 +18,14 @@ export default function CategoryItem({
index, index,
}: CategoryItemProps): JSX.Element { }: CategoryItemProps): JSX.Element {
const ref = useRef<HTMLLIElement>(); const ref = useRef<HTMLLIElement>();
const className = `${styles["item"]} ${ const className = `${styles['item']} ${
category.id === categoryActive.id ? styles["active"] : "" category.id === categoryActive.id ? styles['active'] : ''
}`; }`;
const onClick = () => handleSelectCategory(category); const onClick = () => handleSelectCategory(category);
useEffect(() => { useEffect(() => {
if (category.id === categoryActive.id) { if (category.id === categoryActive.id) {
ref.current.scrollIntoView({ behavior: "smooth", block: "center" }); ref.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
} }
}, [category.id, categoryActive.id]); }, [category.id, categoryActive.id]);
@@ -34,7 +34,7 @@ export default function CategoryItem({
initial={{ opacity: 0, scale: 0 }} initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ transition={{
type: "spring", type: 'spring',
stiffness: 260, stiffness: 260,
damping: 25, damping: 25,
delay: index * 0.02, delay: index * 0.02,
@@ -44,10 +44,10 @@ export default function CategoryItem({
ref={ref} ref={ref}
onClick={onClick} onClick={onClick}
style={{ style={{
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
gap: ".25em", gap: '.25em',
transition: "none", transition: 'none',
}} }}
title={category.name} title={category.name}
> >
@@ -57,9 +57,9 @@ export default function CategoryItem({
<AiOutlineFolder size={24} /> <AiOutlineFolder size={24} />
)} )}
<div className={styles["content"]}> <div className={styles['content']}>
<span className={styles["name"]}>{category.name}</span> <span className={styles['name']}>{category.name}</span>
<span className={styles["links-count"]}> {category.links.length}</span> <span className={styles['links-count']}> {category.links.length}</span>
</div> </div>
</motion.li> </motion.li>
); );

View File

@@ -1,5 +1,5 @@
@import "styles/keyframes.scss"; @import 'styles/keyframes.scss';
@import "styles/colors.scss"; @import 'styles/colors.scss';
.categories { .categories {
height: 100%; height: 100%;

View File

@@ -1,18 +1,27 @@
import LinkTag from "next/link"; import LinkTag from 'next/link';
import LinkFavicon from "components/Links/LinkFavicon"; import LinkFavicon from 'components/Links/LinkFavicon';
import { Link } from "types"; import { Link } from 'types';
import styles from "./favorites.module.scss"; import styles from './favorites.module.scss';
export default function FavoriteItem({ link }: { link: Link }): JSX.Element { export default function FavoriteItem({ link }: { link: Link }): JSX.Element {
const { name, url, category } = link; const { name, url, category } = link;
return ( return (
<li className={styles["item"]}> <li className={styles['item']}>
<LinkTag href={url} target={"_blank"} rel={"noreferrer"} title={name}> <LinkTag
<LinkFavicon url={url} size={24} noMargin /> href={url}
<span className={styles["link-name"]}>{name}</span> target={'_blank'}
<span className={styles["category"]}> - {category.name}</span> rel={'noreferrer'}
title={name}
>
<LinkFavicon
url={url}
size={24}
noMargin
/>
<span className={styles['link-name']}>{name}</span>
<span className={styles['category']}> - {category.name}</span>
</LinkTag> </LinkTag>
</li> </li>
); );

View File

@@ -1,19 +1,22 @@
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import { Link } from "types"; import { Link } from 'types';
import FavoriteItem from "./FavoriteItem"; import FavoriteItem from './FavoriteItem';
import styles from "./favorites.module.scss"; import styles from './favorites.module.scss';
import clsx from "clsx"; import clsx from 'clsx';
export default function Favorites({ favorites }: { favorites: Link[] }) { export default function Favorites({ favorites }: { favorites: Link[] }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
favorites.length !== 0 && ( favorites.length !== 0 && (
<div className={styles["favorites"]}> <div className={styles['favorites']}>
<h4>{t("common:favorite")}</h4> <h4>{t('common:favorite')}</h4>
<ul className={clsx(styles["items"], "reset")}> <ul className={clsx(styles['items'], 'reset')}>
{favorites.map((link) => ( {favorites.map((link) => (
<FavoriteItem link={link} key={link.id} /> <FavoriteItem
link={link}
key={link.id}
/>
))} ))}
</ul> </ul>
</div> </div>

View File

@@ -1,4 +1,4 @@
@import "styles/colors.scss"; @import 'styles/colors.scss';
.favorites { .favorites {
height: auto; height: auto;
@@ -38,7 +38,6 @@
display: block; display: block;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden; overflow: hidden;
} }

View File

@@ -1,35 +1,35 @@
import ButtonLink from "components/ButtonLink"; import ButtonLink from 'components/ButtonLink';
import PATHS from "constants/paths"; import PATHS from 'constants/paths';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import { SideMenuProps } from "./SideMenu"; import { SideMenuProps } from './SideMenu';
import styles from "./sidemenu.module.scss"; import styles from './sidemenu.module.scss';
export default function NavigationLinks({ export default function NavigationLinks({
categoryActive, categoryActive,
openSearchModal, openSearchModal,
}: { }: {
categoryActive: SideMenuProps["categoryActive"]; categoryActive: SideMenuProps['categoryActive'];
openSearchModal: SideMenuProps["openSearchModal"]; openSearchModal: SideMenuProps['openSearchModal'];
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className={styles["menu-controls"]}> <div className={styles['menu-controls']}>
<div className={styles["action"]}> <div className={styles['action']}>
<ButtonLink onClick={openSearchModal}>{t("common:search")}</ButtonLink> <ButtonLink onClick={openSearchModal}>{t('common:search')}</ButtonLink>
<kbd>S</kbd> <kbd>S</kbd>
</div> </div>
<div className={styles["action"]}> <div className={styles['action']}>
<ButtonLink href={PATHS.CATEGORY.CREATE}> <ButtonLink href={PATHS.CATEGORY.CREATE}>
{t("common:category.create")} {t('common:category.create')}
</ButtonLink> </ButtonLink>
<kbd>C</kbd> <kbd>C</kbd>
</div> </div>
<div className={styles["action"]}> <div className={styles['action']}>
<ButtonLink <ButtonLink
href={`${PATHS.LINK.CREATE}?categoryId=${categoryActive.id}`} href={`${PATHS.LINK.CREATE}?categoryId=${categoryActive.id}`}
> >
{t("common:link.create")} {t('common:link.create')}
</ButtonLink> </ButtonLink>
<kbd>L</kbd> <kbd>L</kbd>
</div> </div>

View File

@@ -1,12 +1,12 @@
import BlockWrapper from "components/BlockWrapper/BlockWrapper"; import BlockWrapper from 'components/BlockWrapper/BlockWrapper';
import * as Keys from "constants/keys"; import * as Keys from 'constants/keys';
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from 'react-hotkeys-hook';
import { Category, Link } from "types"; import { Category, Link } from 'types';
import Categories from "./Categories/Categories"; import Categories from './Categories/Categories';
import Favorites from "./Favorites/Favorites"; import Favorites from './Favorites/Favorites';
import NavigationLinks from "./NavigationLinks"; import NavigationLinks from './NavigationLinks';
import UserCard from "./UserCard/UserCard"; import UserCard from './UserCard/UserCard';
import styles from "./sidemenu.module.scss"; import styles from './sidemenu.module.scss';
export interface SideMenuProps { export interface SideMenuProps {
categories: Category[]; categories: Category[];
@@ -56,11 +56,11 @@ export default function SideMenu({
); );
return ( return (
<div className={styles["side-menu"]}> <div className={styles['side-menu']}>
<BlockWrapper> <BlockWrapper>
<Favorites favorites={favorites} /> <Favorites favorites={favorites} />
</BlockWrapper> </BlockWrapper>
<BlockWrapper style={{ minHeight: "0", flex: "1" }}> <BlockWrapper style={{ minHeight: '0', flex: '1' }}>
<Categories <Categories
categories={categories} categories={categories}
categoryActive={categoryActive} categoryActive={categoryActive}

View File

@@ -1,20 +1,20 @@
import { useSession } from "next-auth/react"; import { useSession } from 'next-auth/react';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import Image from "next/image"; import Image from 'next/image';
import { TFunctionParam } from "types/i18next"; import { TFunctionParam } from 'types/i18next';
import styles from "./user-card.module.scss"; import styles from './user-card.module.scss';
import SettingsModal from "components/Settings/SettingsModal"; import SettingsModal from 'components/Settings/SettingsModal';
export default function UserCard() { export default function UserCard() {
const { data } = useSession({ required: true }); const { data } = useSession({ required: true });
const { t } = useTranslation(); const { t } = useTranslation();
const avatarLabel = t("common:avatar", { const avatarLabel = t('common:avatar', {
name: data.user.name, name: data.user.name,
} as TFunctionParam); } as TFunctionParam);
return ( return (
<div className={styles["user-card-wrapper"]}> <div className={styles['user-card-wrapper']}>
<div className={styles["user-card"]}> <div className={styles['user-card']}>
<Image <Image
src={data.user.image} src={data.user.image}
width={28} width={28}

View File

@@ -1,4 +1,4 @@
@import "styles/colors.scss"; @import 'styles/colors.scss';
.user-card-wrapper { .user-card-wrapper {
user-select: none; user-select: none;

View File

@@ -1,4 +1,4 @@
@import "styles/colors.scss"; @import 'styles/colors.scss';
.side-menu { .side-menu {
height: 100%; height: 100%;

View File

@@ -1,4 +1,4 @@
import { MutableRefObject, useState } from "react"; import { MutableRefObject, useState } from 'react';
interface InputProps { interface InputProps {
name: string; name: string;
@@ -17,9 +17,9 @@ export default function TextBox({
label, label,
disabled = false, disabled = false,
innerRef = null, innerRef = null,
placeholder = "Type something...", placeholder = 'Type something...',
fieldClass = "", fieldClass = '',
inputClass = "", inputClass = '',
value, value,
onChangeCallback, onChangeCallback,
}: InputProps): JSX.Element { }: InputProps): JSX.Element {
@@ -35,7 +35,10 @@ export default function TextBox({
return ( return (
<div className={`input-field ${fieldClass}`}> <div className={`input-field ${fieldClass}`}>
{label && ( {label && (
<label htmlFor={name} title={`${name} field`}> <label
htmlFor={name}
title={`${name} field`}
>
{label} {label}
</label> </label>
)} )}

View File

@@ -1,10 +1,8 @@
export const OPEN_SEARCH_KEY = "s"; export const OPEN_SEARCH_KEY = 's';
export const CLOSE_SEARCH_KEY = "escape"; export const CLOSE_SEARCH_KEY = 'escape';
export const OPEN_CREATE_LINK_KEY = "l"; export const OPEN_CREATE_LINK_KEY = 'l';
export const OPEN_CREATE_CATEGORY_KEY = "c"; export const OPEN_CREATE_CATEGORY_KEY = 'c';
export const ARROW_UP = "ArrowUp"; export const ARROW_UP = 'ArrowUp';
export const ARROW_DOWN = "ArrowDown"; export const ARROW_DOWN = 'ArrowDown';
export const ARROW_LEFT = "ArrowLeft";
export const ARROW_RIGHT = "ArrowRight";

View File

@@ -1,29 +1,29 @@
const PATHS = { const PATHS = {
LOGIN: "/login", LOGIN: '/login',
LOGOUT: "/logout", LOGOUT: '/logout',
HOME: "/", HOME: '/',
PRIVACY: "/privacy", PRIVACY: '/privacy',
TERMS: "/terms", TERMS: '/terms',
CATEGORY: { CATEGORY: {
CREATE: "/category/create", CREATE: '/category/create',
EDIT: "/category/edit", EDIT: '/category/edit',
REMOVE: "/category/remove", REMOVE: '/category/remove',
}, },
LINK: { LINK: {
CREATE: "/link/create", CREATE: '/link/create',
EDIT: "/link/edit", EDIT: '/link/edit',
REMOVE: "/link/remove", REMOVE: '/link/remove',
}, },
API: { API: {
CATEGORY: "/api/category", CATEGORY: '/api/category',
LINK: "/api/link", LINK: '/api/link',
}, },
NOT_FOUND: "/404", NOT_FOUND: '/404',
SERVER_ERROR: "/505", SERVER_ERROR: '/505',
AUTHOR: "https://www.sonny.dev/", AUTHOR: 'https://www.sonny.dev/',
REPO_GITHUB: "https://github.com/Sonny93/my-links", REPO_GITHUB: 'https://github.com/Sonny93/my-links',
EXTENSION: EXTENSION:
"https://chromewebstore.google.com/detail/mylinks/agkmlplihacolkakgeccnbhphnepphma", 'https://chromewebstore.google.com/detail/mylinks/agkmlplihacolkakgeccnbhphnepphma',
}; };
export default PATHS; export default PATHS;

View File

@@ -1 +1 @@
export const GOOGLE_SEARCH_URL = "https://google.com/search?q="; export const GOOGLE_SEARCH_URL = 'https://google.com/search?q=';

View File

@@ -1,15 +1,12 @@
export const VALID_URL_REGEX =
/^(?:http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.\%]+$/;
export const USER_AGENT = export const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0"; 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0';
export const REL_LIST = [ export const REL_LIST = [
"icon", 'icon',
"shortcut icon", 'shortcut icon',
"apple-touch-icon", 'apple-touch-icon',
"apple-touch-icon-precomposed", 'apple-touch-icon-precomposed',
"apple-touch-startup-image", 'apple-touch-startup-image',
"mask-icon", 'mask-icon',
"fluid-icon", 'fluid-icon',
]; ];

View File

@@ -1,11 +1,9 @@
import { useCallback } from "react"; import { useCallback } from 'react';
export default function useAutoFocus() { export default function useAutoFocus() {
const inputRef = useCallback((inputElement: any) => { return useCallback((inputElement: any) => {
if (inputElement) { if (inputElement) {
inputElement.focus(); inputElement.focus();
} }
}, []); }, []);
return inputRef;
} }

View File

@@ -2,7 +2,7 @@ const MOBILE_SCREEN_SIZE = 768;
export default function useIsMobile() { export default function useIsMobile() {
return ( return (
typeof window !== "undefined" && typeof window !== 'undefined' &&
window.matchMedia(`screen and (max-width: ${MOBILE_SCREEN_SIZE}px)`).matches window.matchMedia(`screen and (max-width: ${MOBILE_SCREEN_SIZE}px)`).matches
); );
} }

View File

@@ -1,8 +1,8 @@
import { useState } from "react"; import { useState } from 'react';
export function useLocalStorage(key: string, initialValue: any) { export function useLocalStorage(key: string, initialValue: any) {
const [storedValue, setStoredValue] = useState(() => { const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") { if (typeof window === 'undefined') {
return initialValue; return initialValue;
} }
try { try {
@@ -19,7 +19,7 @@ export function useLocalStorage(key: string, initialValue: any) {
const valueToStore = const valueToStore =
value instanceof Function ? value(storedValue) : value; value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore); setStoredValue(valueToStore);
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore)); window.localStorage.setItem(key, JSON.stringify(valueToStore));
} }
} catch (error) { } catch (error) {

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
export function useMediaQuery(query: string): boolean { export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState<boolean>(getMediaMatches(query)); const [matches, setMatches] = useState<boolean>(getMediaMatches(query));
@@ -9,8 +9,8 @@ export function useMediaQuery(query: string): boolean {
const matchMedia = window.matchMedia(query); const matchMedia = window.matchMedia(query);
handleMediaChange(); handleMediaChange();
matchMedia.addEventListener("change", handleMediaChange); matchMedia.addEventListener('change', handleMediaChange);
return () => matchMedia.removeEventListener("change", handleMediaChange); return () => matchMedia.removeEventListener('change', handleMediaChange);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]); }, [query]);
@@ -18,7 +18,7 @@ export function useMediaQuery(query: string): boolean {
} }
function getMediaMatches(query: string): boolean { function getMediaMatches(query: string): boolean {
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
return window.matchMedia(query).matches; return window.matchMedia(query).matches;
} }
return false; return false;

View File

@@ -1,4 +1,4 @@
import { useState } from "react"; import { useState } from 'react';
const useModal = () => { const useModal = () => {
const [isShowing, setIsShowing] = useState<boolean>(false); const [isShowing, setIsShowing] = useState<boolean>(false);

View File

@@ -1,13 +1,13 @@
import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import nextI18NextConfig from "../../next-i18next.config"; import nextI18NextConfig from '../../next-i18next.config';
async function getServerSideTranslation( async function getServerSideTranslation(
locale: string = "en", locale: string = 'en',
requiredNs: string[] = [], requiredNs: string[] = [],
) { ) {
return await serverSideTranslations( return await serverSideTranslations(
locale, locale,
["common", ...requiredNs], ['common', ...requiredNs],
nextI18NextConfig, nextI18NextConfig,
); );
} }

View File

@@ -1,8 +1,8 @@
import common from "../../public/locales/en/common.json"; import common from '../../public/locales/en/common.json';
import home from "../../public/locales/en/home.json"; import home from '../../public/locales/en/home.json';
import login from "../../public/locales/en/login.json"; import login from '../../public/locales/en/login.json';
import privacy from "../../public/locales/en/privacy.json"; import privacy from '../../public/locales/en/privacy.json';
import terms from "../../public/locales/en/terms.json"; import terms from '../../public/locales/en/terms.json';
const resources = { const resources = {
common, common,

View File

@@ -1,9 +1,9 @@
import { User } from "@prisma/client"; import { User } from '@prisma/client';
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import getUserOrThrow from "lib/user/getUserOrThrow"; import getUserOrThrow from 'lib/user/getUserOrThrow';
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from 'next';
import { Session } from "next-auth"; import { Session } from 'next-auth';
import { getSession } from "utils/session"; import { getSession } from 'utils/session';
type ApiHandlerMethod = ({ type ApiHandlerMethod = ({
req, req,
@@ -46,23 +46,23 @@ export function apiHandler(handler: {
} }
function errorHandler(error: any, response: NextApiResponse) { function errorHandler(error: any, response: NextApiResponse) {
if (typeof error === "string") { if (typeof error === 'string') {
const is404 = error.toLowerCase().endsWith("not found"); const is404 = error.toLowerCase().endsWith('not found');
const statusCode = is404 ? 404 : 400; const statusCode = is404 ? 404 : 400;
return response.status(statusCode).json({ message: error }); return response.status(statusCode).json({ message: error });
} }
// does not fit with current error throwed // does not fit with current error thrown
// TODO: fix errors returned // TODO: fix errors returned
// by getSessionOrThrow or getUserOrThrow // by getSessionOrThrow or getUserOrThrow
if (error.name === "UnauthorizedError") { if (error.name === 'UnauthorizedError') {
// authentication error // authentication error
return response.status(401).json({ message: "You must be connected" }); return response.status(401).json({ message: 'You must be connected' });
} }
const errorMessage = const errorMessage =
error.constructor.name === "PrismaClientKnownRequestError" error.constructor.name === 'PrismaClientKnownRequestError'
? handlePrismaError(error) // Handle Prisma specific errors ? handlePrismaError(error) // Handle Prisma specific errors
: error.message; : error.message;
@@ -75,16 +75,14 @@ function handlePrismaError({
message, message,
}: PrismaClientKnownRequestError) { }: PrismaClientKnownRequestError) {
switch (code) { switch (code) {
case "P2002": case 'P2002':
return `Duplicate field value: ${meta.target}`; return `Duplicate field value: ${meta.target}`;
case "P2003": case 'P2014':
return `Foreign key constraint failed on the field: ${meta.field_name}`;
case "P2014":
return `Invalid ID: ${meta.target}`; return `Invalid ID: ${meta.target}`;
case "P2003": case 'P2003':
return `Invalid input data: ${meta.target}`; return `Invalid input data: ${meta.target}`;
// Details should not leak to client, be carreful with this // Details should not leak to client, be careful with this
default: default:
return `Something went wrong: ${message}`; return `Something went wrong: ${message}`;
} }

View File

@@ -1,14 +1,14 @@
import { number, object, string } from "yup"; import { number, object, string } from 'yup';
const CategorieBodySchema = object({ const CategoryBodySchema = object({
name: string() name: string()
.trim() .trim()
.required("Category name's required") .required("Category name's required")
.max(128, "Category name's too long"), .max(128, "Category name's too long"),
}).typeError("Missing request Body"); }).typeError('Missing request Body');
const CategorieQuerySchema = object({ const CategoryQuerySchema = object({
cid: number().required(), cid: number().required(),
}); });
export { CategorieBodySchema, CategorieQuerySchema }; export { CategoryBodySchema, CategoryQuerySchema };

View File

@@ -1,5 +1,5 @@
import { User } from "@prisma/client"; import { User } from '@prisma/client';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default async function getUserCategories(user: User) { export default async function getUserCategories(user: User) {
return await prisma.category.findMany({ return await prisma.category.findMany({

View File

@@ -1,5 +1,5 @@
import { User } from "@prisma/client"; import { User } from '@prisma/client';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default async function getUserCategoriesCount(user: User) { export default async function getUserCategoriesCount(user: User) {
return await prisma.category.count({ return await prisma.category.count({

View File

@@ -1,7 +1,7 @@
import { Category, User } from "@prisma/client"; import { Category, User } from '@prisma/client';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default async function getUserCategory(user: User, id: Category["id"]) { export default async function getUserCategory(user: User, id: Category['id']) {
return await prisma.category.findFirst({ return await prisma.category.findFirst({
where: { where: {
authorId: user?.id, authorId: user?.id,

View File

@@ -1,9 +1,9 @@
import { Category, User } from "@prisma/client"; import { Category, User } from '@prisma/client';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default async function getUserCategoryByName( export default async function getUserCategoryByName(
user: User, user: User,
name: Category["name"], name: Category['name'],
) { ) {
return await prisma.category.findFirst({ return await prisma.category.findFirst({
where: { name, authorId: user.id }, where: { name, authorId: user.id },

View File

@@ -1,6 +1,6 @@
export const isImage = (type: string) => type.includes("image"); export const isImage = (type: string) => type.includes('image');
export const isBase64Image = (data: string) => data.startsWith("data:image/"); export const isBase64Image = (data: string) => data.startsWith('data:image/');
export const convertBase64ToBuffer = (base64: string) => export const convertBase64ToBuffer = (base64: string) =>
Buffer.from(base64, "base64"); Buffer.from(base64, 'base64');

View File

@@ -1,10 +1,10 @@
import { Category, Link, User } from "@prisma/client"; import { Category, Link, User } from '@prisma/client';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default async function getLinkFromCategoryByName( export default async function getLinkFromCategoryByName(
user: User, user: User,
name: Link["name"], name: Link['name'],
categoryId: Category["id"], categoryId: Category['id'],
) { ) {
return await prisma.link.findFirst({ return await prisma.link.findFirst({
where: { where: {

View File

@@ -1,7 +1,7 @@
import { Link, User } from "@prisma/client"; import { Link, User } from '@prisma/client';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default async function getUserLink(user: User, id: Link["id"]) { export default async function getUserLink(user: User, id: Link['id']) {
return await prisma.link.findFirst({ return await prisma.link.findFirst({
where: { where: {
id, id,

View File

@@ -1,5 +1,5 @@
import { User } from "@prisma/client"; import { User } from '@prisma/client';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default async function getUserLinks(user: User) { export default async function getUserLinks(user: User) {
return await prisma.link.findMany({ return await prisma.link.findMany({

View File

@@ -1,18 +1,18 @@
import { boolean, number, object, string } from "yup"; import { boolean, number, object, string } from 'yup';
import { isValidHttpUrl } from "../url"; import { isValidHttpUrl } from '../url';
const LinkBodySchema = object({ const LinkBodySchema = object({
name: string() name: string()
.trim() .trim()
.required("Link name is required") .required('Link name is required')
.max(128, "Link name is too long"), .max(128, 'Link name is too long'),
url: string() url: string()
.trim() .trim()
.required("URl is required") .required('URl is required')
.test("test_url", "Invalid URL format", (value) => isValidHttpUrl(value)), .test('test_url', 'Invalid URL format', (value) => isValidHttpUrl(value)),
categoryId: number().required("CategoryId must be a number"), categoryId: number().required('CategoryId must be a number'),
favorite: boolean().default(() => false), favorite: boolean().default(() => false),
}).typeError("Missing request Body"); }).typeError('Missing request Body');
const LinkQuerySchema = object({ const LinkQuerySchema = object({
lid: number().required(), lid: number().required(),

View File

@@ -1,15 +1,15 @@
import nProgress from "nprogress"; import nProgress from 'nprogress';
import { i18n } from "next-i18next"; import { i18n } from 'next-i18next';
import { USER_AGENT } from "constants/url"; import { USER_AGENT } from 'constants/url';
import { Favicon } from "types/types"; import { Favicon } from 'types/types';
import { isImage } from "./image"; import { isImage } from './image';
export async function makeRequest({ export async function makeRequest({
method = "GET", method = 'GET',
url, url,
body, body,
}: { }: {
method?: RequestInit["method"]; method?: RequestInit['method'];
url: string; url: string;
body?: object | any[]; body?: object | any[];
}): Promise<any> { }): Promise<any> {
@@ -18,7 +18,7 @@ export async function makeRequest({
method, method,
body: body ? JSON.stringify(body) : undefined, body: body ? JSON.stringify(body) : undefined,
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
nProgress.done(); nProgress.done();
@@ -26,12 +26,12 @@ export async function makeRequest({
const data = await request.json(); const data = await request.json();
return request.ok return request.ok
? data ? data
: Promise.reject(data?.error || i18n.t("common:generic-error")); : Promise.reject(data?.error || i18n.t('common:generic-error'));
} }
export async function makeRequestWithUserAgent(url: string) { export async function makeRequestWithUserAgent(url: string) {
const headers = new Headers(); const headers = new Headers();
headers.set("User-Agent", USER_AGENT); headers.set('User-Agent', USER_AGENT);
return await fetch(url, { headers }); return await fetch(url, { headers });
} }
@@ -39,7 +39,7 @@ export async function makeRequestWithUserAgent(url: string) {
export async function downloadImageFromUrl(url: string): Promise<Favicon> { export async function downloadImageFromUrl(url: string): Promise<Favicon> {
const request = await makeRequestWithUserAgent(url); const request = await makeRequestWithUserAgent(url);
if (!request.ok) { if (!request.ok) {
throw new Error("Request failed"); throw new Error('Request failed');
} }
const blob = await request.blob(); const blob = await request.blob();
@@ -52,11 +52,11 @@ export async function downloadImageFromUrl(url: string): Promise<Favicon> {
} }
export async function getFavicon(url: string): Promise<Favicon> { export async function getFavicon(url: string): Promise<Favicon> {
if (!url) throw new Error("Missing URL"); if (!url) throw new Error('Missing URL');
const favicon = await downloadImageFromUrl(url); const favicon = await downloadImageFromUrl(url);
if (!isImage(favicon.type) || favicon.size === 0) { if (!isImage(favicon.type) || favicon.size === 0) {
throw new Error("Favicon path does not return an image"); throw new Error('Favicon path does not return an image');
} }
return favicon; return favicon;

View File

@@ -1,9 +1,9 @@
import { parse } from "node-html-parser"; import { parse } from 'node-html-parser';
import { REL_LIST } from "constants/url"; import { REL_LIST } from 'constants/url';
export function buildFaviconUrl(urlParam: string, faviconPath: string) { export function buildFaviconUrl(urlParam: string, faviconPath: string) {
const { origin } = new URL(urlParam); const { origin } = new URL(urlParam);
if (faviconPath.startsWith("/")) { if (faviconPath.startsWith('/')) {
// https://example.com + /favicon.ico // https://example.com + /favicon.ico
return origin + faviconPath; return origin + faviconPath;
} }
@@ -11,14 +11,14 @@ export function buildFaviconUrl(urlParam: string, faviconPath: string) {
const slimUrl = urlWithoutSearchParams(urlParam); const slimUrl = urlWithoutSearchParams(urlParam);
// https://example.com/a/b/ -> https://example.com/a/b // https://example.com/a/b/ -> https://example.com/a/b
const url = slimUrl.endsWith("/") ? slimUrl.slice(0, -1) : slimUrl; const url = slimUrl.endsWith('/') ? slimUrl.slice(0, -1) : slimUrl;
if (url === origin) { if (url === origin) {
return `${url}/${faviconPath}`; return `${url}/${faviconPath}`;
} }
// https://example.com/a/b or https://example.com/a/b/cdef -> https://example.com/a/ // https://example.com/a/b or https://example.com/a/b/cdef -> https://example.com/a/
const relativeUrl = removeLastSectionUrl(url) + "/"; const relativeUrl = removeLastSectionUrl(url) + '/';
if (relativeUrl.endsWith("/")) { if (relativeUrl.endsWith('/')) {
return relativeUrl + faviconPath; return relativeUrl + faviconPath;
} }
@@ -28,24 +28,24 @@ export function buildFaviconUrl(urlParam: string, faviconPath: string) {
function urlWithoutSearchParams(urlParam: string) { function urlWithoutSearchParams(urlParam: string) {
const url = new URL(urlParam); const url = new URL(urlParam);
return url.protocol + "//" + url.host + url.pathname; return url.protocol + '//' + url.host + url.pathname;
} }
export function removeLastSectionUrl(urlParam: string) { export function removeLastSectionUrl(urlParam: string) {
const urlArr = urlParam.split("/"); const urlArr = urlParam.split('/');
urlArr.pop(); urlArr.pop();
return urlArr.join("/"); return urlArr.join('/');
} }
export function findFaviconPath(text: string) { export function findFaviconPath(text: string) {
const document = parse(text); const document = parse(text);
const favicon = Array.from(document.getElementsByTagName("link")).find( const favicon = Array.from(document.getElementsByTagName('link')).find(
(element) => (element) =>
REL_LIST.includes(element.getAttribute("rel")) && REL_LIST.includes(element.getAttribute('rel')) &&
element.getAttribute("href"), element.getAttribute('href'),
); );
return favicon?.getAttribute("href") || undefined; return favicon?.getAttribute('href') || undefined;
} }
export function isValidHttpUrl(urlParam: string) { export function isValidHttpUrl(urlParam: string) {
@@ -57,5 +57,5 @@ export function isValidHttpUrl(urlParam: string) {
return false; return false;
} }
return url.protocol === "http:" || url.protocol === "https:"; return url.protocol === 'http:' || url.protocol === 'https:';
} }

View File

@@ -1,5 +1,5 @@
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
import { Profile } from "next-auth"; import { Profile } from 'next-auth';
export default async function createUser(profile: Profile) { export default async function createUser(profile: Profile) {
return await prisma.user.create({ return await prisma.user.create({

View File

@@ -1,5 +1,5 @@
import { Session } from "next-auth"; import { Session } from 'next-auth';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default async function getUser(session: Session) { export default async function getUser(session: Session) {
if (!session?.user) { if (!session?.user) {

View File

@@ -1,5 +1,5 @@
import { Profile } from "next-auth"; import { Profile } from 'next-auth';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default async function getUserByProfileProvider(profile: Profile) { export default async function getUserByProfileProvider(profile: Profile) {
return await prisma.user.findFirst({ return await prisma.user.findFirst({

View File

@@ -1,10 +1,11 @@
import { Session } from "next-auth"; import { Session } from 'next-auth';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default async function getUserOrThrow(session: Session) { export default async function getUserOrThrow(session: Session) {
if (!session || session === null) { if (!session) {
throw new Error("You must be connected"); throw new Error('You must be connected');
} }
return await prisma.user.findFirstOrThrow({ return await prisma.user.findFirstOrThrow({
where: { where: {
email: session?.user?.email, email: session?.user?.email,

View File

@@ -1,5 +1,5 @@
import prisma from "../../utils/prisma"; import prisma from '../../utils/prisma';
import { Profile } from "next-auth"; import { Profile } from 'next-auth';
export default async function updateUser(profile: Profile) { export default async function updateUser(profile: Profile) {
return await prisma.user.update({ return await prisma.user.update({

View File

@@ -1,12 +1,15 @@
import PageTransition from "components/PageTransition"; import PageTransition from 'components/PageTransition';
import { NextSeo } from "next-seo"; import { NextSeo } from 'next-seo';
import styles from "styles/error-page.module.scss"; import styles from 'styles/error-page.module.scss';
import NavbarUntranslated from "../components/Navbar/NavbarUntranslated"; import NavbarUntranslated from '../components/Navbar/NavbarUntranslated';
export default function Custom404() { export default function Custom404() {
return ( return (
<PageTransition className={styles["App"]} hideLangageSelector> <PageTransition
<NextSeo title="Page not found" /> className={styles['App']}
hideLangageSelector
>
<NextSeo title='Page not found' />
<NavbarUntranslated /> <NavbarUntranslated />
<header> <header>
<h1>404</h1> <h1>404</h1>

View File

@@ -1,12 +1,15 @@
import PageTransition from "components/PageTransition"; import PageTransition from 'components/PageTransition';
import { NextSeo } from "next-seo"; import { NextSeo } from 'next-seo';
import styles from "styles/error-page.module.scss"; import styles from 'styles/error-page.module.scss';
import NavbarUntranslated from "../components/Navbar/NavbarUntranslated"; import NavbarUntranslated from '../components/Navbar/NavbarUntranslated';
export default function Custom500() { export default function Custom500() {
return ( return (
<PageTransition className={styles["App"]} hideLangageSelector> <PageTransition
<NextSeo title="Internal server error" /> className={styles['App']}
hideLangageSelector
>
<NextSeo title='Internal server error' />
<NavbarUntranslated /> <NavbarUntranslated />
<header> <header>
<h1>500</h1> <h1>500</h1>

View File

@@ -1,41 +1,44 @@
import ErrorBoundary from "components/ErrorBoundary/ErrorBoundary"; import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
import * as Keys from "constants/keys"; import * as Keys from 'constants/keys';
import PATHS from "constants/paths"; import PATHS from 'constants/paths';
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from 'next-auth/react';
import { appWithTranslation } from "next-i18next"; import { appWithTranslation } from 'next-i18next';
import { DefaultSeo } from "next-seo"; import { DefaultSeo } from 'next-seo';
import { useRouter } from "next/router"; import { useRouter } from 'next/router';
import nProgress from "nprogress"; import nProgress from 'nprogress';
import "nprogress/nprogress.css"; import 'nprogress/nprogress.css';
import { useEffect } from "react"; import { useEffect } from 'react';
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from 'react-hotkeys-hook';
import "styles/globals.scss"; import 'styles/globals.scss';
import nextI18nextConfig from "../../next-i18next.config"; import nextI18nextConfig from '../../next-i18next.config';
function MyApp({ Component, pageProps: { session, ...pageProps } }) { function MyApp({ Component, pageProps: { session, ...pageProps } }) {
const router = useRouter(); const router = useRouter();
useHotkeys(Keys.CLOSE_SEARCH_KEY, () => router.push(PATHS.HOME), { useHotkeys(Keys.CLOSE_SEARCH_KEY, () => router.push(PATHS.HOME), {
enabled: router.pathname !== PATHS.HOME, enabled: router.pathname !== PATHS.HOME,
enableOnFormTags: ["INPUT"], enableOnFormTags: ['INPUT'],
}); });
useEffect(() => { useEffect(() => {
// Page loading events // Page loading events
router.events.on("routeChangeStart", nProgress.start); router.events.on('routeChangeStart', nProgress.start);
router.events.on("routeChangeComplete", nProgress.done); router.events.on('routeChangeComplete', nProgress.done);
router.events.on("routeChangeError", nProgress.done); router.events.on('routeChangeError', nProgress.done);
return () => { return () => {
router.events.off("routeChangeStart", nProgress.start); router.events.off('routeChangeStart', nProgress.start);
router.events.off("routeChangeComplete", nProgress.done); router.events.off('routeChangeComplete', nProgress.done);
router.events.off("routeChangeError", nProgress.done); router.events.off('routeChangeError', nProgress.done);
}; };
}, [router.events]); }, [router.events]);
return ( return (
<SessionProvider session={session}> <SessionProvider session={session}>
<DefaultSeo titleTemplate="MyLinks — %s" defaultTitle="MyLinks" /> <DefaultSeo
titleTemplate='MyLinks — %s'
defaultTitle='MyLinks'
/>
<ErrorBoundary> <ErrorBoundary>
<Component {...pageProps} /> <Component {...pageProps} />
</ErrorBoundary> </ErrorBoundary>

View File

@@ -1,15 +1,22 @@
import { Head, Html, Main, NextScript } from "next/document"; import { Head, Html, Main, NextScript } from 'next/document';
const Document = () => ( const Document = () => (
<Html lang="fr"> <Html lang='fr'>
<Head> <Head>
<meta name="theme-color" content="#f0eef6" /> <meta
<link name='theme-color'
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=Rubik:ital,wght@0,400;0,700;1,400;1,700&display=swap" content='#f0eef6'
rel="stylesheet" />
<link
href='https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=Rubik:ital,wght@0,400;0,700;1,400;1,700&display=swap'
rel='stylesheet'
/>
<meta charSet='UTF-8' />
<link
rel='shortcut icon'
href='/favicon.png'
type='image/png'
/> />
<meta charSet="UTF-8" />
<link rel="shortcut icon" href="/favicon.png" type="image/png" />
</Head> </Head>
<body> <body>
<noscript>Vous devez activer JavaScript pour utiliser ce site</noscript> <noscript>Vous devez activer JavaScript pour utiliser ce site</noscript>

View File

@@ -1,27 +1,27 @@
import PATHS from "constants/paths"; import PATHS from 'constants/paths';
import NextAuth, { NextAuthOptions, Profile } from "next-auth"; import NextAuth, { NextAuthOptions, Profile } from 'next-auth';
import GoogleProvider from "next-auth/providers/google"; import GoogleProvider from 'next-auth/providers/google';
import getUserByProfileProvider from "lib/user/getUserByProfileProvider"; import getUserByProfileProvider from 'lib/user/getUserByProfileProvider';
import createUser from "lib/user/createUser"; import createUser from 'lib/user/createUser';
import updateUser from "lib/user/updateUser"; import updateUser from 'lib/user/updateUser';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
const authLogger = (profile: Profile, ...args: any[]) => const authLogger = (profile: Profile, ...args: any[]) =>
console.log( console.log(
"[AUTH]", '[AUTH]',
profile.email, profile.email,
`(${profile.name} - ${profile.sub})`, `(${profile.name} - ${profile.sub})`,
...args, ...args,
); );
const redirectUser = (errorKey: string) => `${PATHS.LOGIN}?error=${errorKey}`; const redirectUser = (errorKey: string) => `${PATHS.LOGIN}?error=${errorKey}`;
const checkProvider = (provider: string) => provider === "google"; const checkProvider = (provider: string) => provider === 'google';
const checkAccountDataReceived = (profile: Profile) => const checkAccountDataReceived = (profile: Profile) =>
!!profile?.sub && !!profile?.email; !!profile?.sub && !!profile?.email;
const cookieOptions = { const cookieOptions = {
sameSite: "None", sameSite: 'None',
path: "/", path: '/',
secure: true, secure: true,
}; };
@@ -32,9 +32,9 @@ export const authOptions = {
clientSecret: process.env.GOOGLE_CLIENT_SECRET, clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: { authorization: {
params: { params: {
prompt: "consent", prompt: 'consent',
access_type: "offline", access_type: 'offline',
response_type: "code", response_type: 'code',
}, },
}, },
}), }),
@@ -49,30 +49,30 @@ export const authOptions = {
}, },
async signIn({ account: accountParam, profile }) { async signIn({ account: accountParam, profile }) {
if (!checkProvider(accountParam.provider)) { if (!checkProvider(accountParam.provider)) {
authLogger(profile, "rejected : forbidden provider"); authLogger(profile, 'rejected : forbidden provider');
return redirectUser("AUTH_REQUIRED"); return redirectUser('AUTH_REQUIRED');
} }
if (!checkAccountDataReceived(profile)) { if (!checkAccountDataReceived(profile)) {
authLogger(profile, "rejected : missing data from provider", profile); authLogger(profile, 'rejected : missing data from provider', profile);
return redirectUser("MISSING_PROVIDER_VALUES"); return redirectUser('MISSING_PROVIDER_VALUES');
} }
try { try {
const isUserExists = await getUserByProfileProvider(profile); const isUserExists = await getUserByProfileProvider(profile);
if (isUserExists) { if (isUserExists) {
await updateUser(profile); await updateUser(profile);
authLogger(profile, "success : user updated"); authLogger(profile, 'success : user updated');
} else { } else {
await createUser(profile); await createUser(profile);
authLogger(profile, "success : user created"); authLogger(profile, 'success : user created');
} }
return true; return true;
} catch (error) { } catch (error) {
authLogger(profile, "rejected : unhandled error"); authLogger(profile, 'rejected : unhandled error');
console.error(error); console.error(error);
return redirectUser("AUTH_ERROR"); return redirectUser('AUTH_ERROR');
} }
}, },
}, },
@@ -83,15 +83,15 @@ export const authOptions = {
}, },
cookies: { cookies: {
sessionToken: { sessionToken: {
name: "next-auth.session-token", name: 'next-auth.session-token',
options: cookieOptions, options: cookieOptions,
}, },
callbackUrl: { callbackUrl: {
name: "next-auth.callback-url", name: 'next-auth.callback-url',
options: cookieOptions, options: cookieOptions,
}, },
csrfToken: { csrfToken: {
name: "next-auth.csrf-token", name: 'next-auth.csrf-token',
options: cookieOptions, options: cookieOptions,
}, },
}, },

View File

@@ -1,12 +1,12 @@
import { apiHandler } from "lib/api/handler"; import { apiHandler } from 'lib/api/handler';
import { import {
CategorieBodySchema, CategoryBodySchema,
CategorieQuerySchema, CategoryQuerySchema,
} from "lib/category/categoryValidationSchema"; } from 'lib/category/categoryValidationSchema';
import getUserCategory from "lib/category/getUserCategory"; import getUserCategory from 'lib/category/getUserCategory';
import getUserCategoryByName from "lib/category/getUserCategoryByName"; import getUserCategoryByName from 'lib/category/getUserCategoryByName';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default apiHandler({ export default apiHandler({
put: editCategory, put: editCategory,
@@ -14,21 +14,21 @@ export default apiHandler({
}); });
async function editCategory({ req, res, user }) { async function editCategory({ req, res, user }) {
const { cid } = await CategorieQuerySchema.validate(req.query); const { cid } = await CategoryQuerySchema.validate(req.query);
const { name } = await CategorieBodySchema.validate(req.body); const { name } = await CategoryBodySchema.validate(req.body);
const category = await getUserCategory(user, cid); const category = await getUserCategory(user, cid);
if (!category) { if (!category) {
throw new Error("Unable to find category " + cid); throw new Error('Unable to find category ' + cid);
} }
const isCategoryNameAlreadyused = await getUserCategoryByName(user, name); const isCategoryNameAlreadyUsed = await getUserCategoryByName(user, name);
if (isCategoryNameAlreadyused) { if (isCategoryNameAlreadyUsed) {
throw new Error("Category name already used"); throw new Error('Category name already used');
} }
if (category.name === name) { if (category.name === name) {
throw new Error("New category name must be different"); throw new Error('New category name must be different');
} }
await prisma.category.update({ await prisma.category.update({
@@ -36,28 +36,28 @@ async function editCategory({ req, res, user }) {
data: { name }, data: { name },
}); });
return res.send({ return res.send({
success: "Category successfully updated", success: 'Category successfully updated',
categoryId: category.id, categoryId: category.id,
}); });
} }
async function deleteCategory({ req, res, user }) { async function deleteCategory({ req, res, user }) {
const { cid } = await CategorieQuerySchema.validate(req.query); const { cid } = await CategoryQuerySchema.validate(req.query);
const category = await getUserCategory(user, cid); const category = await getUserCategory(user, cid);
if (!category) { if (!category) {
throw new Error("Unable to find category " + cid); throw new Error('Unable to find category ' + cid);
} }
if (category.links.length !== 0) { if (category.links.length !== 0) {
throw new Error("You cannot remove category with links"); throw new Error('You cannot remove category with links');
} }
await prisma.category.delete({ await prisma.category.delete({
where: { id: cid }, where: { id: cid },
}); });
return res.send({ return res.send({
success: "Category successfully deleted", success: 'Category successfully deleted',
categoryId: category.id, categoryId: category.id,
}); });
} }

View File

@@ -1,16 +1,16 @@
import { apiHandler } from "lib/api/handler"; import { apiHandler } from 'lib/api/handler';
import { CategorieBodySchema } from "lib/category/categoryValidationSchema"; import { CategoryBodySchema } from 'lib/category/categoryValidationSchema';
import getUserCategories from "lib/category/getUserCategories"; import getUserCategories from 'lib/category/getUserCategories';
import getUserCategoryByName from "lib/category/getUserCategoryByName"; import getUserCategoryByName from 'lib/category/getUserCategoryByName';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default apiHandler({ export default apiHandler({
get: getCatgories, get: getCategories,
post: createCategory, post: createCategory,
}); });
async function getCatgories({ res, user }) { async function getCategories({ res, user }) {
const categories = await getUserCategories(user); const categories = await getUserCategories(user);
return res.status(200).send({ return res.status(200).send({
categories, categories,
@@ -18,18 +18,18 @@ async function getCatgories({ res, user }) {
} }
async function createCategory({ req, res, user }) { async function createCategory({ req, res, user }) {
const { name } = await CategorieBodySchema.validate(req.body); const { name } = await CategoryBodySchema.validate(req.body);
const category = await getUserCategoryByName(user, name); const category = await getUserCategoryByName(user, name);
if (category) { if (category) {
throw new Error("Category name already used"); throw new Error('Category name already used');
} }
const categoryCreated = await prisma.category.create({ const categoryCreated = await prisma.category.create({
data: { name, authorId: user.id }, data: { name, authorId: user.id },
}); });
return res.status(200).send({ return res.status(200).send({
success: "Category successfully created", success: 'Category successfully created',
categoryId: categoryCreated.id, categoryId: categoryCreated.id,
}); });
} }

View File

@@ -1,32 +1,32 @@
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from 'next';
import { createReadStream } from "node:fs"; import { createReadStream } from 'node:fs';
import { resolve } from "node:path"; import { resolve } from 'node:path';
import { import {
downloadImageFromUrl, downloadImageFromUrl,
getFavicon, getFavicon,
makeRequestWithUserAgent, makeRequestWithUserAgent,
} from "lib/request"; } from 'lib/request';
import { Favicon } from "types/types"; import { Favicon } from 'types/types';
import { buildFaviconUrl, findFaviconPath } from "lib/url"; import { buildFaviconUrl, findFaviconPath } from 'lib/url';
import { convertBase64ToBuffer, isBase64Image, isImage } from "lib/image"; import { convertBase64ToBuffer, isBase64Image, isImage } from 'lib/image';
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse,
) { ) {
const urlParam = (req.query?.urlParam as string) || ""; const urlParam = (req.query?.urlParam as string) || '';
if (!urlParam) { if (!urlParam) {
throw new Error("URL is missing"); throw new Error('URL is missing');
} }
const faviconRequestUrl = buildFaviconUrl(urlParam, "/favicon.ico"); const faviconRequestUrl = buildFaviconUrl(urlParam, '/favicon.ico');
try { try {
return sendImage(res, await getFavicon(faviconRequestUrl)); return sendImage(res, await getFavicon(faviconRequestUrl));
} catch (error) { } catch (error) {
console.error( console.error(
"[Favicon]", '[Favicon]',
`[first: ${faviconRequestUrl}]`, `[first: ${faviconRequestUrl}]`,
"Unable to retrieve favicon from favicon.ico url", 'Unable to retrieve favicon from favicon.ico url',
); );
} }
@@ -36,9 +36,9 @@ export default async function handler(
const faviconPath = findFaviconPath(documentAsText); const faviconPath = findFaviconPath(documentAsText);
if (!faviconPath) { if (!faviconPath) {
console.error( console.error(
"[Favicon]", '[Favicon]',
`[first: ${faviconRequestUrl}]`, `[first: ${faviconRequestUrl}]`,
"No link/href attribute found", 'No link/href attribute found',
); );
return sendDefaultImage(res); return sendDefaultImage(res);
} }
@@ -46,50 +46,50 @@ export default async function handler(
const finalUrl = buildFaviconUrl(requestDocument.url, faviconPath); const finalUrl = buildFaviconUrl(requestDocument.url, faviconPath);
try { try {
if (!faviconPath) { if (!faviconPath) {
throw new Error("Unable to find favicon path"); throw new Error('Unable to find favicon path');
} }
if (isBase64Image(faviconPath)) { if (isBase64Image(faviconPath)) {
console.log( console.log(
"[Favicon]", '[Favicon]',
`[second: ${faviconRequestUrl}]`, `[second: ${faviconRequestUrl}]`,
"info: base64, convert it to buffer", 'info: base64, convert it to buffer',
); );
const buffer = convertBase64ToBuffer(faviconPath); const buffer = convertBase64ToBuffer(faviconPath);
return sendImage(res, { return sendImage(res, {
buffer, buffer,
type: "image/x-icon", type: 'image/x-icon',
size: buffer.length, size: buffer.length,
url: faviconPath, url: faviconPath,
}); });
} }
const finalUrl = faviconPath.startsWith("http") const finalUrl = faviconPath.startsWith('http')
? faviconPath ? faviconPath
: buildFaviconUrl(requestDocument.url, faviconPath); : buildFaviconUrl(requestDocument.url, faviconPath);
const favicon = await downloadImageFromUrl(finalUrl); const favicon = await downloadImageFromUrl(finalUrl);
if (!isImage(favicon.type)) { if (!isImage(favicon.type)) {
throw new Error("Favicon path does not return an image"); throw new Error('Favicon path does not return an image');
} }
console.log("[Favicon]", `[second: ${finalUrl}]`, "success: image found"); console.log('[Favicon]', `[second: ${finalUrl}]`, 'success: image found');
return sendImage(res, favicon); return sendImage(res, favicon);
} catch (error) { } catch (error) {
const errorMessage = error?.message || "Unable to retrieve favicon"; const errorMessage = error?.message || 'Unable to retrieve favicon';
console.log("[Favicon]", `[second: ${finalUrl}], error:`, errorMessage); console.log('[Favicon]', `[second: ${finalUrl}], error:`, errorMessage);
return sendDefaultImage(res); return sendDefaultImage(res);
} }
} }
function sendImage(res: NextApiResponse, { buffer, type, size }: Favicon) { function sendImage(res: NextApiResponse, { buffer, type, size }: Favicon) {
res.setHeader("Content-Type", type); res.setHeader('Content-Type', type);
res.setHeader("Content-Length", size); res.setHeader('Content-Length', size);
res.send(buffer); res.send(buffer);
} }
function sendDefaultImage(res: NextApiResponse) { function sendDefaultImage(res: NextApiResponse) {
const readStream = createReadStream( const readStream = createReadStream(
resolve(process.cwd(), "./public/empty-image.png"), resolve(process.cwd(), './public/empty-image.png'),
); );
res.writeHead(206); res.writeHead(206);
readStream.pipe(res); readStream.pipe(res);

View File

@@ -1,7 +1,7 @@
import { apiHandler } from "lib/api/handler"; import { apiHandler } from 'lib/api/handler';
import getUserLink from "lib/link/getUserLink"; import getUserLink from 'lib/link/getUserLink';
import { LinkBodySchema, LinkQuerySchema } from "lib/link/linkValidationSchema"; import { LinkBodySchema, LinkQuerySchema } from 'lib/link/linkValidationSchema';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default apiHandler({ export default apiHandler({
put: editLink, put: editLink,
@@ -16,7 +16,7 @@ async function editLink({ req, res, user }) {
const link = await getUserLink(user, lid); const link = await getUserLink(user, lid);
if (!link) { if (!link) {
throw new Error("Unable to find link " + lid); throw new Error('Unable to find link ' + lid);
} }
if ( if (
@@ -25,7 +25,7 @@ async function editLink({ req, res, user }) {
link.favorite === favorite && link.favorite === favorite &&
link.categoryId === categoryId link.categoryId === categoryId
) { ) {
throw new Error("You must update at least one field"); throw new Error('You must update at least one field');
} }
await prisma.link.update({ await prisma.link.update({
@@ -40,7 +40,7 @@ async function editLink({ req, res, user }) {
return res return res
.status(200) .status(200)
.send({ success: "Link successfully updated", categoryId }); .send({ success: 'Link successfully updated', categoryId });
} }
async function deleteLink({ req, res, user }) { async function deleteLink({ req, res, user }) {
@@ -48,7 +48,7 @@ async function deleteLink({ req, res, user }) {
const link = await getUserLink(user, lid); const link = await getUserLink(user, lid);
if (!link) { if (!link) {
throw new Error("Unable to find link " + lid); throw new Error('Unable to find link ' + lid);
} }
await prisma.link.delete({ await prisma.link.delete({
@@ -56,7 +56,7 @@ async function deleteLink({ req, res, user }) {
}); });
return res.send({ return res.send({
success: "Link successfully deleted", success: 'Link successfully deleted',
categoryId: link.categoryId, categoryId: link.categoryId,
}); });
} }

View File

@@ -1,9 +1,9 @@
import { apiHandler } from "lib/api/handler"; import { apiHandler } from 'lib/api/handler';
import getUserCategory from "lib/category/getUserCategory"; import getUserCategory from 'lib/category/getUserCategory';
import getUserLinkByName from "lib/link/getLinkFromCategoryByName"; import getUserLinkByName from 'lib/link/getLinkFromCategoryByName';
import { LinkBodySchema } from "lib/link/linkValidationSchema"; import { LinkBodySchema } from 'lib/link/linkValidationSchema';
import prisma from "utils/prisma"; import prisma from 'utils/prisma';
export default apiHandler({ export default apiHandler({
post: createLink, post: createLink,
@@ -16,12 +16,12 @@ async function createLink({ req, res, user }) {
const link = await getUserLinkByName(user, name, categoryId); const link = await getUserLinkByName(user, name, categoryId);
if (link) { if (link) {
throw new Error("Link name is already used in this category"); throw new Error('Link name is already used in this category');
} }
const category = await getUserCategory(user, categoryId); const category = await getUserCategory(user, categoryId);
if (!category) { if (!category) {
throw new Error("Unable to find category " + categoryId); throw new Error('Unable to find category ' + categoryId);
} }
await prisma.link.create({ await prisma.link.create({
@@ -34,5 +34,5 @@ async function createLink({ req, res, user }) {
}, },
}); });
return res.send({ success: "Link successfully created", categoryId }); return res.send({ success: 'Link successfully created', categoryId });
} }

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