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
# 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,
"proseWrap": "preserve",
"insertPragma": false,
"printWidth": 100,
"printWidth": 80,
"requirePragma": false,
"tabWidth": 2,
"useTabs": false,

View File

@@ -25,7 +25,7 @@ npm run dev
## 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
cd docker
@@ -33,7 +33,7 @@ make build
make start-prod
```
## Github Actions
## GitHub Actions
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:
my-links-dev-db:
@@ -8,4 +8,4 @@ services:
env_file:
- .env
ports:
- 3306:3306
- '3306:3306'

View File

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

View File

@@ -1,40 +1,40 @@
import acceptLanguage from "accept-language";
import { NextResponse } from "next/server";
import { i18n } from "./next-i18next.config";
import acceptLanguage from 'accept-language';
import { NextResponse } from 'next/server';
import { i18n } from './next-i18next.config';
acceptLanguage.languages(i18n.locales);
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
export function middleware(req) {
if (
req.nextUrl.pathname.indexOf("icon") > -1 ||
req.nextUrl.pathname.indexOf("chrome") > -1
req.nextUrl.pathname.indexOf('icon') > -1 ||
req.nextUrl.pathname.indexOf('chrome') > -1
)
return NextResponse.next();
let lng;
if (req.cookies.has(cookieName))
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;
// Redirect if lng in path is not supported
if (
!i18n.locales.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
!req.nextUrl.pathname.startsWith("/_next")
!req.nextUrl.pathname.startsWith('/_next')
) {
return NextResponse.redirect(
new URL(`/${lng}${req.nextUrl.pathname}`, req.url),
);
}
if (req.headers.has("referer")) {
const refererUrl = new URL(req.headers.get("referer"));
if (req.headers.has('referer')) {
const refererUrl = new URL(req.headers.get('referer'));
const lngInReferer = i18n.locales.find((l) =>
refererUrl.pathname.startsWith(`/${l}`),
);

View File

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

View File

@@ -1,4 +1,4 @@
const { i18n } = require("./next-i18next.config");
const { i18n } = require('./next-i18next.config');
/** @type {import("next").NextConfig} */
const config = {
@@ -6,22 +6,22 @@ const config = {
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
use: ["@svgr/webpack"],
use: ['@svgr/webpack'],
});
return config;
},
images: {
remotePatterns: [
{ hostname: "localhost" },
{ hostname: "t3.gstatic.com" },
{ hostname: "lh3.googleusercontent.com" },
{ hostname: "www.mylinks.app" },
{ hostname: 'localhost' },
{ hostname: 't3.gstatic.com' },
{ hostname: 'lh3.googleusercontent.com' },
{ hostname: 'www.mylinks.app' },
],
formats: ["image/webp"],
formats: ['image/webp'],
},
reactStrictMode: false,
output: "standalone",
output: 'standalone',
};
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",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"prepare": "husky install"
},
"dependencies": {
"@prisma/client": "^5.6.0",
@@ -40,6 +41,7 @@
"eslint": "^8.54.0",
"eslint-config-next": "14.0.3",
"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"?>
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">
<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 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"/>
<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" />
<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>

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",
"no-link": "No link for <b>{{name}}</b>",
"footer": {

View File

@@ -30,7 +30,7 @@
"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."
},
"rgpd": {
"gdpr": {
"title": "5. GDPR Compliance",
"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",
"create": "Créer une catégorie",
"edit": "Modifier une catégorie",
"remove": "Supprimer une categorie",
"remove": "Supprimer une catégorie",
"remove-confirm": "Confirmer la suppression ?",
"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",
"no-link": "Aucun lien pour <b>{{name}}</b>",
"footer": {

View File

@@ -36,7 +36,7 @@
"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."
},
"rgpd": {
"gdpr": {
"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."
},

View File

@@ -1,25 +1,25 @@
<?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">
<g id="tight-bounds" transform="matrix(1.264159, 0, 0, 1.265787, 0, 0)" style="" bx:origin="0.499856 0.5">
<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)">
<svg viewBox="0 0 395.52 130.3536553275838" height="130.3536553275838" width="395.52">
<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>
<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 transform="matrix(1,0,0,1,252.44232580181685,0.4999999999999858)">
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
<g id="textblocktransform">
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103" id="textblock">
<g>
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
<g>
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
<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/>
<svg/>
<g class="wordmark-text-0" data-fill-palette-color="primary" id="text-0">
<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="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"/>
<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" 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" style="mix-blend-mode: normal; fill: rgb(72, 162, 255);" stroke="none"/>
</g>
</svg>
</g>
@@ -33,8 +33,8 @@
<svg viewBox="0 0 200.44498811830644 199.99999999999997" height="199.99999999999997" width="200.44498811830644">
<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">
<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="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="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" style="fill: rgb(72, 162, 255);"/>
</svg>
</g>
</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 {
children: ReactNode;
@@ -9,7 +9,10 @@ interface BlockWrapperProps {
export default function BlockWrapper({ children, style }: BlockWrapperProps) {
return (
<section className={styles["block-wrapper"]} style={style}>
<section
className={styles['block-wrapper']}
style={style}
>
{children}
</section>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
@import "styles/keyframes.scss";
@import "styles/colors.scss";
@import 'styles/keyframes.scss';
@import 'styles/colors.scss';
.no-link,
.no-category {
@@ -22,10 +22,9 @@
}
.links-wrapper {
height: calc(100%); // FIXME: eurk
height: 100%;
min-width: 0;
padding: 0.5em;
padding-bottom: 0;
padding: 0.5em 0.5em 0;
display: flex;
flex: 1;
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 {
error?: string;
@@ -18,14 +18,14 @@ export default function MessageManager({
return (
<>
{info && <div className={styles["info-msg"]}>{info}</div>}
{infoUrl && <div className={styles["info-msg"]}>{infoUrl}</div>}
{info && <div className={styles['info-msg']}>{info}</div>}
{infoUrl && <div className={styles['info-msg']}>{infoUrl}</div>}
{error && <div className={styles["error-msg"]}>{error}</div>}
{errorUrl && <div className={styles["error-msg"]}>{errorUrl}</div>}
{error && <div className={styles['error-msg']}>{error}</div>}
{errorUrl && <div className={styles['error-msg']}>{errorUrl}</div>}
{success && <div className={styles["success-msg"]}>{success}</div>}
{successUrl && <div className={styles["success-msg"]}>{successUrl}</div>}
{success && <div className={styles['success-msg']}>{success}</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 {
height: fit-content;

View File

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

View File

@@ -1,4 +1,4 @@
@import "styles/colors.scss";
@import 'styles/colors.scss';
.modal-wrapper {
z-index: 9999;
@@ -23,7 +23,7 @@
align-items: center;
justify-content: center;
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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import ButtonLink from "components/ButtonLink";
import { BiSearchAlt } from "react-icons/bi";
import styles from "./quickactions.module.scss";
import ButtonLink from 'components/ButtonLink';
import { BiSearchAlt } from 'react-icons/bi';
import styles from './quickactions.module.scss';
export default function QuickActionSearch({
openSearchModal,
@@ -8,7 +8,10 @@ export default function QuickActionSearch({
openSearchModal: () => void;
}) {
return (
<ButtonLink className={styles["action"]} onClick={openSearchModal}>
<ButtonLink
className={styles['action']}
onClick={openSearchModal}
>
<BiSearchAlt />
</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() {
return (
<i className={styles["search-with-google"]}>
Recherche avec{" "}
<i className={styles['search-with-google']}>
Recherche avec{' '}
<span>
<FcGoogle size={24} />
oogle

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
@import "styles/keyframes.scss";
@import "styles/colors.scss";
@import 'styles/keyframes.scss';
@import 'styles/colors.scss';
.categories {
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 { Link } from "types";
import LinkFavicon from 'components/Links/LinkFavicon';
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 {
const { name, url, category } = link;
return (
<li className={styles["item"]}>
<LinkTag href={url} target={"_blank"} rel={"noreferrer"} title={name}>
<LinkFavicon url={url} size={24} noMargin />
<span className={styles["link-name"]}>{name}</span>
<span className={styles["category"]}> - {category.name}</span>
<li className={styles['item']}>
<LinkTag
href={url}
target={'_blank'}
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>
</li>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,29 +1,29 @@
const PATHS = {
LOGIN: "/login",
LOGOUT: "/logout",
HOME: "/",
PRIVACY: "/privacy",
TERMS: "/terms",
LOGIN: '/login',
LOGOUT: '/logout',
HOME: '/',
PRIVACY: '/privacy',
TERMS: '/terms',
CATEGORY: {
CREATE: "/category/create",
EDIT: "/category/edit",
REMOVE: "/category/remove",
CREATE: '/category/create',
EDIT: '/category/edit',
REMOVE: '/category/remove',
},
LINK: {
CREATE: "/link/create",
EDIT: "/link/edit",
REMOVE: "/link/remove",
CREATE: '/link/create',
EDIT: '/link/edit',
REMOVE: '/link/remove',
},
API: {
CATEGORY: "/api/category",
LINK: "/api/link",
CATEGORY: '/api/category',
LINK: '/api/link',
},
NOT_FOUND: "/404",
SERVER_ERROR: "/505",
AUTHOR: "https://www.sonny.dev/",
REPO_GITHUB: "https://github.com/Sonny93/my-links",
NOT_FOUND: '/404',
SERVER_ERROR: '/505',
AUTHOR: 'https://www.sonny.dev/',
REPO_GITHUB: 'https://github.com/Sonny93/my-links',
EXTENSION:
"https://chromewebstore.google.com/detail/mylinks/agkmlplihacolkakgeccnbhphnepphma",
'https://chromewebstore.google.com/detail/mylinks/agkmlplihacolkakgeccnbhphnepphma',
};
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 =
"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 = [
"icon",
"shortcut icon",
"apple-touch-icon",
"apple-touch-icon-precomposed",
"apple-touch-startup-image",
"mask-icon",
"fluid-icon",
'icon',
'shortcut icon',
'apple-touch-icon',
'apple-touch-icon-precomposed',
'apple-touch-startup-image',
'mask-icon',
'fluid-icon',
];

View File

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

View File

@@ -2,7 +2,7 @@ const MOBILE_SCREEN_SIZE = 768;
export default function useIsMobile() {
return (
typeof window !== "undefined" &&
typeof window !== 'undefined' &&
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) {
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === "undefined") {
if (typeof window === 'undefined') {
return initialValue;
}
try {
@@ -19,7 +19,7 @@ export function useLocalStorage(key: string, initialValue: any) {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { User } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import getUserOrThrow from "lib/user/getUserOrThrow";
import { NextApiRequest, NextApiResponse } from "next";
import { Session } from "next-auth";
import { getSession } from "utils/session";
import { User } from '@prisma/client';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import getUserOrThrow from 'lib/user/getUserOrThrow';
import { NextApiRequest, NextApiResponse } from 'next';
import { Session } from 'next-auth';
import { getSession } from 'utils/session';
type ApiHandlerMethod = ({
req,
@@ -46,23 +46,23 @@ export function apiHandler(handler: {
}
function errorHandler(error: any, response: NextApiResponse) {
if (typeof error === "string") {
const is404 = error.toLowerCase().endsWith("not found");
if (typeof error === 'string') {
const is404 = error.toLowerCase().endsWith('not found');
const statusCode = is404 ? 404 : 400;
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
// by getSessionOrThrow or getUserOrThrow
if (error.name === "UnauthorizedError") {
if (error.name === 'UnauthorizedError') {
// authentication error
return response.status(401).json({ message: "You must be connected" });
return response.status(401).json({ message: 'You must be connected' });
}
const errorMessage =
error.constructor.name === "PrismaClientKnownRequestError"
error.constructor.name === 'PrismaClientKnownRequestError'
? handlePrismaError(error) // Handle Prisma specific errors
: error.message;
@@ -75,16 +75,14 @@ function handlePrismaError({
message,
}: PrismaClientKnownRequestError) {
switch (code) {
case "P2002":
case 'P2002':
return `Duplicate field value: ${meta.target}`;
case "P2003":
return `Foreign key constraint failed on the field: ${meta.field_name}`;
case "P2014":
case 'P2014':
return `Invalid ID: ${meta.target}`;
case "P2003":
case 'P2003':
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:
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()
.trim()
.required("Category name's required")
.max(128, "Category name's too long"),
}).typeError("Missing request Body");
}).typeError('Missing request Body');
const CategorieQuerySchema = object({
const CategoryQuerySchema = object({
cid: number().required(),
});
export { CategorieBodySchema, CategorieQuerySchema };
export { CategoryBodySchema, CategoryQuerySchema };

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { Category, User } from "@prisma/client";
import prisma from "utils/prisma";
import { Category, User } from '@prisma/client';
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({
where: {
authorId: user?.id,

View File

@@ -1,9 +1,9 @@
import { Category, User } from "@prisma/client";
import prisma from "utils/prisma";
import { Category, User } from '@prisma/client';
import prisma from 'utils/prisma';
export default async function getUserCategoryByName(
user: User,
name: Category["name"],
name: Category['name'],
) {
return await prisma.category.findFirst({
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) =>
Buffer.from(base64, "base64");
Buffer.from(base64, 'base64');

View File

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

View File

@@ -1,7 +1,7 @@
import { Link, User } from "@prisma/client";
import prisma from "utils/prisma";
import { Link, User } from '@prisma/client';
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({
where: {
id,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,41 +1,44 @@
import ErrorBoundary from "components/ErrorBoundary/ErrorBoundary";
import * as Keys from "constants/keys";
import PATHS from "constants/paths";
import { SessionProvider } from "next-auth/react";
import { appWithTranslation } from "next-i18next";
import { DefaultSeo } from "next-seo";
import { useRouter } from "next/router";
import nProgress from "nprogress";
import "nprogress/nprogress.css";
import { useEffect } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import "styles/globals.scss";
import nextI18nextConfig from "../../next-i18next.config";
import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
import * as Keys from 'constants/keys';
import PATHS from 'constants/paths';
import { SessionProvider } from 'next-auth/react';
import { appWithTranslation } from 'next-i18next';
import { DefaultSeo } from 'next-seo';
import { useRouter } from 'next/router';
import nProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { useEffect } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import 'styles/globals.scss';
import nextI18nextConfig from '../../next-i18next.config';
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
const router = useRouter();
useHotkeys(Keys.CLOSE_SEARCH_KEY, () => router.push(PATHS.HOME), {
enabled: router.pathname !== PATHS.HOME,
enableOnFormTags: ["INPUT"],
enableOnFormTags: ['INPUT'],
});
useEffect(() => {
// Page loading events
router.events.on("routeChangeStart", nProgress.start);
router.events.on("routeChangeComplete", nProgress.done);
router.events.on("routeChangeError", nProgress.done);
router.events.on('routeChangeStart', nProgress.start);
router.events.on('routeChangeComplete', nProgress.done);
router.events.on('routeChangeError', nProgress.done);
return () => {
router.events.off("routeChangeStart", nProgress.start);
router.events.off("routeChangeComplete", nProgress.done);
router.events.off("routeChangeError", nProgress.done);
router.events.off('routeChangeStart', nProgress.start);
router.events.off('routeChangeComplete', nProgress.done);
router.events.off('routeChangeError', nProgress.done);
};
}, [router.events]);
return (
<SessionProvider session={session}>
<DefaultSeo titleTemplate="MyLinks — %s" defaultTitle="MyLinks" />
<DefaultSeo
titleTemplate='MyLinks — %s'
defaultTitle='MyLinks'
/>
<ErrorBoundary>
<Component {...pageProps} />
</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 = () => (
<Html lang="fr">
<Html lang='fr'>
<Head>
<meta name="theme-color" content="#f0eef6" />
<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
name='theme-color'
content='#f0eef6'
/>
<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>
<body>
<noscript>Vous devez activer JavaScript pour utiliser ce site</noscript>

View File

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

View File

@@ -1,12 +1,12 @@
import { apiHandler } from "lib/api/handler";
import { apiHandler } from 'lib/api/handler';
import {
CategorieBodySchema,
CategorieQuerySchema,
} from "lib/category/categoryValidationSchema";
import getUserCategory from "lib/category/getUserCategory";
import getUserCategoryByName from "lib/category/getUserCategoryByName";
CategoryBodySchema,
CategoryQuerySchema,
} from 'lib/category/categoryValidationSchema';
import getUserCategory from 'lib/category/getUserCategory';
import getUserCategoryByName from 'lib/category/getUserCategoryByName';
import prisma from "utils/prisma";
import prisma from 'utils/prisma';
export default apiHandler({
put: editCategory,
@@ -14,21 +14,21 @@ export default apiHandler({
});
async function editCategory({ req, res, user }) {
const { cid } = await CategorieQuerySchema.validate(req.query);
const { name } = await CategorieBodySchema.validate(req.body);
const { cid } = await CategoryQuerySchema.validate(req.query);
const { name } = await CategoryBodySchema.validate(req.body);
const category = await getUserCategory(user, cid);
if (!category) {
throw new Error("Unable to find category " + cid);
throw new Error('Unable to find category ' + cid);
}
const isCategoryNameAlreadyused = await getUserCategoryByName(user, name);
if (isCategoryNameAlreadyused) {
throw new Error("Category name already used");
const isCategoryNameAlreadyUsed = await getUserCategoryByName(user, name);
if (isCategoryNameAlreadyUsed) {
throw new Error('Category name already used');
}
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({
@@ -36,28 +36,28 @@ async function editCategory({ req, res, user }) {
data: { name },
});
return res.send({
success: "Category successfully updated",
success: 'Category successfully updated',
categoryId: category.id,
});
}
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);
if (!category) {
throw new Error("Unable to find category " + cid);
throw new Error('Unable to find category ' + cid);
}
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({
where: { id: cid },
});
return res.send({
success: "Category successfully deleted",
success: 'Category successfully deleted',
categoryId: category.id,
});
}

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { apiHandler } from "lib/api/handler";
import getUserCategory from "lib/category/getUserCategory";
import getUserLinkByName from "lib/link/getLinkFromCategoryByName";
import { LinkBodySchema } from "lib/link/linkValidationSchema";
import { apiHandler } from 'lib/api/handler';
import getUserCategory from 'lib/category/getUserCategory';
import getUserLinkByName from 'lib/link/getLinkFromCategoryByName';
import { LinkBodySchema } from 'lib/link/linkValidationSchema';
import prisma from "utils/prisma";
import prisma from 'utils/prisma';
export default apiHandler({
post: createLink,
@@ -16,12 +16,12 @@ async function createLink({ req, res, user }) {
const link = await getUserLinkByName(user, name, categoryId);
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);
if (!category) {
throw new Error("Unable to find category " + categoryId);
throw new Error('Unable to find category ' + categoryId);
}
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