mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-09 07:03:25 +00:00
refactor: apply prettier conf and cleanup
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -35,6 +35,3 @@ yarn-error.log*
|
|||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
||||||
# env file
|
|
||||||
.env
|
|
||||||
|
|||||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npm test
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"vueIndentScriptAndStyle": false,
|
"vueIndentScriptAndStyle": false,
|
||||||
"proseWrap": "preserve",
|
"proseWrap": "preserve",
|
||||||
"insertPragma": false,
|
"insertPragma": false,
|
||||||
"printWidth": 100,
|
"printWidth": 80,
|
||||||
"requirePragma": false,
|
"requirePragma": false,
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"useTabs": false,
|
"useTabs": false,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ npm run dev
|
|||||||
|
|
||||||
## Prod
|
## Prod
|
||||||
|
|
||||||
If you want to use your own database leave, the `DATABASE_URL` property filled in `docker/docker-compose.yml` with your databse credentials, otherwise you'll have to delete it.
|
If you want to use your own database leave, the `DATABASE_URL` property filled in `docker/docker-compose.yml` with your database credentials, otherwise you'll have to delete it.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd docker
|
cd docker
|
||||||
@@ -33,7 +33,7 @@ make build
|
|||||||
make start-prod
|
make start-prod
|
||||||
```
|
```
|
||||||
|
|
||||||
## Github Actions
|
## GitHub Actions
|
||||||
|
|
||||||
Env var to define :
|
Env var to define :
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: "3.8"
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
my-links-dev-db:
|
my-links-dev-db:
|
||||||
@@ -8,4 +8,4 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- '3306:3306'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: "3.8"
|
version: '3.8'
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
mylinks_app:
|
mylinks_app:
|
||||||
@@ -11,7 +11,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:5000:3000"
|
- '127.0.0.1:5000:3000'
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
import acceptLanguage from "accept-language";
|
import acceptLanguage from 'accept-language';
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from 'next/server';
|
||||||
import { i18n } from "./next-i18next.config";
|
import { i18n } from './next-i18next.config';
|
||||||
|
|
||||||
acceptLanguage.languages(i18n.locales);
|
acceptLanguage.languages(i18n.locales);
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: ["/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)"],
|
matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const cookieName = "i18next";
|
const cookieName = 'i18next';
|
||||||
|
|
||||||
// Source : https://github.com/i18next/next-app-dir-i18next-example/blob/3d653a46ae33f46abc011b6186f7a4595b84129f/middleware.js
|
// Source : https://github.com/i18next/next-app-dir-i18next-example/blob/3d653a46ae33f46abc011b6186f7a4595b84129f/middleware.js
|
||||||
export function middleware(req) {
|
export function middleware(req) {
|
||||||
if (
|
if (
|
||||||
req.nextUrl.pathname.indexOf("icon") > -1 ||
|
req.nextUrl.pathname.indexOf('icon') > -1 ||
|
||||||
req.nextUrl.pathname.indexOf("chrome") > -1
|
req.nextUrl.pathname.indexOf('chrome') > -1
|
||||||
)
|
)
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
let lng;
|
let lng;
|
||||||
if (req.cookies.has(cookieName))
|
if (req.cookies.has(cookieName))
|
||||||
lng = acceptLanguage.get(req.cookies.get(cookieName).value);
|
lng = acceptLanguage.get(req.cookies.get(cookieName).value);
|
||||||
if (!lng) lng = acceptLanguage.get(req.headers.get("Accept-Language"));
|
if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'));
|
||||||
if (!lng) lng = i18n.defaultLocale;
|
if (!lng) lng = i18n.defaultLocale;
|
||||||
|
|
||||||
// Redirect if lng in path is not supported
|
// Redirect if lng in path is not supported
|
||||||
if (
|
if (
|
||||||
!i18n.locales.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
|
!i18n.locales.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
|
||||||
!req.nextUrl.pathname.startsWith("/_next")
|
!req.nextUrl.pathname.startsWith('/_next')
|
||||||
) {
|
) {
|
||||||
return NextResponse.redirect(
|
return NextResponse.redirect(
|
||||||
new URL(`/${lng}${req.nextUrl.pathname}`, req.url),
|
new URL(`/${lng}${req.nextUrl.pathname}`, req.url),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.headers.has("referer")) {
|
if (req.headers.has('referer')) {
|
||||||
const refererUrl = new URL(req.headers.get("referer"));
|
const refererUrl = new URL(req.headers.get('referer'));
|
||||||
const lngInReferer = i18n.locales.find((l) =>
|
const lngInReferer = i18n.locales.find((l) =>
|
||||||
refererUrl.pathname.startsWith(`/${l}`),
|
refererUrl.pathname.startsWith(`/${l}`),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
// debug: process.env.NODE_ENV === "development",
|
// debug: process.env.NODE_ENV === "development",
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: "en",
|
defaultLocale: 'en',
|
||||||
locales: ["en", "fr"],
|
locales: ['en', 'fr'],
|
||||||
},
|
},
|
||||||
reloadOnPrerender: process.env.NODE_ENV === "development",
|
reloadOnPrerender: process.env.NODE_ENV === 'development',
|
||||||
returnNull: false,
|
returnNull: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { i18n } = require("./next-i18next.config");
|
const { i18n } = require('./next-i18next.config');
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
@@ -6,22 +6,22 @@ const config = {
|
|||||||
webpack(config) {
|
webpack(config) {
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
use: ["@svgr/webpack"],
|
use: ['@svgr/webpack'],
|
||||||
});
|
});
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{ hostname: "localhost" },
|
{ hostname: 'localhost' },
|
||||||
{ hostname: "t3.gstatic.com" },
|
{ hostname: 't3.gstatic.com' },
|
||||||
{ hostname: "lh3.googleusercontent.com" },
|
{ hostname: 'lh3.googleusercontent.com' },
|
||||||
{ hostname: "www.mylinks.app" },
|
{ hostname: 'www.mylinks.app' },
|
||||||
],
|
],
|
||||||
formats: ["image/webp"],
|
formats: ['image/webp'],
|
||||||
},
|
},
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
output: "standalone",
|
output: 'standalone',
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
7986
package-lock.json
generated
7986
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,8 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.6.0",
|
"@prisma/client": "^5.6.0",
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"eslint": "^8.54.0",
|
"eslint": "^8.54.0",
|
||||||
"eslint-config-next": "14.0.3",
|
"eslint-config-next": "14.0.3",
|
||||||
"prisma": "^5.6.0",
|
"prisma": "^5.6.0",
|
||||||
"typescript": "5.3.2"
|
"typescript": "5.3.2",
|
||||||
|
"husky": "^8.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">
|
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M 106.813 73.354 L 86.501 73.355 C 86.501 84.421 95.594 93.398 106.813 93.398 C 124.952 93.398 139.996 108.476 139.996 126.656 L 140 126.656 C 140 144.835 124.956 159.913 106.816 159.913 L 0 159.913 L 62.001 200 L 62.001 189.977 C 62.001 184.441 66.476 179.956 72 179.956 L 106.816 179.956 L 106.816 179.962 C 136.14 179.962 159.999 156.048 159.999 126.656 C 159.999 97.266 136.14 73.354 106.813 73.354 Z" fill="#3f88c5" data-fill-palette-color="accent" style="" bx:origin="0.624436 0.21322"/>
|
<path d="M 106.813 73.354 L 86.501 73.355 C 86.501 84.421 95.594 93.398 106.813 93.398 C 124.952 93.398 139.996 108.476 139.996 126.656 L 140 126.656 C 140 144.835 124.956 159.913 106.816 159.913 L 0 159.913 L 62.001 200 L 62.001 189.977 C 62.001 184.441 66.476 179.956 72 179.956 L 106.816 179.956 L 106.816 179.962 C 136.14 179.962 159.999 156.048 159.999 126.656 C 159.999 97.266 136.14 73.354 106.813 73.354 Z" fill="#3f88c5" />
|
||||||
<path d="M 93.184 126.646 L 113.498 126.645 C 113.498 115.579 104.402 106.603 93.184 106.603 C 75.042 106.603 60 91.524 60 73.344 C 60 55.165 75.044 40.09 93.184 40.09 L 200 40.09 L 137.999 0 L 137.999 10.023 C 137.999 15.559 133.524 20.044 128 20.044 L 93.184 20.044 L 93.184 20.039 C 63.86 20.039 40.001 43.951 40.001 73.344 L 39.997 73.344 C 39.997 102.735 63.855 126.646 93.184 126.646 Z" fill="#3f88c5" data-fill-palette-color="accent" style="" bx:origin="0.374445 0.792419"/>
|
<path d="M 93.184 126.646 L 113.498 126.645 C 113.498 115.579 104.402 106.603 93.184 106.603 C 75.042 106.603 60 91.524 60 73.344 C 60 55.165 75.044 40.09 93.184 40.09 L 200 40.09 L 137.999 0 L 137.999 10.023 C 137.999 15.559 133.524 20.044 128 20.044 L 93.184 20.044 L 93.184 20.039 C 63.86 20.039 40.001 43.951 40.001 73.344 L 39.997 73.344 C 39.997 102.735 63.855 126.646 93.184 126.646 Z" fill="#3f88c5" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 957 B |
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"select-categorie": "Please select a category",
|
"select-category": "Please select a category",
|
||||||
"or-create-one": "or create one",
|
"or-create-one": "or create one",
|
||||||
"no-link": "No link for <b>{{name}}</b>",
|
"no-link": "No link for <b>{{name}}</b>",
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"title": "4. User Rights",
|
"title": "4. User Rights",
|
||||||
"description": "The user has the right to retrieve all their data at any time and/or request the complete deletion of their data."
|
"description": "The user has the right to retrieve all their data at any time and/or request the complete deletion of their data."
|
||||||
},
|
},
|
||||||
"rgpd": {
|
"gdpr": {
|
||||||
"title": "5. GDPR Compliance",
|
"title": "5. GDPR Compliance",
|
||||||
"description": "MyLinks complies with the General Data Protection Regulation (GDPR) of the European Union."
|
"description": "MyLinks complies with the General Data Protection Regulation (GDPR) of the European Union."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"name": "Nom de la catégorie",
|
"name": "Nom de la catégorie",
|
||||||
"create": "Créer une catégorie",
|
"create": "Créer une catégorie",
|
||||||
"edit": "Modifier une catégorie",
|
"edit": "Modifier une catégorie",
|
||||||
"remove": "Supprimer une categorie",
|
"remove": "Supprimer une catégorie",
|
||||||
"remove-confirm": "Confirmer la suppression ?",
|
"remove-confirm": "Confirmer la suppression ?",
|
||||||
"remove-description": "Vous devez supprimer tous les liens de cette catégorie avant de pouvoir supprimer cette catégorie"
|
"remove-description": "Vous devez supprimer tous les liens de cette catégorie avant de pouvoir supprimer cette catégorie"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"select-categorie": "Veuillez séléctionner une categorie",
|
"select-category": "Veuillez sélectionner une categories",
|
||||||
"or-create-one": "ou en créer une",
|
"or-create-one": "ou en créer une",
|
||||||
"no-link": "Aucun lien pour <b>{{name}}</b>",
|
"no-link": "Aucun lien pour <b>{{name}}</b>",
|
||||||
"footer": {
|
"footer": {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
"title": "4. Droits de l'utilisateur",
|
"title": "4. Droits de l'utilisateur",
|
||||||
"description": "L'utilisateur a le droit de récupérer l'ensemble de ses données à tout moment et/ou de demander la suppression complète de ses données."
|
"description": "L'utilisateur a le droit de récupérer l'ensemble de ses données à tout moment et/ou de demander la suppression complète de ses données."
|
||||||
},
|
},
|
||||||
"rgpd": {
|
"gdpr": {
|
||||||
"title": "5. Conformité au RGPD",
|
"title": "5. Conformité au RGPD",
|
||||||
"description": "MyLinks est conforme au Règlement Général sur la Protection des Données (RGPD) de l'Union européenne."
|
"description": "MyLinks est conforme au Règlement Général sur la Protection des Données (RGPD) de l'Union européenne."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<svg viewBox="0 0 500 165" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com">
|
<svg viewBox="0 0 500 165" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g id="tight-bounds" transform="matrix(1.264159, 0, 0, 1.265787, 0, 0)" style="" bx:origin="0.499856 0.5">
|
<g id="tight-bounds" transform="matrix(1.264159, 0, 0, 1.265787, 0, 0)">
|
||||||
<svg viewBox="0 0 395.52 130.3536553275838" height="130.3536553275838" width="395.52">
|
<svg viewBox="0 0 395.52 130.3536553275838" height="130.3536553275838" width="395.52">
|
||||||
<g>
|
<g>
|
||||||
<svg viewBox="0 0 609.8756479073379 200.99999999999997" height="130.3536553275838" width="395.52" transform="matrix(1.0990717554193026, 0, 0, 1.0219297409057617, -1464.3746338148903, -341.48333740234375)" bx:origin="-0.010232 -0.035228">
|
<svg viewBox="0 0 609.8756479073379 200.99999999999997" height="130.3536553275838" width="395.52">
|
||||||
<g>
|
<g>
|
||||||
<rect width="8.33878379307346" height="200.99999999999997" x="222.27426506352492" y="0" stroke-width="0" fill-opacity="1" class="rect-o-0" data-fill-palette-color="primary" rx="1%" id="o-0" data-palette-color="#005aa5" style="fill: rgb(72, 162, 255);" stroke="transparent"/>
|
<rect width="8.33878379307346" height="200.99999999999997" x="222.27426506352492" y="0" stroke-width="0" fill-opacity="1" class="rect-o-0" rx="1%" id="o-0" style="fill: rgb(72, 162, 255);" stroke="transparent"/>
|
||||||
</g>
|
</g>
|
||||||
<g transform="matrix(1,0,0,1,252.44232580181685,0.4999999999999858)">
|
<g transform="matrix(1,0,0,1,252.44232580181685,0.4999999999999858)">
|
||||||
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
|
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
|
||||||
<g id="textblocktransform">
|
<g>
|
||||||
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103" id="textblock">
|
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
|
||||||
<g>
|
<g>
|
||||||
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
|
<svg viewBox="0 0 357.43332210552103 200" height="200" width="357.43332210552103">
|
||||||
<g transform="matrix(1,0,0,1,0,0)">
|
<g transform="matrix(1,0,0,1,0,0)">
|
||||||
<svg width="357.43332210552103" viewBox="3.7404773235321045 -36.008331298828125 142.35955810546875 76.29547882080078" height="200" data-palette-color="#005aa5">
|
<svg width="357.43332210552103" viewBox="3.7404773235321045 -36.008331298828125 142.35955810546875 76.29547882080078" height="200" data-palette-color="#005aa5">
|
||||||
<svg/>
|
<svg/>
|
||||||
<svg/>
|
<svg/>
|
||||||
<g class="wordmark-text-0" data-fill-palette-color="primary" id="text-0">
|
<g>
|
||||||
<path xmlns="http://www.w3.org/2000/svg" d="M39.3-3.35v0c-0.033 0.967-0.4 1.783-1.1 2.45-0.7 0.667-1.533 0.983-2.5 0.95-0.967-0.033-1.783-0.4-2.45-1.1-0.667-0.7-0.983-1.533-0.95-2.5v0c0.067-2.167 0.033-8.567-0.1-19.2v0c-2.467 3.1-4.9 6.417-7.3 9.95v0c-0.567 0.8-1.317 1.29-2.25 1.47-0.933 0.187-1.8 0.013-2.6-0.52v0c-0.1-0.067-0.183-0.133-0.25-0.2v0c-0.367-0.233-0.683-0.517-0.95-0.85v0l-7.7-9.7c0.033 0.067 0.043 3.257 0.03 9.57-0.02 6.32-0.03 9.497-0.03 9.53v0c0 0.967-0.34 1.79-1.02 2.47-0.687 0.687-1.513 1.03-2.48 1.03-0.967 0-1.79-0.343-2.47-1.03-0.687-0.68-1.03-1.503-1.03-2.47v0c0-1.4 0.007-3.75 0.02-7.05 0.02-3.3 0.03-5.85 0.03-7.65 0-1.8-0.033-4.033-0.1-6.7v0c-0.067-2.367-0.183-4.817-0.35-7.35v0c-0.067-0.967 0.217-1.817 0.85-2.55 0.633-0.733 1.433-1.133 2.4-1.2v0c0.5-0.033 0.967 0.033 1.4 0.2v0c0.933 0.133 1.683 0.583 2.25 1.35v0l11.1 13.95c3.6-5 7.267-9.5 11-13.5v0c0.7-1.2 1.733-1.783 3.1-1.75v0c0.967 0.033 1.783 0.4 2.45 1.1 0.667 0.7 0.983 1.533 0.95 2.5v0c-0.1 3.167-0.1 8.007 0 14.52 0.1 6.52 0.117 11.28 0.05 14.28zM54.6-3.3v0c0-0.833-0.007-1.977-0.02-3.43-0.02-1.447-0.03-2.587-0.03-3.42v0-2.85l-5-8.3c-2.467-4.033-4.3-7.183-5.5-9.45v0c-0.467-0.833-0.567-1.71-0.3-2.63 0.267-0.913 0.827-1.603 1.68-2.07 0.847-0.467 1.73-0.567 2.65-0.3 0.913 0.267 1.603 0.833 2.07 1.7v0c1.1 1.967 3.6 6.183 7.5 12.65v0l2.7-4.2 5.7-8.6c0.5-0.8 1.217-1.317 2.15-1.55 0.933-0.233 1.81-0.093 2.63 0.42 0.813 0.52 1.337 1.247 1.57 2.18 0.233 0.933 0.083 1.817-0.45 2.65v0c-0.2 0.333-2.133 3.267-5.8 8.8v0c-1.967 2.967-3.517 5.45-4.65 7.45v0c0.033 0.767 0.05 2.45 0.05 5.05 0 2.6 0.017 4.5 0.05 5.7v0c0.033 0.967-0.283 1.8-0.95 2.5-0.667 0.7-1.483 1.067-2.45 1.1-0.967 0.033-1.8-0.283-2.5-0.95-0.7-0.667-1.067-1.483-1.1-2.45z" fill-rule="nonzero" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal; fill: rgb(72, 162, 255);" data-fill-palette-color="primary" stroke="none"/>
|
<path xmlns="http://www.w3.org/2000/svg" d="M39.3-3.35v0c-0.033 0.967-0.4 1.783-1.1 2.45-0.7 0.667-1.533 0.983-2.5 0.95-0.967-0.033-1.783-0.4-2.45-1.1-0.667-0.7-0.983-1.533-0.95-2.5v0c0.067-2.167 0.033-8.567-0.1-19.2v0c-2.467 3.1-4.9 6.417-7.3 9.95v0c-0.567 0.8-1.317 1.29-2.25 1.47-0.933 0.187-1.8 0.013-2.6-0.52v0c-0.1-0.067-0.183-0.133-0.25-0.2v0c-0.367-0.233-0.683-0.517-0.95-0.85v0l-7.7-9.7c0.033 0.067 0.043 3.257 0.03 9.57-0.02 6.32-0.03 9.497-0.03 9.53v0c0 0.967-0.34 1.79-1.02 2.47-0.687 0.687-1.513 1.03-2.48 1.03-0.967 0-1.79-0.343-2.47-1.03-0.687-0.68-1.03-1.503-1.03-2.47v0c0-1.4 0.007-3.75 0.02-7.05 0.02-3.3 0.03-5.85 0.03-7.65 0-1.8-0.033-4.033-0.1-6.7v0c-0.067-2.367-0.183-4.817-0.35-7.35v0c-0.067-0.967 0.217-1.817 0.85-2.55 0.633-0.733 1.433-1.133 2.4-1.2v0c0.5-0.033 0.967 0.033 1.4 0.2v0c0.933 0.133 1.683 0.583 2.25 1.35v0l11.1 13.95c3.6-5 7.267-9.5 11-13.5v0c0.7-1.2 1.733-1.783 3.1-1.75v0c0.967 0.033 1.783 0.4 2.45 1.1 0.667 0.7 0.983 1.533 0.95 2.5v0c-0.1 3.167-0.1 8.007 0 14.52 0.1 6.52 0.117 11.28 0.05 14.28zM54.6-3.3v0c0-0.833-0.007-1.977-0.02-3.43-0.02-1.447-0.03-2.587-0.03-3.42v0-2.85l-5-8.3c-2.467-4.033-4.3-7.183-5.5-9.45v0c-0.467-0.833-0.567-1.71-0.3-2.63 0.267-0.913 0.827-1.603 1.68-2.07 0.847-0.467 1.73-0.567 2.65-0.3 0.913 0.267 1.603 0.833 2.07 1.7v0c1.1 1.967 3.6 6.183 7.5 12.65v0l2.7-4.2 5.7-8.6c0.5-0.8 1.217-1.317 2.15-1.55 0.933-0.233 1.81-0.093 2.63 0.42 0.813 0.52 1.337 1.247 1.57 2.18 0.233 0.933 0.083 1.817-0.45 2.65v0c-0.2 0.333-2.133 3.267-5.8 8.8v0c-1.967 2.967-3.517 5.45-4.65 7.45v0c0.033 0.767 0.05 2.45 0.05 5.05 0 2.6 0.017 4.5 0.05 5.7v0c0.033 0.967-0.283 1.8-0.95 2.5-0.667 0.7-1.483 1.067-2.45 1.1-0.967 0.033-1.8-0.283-2.5-0.95-0.7-0.667-1.067-1.483-1.1-2.45z" fill-rule="nonzero" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" style="mix-blend-mode: normal; fill: rgb(72, 162, 255);" stroke="none"/>
|
||||||
<path xmlns="http://www.w3.org/2000/svg" d="M7.9 39.93v0c-1.133-0.033-2.05-0.5-2.75-1.4v0c-0.667-0.667-1.017-1.483-1.05-2.45v0c-0.033-3.4-0.1-8.217-0.2-14.45v0l-0.15-14.05c0-0.967 0.333-1.793 1-2.48 0.667-0.68 1.483-1.027 2.45-1.04 0.967-0.02 1.793 0.303 2.48 0.97 0.68 0.667 1.037 1.483 1.07 2.45v0l0.15 13.1 0.15 12.6c1.633 0 4.01-0.017 7.13-0.05 3.113-0.033 5.403-0.05 6.87-0.05v0c0.967 0 1.793 0.323 2.48 0.97 0.68 0.653 1.02 1.447 1.02 2.38 0 0.933-0.34 1.74-1.02 2.42-0.687 0.687-1.513 1.03-2.48 1.03v0c-1.1 0-3.95 0.017-8.55 0.05v0c-4.633 0.033-7.5 0.033-8.6 0zM34.5 39.98v0c-0.967 0-1.8-0.327-2.5-0.98-0.7-0.647-1.067-1.453-1.1-2.42v0c0-0.867 0.277-1.643 0.83-2.33 0.547-0.68 1.237-1.087 2.07-1.22v0c0.033-5.8 0.017-13.15-0.05-22.05v0c-0.8-0.167-1.467-0.567-2-1.2-0.533-0.633-0.8-1.383-0.8-2.25v0c0-0.967 0.35-1.783 1.05-2.45 0.7-0.667 1.533-1 2.5-1v0l5.65 0.05c0.967 0 1.783 0.35 2.45 1.05 0.667 0.7 1 1.533 1 2.5v0c0 0.833-0.273 1.567-0.82 2.2-0.553 0.633-1.247 1.033-2.08 1.2v0c0.1 8.767 0.133 16.033 0.1 21.8v0c0.833 0.133 1.527 0.517 2.08 1.15 0.547 0.633 0.837 1.367 0.87 2.2v0c0 0.967-0.323 1.8-0.97 2.5-0.653 0.7-1.463 1.067-2.43 1.1v0zM76.15 39.63v0c-1.3 0.233-2.383-0.167-3.25-1.2v0c-2.233-2.633-5.05-6.533-8.45-11.7v0c-3.7-5.233-6.317-8.783-7.85-10.65v0c0.033 1.333 0.117 4.29 0.25 8.87 0.133 4.587 0.2 8.397 0.2 11.43v0c0 0.967-0.34 1.79-1.02 2.47-0.687 0.687-1.513 1.03-2.48 1.03-0.967 0-1.79-0.343-2.47-1.03-0.687-0.68-1.03-1.503-1.03-2.47v0c0-2.933-0.093-7.677-0.28-14.23-0.18-6.547-0.203-11.503-0.07-14.87v0c0.033-0.967 0.4-1.783 1.1-2.45 0.7-0.667 1.533-0.983 2.5-0.95v0c0.467 0.033 0.917 0.133 1.35 0.3v0c0.633 0.2 1.183 0.567 1.65 1.1v0c0.567 0.7 1.483 1.807 2.75 3.32 1.267 1.52 2.217 2.663 2.85 3.43v0c1.867 1.933 4.633 5.5 8.3 10.7v0l2.4 3.45c0.1-7.733 0.117-14.017 0.05-18.85v0c0-0.967 0.333-1.793 1-2.48 0.667-0.68 1.483-1.027 2.45-1.04 0.967-0.02 1.793 0.303 2.48 0.97 0.68 0.667 1.037 1.483 1.07 2.45v0c0.033 4.533-0.017 14.183-0.15 28.95v0c-0.033 0.933-0.367 1.723-1 2.37-0.633 0.653-1.417 1.013-2.35 1.08zM109.85 38.83l-11.25-14.45-4.2 3.45c0.1 2.7 0.217 5.617 0.35 8.75v0c0.033 0.933-0.273 1.757-0.92 2.47-0.653 0.72-1.453 1.107-2.4 1.16-0.953 0.047-1.787-0.257-2.5-0.91-0.72-0.647-1.113-1.453-1.18-2.42v0c-0.5-10.267-0.683-20.267-0.55-30v0c0.033-0.967 0.393-1.783 1.08-2.45 0.68-0.667 1.503-0.993 2.47-0.98 0.967 0.02 1.783 0.37 2.45 1.05 0.667 0.687 1 1.513 1 2.48v0c-0.067 4.233-0.067 8.217 0 11.95v0c5.233-4.467 10.083-9.217 14.55-14.25v0c0.667-0.7 1.477-1.067 2.43-1.1 0.947-0.033 1.78 0.283 2.5 0.95 0.713 0.667 1.087 1.473 1.12 2.42 0.033 0.953-0.283 1.797-0.95 2.53v0c-3.267 3.733-6.6 7.15-10 10.25v0l11.5 14.8c0.6 0.733 0.833 1.583 0.7 2.55v0c-0.1 0.967-0.523 1.75-1.27 2.35-0.753 0.6-1.613 0.843-2.58 0.73-0.967-0.12-1.75-0.563-2.35-1.33zM132.85 40.28v0c-2.567 0.067-4.883-0.333-6.95-1.2-2.067-0.867-3.7-2.133-4.9-3.8-1.2-1.667-1.8-3.6-1.8-5.8v0c0-1.333 0.383-2.377 1.15-3.13 0.767-0.747 1.633-1.12 2.6-1.12v0c0.967 0 1.75 0.34 2.35 1.02 0.6 0.687 0.9 1.447 0.9 2.28v0c0 0.833 0.05 1.527 0.15 2.08 0.1 0.547 0.35 1.053 0.75 1.52v0c0.767 0.933 2.717 1.4 5.85 1.4v0c1.7 0 3.15-0.35 4.35-1.05 1.2-0.7 1.8-1.7 1.8-3v0c0-0.867-0.7-1.7-2.1-2.5v0c-0.967-0.533-3.117-1.367-6.45-2.5v0c-3.633-1.2-6.367-2.593-8.2-4.18-1.833-1.58-2.75-3.737-2.75-6.47v0c0-2.1 0.6-3.933 1.8-5.5 1.2-1.567 2.777-2.767 4.73-3.6 1.947-0.833 4.02-1.25 6.22-1.25v0c2.267-0.067 4.45 0.3 6.55 1.1 2.1 0.8 3.793 1.94 5.08 3.42 1.28 1.487 1.92 3.163 1.92 5.03v0c0 1-0.367 1.857-1.1 2.57-0.733 0.72-1.583 1.08-2.55 1.08v0c-0.667 0-1.317-0.2-1.95-0.6-0.633-0.4-1.033-0.9-1.2-1.5v0c-0.2-0.8-0.49-1.417-0.87-1.85-0.387-0.433-0.897-0.833-1.53-1.2v0c-0.833-0.467-1.583-0.8-2.25-1-0.667-0.2-1.617-0.3-2.85-0.3v0c-1.467 0-2.673 0.273-3.62 0.82-0.953 0.553-1.43 1.263-1.43 2.13v0c0 1.1 0.543 2 1.63 2.7 1.08 0.7 2.603 1.367 4.57 2v0c3.8 1.267 6.333 2.25 7.6 2.95v0c2.1 1.167 3.583 2.4 4.45 3.7 0.867 1.3 1.3 2.933 1.3 4.9v0c0 2.1-0.633 3.973-1.9 5.62-1.267 1.653-2.923 2.937-4.97 3.85-2.053 0.92-4.18 1.38-6.38 1.38z" fill-rule="nonzero" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal; fill: rgb(72, 162, 255);" data-fill-palette-color="primary" stroke="none"/>
|
<path xmlns="http://www.w3.org/2000/svg" d="M7.9 39.93v0c-1.133-0.033-2.05-0.5-2.75-1.4v0c-0.667-0.667-1.017-1.483-1.05-2.45v0c-0.033-3.4-0.1-8.217-0.2-14.45v0l-0.15-14.05c0-0.967 0.333-1.793 1-2.48 0.667-0.68 1.483-1.027 2.45-1.04 0.967-0.02 1.793 0.303 2.48 0.97 0.68 0.667 1.037 1.483 1.07 2.45v0l0.15 13.1 0.15 12.6c1.633 0 4.01-0.017 7.13-0.05 3.113-0.033 5.403-0.05 6.87-0.05v0c0.967 0 1.793 0.323 2.48 0.97 0.68 0.653 1.02 1.447 1.02 2.38 0 0.933-0.34 1.74-1.02 2.42-0.687 0.687-1.513 1.03-2.48 1.03v0c-1.1 0-3.95 0.017-8.55 0.05v0c-4.633 0.033-7.5 0.033-8.6 0zM34.5 39.98v0c-0.967 0-1.8-0.327-2.5-0.98-0.7-0.647-1.067-1.453-1.1-2.42v0c0-0.867 0.277-1.643 0.83-2.33 0.547-0.68 1.237-1.087 2.07-1.22v0c0.033-5.8 0.017-13.15-0.05-22.05v0c-0.8-0.167-1.467-0.567-2-1.2-0.533-0.633-0.8-1.383-0.8-2.25v0c0-0.967 0.35-1.783 1.05-2.45 0.7-0.667 1.533-1 2.5-1v0l5.65 0.05c0.967 0 1.783 0.35 2.45 1.05 0.667 0.7 1 1.533 1 2.5v0c0 0.833-0.273 1.567-0.82 2.2-0.553 0.633-1.247 1.033-2.08 1.2v0c0.1 8.767 0.133 16.033 0.1 21.8v0c0.833 0.133 1.527 0.517 2.08 1.15 0.547 0.633 0.837 1.367 0.87 2.2v0c0 0.967-0.323 1.8-0.97 2.5-0.653 0.7-1.463 1.067-2.43 1.1v0zM76.15 39.63v0c-1.3 0.233-2.383-0.167-3.25-1.2v0c-2.233-2.633-5.05-6.533-8.45-11.7v0c-3.7-5.233-6.317-8.783-7.85-10.65v0c0.033 1.333 0.117 4.29 0.25 8.87 0.133 4.587 0.2 8.397 0.2 11.43v0c0 0.967-0.34 1.79-1.02 2.47-0.687 0.687-1.513 1.03-2.48 1.03-0.967 0-1.79-0.343-2.47-1.03-0.687-0.68-1.03-1.503-1.03-2.47v0c0-2.933-0.093-7.677-0.28-14.23-0.18-6.547-0.203-11.503-0.07-14.87v0c0.033-0.967 0.4-1.783 1.1-2.45 0.7-0.667 1.533-0.983 2.5-0.95v0c0.467 0.033 0.917 0.133 1.35 0.3v0c0.633 0.2 1.183 0.567 1.65 1.1v0c0.567 0.7 1.483 1.807 2.75 3.32 1.267 1.52 2.217 2.663 2.85 3.43v0c1.867 1.933 4.633 5.5 8.3 10.7v0l2.4 3.45c0.1-7.733 0.117-14.017 0.05-18.85v0c0-0.967 0.333-1.793 1-2.48 0.667-0.68 1.483-1.027 2.45-1.04 0.967-0.02 1.793 0.303 2.48 0.97 0.68 0.667 1.037 1.483 1.07 2.45v0c0.033 4.533-0.017 14.183-0.15 28.95v0c-0.033 0.933-0.367 1.723-1 2.37-0.633 0.653-1.417 1.013-2.35 1.08zM109.85 38.83l-11.25-14.45-4.2 3.45c0.1 2.7 0.217 5.617 0.35 8.75v0c0.033 0.933-0.273 1.757-0.92 2.47-0.653 0.72-1.453 1.107-2.4 1.16-0.953 0.047-1.787-0.257-2.5-0.91-0.72-0.647-1.113-1.453-1.18-2.42v0c-0.5-10.267-0.683-20.267-0.55-30v0c0.033-0.967 0.393-1.783 1.08-2.45 0.68-0.667 1.503-0.993 2.47-0.98 0.967 0.02 1.783 0.37 2.45 1.05 0.667 0.687 1 1.513 1 2.48v0c-0.067 4.233-0.067 8.217 0 11.95v0c5.233-4.467 10.083-9.217 14.55-14.25v0c0.667-0.7 1.477-1.067 2.43-1.1 0.947-0.033 1.78 0.283 2.5 0.95 0.713 0.667 1.087 1.473 1.12 2.42 0.033 0.953-0.283 1.797-0.95 2.53v0c-3.267 3.733-6.6 7.15-10 10.25v0l11.5 14.8c0.6 0.733 0.833 1.583 0.7 2.55v0c-0.1 0.967-0.523 1.75-1.27 2.35-0.753 0.6-1.613 0.843-2.58 0.73-0.967-0.12-1.75-0.563-2.35-1.33zM132.85 40.28v0c-2.567 0.067-4.883-0.333-6.95-1.2-2.067-0.867-3.7-2.133-4.9-3.8-1.2-1.667-1.8-3.6-1.8-5.8v0c0-1.333 0.383-2.377 1.15-3.13 0.767-0.747 1.633-1.12 2.6-1.12v0c0.967 0 1.75 0.34 2.35 1.02 0.6 0.687 0.9 1.447 0.9 2.28v0c0 0.833 0.05 1.527 0.15 2.08 0.1 0.547 0.35 1.053 0.75 1.52v0c0.767 0.933 2.717 1.4 5.85 1.4v0c1.7 0 3.15-0.35 4.35-1.05 1.2-0.7 1.8-1.7 1.8-3v0c0-0.867-0.7-1.7-2.1-2.5v0c-0.967-0.533-3.117-1.367-6.45-2.5v0c-3.633-1.2-6.367-2.593-8.2-4.18-1.833-1.58-2.75-3.737-2.75-6.47v0c0-2.1 0.6-3.933 1.8-5.5 1.2-1.567 2.777-2.767 4.73-3.6 1.947-0.833 4.02-1.25 6.22-1.25v0c2.267-0.067 4.45 0.3 6.55 1.1 2.1 0.8 3.793 1.94 5.08 3.42 1.28 1.487 1.92 3.163 1.92 5.03v0c0 1-0.367 1.857-1.1 2.57-0.733 0.72-1.583 1.08-2.55 1.08v0c-0.667 0-1.317-0.2-1.95-0.6-0.633-0.4-1.033-0.9-1.2-1.5v0c-0.2-0.8-0.49-1.417-0.87-1.85-0.387-0.433-0.897-0.833-1.53-1.2v0c-0.833-0.467-1.583-0.8-2.25-1-0.667-0.2-1.617-0.3-2.85-0.3v0c-1.467 0-2.673 0.273-3.62 0.82-0.953 0.553-1.43 1.263-1.43 2.13v0c0 1.1 0.543 2 1.63 2.7 1.08 0.7 2.603 1.367 4.57 2v0c3.8 1.267 6.333 2.25 7.6 2.95v0c2.1 1.167 3.583 2.4 4.45 3.7 0.867 1.3 1.3 2.933 1.3 4.9v0c0 2.1-0.633 3.973-1.9 5.62-1.267 1.653-2.923 2.937-4.97 3.85-2.053 0.92-4.18 1.38-6.38 1.38z" fill-rule="nonzero" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" style="mix-blend-mode: normal; fill: rgb(72, 162, 255);" stroke="none"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</g>
|
</g>
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
<svg viewBox="0 0 200.44498811830644 199.99999999999997" height="199.99999999999997" width="200.44498811830644">
|
<svg viewBox="0 0 200.44498811830644 199.99999999999997" height="199.99999999999997" width="200.44498811830644">
|
||||||
<g>
|
<g>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0.11100006103515625 100 99.77799987792969" enable-background="new 0 0 100 100" height="199.99999999999997" width="200.44498811830644" class="icon-icon-0" data-fill-palette-color="accent" id="icon-0">
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0" y="0" viewBox="0 0.11100006103515625 100 99.77799987792969" enable-background="new 0 0 100 100" height="199.99999999999997" width="200.44498811830644" class="icon-icon-0" data-fill-palette-color="accent" id="icon-0">
|
||||||
<path d="M53.406 36.706L43.25 36.707c0 5.521 4.547 9.999 10.156 9.999 9.07 0 16.592 7.522 16.592 16.592H70c0 9.07-7.522 16.592-16.592 16.592H0l31 19.999v-5c0-2.762 2.238-5 5-5h17.408v0.003C68.07 89.892 80 77.962 80 63.298 80 48.636 68.07 36.706 53.406 36.706z" data-fill-palette-color="accent" style="fill: rgb(72, 162, 255);"/>
|
<path d="M53.406 36.706L43.25 36.707c0 5.521 4.547 9.999 10.156 9.999 9.07 0 16.592 7.522 16.592 16.592H70c0 9.07-7.522 16.592-16.592 16.592H0l31 19.999v-5c0-2.762 2.238-5 5-5h17.408v0.003C68.07 89.892 80 77.962 80 63.298 80 48.636 68.07 36.706 53.406 36.706z" style="fill: rgb(72, 162, 255);"/>
|
||||||
<path d="M46.592 63.294l10.157-0.001c0-5.521-4.548-9.999-10.157-9.999C37.521 53.294 30 45.771 30 36.702c0-9.07 7.522-16.591 16.592-16.591H100l-31-20v5c0 2.762-2.238 5-5 5H46.592v-0.003C31.93 10.108 20 22.038 20 36.702h-0.002C19.998 51.364 31.928 63.294 46.592 63.294z" data-fill-palette-color="accent" style="fill: rgb(72, 162, 255);"/>
|
<path d="M46.592 63.294l10.157-0.001c0-5.521-4.548-9.999-10.157-9.999C37.521 53.294 30 45.771 30 36.702c0-9.07 7.522-16.591 16.592-16.591H100l-31-20v5c0 2.762-2.238 5-5 5H46.592v-0.003C31.93 10.108 20 22.038 20 36.702h-0.002C19.998 51.364 31.928 63.294 46.592 63.294z" style="fill: rgb(72, 162, 255);"/>
|
||||||
</svg>
|
</svg>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.0 KiB |
@@ -1,6 +1,6 @@
|
|||||||
import { CSSProperties, ReactNode } from "react";
|
import { CSSProperties, ReactNode } from 'react';
|
||||||
|
|
||||||
import styles from "./block-wrapper.module.scss";
|
import styles from './block-wrapper.module.scss';
|
||||||
|
|
||||||
interface BlockWrapperProps {
|
interface BlockWrapperProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -9,7 +9,10 @@ interface BlockWrapperProps {
|
|||||||
|
|
||||||
export default function BlockWrapper({ children, style }: BlockWrapperProps) {
|
export default function BlockWrapper({ children, style }: BlockWrapperProps) {
|
||||||
return (
|
return (
|
||||||
<section className={styles["block-wrapper"]} style={style}>
|
<section
|
||||||
|
className={styles['block-wrapper']}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@import "styles/keyframes.scss";
|
@import 'styles/keyframes.scss';
|
||||||
@import "styles/colors.scss";
|
@import 'styles/colors.scss';
|
||||||
|
|
||||||
.block-wrapper {
|
.block-wrapper {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import Link from "next/link";
|
import Link from 'next/link';
|
||||||
import { CSSProperties, ReactNode } from "react";
|
import { CSSProperties, ReactNode } from 'react';
|
||||||
|
|
||||||
export default function ButtonLink({
|
export default function ButtonLink({
|
||||||
href = "#",
|
href = '#',
|
||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
style = {},
|
style = {},
|
||||||
className = "",
|
className = '',
|
||||||
}: {
|
}: {
|
||||||
href?: string;
|
href?: string;
|
||||||
onClick?: (...args: any) => any;
|
onClick?: (...args: any) => any;
|
||||||
@@ -15,13 +15,18 @@ export default function ButtonLink({
|
|||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const handleClick = (event) => {
|
const handleClick = (event) => {
|
||||||
if (!href || href === "#") {
|
if (!href || href === '#') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
onClick && onClick();
|
onClick && onClick();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Link href={href} onClick={handleClick} style={style} className={className}>
|
<Link
|
||||||
|
href={href}
|
||||||
|
onClick={handleClick}
|
||||||
|
style={style}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MutableRefObject, useState } from "react";
|
import { MutableRefObject, useState } from 'react';
|
||||||
|
|
||||||
interface SelectorProps {
|
interface SelectorProps {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -18,8 +18,8 @@ export default function Checkbox({
|
|||||||
labelComponent,
|
labelComponent,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
innerRef = null,
|
innerRef = null,
|
||||||
fieldClass = "",
|
fieldClass = '',
|
||||||
placeholder = "Type something...",
|
placeholder = 'Type something...',
|
||||||
isChecked,
|
isChecked,
|
||||||
onChangeCallback,
|
onChangeCallback,
|
||||||
}: SelectorProps): JSX.Element {
|
}: SelectorProps): JSX.Element {
|
||||||
@@ -35,17 +35,23 @@ export default function Checkbox({
|
|||||||
return (
|
return (
|
||||||
<div className={`checkbox-field ${fieldClass}`}>
|
<div className={`checkbox-field ${fieldClass}`}>
|
||||||
{label && (
|
{label && (
|
||||||
<label htmlFor={name} title={`${name} field`}>
|
<label
|
||||||
|
htmlFor={name}
|
||||||
|
title={`${name} field`}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{labelComponent && (
|
{labelComponent && (
|
||||||
<label htmlFor={name} title={`${name} field`}>
|
<label
|
||||||
|
htmlFor={name}
|
||||||
|
title={`${name} field`}
|
||||||
|
>
|
||||||
{labelComponent}
|
{labelComponent}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type='checkbox'
|
||||||
id={name}
|
id={name}
|
||||||
name={name}
|
name={name}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { withTranslation } from "next-i18next";
|
import { withTranslation } from 'next-i18next';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import LangSelector from "../LangSelector";
|
import LangSelector from '../LangSelector';
|
||||||
import styles from "./error-boundary.module.scss";
|
import styles from './error-boundary.module.scss';
|
||||||
|
|
||||||
class ErrorBoundary extends React.Component {
|
class ErrorBoundary extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -21,22 +21,22 @@ class ErrorBoundary extends React.Component {
|
|||||||
if (!this.state.errorInfo) return this.props.children;
|
if (!this.state.errorInfo) return this.props.children;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["error-boundary"]}>
|
<div className={styles['error-boundary']}>
|
||||||
<div className={styles["boundary-content"]}>
|
<div className={styles['boundary-content']}>
|
||||||
<h1>{this.props.t("common:generic-error")}</h1>
|
<h1>{this.props.t('common:generic-error')}</h1>
|
||||||
<p
|
<p
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: this.props.t("common:generic-error-description"),
|
__html: this.props.t('common:generic-error-description'),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button onClick={() => window.location.reload()}>
|
<button onClick={() => window.location.reload()}>
|
||||||
{this.props.t("common:retry")}
|
{this.props.t('common:retry')}
|
||||||
</button>
|
</button>
|
||||||
<details>
|
<details>
|
||||||
<summary>{this.state.error && this.state.error.toString()}</summary>
|
<summary>{this.state.error && this.state.error.toString()}</summary>
|
||||||
<code>{this.state.errorInfo.componentStack}</code>
|
<code>{this.state.errorInfo.componentStack}</code>
|
||||||
</details>
|
</details>
|
||||||
<div className="lang-selector">
|
<div className='lang-selector'>
|
||||||
<LangSelector />
|
<LangSelector />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import MessageManager from "components/MessageManager/MessageManager";
|
import MessageManager from 'components/MessageManager/MessageManager';
|
||||||
import { i18n, useTranslation } from "next-i18next";
|
import { i18n, useTranslation } from 'next-i18next';
|
||||||
import { NextSeo } from "next-seo";
|
import { NextSeo } from 'next-seo';
|
||||||
import Link from "next/link";
|
import Link from 'next/link';
|
||||||
|
|
||||||
interface FormProps {
|
interface FormProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -29,8 +29,8 @@ export default function Form({
|
|||||||
infoMessage,
|
infoMessage,
|
||||||
canSubmit,
|
canSubmit,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
textBtnConfirm = i18n.t("common:confirm"),
|
textBtnConfirm = i18n.t('common:confirm'),
|
||||||
classBtnConfirm = "",
|
classBtnConfirm = '',
|
||||||
children,
|
children,
|
||||||
disableHomeLink = false,
|
disableHomeLink = false,
|
||||||
}: FormProps) {
|
}: FormProps) {
|
||||||
@@ -42,13 +42,17 @@ export default function Form({
|
|||||||
<h2>{title}</h2>
|
<h2>{title}</h2>
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
{children}
|
{children}
|
||||||
<button type="submit" className={classBtnConfirm} disabled={!canSubmit}>
|
<button
|
||||||
|
type='submit'
|
||||||
|
className={classBtnConfirm}
|
||||||
|
disabled={!canSubmit}
|
||||||
|
>
|
||||||
{textBtnConfirm}
|
{textBtnConfirm}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{!disableHomeLink && (
|
{!disableHomeLink && (
|
||||||
<Link href={categoryId ? `/?categoryId=${categoryId}` : "/"}>
|
<Link href={categoryId ? `/?categoryId=${categoryId}` : '/'}>
|
||||||
{t("common:back-home")}
|
{t('common:back-home')}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<MessageManager
|
<MessageManager
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
export default function LangSelector() {
|
export default function LangSelector() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -10,20 +10,23 @@ export default function LangSelector() {
|
|||||||
i18n.changeLanguage(newLocale);
|
i18n.changeLanguage(newLocale);
|
||||||
router.push({ pathname, query }, asPath, { locale: newLocale });
|
router.push({ pathname, query }, asPath, { locale: newLocale });
|
||||||
};
|
};
|
||||||
const languages = ["en", "fr"];
|
const languages = ['en', 'fr'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
name="lng-select"
|
name='lng-select'
|
||||||
id="lng-select"
|
id='lng-select'
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
onToggleLanguageClick(event.target.value);
|
onToggleLanguageClick(event.target.value);
|
||||||
}}
|
}}
|
||||||
value={i18n.language}
|
value={i18n.language}
|
||||||
>
|
>
|
||||||
{languages.map((lang) => (
|
{languages.map((lang) => (
|
||||||
<option key={lang} value={lang}>
|
<option
|
||||||
{t(`common:language.${lang as "fr" | "en"}`)}
|
key={lang}
|
||||||
|
value={lang}
|
||||||
|
>
|
||||||
|
{t(`common:language.${lang as 'fr' | 'en'}`)}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import Image from "next/image";
|
import Image from 'next/image';
|
||||||
import { useState } from "react";
|
import { useState } from 'react';
|
||||||
import { TbLoader3 } from "react-icons/tb";
|
import { TbLoader3 } from 'react-icons/tb';
|
||||||
import { TfiWorld } from "react-icons/tfi";
|
import { TfiWorld } from 'react-icons/tfi';
|
||||||
|
|
||||||
import styles from "./links.module.scss";
|
import styles from './links.module.scss';
|
||||||
|
|
||||||
interface LinkFaviconProps {
|
interface LinkFaviconProps {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -22,9 +22,9 @@ export default function LinkFavicon({
|
|||||||
const [isLoading, setLoading] = useState<boolean>(true);
|
const [isLoading, setLoading] = useState<boolean>(true);
|
||||||
const baseUrlApi =
|
const baseUrlApi =
|
||||||
process.env.NEXT_PUBLIC_API_URL ||
|
process.env.NEXT_PUBLIC_API_URL ||
|
||||||
(typeof window !== "undefined" && window)?.location?.origin + "/api";
|
(typeof window !== 'undefined' && window)?.location?.origin + '/api';
|
||||||
if (!baseUrlApi) {
|
if (!baseUrlApi) {
|
||||||
console.warn("Missing API URL");
|
console.warn('Missing API URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
const setFallbackFavicon = () => setFailed(true);
|
const setFallbackFavicon = () => setFailed(true);
|
||||||
@@ -32,8 +32,8 @@ export default function LinkFavicon({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles["favicon"]}
|
className={styles['favicon']}
|
||||||
style={{ marginRight: !noMargin ? "1em" : "0" }}
|
style={{ marginRight: !noMargin ? '1em' : '0' }}
|
||||||
>
|
>
|
||||||
{!isFailed && baseUrlApi ? (
|
{!isFailed && baseUrlApi ? (
|
||||||
<Image
|
<Image
|
||||||
@@ -45,14 +45,14 @@ export default function LinkFavicon({
|
|||||||
onLoad={handleStopLoading}
|
onLoad={handleStopLoading}
|
||||||
height={size}
|
height={size}
|
||||||
width={size}
|
width={size}
|
||||||
alt="icon"
|
alt='icon'
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TfiWorld size={size} />
|
<TfiWorld size={size} />
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<span
|
<span
|
||||||
className={styles["favicon-loader"]}
|
className={styles['favicon-loader']}
|
||||||
style={{ height: `${size}px`, width: `${size}px` }}
|
style={{ height: `${size}px`, width: `${size}px` }}
|
||||||
>
|
>
|
||||||
<TbLoader3 size={size} />
|
<TbLoader3 size={size} />
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from 'framer-motion';
|
||||||
import LinkTag from "next/link";
|
import LinkTag from 'next/link';
|
||||||
import { AiFillStar } from "react-icons/ai";
|
import { AiFillStar } from 'react-icons/ai';
|
||||||
import PATHS from "constants/paths";
|
import PATHS from 'constants/paths';
|
||||||
import { Link } from "types";
|
import { Link } from 'types';
|
||||||
import EditItem from "components/QuickActions/EditItem";
|
import EditItem from 'components/QuickActions/EditItem';
|
||||||
import FavoriteItem from "components/QuickActions/FavoriteItem";
|
import FavoriteItem from 'components/QuickActions/FavoriteItem';
|
||||||
import RemoveItem from "components/QuickActions/RemoveItem";
|
import RemoveItem from 'components/QuickActions/RemoveItem';
|
||||||
import LinkFavicon from "./LinkFavicon";
|
import LinkFavicon from './LinkFavicon';
|
||||||
import styles from "./links.module.scss";
|
import styles from './links.module.scss';
|
||||||
import { makeRequest } from "lib/request";
|
import { makeRequest } from 'lib/request';
|
||||||
|
|
||||||
export default function LinkItem({
|
export default function LinkItem({
|
||||||
link,
|
link,
|
||||||
@@ -16,14 +16,14 @@ export default function LinkItem({
|
|||||||
index,
|
index,
|
||||||
}: {
|
}: {
|
||||||
link: Link;
|
link: Link;
|
||||||
toggleFavorite: (linkId: Link["id"]) => void;
|
toggleFavorite: (linkId: Link['id']) => void;
|
||||||
index: number;
|
index: number;
|
||||||
}) {
|
}) {
|
||||||
const { id, name, url, favorite } = link;
|
const { id, name, url, favorite } = link;
|
||||||
const onFavorite = () => {
|
const onFavorite = () => {
|
||||||
makeRequest({
|
makeRequest({
|
||||||
url: `${PATHS.API.LINK}/${link.id}`,
|
url: `${PATHS.API.LINK}/${link.id}`,
|
||||||
method: "PUT",
|
method: 'PUT',
|
||||||
body: {
|
body: {
|
||||||
name,
|
name,
|
||||||
url,
|
url,
|
||||||
@@ -37,57 +37,70 @@ export default function LinkItem({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.li
|
<motion.li
|
||||||
className={styles["link"]}
|
className={styles['link']}
|
||||||
key={id}
|
key={id}
|
||||||
initial={{ x: -30, opacity: 0 }}
|
initial={{ x: -30, opacity: 0 }}
|
||||||
animate={{ x: 0, opacity: 1 }}
|
animate={{ x: 0, opacity: 1 }}
|
||||||
transition={{
|
transition={{
|
||||||
type: "spring",
|
type: 'spring',
|
||||||
stiffness: 260,
|
stiffness: 260,
|
||||||
damping: 20,
|
damping: 20,
|
||||||
delay: index * 0.05,
|
delay: index * 0.05,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LinkFavicon url={url} />
|
<LinkFavicon url={url} />
|
||||||
<LinkTag href={url} target={"_blank"} rel="noreferrer">
|
<LinkTag
|
||||||
<span className={styles["link-name"]}>
|
href={url}
|
||||||
{name} {favorite && <AiFillStar color="#ffc107" />}
|
target={'_blank'}
|
||||||
|
rel='noreferrer'
|
||||||
|
>
|
||||||
|
<span className={styles['link-name']}>
|
||||||
|
{name} {favorite && <AiFillStar color='#ffc107' />}
|
||||||
</span>
|
</span>
|
||||||
<LinkItemURL url={url} />
|
<LinkItemURL url={url} />
|
||||||
</LinkTag>
|
</LinkTag>
|
||||||
<div className={styles["controls"]}>
|
<div className={styles['controls']}>
|
||||||
<FavoriteItem isFavorite={favorite} onClick={onFavorite} />
|
<FavoriteItem
|
||||||
<EditItem type="link" id={id} />
|
isFavorite={favorite}
|
||||||
<RemoveItem type="link" id={id} />
|
onClick={onFavorite}
|
||||||
|
/>
|
||||||
|
<EditItem
|
||||||
|
type='link'
|
||||||
|
id={id}
|
||||||
|
/>
|
||||||
|
<RemoveItem
|
||||||
|
type='link'
|
||||||
|
id={id}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</motion.li>
|
</motion.li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LinkItemURL({ url }: { url: Link["url"] }) {
|
function LinkItemURL({ url }: { url: Link['url'] }) {
|
||||||
try {
|
try {
|
||||||
const { origin, pathname, search } = new URL(url);
|
const { origin, pathname, search } = new URL(url);
|
||||||
let text = "";
|
let text = '';
|
||||||
|
|
||||||
if (pathname !== "/") {
|
if (pathname !== '/') {
|
||||||
text += pathname;
|
text += pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (search !== "") {
|
if (search !== '') {
|
||||||
if (text === "") {
|
if (text === '') {
|
||||||
text += "/";
|
text += '/';
|
||||||
}
|
}
|
||||||
text += search;
|
text += search;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={styles["link-url"]}>
|
<span className={styles['link-url']}>
|
||||||
{origin}
|
{origin}
|
||||||
<span className={styles["url-pathname"]}>{text}</span>
|
<span className={styles['url-pathname']}>{text}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("error", error);
|
console.error('error', error);
|
||||||
return <span className={styles["link-url"]}>{url}</span>;
|
return <span className={styles['link-url']}>{url}</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import ButtonLink from "components/ButtonLink";
|
import ButtonLink from 'components/ButtonLink';
|
||||||
import CreateItem from "components/QuickActions/CreateItem";
|
import CreateItem from 'components/QuickActions/CreateItem';
|
||||||
import EditItem from "components/QuickActions/EditItem";
|
import EditItem from 'components/QuickActions/EditItem';
|
||||||
import RemoveItem from "components/QuickActions/RemoveItem";
|
import RemoveItem from 'components/QuickActions/RemoveItem';
|
||||||
import QuickActionSearch from "components/QuickActions/Search";
|
import QuickActionSearch from 'components/QuickActions/Search';
|
||||||
import { motion } from "framer-motion";
|
import { motion } from 'framer-motion';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import LinkTag from "next/link";
|
import LinkTag from 'next/link';
|
||||||
import { RxHamburgerMenu } from "react-icons/rx";
|
import { RxHamburgerMenu } from 'react-icons/rx';
|
||||||
import { Category, Link } from "types";
|
import { Category, Link } from 'types';
|
||||||
import { TFunctionParam } from "types/i18next";
|
import { TFunctionParam } from 'types/i18next';
|
||||||
import LinkItem from "./LinkItem";
|
import LinkItem from './LinkItem';
|
||||||
import styles from "./links.module.scss";
|
import styles from './links.module.scss';
|
||||||
import clsx from "clsx";
|
import clsx from 'clsx';
|
||||||
import PATHS from "constants/paths";
|
import PATHS from 'constants/paths';
|
||||||
|
|
||||||
export default function Links({
|
export default function Links({
|
||||||
category,
|
category,
|
||||||
@@ -22,51 +22,63 @@ export default function Links({
|
|||||||
openSearchModal,
|
openSearchModal,
|
||||||
}: {
|
}: {
|
||||||
category: Category;
|
category: Category;
|
||||||
toggleFavorite: (linkId: Link["id"]) => void;
|
toggleFavorite: (linkId: Link['id']) => void;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
openMobileModal: () => void;
|
openMobileModal: () => void;
|
||||||
openSearchModal: () => void;
|
openSearchModal: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation("home");
|
const { t } = useTranslation('home');
|
||||||
|
|
||||||
if (category === null) {
|
if (category === null) {
|
||||||
return (
|
return (
|
||||||
<div className={styles["no-category"]}>
|
<div className={styles['no-category']}>
|
||||||
<p>{t("home:select-categorie")}</p>
|
<p>{t('home:select-category')}</p>
|
||||||
<LinkTag href="/category/create">{t("home:or-create-one")}</LinkTag>
|
<LinkTag href='/category/create'>{t('home:or-create-one')}</LinkTag>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, name, links } = category;
|
const { id, name, links } = category;
|
||||||
return (
|
return (
|
||||||
<div className={styles["links-wrapper"]}>
|
<div className={styles['links-wrapper']}>
|
||||||
<h2 className={styles["category-header"]}>
|
<h2 className={styles['category-header']}>
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<ButtonLink
|
<ButtonLink
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
}}
|
}}
|
||||||
onClick={openMobileModal}
|
onClick={openMobileModal}
|
||||||
>
|
>
|
||||||
<RxHamburgerMenu size={"1.5em"} style={{ marginRight: ".5em" }} />
|
<RxHamburgerMenu
|
||||||
|
size={'1.5em'}
|
||||||
|
style={{ marginRight: '.5em' }}
|
||||||
|
/>
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
)}
|
)}
|
||||||
<span className={styles["category-name"]}>
|
<span className={styles['category-name']}>
|
||||||
{name}
|
{name}
|
||||||
{links.length > 0 && (
|
{links.length > 0 && (
|
||||||
<span className={styles["links-count"]}> — {links.length}</span>
|
<span className={styles['links-count']}> — {links.length}</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className={styles["category-controls"]}>
|
<span className={styles['category-controls']}>
|
||||||
<QuickActionSearch openSearchModal={openSearchModal} />
|
<QuickActionSearch openSearchModal={openSearchModal} />
|
||||||
<CreateItem type="link" categoryId={category.id} />
|
<CreateItem
|
||||||
<EditItem type="category" id={id} />
|
type='link'
|
||||||
<RemoveItem type="category" id={id} />
|
categoryId={category.id}
|
||||||
|
/>
|
||||||
|
<EditItem
|
||||||
|
type='category'
|
||||||
|
id={id}
|
||||||
|
/>
|
||||||
|
<RemoveItem
|
||||||
|
type='category'
|
||||||
|
id={id}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
{links.length !== 0 ? (
|
{links.length !== 0 ? (
|
||||||
<ul className={clsx(styles["links"], "reset")}>
|
<ul className={clsx(styles['links'], 'reset')}>
|
||||||
{links.map((link, index) => (
|
{links.map((link, index) => (
|
||||||
<LinkItem
|
<LinkItem
|
||||||
link={link}
|
link={link}
|
||||||
@@ -77,45 +89,54 @@ export default function Links({
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles["no-link"]}>
|
<div className={styles['no-link']}>
|
||||||
<motion.p
|
<motion.p
|
||||||
key={Math.random()}
|
key={Math.random()}
|
||||||
initial={{ opacity: 0, scale: 0.85 }}
|
initial={{ opacity: 0, scale: 0.85 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{
|
transition={{
|
||||||
type: "spring",
|
type: 'spring',
|
||||||
stiffness: 260,
|
stiffness: 260,
|
||||||
damping: 20,
|
damping: 20,
|
||||||
duration: 0.01,
|
duration: 0.01,
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: t("home:no-link", { name } as TFunctionParam, {
|
__html: t('home:no-link', { name } as TFunctionParam, {
|
||||||
interpolation: { escapeValue: false },
|
interpolation: { escapeValue: false },
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<LinkTag href={`/link/create?categoryId=${id}`}>
|
<LinkTag href={`/link/create?categoryId=${id}`}>
|
||||||
{t("common:link.create")}
|
{t('common:link.create')}
|
||||||
</LinkTag>
|
</LinkTag>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<footer className={styles["footer"]}>
|
<footer className={styles['footer']}>
|
||||||
<div className="top">
|
<div className='top'>
|
||||||
<LinkTag href={PATHS.PRIVACY}>{t("common:privacy")}</LinkTag>
|
<LinkTag href={PATHS.PRIVACY}>{t('common:privacy')}</LinkTag>
|
||||||
{" • "}
|
{' • '}
|
||||||
<LinkTag href={PATHS.TERMS}>{t("common:terms")}</LinkTag>
|
<LinkTag href={PATHS.TERMS}>{t('common:terms')}</LinkTag>
|
||||||
</div>
|
</div>
|
||||||
<div className="bottom">
|
<div className='bottom'>
|
||||||
{t("home:footer.made_by")}{" "}
|
{t('home:footer.made_by')}{' '}
|
||||||
<LinkTag href={PATHS.AUTHOR} target="_blank">
|
<LinkTag
|
||||||
|
href={PATHS.AUTHOR}
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
Sonny
|
Sonny
|
||||||
</LinkTag>
|
</LinkTag>
|
||||||
{" • "}
|
{' • '}
|
||||||
<LinkTag href={PATHS.REPO_GITHUB} target="_blank">
|
<LinkTag
|
||||||
|
href={PATHS.REPO_GITHUB}
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
Github
|
Github
|
||||||
</LinkTag>
|
</LinkTag>
|
||||||
{" • "}
|
{' • '}
|
||||||
<LinkTag href={PATHS.EXTENSION} target="_blank">
|
<LinkTag
|
||||||
|
href={PATHS.EXTENSION}
|
||||||
|
target='_blank'
|
||||||
|
>
|
||||||
Extension
|
Extension
|
||||||
</LinkTag>
|
</LinkTag>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@import "styles/keyframes.scss";
|
@import 'styles/keyframes.scss';
|
||||||
@import "styles/colors.scss";
|
@import 'styles/colors.scss';
|
||||||
|
|
||||||
.no-link,
|
.no-link,
|
||||||
.no-category {
|
.no-category {
|
||||||
@@ -22,10 +22,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.links-wrapper {
|
.links-wrapper {
|
||||||
height: calc(100%); // FIXME: eurk
|
height: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0.5em;
|
padding: 0.5em 0.5em 0;
|
||||||
padding-bottom: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useRouter } from "next/dist/client/router";
|
import { useRouter } from 'next/dist/client/router';
|
||||||
|
|
||||||
import styles from "./message-manager.module.scss";
|
import styles from './message-manager.module.scss';
|
||||||
|
|
||||||
interface MessageManagerProps {
|
interface MessageManagerProps {
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -18,14 +18,14 @@ export default function MessageManager({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{info && <div className={styles["info-msg"]}>{info}</div>}
|
{info && <div className={styles['info-msg']}>{info}</div>}
|
||||||
{infoUrl && <div className={styles["info-msg"]}>{infoUrl}</div>}
|
{infoUrl && <div className={styles['info-msg']}>{infoUrl}</div>}
|
||||||
|
|
||||||
{error && <div className={styles["error-msg"]}>{error}</div>}
|
{error && <div className={styles['error-msg']}>{error}</div>}
|
||||||
{errorUrl && <div className={styles["error-msg"]}>{errorUrl}</div>}
|
{errorUrl && <div className={styles['error-msg']}>{errorUrl}</div>}
|
||||||
|
|
||||||
{success && <div className={styles["success-msg"]}>{success}</div>}
|
{success && <div className={styles['success-msg']}>{success}</div>}
|
||||||
{successUrl && <div className={styles["success-msg"]}>{successUrl}</div>}
|
{successUrl && <div className={styles['success-msg']}>{successUrl}</div>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "styles/colors.scss";
|
@import 'styles/colors.scss';
|
||||||
|
|
||||||
.info-msg {
|
.info-msg {
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from 'react';
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from 'react-dom';
|
||||||
import { GrClose } from "react-icons/gr";
|
import { GrClose } from 'react-icons/gr';
|
||||||
import { motion } from "framer-motion";
|
import { motion } from 'framer-motion';
|
||||||
import styles from "./modal.module.scss";
|
import styles from './modal.module.scss';
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
close?: (...args: any) => void | Promise<void>;
|
close?: (...args: any) => void | Promise<void>;
|
||||||
@@ -21,41 +21,41 @@ export default function Modal({
|
|||||||
children,
|
children,
|
||||||
showCloseBtn = true,
|
showCloseBtn = true,
|
||||||
noHeader = false,
|
noHeader = false,
|
||||||
padding = "1em 1.5em",
|
padding = '1em 1.5em',
|
||||||
}: ModalProps) {
|
}: ModalProps) {
|
||||||
const handleWrapperClick = (event) =>
|
const handleWrapperClick = (event) =>
|
||||||
event.target.classList?.[0] === styles["modal-wrapper"] && close && close();
|
event.target.classList?.[0] === styles['modal-wrapper'] && close && close();
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<motion.div
|
<motion.div
|
||||||
className={styles["modal-wrapper"]}
|
className={styles['modal-wrapper']}
|
||||||
onClick={handleWrapperClick}
|
onClick={handleWrapperClick}
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0, transition: { duration: 0.1, delay: 0.1 } }}
|
exit={{ opacity: 0, transition: { duration: 0.1, delay: 0.1 } }}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
className={styles["modal-container"]}
|
className={styles['modal-container']}
|
||||||
style={{ padding }}
|
style={{ padding }}
|
||||||
initial={{ opacity: 0, y: "-6em" }}
|
initial={{ opacity: 0, y: '-6em' }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: "-6em", transition: { duration: 0.1 } }}
|
exit={{ opacity: 0, y: '-6em', transition: { duration: 0.1 } }}
|
||||||
transition={{ type: "tween" }}
|
transition={{ type: 'tween' }}
|
||||||
>
|
>
|
||||||
{!noHeader && (
|
{!noHeader && (
|
||||||
<div className={styles["modal-header"]}>
|
<div className={styles['modal-header']}>
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
{showCloseBtn && (
|
{showCloseBtn && (
|
||||||
<button
|
<button
|
||||||
onClick={close}
|
onClick={close}
|
||||||
className={`${styles["btn-close"]} reset`}
|
className={`${styles['btn-close']} reset`}
|
||||||
>
|
>
|
||||||
<GrClose />
|
<GrClose />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles["modal-body"]}>{children}</div>
|
<div className={styles['modal-body']}>{children}</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>,
|
</motion.div>,
|
||||||
document.body,
|
document.body,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "styles/colors.scss";
|
@import 'styles/colors.scss';
|
||||||
|
|
||||||
.modal-wrapper {
|
.modal-wrapper {
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-shadow: 0 0 1em 0px rgba($black, 0.25);
|
box-shadow: 0 0 1em 0 rgba($black, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
import LinkTag from "next/link";
|
import LinkTag from 'next/link';
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from 'next-auth/react';
|
||||||
import PATHS from "constants/paths";
|
import PATHS from 'constants/paths';
|
||||||
import styles from "./navbar.module.scss";
|
import styles from './navbar.module.scss';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import Image from "next/image";
|
import Image from 'next/image';
|
||||||
import { TFunctionParam } from "types/i18next";
|
import { TFunctionParam } from 'types/i18next';
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const { data, status } = useSession();
|
const { data, status } = useSession();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const avatarLabel = t("common:avatar", {
|
const avatarLabel = t('common:avatar', {
|
||||||
name: data?.user?.name,
|
name: data?.user?.name,
|
||||||
} as TFunctionParam);
|
} as TFunctionParam);
|
||||||
return (
|
return (
|
||||||
<nav className={styles["navbar"]}>
|
<nav className={styles['navbar']}>
|
||||||
<ul className="reset">
|
<ul className='reset'>
|
||||||
<li>
|
<li>
|
||||||
<LinkTag href={PATHS.HOME}>MyLinks</LinkTag>
|
<LinkTag href={PATHS.HOME}>MyLinks</LinkTag>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<LinkTag href={PATHS.PRIVACY}>{t("common:privacy")}</LinkTag>
|
<LinkTag href={PATHS.PRIVACY}>{t('common:privacy')}</LinkTag>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<LinkTag href={PATHS.TERMS}>{t("common:terms")}</LinkTag>
|
<LinkTag href={PATHS.TERMS}>{t('common:terms')}</LinkTag>
|
||||||
</li>
|
</li>
|
||||||
{status === "authenticated" ? (
|
{status === 'authenticated' ? (
|
||||||
<>
|
<>
|
||||||
<li className={styles["user"]}>
|
<li className={styles['user']}>
|
||||||
<Image
|
<Image
|
||||||
src={data.user.image}
|
src={data.user.image}
|
||||||
width={24}
|
width={24}
|
||||||
@@ -38,12 +38,12 @@ export default function Navbar() {
|
|||||||
{data.user.name}
|
{data.user.name}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<LinkTag href={PATHS.LOGOUT}>{t("common:logout")}</LinkTag>
|
<LinkTag href={PATHS.LOGOUT}>{t('common:logout')}</LinkTag>
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<li>
|
<li>
|
||||||
<LinkTag href={PATHS.LOGIN}>{t("common:login")}</LinkTag>
|
<LinkTag href={PATHS.LOGIN}>{t('common:login')}</LinkTag>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import LinkTag from "next/link";
|
import LinkTag from 'next/link';
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from 'next-auth/react';
|
||||||
import PATHS from "constants/paths";
|
import PATHS from 'constants/paths';
|
||||||
import styles from "./navbar.module.scss";
|
import styles from './navbar.module.scss';
|
||||||
|
|
||||||
export default function NavbarUntranslated() {
|
export default function NavbarUntranslated() {
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={styles["navbar"]}>
|
<nav className={styles['navbar']}>
|
||||||
<ul className="reset">
|
<ul className='reset'>
|
||||||
<li>
|
<li>
|
||||||
<LinkTag href={PATHS.HOME}>MyLinks</LinkTag>
|
<LinkTag href={PATHS.HOME}>MyLinks</LinkTag>
|
||||||
</li>
|
</li>
|
||||||
@@ -18,7 +18,7 @@ export default function NavbarUntranslated() {
|
|||||||
<li>
|
<li>
|
||||||
<LinkTag href={PATHS.TERMS}>Terms of use</LinkTag>
|
<LinkTag href={PATHS.TERMS}>Terms of use</LinkTag>
|
||||||
</li>
|
</li>
|
||||||
{status === "authenticated" ? (
|
{status === 'authenticated' ? (
|
||||||
<li>
|
<li>
|
||||||
<LinkTag href={PATHS.LOGOUT}>Logout</LinkTag>
|
<LinkTag href={PATHS.LOGOUT}>Logout</LinkTag>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from 'framer-motion';
|
||||||
import useIsMobile from "hooks/useIsMobile";
|
import useIsMobile from 'hooks/useIsMobile';
|
||||||
import { CSSProperties, ReactNode } from "react";
|
import { CSSProperties, ReactNode } from 'react';
|
||||||
import LangSelector from "./LangSelector";
|
import LangSelector from './LangSelector';
|
||||||
|
|
||||||
export default function PageTransition({
|
export default function PageTransition({
|
||||||
className,
|
className,
|
||||||
@@ -21,12 +21,12 @@ export default function PageTransition({
|
|||||||
className={className}
|
className={className}
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ type: "tween" }}
|
transition={{ type: 'tween' }}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{!hideLangageSelector && !isMobile && (
|
{!hideLangageSelector && !isMobile && (
|
||||||
<div className="lang-selector">
|
<div className='lang-selector'>
|
||||||
<LangSelector />
|
<LangSelector />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import LinkTag from "next/link";
|
import LinkTag from 'next/link';
|
||||||
import { IoAddOutline } from "react-icons/io5";
|
import { IoAddOutline } from 'react-icons/io5';
|
||||||
import { Category } from "types";
|
import { Category } from 'types';
|
||||||
import styles from "./quickactions.module.scss";
|
import styles from './quickactions.module.scss';
|
||||||
|
|
||||||
export default function CreateItem({
|
export default function CreateItem({
|
||||||
type,
|
type,
|
||||||
categoryId,
|
categoryId,
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
type: "category" | "link";
|
type: 'category' | 'link';
|
||||||
categoryId?: Category["id"];
|
categoryId?: Category['id'];
|
||||||
onClick?: (event: any) => void;
|
onClick?: (event: any) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation("home");
|
const { t } = useTranslation('home');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinkTag
|
<LinkTag
|
||||||
href={`/${type}/create${categoryId && `?categoryId=${categoryId}`}`}
|
href={`/${type}/create${categoryId && `?categoryId=${categoryId}`}`}
|
||||||
title={t(`common:${type}.create`)}
|
title={t(`common:${type}.create`)}
|
||||||
className={styles["action"]}
|
className={styles['action']}
|
||||||
onClick={onClick && onClick}
|
onClick={onClick && onClick}
|
||||||
>
|
>
|
||||||
<IoAddOutline />
|
<IoAddOutline />
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import LinkTag from "next/link";
|
import LinkTag from 'next/link';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import { AiOutlineEdit } from "react-icons/ai";
|
import { AiOutlineEdit } from 'react-icons/ai';
|
||||||
import { Category, Link } from "types";
|
import { Category, Link } from 'types';
|
||||||
import styles from "./quickactions.module.scss";
|
import styles from './quickactions.module.scss';
|
||||||
|
|
||||||
export default function EditItem({
|
export default function EditItem({
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
onClick,
|
onClick,
|
||||||
className = "",
|
className = '',
|
||||||
}: {
|
}: {
|
||||||
type: "category" | "link";
|
type: 'category' | 'link';
|
||||||
id: Link["id"] | Category["id"];
|
id: Link['id'] | Category['id'];
|
||||||
onClick?: (event: any) => void;
|
onClick?: (event: any) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation("home");
|
const { t } = useTranslation('home');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinkTag
|
<LinkTag
|
||||||
href={`/${type}/edit/${id}`}
|
href={`/${type}/edit/${id}`}
|
||||||
title={t(`common:${type}.edit`)}
|
title={t(`common:${type}.edit`)}
|
||||||
className={`${styles["action"]} ${className ? className : ""}`}
|
className={`${styles['action']} ${className ? className : ''}`}
|
||||||
onClick={onClick && onClick}
|
onClick={onClick && onClick}
|
||||||
>
|
>
|
||||||
<AiOutlineEdit />
|
<AiOutlineEdit />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from 'react';
|
||||||
import { AiFillStar, AiOutlineStar } from "react-icons/ai";
|
import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
|
||||||
|
|
||||||
export default function FavoriteItem({
|
export default function FavoriteItem({
|
||||||
isFavorite,
|
isFavorite,
|
||||||
@@ -10,7 +10,7 @@ export default function FavoriteItem({
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}) {
|
}) {
|
||||||
const starColor = "#ffc107";
|
const starColor = '#ffc107';
|
||||||
return (
|
return (
|
||||||
<div onClick={onClick}>
|
<div onClick={onClick}>
|
||||||
{isFavorite ? (
|
{isFavorite ? (
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import LinkTag from "next/link";
|
import LinkTag from 'next/link';
|
||||||
import { CgTrashEmpty } from "react-icons/cg";
|
import { CgTrashEmpty } from 'react-icons/cg';
|
||||||
import { Category, Link } from "types";
|
import { Category, Link } from 'types';
|
||||||
import styles from "./quickactions.module.scss";
|
import styles from './quickactions.module.scss';
|
||||||
|
|
||||||
export default function RemoveItem({
|
export default function RemoveItem({
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
type: "category" | "link";
|
type: 'category' | 'link';
|
||||||
id: Link["id"] | Category["id"];
|
id: Link['id'] | Category['id'];
|
||||||
onClick?: (event: any) => void;
|
onClick?: (event: any) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation("home");
|
const { t } = useTranslation('home');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinkTag
|
<LinkTag
|
||||||
href={`/${type}/remove/${id}`}
|
href={`/${type}/remove/${id}`}
|
||||||
title={t(`common:${type}.remove`)}
|
title={t(`common:${type}.remove`)}
|
||||||
className={styles["action"]}
|
className={styles['action']}
|
||||||
onClick={onClick && onClick}
|
onClick={onClick && onClick}
|
||||||
>
|
>
|
||||||
<CgTrashEmpty color="red" />
|
<CgTrashEmpty color='red' />
|
||||||
</LinkTag>
|
</LinkTag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import ButtonLink from "components/ButtonLink";
|
import ButtonLink from 'components/ButtonLink';
|
||||||
import { BiSearchAlt } from "react-icons/bi";
|
import { BiSearchAlt } from 'react-icons/bi';
|
||||||
import styles from "./quickactions.module.scss";
|
import styles from './quickactions.module.scss';
|
||||||
|
|
||||||
export default function QuickActionSearch({
|
export default function QuickActionSearch({
|
||||||
openSearchModal,
|
openSearchModal,
|
||||||
@@ -8,7 +8,10 @@ export default function QuickActionSearch({
|
|||||||
openSearchModal: () => void;
|
openSearchModal: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ButtonLink className={styles["action"]} onClick={openSearchModal}>
|
<ButtonLink
|
||||||
|
className={styles['action']}
|
||||||
|
onClick={openSearchModal}
|
||||||
|
>
|
||||||
<BiSearchAlt />
|
<BiSearchAlt />
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { FcGoogle } from "react-icons/fc";
|
import { FcGoogle } from 'react-icons/fc';
|
||||||
|
|
||||||
import styles from "./search.module.scss";
|
import styles from './search.module.scss';
|
||||||
|
|
||||||
export default function LabelSearchWithGoogle() {
|
export default function LabelSearchWithGoogle() {
|
||||||
return (
|
return (
|
||||||
<i className={styles["search-with-google"]}>
|
<i className={styles['search-with-google']}>
|
||||||
Recherche avec{" "}
|
Recherche avec{' '}
|
||||||
<span>
|
<span>
|
||||||
<FcGoogle size={24} />
|
<FcGoogle size={24} />
|
||||||
oogle
|
oogle
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import * as Keys from "constants/keys";
|
import * as Keys from 'constants/keys';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ReactNode, useEffect, useMemo } from "react";
|
import { ReactNode, useEffect, useMemo } from 'react';
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { SearchItem } from "types";
|
import { SearchItem } from 'types';
|
||||||
import { groupItemBy } from "utils/array";
|
import { groupItemBy } from 'utils/array';
|
||||||
import SearchListItem from "./SearchListItem";
|
import SearchListItem from './SearchListItem';
|
||||||
import styles from "./search.module.scss";
|
import styles from './search.module.scss';
|
||||||
import clsx from "clsx";
|
import clsx from 'clsx';
|
||||||
|
|
||||||
const isActiveItem = (item: SearchItem, otherItem: SearchItem) =>
|
const isActiveItem = (item: SearchItem, otherItem: SearchItem) =>
|
||||||
item?.id === otherItem?.id && item?.type === otherItem?.type;
|
item?.id === otherItem?.id && item?.type === otherItem?.type;
|
||||||
@@ -24,7 +24,7 @@ export default function SearchList({
|
|||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
}) {
|
}) {
|
||||||
const searchItemsGrouped = useMemo(
|
const searchItemsGrouped = useMemo(
|
||||||
() => groupItemBy(items, "category.name"),
|
() => groupItemBy(items, 'category.name'),
|
||||||
[items],
|
[items],
|
||||||
);
|
);
|
||||||
const groupedItems = useMemo<any>(
|
const groupedItems = useMemo<any>(
|
||||||
@@ -41,7 +41,7 @@ export default function SearchList({
|
|||||||
Keys.ARROW_UP,
|
Keys.ARROW_UP,
|
||||||
() => setSelectedItem(items[selectedItemIndex - 1]),
|
() => setSelectedItem(items[selectedItemIndex - 1]),
|
||||||
{
|
{
|
||||||
enableOnFormTags: ["INPUT"],
|
enableOnFormTags: ['INPUT'],
|
||||||
enabled: items.length > 1 && selectedItemIndex !== 0,
|
enabled: items.length > 1 && selectedItemIndex !== 0,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
@@ -50,7 +50,7 @@ export default function SearchList({
|
|||||||
Keys.ARROW_DOWN,
|
Keys.ARROW_DOWN,
|
||||||
() => setSelectedItem(items[selectedItemIndex + 1]),
|
() => setSelectedItem(items[selectedItemIndex + 1]),
|
||||||
{
|
{
|
||||||
enableOnFormTags: ["INPUT"],
|
enableOnFormTags: ['INPUT'],
|
||||||
enabled: items.length > 1 && selectedItemIndex !== items.length - 1,
|
enabled: items.length > 1 && selectedItemIndex !== items.length - 1,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
@@ -61,12 +61,12 @@ export default function SearchList({
|
|||||||
}, [items, setSelectedItem]);
|
}, [items, setSelectedItem]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className={clsx(styles["search-list"], "reset")}>
|
<ul className={clsx(styles['search-list'], 'reset')}>
|
||||||
{groupedItems.length > 0 ? (
|
{groupedItems.length > 0 ? (
|
||||||
groupedItems.map(([key, items]) => (
|
groupedItems.map(([key, items]) => (
|
||||||
<li key={`${key}-${key}`}>
|
<li key={`${key}-${key}`}>
|
||||||
<span>{typeof key === "undefined" ? "-" : key}</span>
|
<span>{typeof key === 'undefined' ? '-' : key}</span>
|
||||||
<ul className="reset">
|
<ul className='reset'>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<SearchListItem
|
<SearchListItem
|
||||||
item={item}
|
item={item}
|
||||||
@@ -88,6 +88,6 @@ export default function SearchList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function LabelNoItem() {
|
function LabelNoItem() {
|
||||||
const { t } = useTranslation("home");
|
const { t } = useTranslation('home');
|
||||||
return <i className={styles["no-item"]}>{t("common:no-item-found")}</i>;
|
return <i className={styles['no-item']}>{t('common:no-item-found')}</i>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import LinkTag from "next/link";
|
import LinkTag from 'next/link';
|
||||||
import { AiOutlineFolder } from "react-icons/ai";
|
import { AiOutlineFolder } from 'react-icons/ai';
|
||||||
|
|
||||||
import LinkFavicon from "components/Links/LinkFavicon";
|
import LinkFavicon from 'components/Links/LinkFavicon';
|
||||||
import { SearchItem } from "types";
|
import { SearchItem } from 'types';
|
||||||
|
|
||||||
import { useEffect, useId, useRef } from "react";
|
import { useEffect, useId, useRef } from 'react';
|
||||||
import styles from "./search.module.scss";
|
import styles from './search.module.scss';
|
||||||
|
|
||||||
export default function SearchListItem({
|
export default function SearchListItem({
|
||||||
item,
|
item,
|
||||||
@@ -23,14 +23,14 @@ export default function SearchListItem({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
ref.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
}
|
}
|
||||||
}, [selected]);
|
}, [selected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className={
|
className={
|
||||||
styles["search-item"] + (selected ? ` ${styles["selected"]}` : "")
|
styles['search-item'] + (selected ? ` ${styles['selected']}` : '')
|
||||||
}
|
}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
key={id}
|
key={id}
|
||||||
@@ -38,12 +38,16 @@ export default function SearchListItem({
|
|||||||
>
|
>
|
||||||
<LinkTag
|
<LinkTag
|
||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="no-referrer"
|
rel='no-referrer'
|
||||||
onClick={closeModal}
|
onClick={closeModal}
|
||||||
>
|
>
|
||||||
{type === "link" ? (
|
{type === 'link' ? (
|
||||||
<LinkFavicon url={item.url} noMargin size={24} />
|
<LinkFavicon
|
||||||
|
url={item.url}
|
||||||
|
noMargin
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<AiOutlineFolder size={24} />
|
<AiOutlineFolder size={24} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import Modal from "components/Modal/Modal";
|
import Modal from 'components/Modal/Modal';
|
||||||
import TextBox from "components/TextBox";
|
import TextBox from 'components/TextBox';
|
||||||
import { GOOGLE_SEARCH_URL } from "constants/search-urls";
|
import { GOOGLE_SEARCH_URL } from 'constants/search-urls';
|
||||||
import useAutoFocus from "hooks/useAutoFocus";
|
import useAutoFocus from 'hooks/useAutoFocus';
|
||||||
import { useLocalStorage } from "hooks/useLocalStorage";
|
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import { FormEvent, useCallback, useMemo, useState } from "react";
|
import { FormEvent, useCallback, useMemo, useState } from 'react';
|
||||||
import { BsSearch } from "react-icons/bs";
|
import { BsSearch } from 'react-icons/bs';
|
||||||
import { Category, SearchItem } from "types";
|
import { Category, SearchItem } from 'types';
|
||||||
import LabelSearchWithGoogle from "./LabelSearchWithGoogle";
|
import LabelSearchWithGoogle from './LabelSearchWithGoogle';
|
||||||
import SearchList from "./SearchList";
|
import SearchList from './SearchList';
|
||||||
import styles from "./search.module.scss";
|
import styles from './search.module.scss';
|
||||||
|
|
||||||
export default function SearchModal({
|
export default function SearchModal({
|
||||||
close,
|
close,
|
||||||
@@ -28,15 +28,15 @@ export default function SearchModal({
|
|||||||
const autoFocusRef = useAutoFocus();
|
const autoFocusRef = useAutoFocus();
|
||||||
|
|
||||||
const [canSearchLink, setCanSearchLink] = useLocalStorage(
|
const [canSearchLink, setCanSearchLink] = useLocalStorage(
|
||||||
"search-link",
|
'search-link',
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
const [canSearchCategory, setCanSearchCategory] = useLocalStorage(
|
const [canSearchCategory, setCanSearchCategory] = useLocalStorage(
|
||||||
"search-category",
|
'search-category',
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [search, setSearch] = useState<string>("");
|
const [search, setSearch] = useState<string>('');
|
||||||
const [selectedItem, setSelectedItem] = useState<SearchItem>(items[0]);
|
const [selectedItem, setSelectedItem] = useState<SearchItem>(items[0]);
|
||||||
|
|
||||||
const canSubmit = useMemo<boolean>(() => search.length > 0, [search]);
|
const canSubmit = useMemo<boolean>(() => search.length > 0, [search]);
|
||||||
@@ -48,8 +48,8 @@ export default function SearchModal({
|
|||||||
? []
|
? []
|
||||||
: items.filter(
|
: items.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
((item.type === "category" && canSearchCategory) ||
|
((item.type === 'category' && canSearchCategory) ||
|
||||||
(item.type === "link" && canSearchLink)) &&
|
(item.type === 'link' && canSearchLink)) &&
|
||||||
item.name
|
item.name
|
||||||
.toLocaleLowerCase()
|
.toLocaleLowerCase()
|
||||||
.includes(search.toLocaleLowerCase().trim()),
|
.includes(search.toLocaleLowerCase().trim()),
|
||||||
@@ -58,7 +58,7 @@ export default function SearchModal({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const resetForm = useCallback(() => {
|
const resetForm = useCallback(() => {
|
||||||
setSearch("");
|
setSearch('');
|
||||||
close();
|
close();
|
||||||
}, [close]);
|
}, [close]);
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ export default function SearchModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const category = categories.find((c) => c.id === selectedItem.id);
|
const category = categories.find((c) => c.id === selectedItem.id);
|
||||||
if (selectedItem.type === "category" && category) {
|
if (selectedItem.type === 'category' && category) {
|
||||||
return handleSelectCategory(category);
|
return handleSelectCategory(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,20 +98,27 @@ export default function SearchModal({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal close={close} noHeader={noHeader} padding={"0"}>
|
<Modal
|
||||||
<form onSubmit={handleSubmit} className={styles["search-form"]}>
|
close={close}
|
||||||
<div className={styles["search-input-wrapper"]}>
|
noHeader={noHeader}
|
||||||
<label htmlFor="search">
|
padding={'0'}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className={styles['search-form']}
|
||||||
|
>
|
||||||
|
<div className={styles['search-input-wrapper']}>
|
||||||
|
<label htmlFor='search'>
|
||||||
<BsSearch size={24} />
|
<BsSearch size={24} />
|
||||||
</label>
|
</label>
|
||||||
<TextBox
|
<TextBox
|
||||||
name="search"
|
name='search'
|
||||||
onChangeCallback={handleSearchInputChange}
|
onChangeCallback={handleSearchInputChange}
|
||||||
value={search}
|
value={search}
|
||||||
placeholder={t("common:search")}
|
placeholder={t('common:search')}
|
||||||
innerRef={autoFocusRef}
|
innerRef={autoFocusRef}
|
||||||
fieldClass={styles["search-input-field"]}
|
fieldClass={styles['search-input-field']}
|
||||||
inputClass={"reset"}
|
inputClass={'reset'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SearchFilter
|
<SearchFilter
|
||||||
@@ -129,8 +136,12 @@ export default function SearchModal({
|
|||||||
closeModal={close}
|
closeModal={close}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<button type="submit" disabled={!canSubmit} style={{ display: "none" }}>
|
<button
|
||||||
{t("common:confirm")}
|
type='submit'
|
||||||
|
disabled={!canSubmit}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
>
|
||||||
|
{t('common:confirm')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -153,33 +164,33 @@ function SearchFilter({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
gap: "1em",
|
gap: '1em',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
marginBottom: "1em",
|
marginBottom: '1em',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", gap: ".25em" }}>
|
<div style={{ display: 'flex', gap: '.25em' }}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type='checkbox'
|
||||||
name="filter-link"
|
name='filter-link'
|
||||||
id="filter-link"
|
id='filter-link'
|
||||||
onChange={({ target }) => setCanSearchLink(target.checked)}
|
onChange={({ target }) => setCanSearchLink(target.checked)}
|
||||||
checked={canSearchLink}
|
checked={canSearchLink}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="filter-link">{t("common:link.links")}</label>
|
<label htmlFor='filter-link'>{t('common:link.links')}</label>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: ".25em" }}>
|
<div style={{ display: 'flex', gap: '.25em' }}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type='checkbox'
|
||||||
name="filter-category"
|
name='filter-category'
|
||||||
id="filter-category"
|
id='filter-category'
|
||||||
onChange={({ target }) => setCanSearchCategory(target.checked)}
|
onChange={({ target }) => setCanSearchCategory(target.checked)}
|
||||||
checked={canSearchCategory}
|
checked={canSearchCategory}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="filter-category">
|
<label htmlFor='filter-category'>
|
||||||
{t("common:category.categories")}
|
{t('common:category.categories')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "styles/colors.scss";
|
@import 'styles/colors.scss';
|
||||||
|
|
||||||
form.search-form {
|
form.search-form {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -38,7 +38,7 @@ form.search-form {
|
|||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: "center";
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { MutableRefObject, useEffect, useState } from "react";
|
import { MutableRefObject, useEffect, useState } from 'react';
|
||||||
import Select, { OptionsOrGroups, GroupBase } from "react-select";
|
import Select, { OptionsOrGroups, GroupBase } from 'react-select';
|
||||||
|
|
||||||
type Option = { label: string | number; value: string | number };
|
type Option = { label: string | number; value: string | number };
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export default function Selector({
|
|||||||
label,
|
label,
|
||||||
labelComponent,
|
labelComponent,
|
||||||
innerRef = null,
|
innerRef = null,
|
||||||
fieldClass = "",
|
fieldClass = '',
|
||||||
value,
|
value,
|
||||||
options = [],
|
options = [],
|
||||||
onChangeCallback,
|
onChangeCallback,
|
||||||
@@ -49,12 +49,18 @@ export default function Selector({
|
|||||||
return (
|
return (
|
||||||
<div className={`input-field ${fieldClass}`}>
|
<div className={`input-field ${fieldClass}`}>
|
||||||
{label && (
|
{label && (
|
||||||
<label htmlFor={name} title={`${name} field`}>
|
<label
|
||||||
|
htmlFor={name}
|
||||||
|
title={`${name} field`}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{labelComponent && (
|
{labelComponent && (
|
||||||
<label htmlFor={name} title={`${name} field`}>
|
<label
|
||||||
|
htmlFor={name}
|
||||||
|
title={`${name} field`}
|
||||||
|
>
|
||||||
{labelComponent}
|
{labelComponent}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,26 +1,33 @@
|
|||||||
import { AnimatePresence } from "framer-motion";
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import useModal from "hooks/useModal";
|
import useModal from 'hooks/useModal';
|
||||||
import Modal from "../Modal/Modal";
|
import Modal from '../Modal/Modal';
|
||||||
import * as Keys from "constants/keys";
|
import * as Keys from 'constants/keys';
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { IoSettingsOutline } from "react-icons/io5";
|
import { IoSettingsOutline } from 'react-icons/io5';
|
||||||
import LangSelector from "../LangSelector";
|
import LangSelector from '../LangSelector';
|
||||||
|
|
||||||
export default function SettingsModal() {
|
export default function SettingsModal() {
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
useHotkeys(Keys.CLOSE_SEARCH_KEY, modal.close, {
|
useHotkeys(Keys.CLOSE_SEARCH_KEY, modal.close, {
|
||||||
enabled: modal.isShowing,
|
enabled: modal.isShowing,
|
||||||
enableOnFormTags: ["INPUT"],
|
enableOnFormTags: ['INPUT'],
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button onClick={modal.open} className="reset" title="Settings">
|
<button
|
||||||
|
onClick={modal.open}
|
||||||
|
className='reset'
|
||||||
|
title='Settings'
|
||||||
|
>
|
||||||
<IoSettingsOutline size={24} />
|
<IoSettingsOutline size={24} />
|
||||||
</button>
|
</button>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{modal.isShowing && (
|
{modal.isShowing && (
|
||||||
<Modal title="Settings" close={modal.close}>
|
<Modal
|
||||||
|
title='Settings'
|
||||||
|
close={modal.close}
|
||||||
|
>
|
||||||
<LangSelector />
|
<LangSelector />
|
||||||
<p>about tab with all links related to MyLinks</p>
|
<p>about tab with all links related to MyLinks</p>
|
||||||
<button>disconnect</button>
|
<button>disconnect</button>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useMemo } from "react";
|
import { useMemo } from 'react';
|
||||||
import { Category } from "types";
|
import { Category } from 'types';
|
||||||
import CategoryItem from "./CategoryItem";
|
import CategoryItem from './CategoryItem';
|
||||||
import styles from "./categories.module.scss";
|
import styles from './categories.module.scss';
|
||||||
import clsx from "clsx";
|
import clsx from 'clsx';
|
||||||
|
|
||||||
interface CategoriesProps {
|
interface CategoriesProps {
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
@@ -23,11 +23,11 @@ export default function Categories({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["categories"]}>
|
<div className={styles['categories']}>
|
||||||
<h4>
|
<h4>
|
||||||
{t("common:category.categories")} • {linksCount}
|
{t('common:category.categories')} • {linksCount}
|
||||||
</h4>
|
</h4>
|
||||||
<ul className={clsx(styles["items"], "reset")}>
|
<ul className={clsx(styles['items'], 'reset')}>
|
||||||
{categories.map((category, index) => (
|
{categories.map((category, index) => (
|
||||||
<CategoryItem
|
<CategoryItem
|
||||||
category={category}
|
category={category}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from 'framer-motion';
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from 'react';
|
||||||
import { AiFillFolderOpen, AiOutlineFolder } from "react-icons/ai";
|
import { AiFillFolderOpen, AiOutlineFolder } from 'react-icons/ai';
|
||||||
import { Category } from "types";
|
import { Category } from 'types';
|
||||||
import styles from "./categories.module.scss";
|
import styles from './categories.module.scss';
|
||||||
|
|
||||||
interface CategoryItemProps {
|
interface CategoryItemProps {
|
||||||
category: Category;
|
category: Category;
|
||||||
@@ -18,14 +18,14 @@ export default function CategoryItem({
|
|||||||
index,
|
index,
|
||||||
}: CategoryItemProps): JSX.Element {
|
}: CategoryItemProps): JSX.Element {
|
||||||
const ref = useRef<HTMLLIElement>();
|
const ref = useRef<HTMLLIElement>();
|
||||||
const className = `${styles["item"]} ${
|
const className = `${styles['item']} ${
|
||||||
category.id === categoryActive.id ? styles["active"] : ""
|
category.id === categoryActive.id ? styles['active'] : ''
|
||||||
}`;
|
}`;
|
||||||
const onClick = () => handleSelectCategory(category);
|
const onClick = () => handleSelectCategory(category);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (category.id === categoryActive.id) {
|
if (category.id === categoryActive.id) {
|
||||||
ref.current.scrollIntoView({ behavior: "smooth", block: "center" });
|
ref.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
}
|
}
|
||||||
}, [category.id, categoryActive.id]);
|
}, [category.id, categoryActive.id]);
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export default function CategoryItem({
|
|||||||
initial={{ opacity: 0, scale: 0 }}
|
initial={{ opacity: 0, scale: 0 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{
|
transition={{
|
||||||
type: "spring",
|
type: 'spring',
|
||||||
stiffness: 260,
|
stiffness: 260,
|
||||||
damping: 25,
|
damping: 25,
|
||||||
delay: index * 0.02,
|
delay: index * 0.02,
|
||||||
@@ -44,10 +44,10 @@ export default function CategoryItem({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
gap: ".25em",
|
gap: '.25em',
|
||||||
transition: "none",
|
transition: 'none',
|
||||||
}}
|
}}
|
||||||
title={category.name}
|
title={category.name}
|
||||||
>
|
>
|
||||||
@@ -57,9 +57,9 @@ export default function CategoryItem({
|
|||||||
<AiOutlineFolder size={24} />
|
<AiOutlineFolder size={24} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles["content"]}>
|
<div className={styles['content']}>
|
||||||
<span className={styles["name"]}>{category.name}</span>
|
<span className={styles['name']}>{category.name}</span>
|
||||||
<span className={styles["links-count"]}>— {category.links.length}</span>
|
<span className={styles['links-count']}>— {category.links.length}</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.li>
|
</motion.li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@import "styles/keyframes.scss";
|
@import 'styles/keyframes.scss';
|
||||||
@import "styles/colors.scss";
|
@import 'styles/colors.scss';
|
||||||
|
|
||||||
.categories {
|
.categories {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import LinkTag from "next/link";
|
import LinkTag from 'next/link';
|
||||||
|
|
||||||
import LinkFavicon from "components/Links/LinkFavicon";
|
import LinkFavicon from 'components/Links/LinkFavicon';
|
||||||
import { Link } from "types";
|
import { Link } from 'types';
|
||||||
|
|
||||||
import styles from "./favorites.module.scss";
|
import styles from './favorites.module.scss';
|
||||||
|
|
||||||
export default function FavoriteItem({ link }: { link: Link }): JSX.Element {
|
export default function FavoriteItem({ link }: { link: Link }): JSX.Element {
|
||||||
const { name, url, category } = link;
|
const { name, url, category } = link;
|
||||||
return (
|
return (
|
||||||
<li className={styles["item"]}>
|
<li className={styles['item']}>
|
||||||
<LinkTag href={url} target={"_blank"} rel={"noreferrer"} title={name}>
|
<LinkTag
|
||||||
<LinkFavicon url={url} size={24} noMargin />
|
href={url}
|
||||||
<span className={styles["link-name"]}>{name}</span>
|
target={'_blank'}
|
||||||
<span className={styles["category"]}> - {category.name}</span>
|
rel={'noreferrer'}
|
||||||
|
title={name}
|
||||||
|
>
|
||||||
|
<LinkFavicon
|
||||||
|
url={url}
|
||||||
|
size={24}
|
||||||
|
noMargin
|
||||||
|
/>
|
||||||
|
<span className={styles['link-name']}>{name}</span>
|
||||||
|
<span className={styles['category']}> - {category.name}</span>
|
||||||
</LinkTag>
|
</LinkTag>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import { Link } from "types";
|
import { Link } from 'types';
|
||||||
import FavoriteItem from "./FavoriteItem";
|
import FavoriteItem from './FavoriteItem';
|
||||||
import styles from "./favorites.module.scss";
|
import styles from './favorites.module.scss';
|
||||||
import clsx from "clsx";
|
import clsx from 'clsx';
|
||||||
|
|
||||||
export default function Favorites({ favorites }: { favorites: Link[] }) {
|
export default function Favorites({ favorites }: { favorites: Link[] }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
favorites.length !== 0 && (
|
favorites.length !== 0 && (
|
||||||
<div className={styles["favorites"]}>
|
<div className={styles['favorites']}>
|
||||||
<h4>{t("common:favorite")}</h4>
|
<h4>{t('common:favorite')}</h4>
|
||||||
<ul className={clsx(styles["items"], "reset")}>
|
<ul className={clsx(styles['items'], 'reset')}>
|
||||||
{favorites.map((link) => (
|
{favorites.map((link) => (
|
||||||
<FavoriteItem link={link} key={link.id} />
|
<FavoriteItem
|
||||||
|
link={link}
|
||||||
|
key={link.id}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "styles/colors.scss";
|
@import 'styles/colors.scss';
|
||||||
|
|
||||||
.favorites {
|
.favorites {
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -38,7 +38,6 @@
|
|||||||
display: block;
|
display: block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
import ButtonLink from "components/ButtonLink";
|
import ButtonLink from 'components/ButtonLink';
|
||||||
import PATHS from "constants/paths";
|
import PATHS from 'constants/paths';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import { SideMenuProps } from "./SideMenu";
|
import { SideMenuProps } from './SideMenu';
|
||||||
import styles from "./sidemenu.module.scss";
|
import styles from './sidemenu.module.scss';
|
||||||
|
|
||||||
export default function NavigationLinks({
|
export default function NavigationLinks({
|
||||||
categoryActive,
|
categoryActive,
|
||||||
openSearchModal,
|
openSearchModal,
|
||||||
}: {
|
}: {
|
||||||
categoryActive: SideMenuProps["categoryActive"];
|
categoryActive: SideMenuProps['categoryActive'];
|
||||||
openSearchModal: SideMenuProps["openSearchModal"];
|
openSearchModal: SideMenuProps['openSearchModal'];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["menu-controls"]}>
|
<div className={styles['menu-controls']}>
|
||||||
<div className={styles["action"]}>
|
<div className={styles['action']}>
|
||||||
<ButtonLink onClick={openSearchModal}>{t("common:search")}</ButtonLink>
|
<ButtonLink onClick={openSearchModal}>{t('common:search')}</ButtonLink>
|
||||||
<kbd>S</kbd>
|
<kbd>S</kbd>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["action"]}>
|
<div className={styles['action']}>
|
||||||
<ButtonLink href={PATHS.CATEGORY.CREATE}>
|
<ButtonLink href={PATHS.CATEGORY.CREATE}>
|
||||||
{t("common:category.create")}
|
{t('common:category.create')}
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
<kbd>C</kbd>
|
<kbd>C</kbd>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["action"]}>
|
<div className={styles['action']}>
|
||||||
<ButtonLink
|
<ButtonLink
|
||||||
href={`${PATHS.LINK.CREATE}?categoryId=${categoryActive.id}`}
|
href={`${PATHS.LINK.CREATE}?categoryId=${categoryActive.id}`}
|
||||||
>
|
>
|
||||||
{t("common:link.create")}
|
{t('common:link.create')}
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
<kbd>L</kbd>
|
<kbd>L</kbd>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import BlockWrapper from "components/BlockWrapper/BlockWrapper";
|
import BlockWrapper from 'components/BlockWrapper/BlockWrapper';
|
||||||
import * as Keys from "constants/keys";
|
import * as Keys from 'constants/keys';
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { Category, Link } from "types";
|
import { Category, Link } from 'types';
|
||||||
import Categories from "./Categories/Categories";
|
import Categories from './Categories/Categories';
|
||||||
import Favorites from "./Favorites/Favorites";
|
import Favorites from './Favorites/Favorites';
|
||||||
import NavigationLinks from "./NavigationLinks";
|
import NavigationLinks from './NavigationLinks';
|
||||||
import UserCard from "./UserCard/UserCard";
|
import UserCard from './UserCard/UserCard';
|
||||||
import styles from "./sidemenu.module.scss";
|
import styles from './sidemenu.module.scss';
|
||||||
|
|
||||||
export interface SideMenuProps {
|
export interface SideMenuProps {
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
@@ -56,11 +56,11 @@ export default function SideMenu({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["side-menu"]}>
|
<div className={styles['side-menu']}>
|
||||||
<BlockWrapper>
|
<BlockWrapper>
|
||||||
<Favorites favorites={favorites} />
|
<Favorites favorites={favorites} />
|
||||||
</BlockWrapper>
|
</BlockWrapper>
|
||||||
<BlockWrapper style={{ minHeight: "0", flex: "1" }}>
|
<BlockWrapper style={{ minHeight: '0', flex: '1' }}>
|
||||||
<Categories
|
<Categories
|
||||||
categories={categories}
|
categories={categories}
|
||||||
categoryActive={categoryActive}
|
categoryActive={categoryActive}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from 'next-auth/react';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import Image from "next/image";
|
import Image from 'next/image';
|
||||||
import { TFunctionParam } from "types/i18next";
|
import { TFunctionParam } from 'types/i18next';
|
||||||
import styles from "./user-card.module.scss";
|
import styles from './user-card.module.scss';
|
||||||
import SettingsModal from "components/Settings/SettingsModal";
|
import SettingsModal from 'components/Settings/SettingsModal';
|
||||||
|
|
||||||
export default function UserCard() {
|
export default function UserCard() {
|
||||||
const { data } = useSession({ required: true });
|
const { data } = useSession({ required: true });
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const avatarLabel = t("common:avatar", {
|
const avatarLabel = t('common:avatar', {
|
||||||
name: data.user.name,
|
name: data.user.name,
|
||||||
} as TFunctionParam);
|
} as TFunctionParam);
|
||||||
return (
|
return (
|
||||||
<div className={styles["user-card-wrapper"]}>
|
<div className={styles['user-card-wrapper']}>
|
||||||
<div className={styles["user-card"]}>
|
<div className={styles['user-card']}>
|
||||||
<Image
|
<Image
|
||||||
src={data.user.image}
|
src={data.user.image}
|
||||||
width={28}
|
width={28}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "styles/colors.scss";
|
@import 'styles/colors.scss';
|
||||||
|
|
||||||
.user-card-wrapper {
|
.user-card-wrapper {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "styles/colors.scss";
|
@import 'styles/colors.scss';
|
||||||
|
|
||||||
.side-menu {
|
.side-menu {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MutableRefObject, useState } from "react";
|
import { MutableRefObject, useState } from 'react';
|
||||||
|
|
||||||
interface InputProps {
|
interface InputProps {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -17,9 +17,9 @@ export default function TextBox({
|
|||||||
label,
|
label,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
innerRef = null,
|
innerRef = null,
|
||||||
placeholder = "Type something...",
|
placeholder = 'Type something...',
|
||||||
fieldClass = "",
|
fieldClass = '',
|
||||||
inputClass = "",
|
inputClass = '',
|
||||||
value,
|
value,
|
||||||
onChangeCallback,
|
onChangeCallback,
|
||||||
}: InputProps): JSX.Element {
|
}: InputProps): JSX.Element {
|
||||||
@@ -35,7 +35,10 @@ export default function TextBox({
|
|||||||
return (
|
return (
|
||||||
<div className={`input-field ${fieldClass}`}>
|
<div className={`input-field ${fieldClass}`}>
|
||||||
{label && (
|
{label && (
|
||||||
<label htmlFor={name} title={`${name} field`}>
|
<label
|
||||||
|
htmlFor={name}
|
||||||
|
title={`${name} field`}
|
||||||
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
export const OPEN_SEARCH_KEY = "s";
|
export const OPEN_SEARCH_KEY = 's';
|
||||||
export const CLOSE_SEARCH_KEY = "escape";
|
export const CLOSE_SEARCH_KEY = 'escape';
|
||||||
|
|
||||||
export const OPEN_CREATE_LINK_KEY = "l";
|
export const OPEN_CREATE_LINK_KEY = 'l';
|
||||||
export const OPEN_CREATE_CATEGORY_KEY = "c";
|
export const OPEN_CREATE_CATEGORY_KEY = 'c';
|
||||||
|
|
||||||
export const ARROW_UP = "ArrowUp";
|
export const ARROW_UP = 'ArrowUp';
|
||||||
export const ARROW_DOWN = "ArrowDown";
|
export const ARROW_DOWN = 'ArrowDown';
|
||||||
export const ARROW_LEFT = "ArrowLeft";
|
|
||||||
export const ARROW_RIGHT = "ArrowRight";
|
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
const PATHS = {
|
const PATHS = {
|
||||||
LOGIN: "/login",
|
LOGIN: '/login',
|
||||||
LOGOUT: "/logout",
|
LOGOUT: '/logout',
|
||||||
HOME: "/",
|
HOME: '/',
|
||||||
PRIVACY: "/privacy",
|
PRIVACY: '/privacy',
|
||||||
TERMS: "/terms",
|
TERMS: '/terms',
|
||||||
CATEGORY: {
|
CATEGORY: {
|
||||||
CREATE: "/category/create",
|
CREATE: '/category/create',
|
||||||
EDIT: "/category/edit",
|
EDIT: '/category/edit',
|
||||||
REMOVE: "/category/remove",
|
REMOVE: '/category/remove',
|
||||||
},
|
},
|
||||||
LINK: {
|
LINK: {
|
||||||
CREATE: "/link/create",
|
CREATE: '/link/create',
|
||||||
EDIT: "/link/edit",
|
EDIT: '/link/edit',
|
||||||
REMOVE: "/link/remove",
|
REMOVE: '/link/remove',
|
||||||
},
|
},
|
||||||
API: {
|
API: {
|
||||||
CATEGORY: "/api/category",
|
CATEGORY: '/api/category',
|
||||||
LINK: "/api/link",
|
LINK: '/api/link',
|
||||||
},
|
},
|
||||||
NOT_FOUND: "/404",
|
NOT_FOUND: '/404',
|
||||||
SERVER_ERROR: "/505",
|
SERVER_ERROR: '/505',
|
||||||
AUTHOR: "https://www.sonny.dev/",
|
AUTHOR: 'https://www.sonny.dev/',
|
||||||
REPO_GITHUB: "https://github.com/Sonny93/my-links",
|
REPO_GITHUB: 'https://github.com/Sonny93/my-links',
|
||||||
EXTENSION:
|
EXTENSION:
|
||||||
"https://chromewebstore.google.com/detail/mylinks/agkmlplihacolkakgeccnbhphnepphma",
|
'https://chromewebstore.google.com/detail/mylinks/agkmlplihacolkakgeccnbhphnepphma',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PATHS;
|
export default PATHS;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const GOOGLE_SEARCH_URL = "https://google.com/search?q=";
|
export const GOOGLE_SEARCH_URL = 'https://google.com/search?q=';
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
export const VALID_URL_REGEX =
|
|
||||||
/^(?:http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.\%]+$/;
|
|
||||||
|
|
||||||
export const USER_AGENT =
|
export const USER_AGENT =
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0";
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0';
|
||||||
|
|
||||||
export const REL_LIST = [
|
export const REL_LIST = [
|
||||||
"icon",
|
'icon',
|
||||||
"shortcut icon",
|
'shortcut icon',
|
||||||
"apple-touch-icon",
|
'apple-touch-icon',
|
||||||
"apple-touch-icon-precomposed",
|
'apple-touch-icon-precomposed',
|
||||||
"apple-touch-startup-image",
|
'apple-touch-startup-image',
|
||||||
"mask-icon",
|
'mask-icon',
|
||||||
"fluid-icon",
|
'fluid-icon',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
export default function useAutoFocus() {
|
export default function useAutoFocus() {
|
||||||
const inputRef = useCallback((inputElement: any) => {
|
return useCallback((inputElement: any) => {
|
||||||
if (inputElement) {
|
if (inputElement) {
|
||||||
inputElement.focus();
|
inputElement.focus();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return inputRef;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const MOBILE_SCREEN_SIZE = 768;
|
|||||||
|
|
||||||
export default function useIsMobile() {
|
export default function useIsMobile() {
|
||||||
return (
|
return (
|
||||||
typeof window !== "undefined" &&
|
typeof window !== 'undefined' &&
|
||||||
window.matchMedia(`screen and (max-width: ${MOBILE_SCREEN_SIZE}px)`).matches
|
window.matchMedia(`screen and (max-width: ${MOBILE_SCREEN_SIZE}px)`).matches
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from 'react';
|
||||||
|
|
||||||
export function useLocalStorage(key: string, initialValue: any) {
|
export function useLocalStorage(key: string, initialValue: any) {
|
||||||
const [storedValue, setStoredValue] = useState(() => {
|
const [storedValue, setStoredValue] = useState(() => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === 'undefined') {
|
||||||
return initialValue;
|
return initialValue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -19,7 +19,7 @@ export function useLocalStorage(key: string, initialValue: any) {
|
|||||||
const valueToStore =
|
const valueToStore =
|
||||||
value instanceof Function ? value(storedValue) : value;
|
value instanceof Function ? value(storedValue) : value;
|
||||||
setStoredValue(valueToStore);
|
setStoredValue(valueToStore);
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== 'undefined') {
|
||||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export function useMediaQuery(query: string): boolean {
|
export function useMediaQuery(query: string): boolean {
|
||||||
const [matches, setMatches] = useState<boolean>(getMediaMatches(query));
|
const [matches, setMatches] = useState<boolean>(getMediaMatches(query));
|
||||||
@@ -9,8 +9,8 @@ export function useMediaQuery(query: string): boolean {
|
|||||||
const matchMedia = window.matchMedia(query);
|
const matchMedia = window.matchMedia(query);
|
||||||
handleMediaChange();
|
handleMediaChange();
|
||||||
|
|
||||||
matchMedia.addEventListener("change", handleMediaChange);
|
matchMedia.addEventListener('change', handleMediaChange);
|
||||||
return () => matchMedia.removeEventListener("change", handleMediaChange);
|
return () => matchMedia.removeEventListener('change', handleMediaChange);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export function useMediaQuery(query: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMediaMatches(query: string): boolean {
|
function getMediaMatches(query: string): boolean {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== 'undefined') {
|
||||||
return window.matchMedia(query).matches;
|
return window.matchMedia(query).matches;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState } from 'react';
|
||||||
|
|
||||||
const useModal = () => {
|
const useModal = () => {
|
||||||
const [isShowing, setIsShowing] = useState<boolean>(false);
|
const [isShowing, setIsShowing] = useState<boolean>(false);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
import nextI18NextConfig from "../../next-i18next.config";
|
import nextI18NextConfig from '../../next-i18next.config';
|
||||||
|
|
||||||
async function getServerSideTranslation(
|
async function getServerSideTranslation(
|
||||||
locale: string = "en",
|
locale: string = 'en',
|
||||||
requiredNs: string[] = [],
|
requiredNs: string[] = [],
|
||||||
) {
|
) {
|
||||||
return await serverSideTranslations(
|
return await serverSideTranslations(
|
||||||
locale,
|
locale,
|
||||||
["common", ...requiredNs],
|
['common', ...requiredNs],
|
||||||
nextI18NextConfig,
|
nextI18NextConfig,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import common from "../../public/locales/en/common.json";
|
import common from '../../public/locales/en/common.json';
|
||||||
import home from "../../public/locales/en/home.json";
|
import home from '../../public/locales/en/home.json';
|
||||||
import login from "../../public/locales/en/login.json";
|
import login from '../../public/locales/en/login.json';
|
||||||
import privacy from "../../public/locales/en/privacy.json";
|
import privacy from '../../public/locales/en/privacy.json';
|
||||||
import terms from "../../public/locales/en/terms.json";
|
import terms from '../../public/locales/en/terms.json';
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
common,
|
common,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { User } from "@prisma/client";
|
import { User } from '@prisma/client';
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
|
||||||
import getUserOrThrow from "lib/user/getUserOrThrow";
|
import getUserOrThrow from 'lib/user/getUserOrThrow';
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { Session } from "next-auth";
|
import { Session } from 'next-auth';
|
||||||
import { getSession } from "utils/session";
|
import { getSession } from 'utils/session';
|
||||||
|
|
||||||
type ApiHandlerMethod = ({
|
type ApiHandlerMethod = ({
|
||||||
req,
|
req,
|
||||||
@@ -46,23 +46,23 @@ export function apiHandler(handler: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function errorHandler(error: any, response: NextApiResponse) {
|
function errorHandler(error: any, response: NextApiResponse) {
|
||||||
if (typeof error === "string") {
|
if (typeof error === 'string') {
|
||||||
const is404 = error.toLowerCase().endsWith("not found");
|
const is404 = error.toLowerCase().endsWith('not found');
|
||||||
const statusCode = is404 ? 404 : 400;
|
const statusCode = is404 ? 404 : 400;
|
||||||
|
|
||||||
return response.status(statusCode).json({ message: error });
|
return response.status(statusCode).json({ message: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
// does not fit with current error throwed
|
// does not fit with current error thrown
|
||||||
// TODO: fix errors returned
|
// TODO: fix errors returned
|
||||||
// by getSessionOrThrow or getUserOrThrow
|
// by getSessionOrThrow or getUserOrThrow
|
||||||
if (error.name === "UnauthorizedError") {
|
if (error.name === 'UnauthorizedError') {
|
||||||
// authentication error
|
// authentication error
|
||||||
return response.status(401).json({ message: "You must be connected" });
|
return response.status(401).json({ message: 'You must be connected' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error.constructor.name === "PrismaClientKnownRequestError"
|
error.constructor.name === 'PrismaClientKnownRequestError'
|
||||||
? handlePrismaError(error) // Handle Prisma specific errors
|
? handlePrismaError(error) // Handle Prisma specific errors
|
||||||
: error.message;
|
: error.message;
|
||||||
|
|
||||||
@@ -75,16 +75,14 @@ function handlePrismaError({
|
|||||||
message,
|
message,
|
||||||
}: PrismaClientKnownRequestError) {
|
}: PrismaClientKnownRequestError) {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case "P2002":
|
case 'P2002':
|
||||||
return `Duplicate field value: ${meta.target}`;
|
return `Duplicate field value: ${meta.target}`;
|
||||||
case "P2003":
|
case 'P2014':
|
||||||
return `Foreign key constraint failed on the field: ${meta.field_name}`;
|
|
||||||
case "P2014":
|
|
||||||
return `Invalid ID: ${meta.target}`;
|
return `Invalid ID: ${meta.target}`;
|
||||||
case "P2003":
|
case 'P2003':
|
||||||
return `Invalid input data: ${meta.target}`;
|
return `Invalid input data: ${meta.target}`;
|
||||||
|
|
||||||
// Details should not leak to client, be carreful with this
|
// Details should not leak to client, be careful with this
|
||||||
default:
|
default:
|
||||||
return `Something went wrong: ${message}`;
|
return `Something went wrong: ${message}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { number, object, string } from "yup";
|
import { number, object, string } from 'yup';
|
||||||
|
|
||||||
const CategorieBodySchema = object({
|
const CategoryBodySchema = object({
|
||||||
name: string()
|
name: string()
|
||||||
.trim()
|
.trim()
|
||||||
.required("Category name's required")
|
.required("Category name's required")
|
||||||
.max(128, "Category name's too long"),
|
.max(128, "Category name's too long"),
|
||||||
}).typeError("Missing request Body");
|
}).typeError('Missing request Body');
|
||||||
|
|
||||||
const CategorieQuerySchema = object({
|
const CategoryQuerySchema = object({
|
||||||
cid: number().required(),
|
cid: number().required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export { CategorieBodySchema, CategorieQuerySchema };
|
export { CategoryBodySchema, CategoryQuerySchema };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { User } from "@prisma/client";
|
import { User } from '@prisma/client';
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default async function getUserCategories(user: User) {
|
export default async function getUserCategories(user: User) {
|
||||||
return await prisma.category.findMany({
|
return await prisma.category.findMany({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { User } from "@prisma/client";
|
import { User } from '@prisma/client';
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default async function getUserCategoriesCount(user: User) {
|
export default async function getUserCategoriesCount(user: User) {
|
||||||
return await prisma.category.count({
|
return await prisma.category.count({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Category, User } from "@prisma/client";
|
import { Category, User } from '@prisma/client';
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default async function getUserCategory(user: User, id: Category["id"]) {
|
export default async function getUserCategory(user: User, id: Category['id']) {
|
||||||
return await prisma.category.findFirst({
|
return await prisma.category.findFirst({
|
||||||
where: {
|
where: {
|
||||||
authorId: user?.id,
|
authorId: user?.id,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Category, User } from "@prisma/client";
|
import { Category, User } from '@prisma/client';
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default async function getUserCategoryByName(
|
export default async function getUserCategoryByName(
|
||||||
user: User,
|
user: User,
|
||||||
name: Category["name"],
|
name: Category['name'],
|
||||||
) {
|
) {
|
||||||
return await prisma.category.findFirst({
|
return await prisma.category.findFirst({
|
||||||
where: { name, authorId: user.id },
|
where: { name, authorId: user.id },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const isImage = (type: string) => type.includes("image");
|
export const isImage = (type: string) => type.includes('image');
|
||||||
|
|
||||||
export const isBase64Image = (data: string) => data.startsWith("data:image/");
|
export const isBase64Image = (data: string) => data.startsWith('data:image/');
|
||||||
|
|
||||||
export const convertBase64ToBuffer = (base64: string) =>
|
export const convertBase64ToBuffer = (base64: string) =>
|
||||||
Buffer.from(base64, "base64");
|
Buffer.from(base64, 'base64');
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Category, Link, User } from "@prisma/client";
|
import { Category, Link, User } from '@prisma/client';
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default async function getLinkFromCategoryByName(
|
export default async function getLinkFromCategoryByName(
|
||||||
user: User,
|
user: User,
|
||||||
name: Link["name"],
|
name: Link['name'],
|
||||||
categoryId: Category["id"],
|
categoryId: Category['id'],
|
||||||
) {
|
) {
|
||||||
return await prisma.link.findFirst({
|
return await prisma.link.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Link, User } from "@prisma/client";
|
import { Link, User } from '@prisma/client';
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default async function getUserLink(user: User, id: Link["id"]) {
|
export default async function getUserLink(user: User, id: Link['id']) {
|
||||||
return await prisma.link.findFirst({
|
return await prisma.link.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { User } from "@prisma/client";
|
import { User } from '@prisma/client';
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default async function getUserLinks(user: User) {
|
export default async function getUserLinks(user: User) {
|
||||||
return await prisma.link.findMany({
|
return await prisma.link.findMany({
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { boolean, number, object, string } from "yup";
|
import { boolean, number, object, string } from 'yup';
|
||||||
import { isValidHttpUrl } from "../url";
|
import { isValidHttpUrl } from '../url';
|
||||||
|
|
||||||
const LinkBodySchema = object({
|
const LinkBodySchema = object({
|
||||||
name: string()
|
name: string()
|
||||||
.trim()
|
.trim()
|
||||||
.required("Link name is required")
|
.required('Link name is required')
|
||||||
.max(128, "Link name is too long"),
|
.max(128, 'Link name is too long'),
|
||||||
url: string()
|
url: string()
|
||||||
.trim()
|
.trim()
|
||||||
.required("URl is required")
|
.required('URl is required')
|
||||||
.test("test_url", "Invalid URL format", (value) => isValidHttpUrl(value)),
|
.test('test_url', 'Invalid URL format', (value) => isValidHttpUrl(value)),
|
||||||
categoryId: number().required("CategoryId must be a number"),
|
categoryId: number().required('CategoryId must be a number'),
|
||||||
favorite: boolean().default(() => false),
|
favorite: boolean().default(() => false),
|
||||||
}).typeError("Missing request Body");
|
}).typeError('Missing request Body');
|
||||||
|
|
||||||
const LinkQuerySchema = object({
|
const LinkQuerySchema = object({
|
||||||
lid: number().required(),
|
lid: number().required(),
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import nProgress from "nprogress";
|
import nProgress from 'nprogress';
|
||||||
import { i18n } from "next-i18next";
|
import { i18n } from 'next-i18next';
|
||||||
import { USER_AGENT } from "constants/url";
|
import { USER_AGENT } from 'constants/url';
|
||||||
import { Favicon } from "types/types";
|
import { Favicon } from 'types/types';
|
||||||
import { isImage } from "./image";
|
import { isImage } from './image';
|
||||||
|
|
||||||
export async function makeRequest({
|
export async function makeRequest({
|
||||||
method = "GET",
|
method = 'GET',
|
||||||
url,
|
url,
|
||||||
body,
|
body,
|
||||||
}: {
|
}: {
|
||||||
method?: RequestInit["method"];
|
method?: RequestInit['method'];
|
||||||
url: string;
|
url: string;
|
||||||
body?: object | any[];
|
body?: object | any[];
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
@@ -18,7 +18,7 @@ export async function makeRequest({
|
|||||||
method,
|
method,
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
nProgress.done();
|
nProgress.done();
|
||||||
@@ -26,12 +26,12 @@ export async function makeRequest({
|
|||||||
const data = await request.json();
|
const data = await request.json();
|
||||||
return request.ok
|
return request.ok
|
||||||
? data
|
? data
|
||||||
: Promise.reject(data?.error || i18n.t("common:generic-error"));
|
: Promise.reject(data?.error || i18n.t('common:generic-error'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function makeRequestWithUserAgent(url: string) {
|
export async function makeRequestWithUserAgent(url: string) {
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.set("User-Agent", USER_AGENT);
|
headers.set('User-Agent', USER_AGENT);
|
||||||
|
|
||||||
return await fetch(url, { headers });
|
return await fetch(url, { headers });
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ export async function makeRequestWithUserAgent(url: string) {
|
|||||||
export async function downloadImageFromUrl(url: string): Promise<Favicon> {
|
export async function downloadImageFromUrl(url: string): Promise<Favicon> {
|
||||||
const request = await makeRequestWithUserAgent(url);
|
const request = await makeRequestWithUserAgent(url);
|
||||||
if (!request.ok) {
|
if (!request.ok) {
|
||||||
throw new Error("Request failed");
|
throw new Error('Request failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = await request.blob();
|
const blob = await request.blob();
|
||||||
@@ -52,11 +52,11 @@ export async function downloadImageFromUrl(url: string): Promise<Favicon> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getFavicon(url: string): Promise<Favicon> {
|
export async function getFavicon(url: string): Promise<Favicon> {
|
||||||
if (!url) throw new Error("Missing URL");
|
if (!url) throw new Error('Missing URL');
|
||||||
|
|
||||||
const favicon = await downloadImageFromUrl(url);
|
const favicon = await downloadImageFromUrl(url);
|
||||||
if (!isImage(favicon.type) || favicon.size === 0) {
|
if (!isImage(favicon.type) || favicon.size === 0) {
|
||||||
throw new Error("Favicon path does not return an image");
|
throw new Error('Favicon path does not return an image');
|
||||||
}
|
}
|
||||||
|
|
||||||
return favicon;
|
return favicon;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { parse } from "node-html-parser";
|
import { parse } from 'node-html-parser';
|
||||||
import { REL_LIST } from "constants/url";
|
import { REL_LIST } from 'constants/url';
|
||||||
|
|
||||||
export function buildFaviconUrl(urlParam: string, faviconPath: string) {
|
export function buildFaviconUrl(urlParam: string, faviconPath: string) {
|
||||||
const { origin } = new URL(urlParam);
|
const { origin } = new URL(urlParam);
|
||||||
if (faviconPath.startsWith("/")) {
|
if (faviconPath.startsWith('/')) {
|
||||||
// https://example.com + /favicon.ico
|
// https://example.com + /favicon.ico
|
||||||
return origin + faviconPath;
|
return origin + faviconPath;
|
||||||
}
|
}
|
||||||
@@ -11,14 +11,14 @@ export function buildFaviconUrl(urlParam: string, faviconPath: string) {
|
|||||||
const slimUrl = urlWithoutSearchParams(urlParam);
|
const slimUrl = urlWithoutSearchParams(urlParam);
|
||||||
|
|
||||||
// https://example.com/a/b/ -> https://example.com/a/b
|
// https://example.com/a/b/ -> https://example.com/a/b
|
||||||
const url = slimUrl.endsWith("/") ? slimUrl.slice(0, -1) : slimUrl;
|
const url = slimUrl.endsWith('/') ? slimUrl.slice(0, -1) : slimUrl;
|
||||||
if (url === origin) {
|
if (url === origin) {
|
||||||
return `${url}/${faviconPath}`;
|
return `${url}/${faviconPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://example.com/a/b or https://example.com/a/b/cdef -> https://example.com/a/
|
// https://example.com/a/b or https://example.com/a/b/cdef -> https://example.com/a/
|
||||||
const relativeUrl = removeLastSectionUrl(url) + "/";
|
const relativeUrl = removeLastSectionUrl(url) + '/';
|
||||||
if (relativeUrl.endsWith("/")) {
|
if (relativeUrl.endsWith('/')) {
|
||||||
return relativeUrl + faviconPath;
|
return relativeUrl + faviconPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,24 +28,24 @@ export function buildFaviconUrl(urlParam: string, faviconPath: string) {
|
|||||||
|
|
||||||
function urlWithoutSearchParams(urlParam: string) {
|
function urlWithoutSearchParams(urlParam: string) {
|
||||||
const url = new URL(urlParam);
|
const url = new URL(urlParam);
|
||||||
return url.protocol + "//" + url.host + url.pathname;
|
return url.protocol + '//' + url.host + url.pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeLastSectionUrl(urlParam: string) {
|
export function removeLastSectionUrl(urlParam: string) {
|
||||||
const urlArr = urlParam.split("/");
|
const urlArr = urlParam.split('/');
|
||||||
urlArr.pop();
|
urlArr.pop();
|
||||||
return urlArr.join("/");
|
return urlArr.join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findFaviconPath(text: string) {
|
export function findFaviconPath(text: string) {
|
||||||
const document = parse(text);
|
const document = parse(text);
|
||||||
const favicon = Array.from(document.getElementsByTagName("link")).find(
|
const favicon = Array.from(document.getElementsByTagName('link')).find(
|
||||||
(element) =>
|
(element) =>
|
||||||
REL_LIST.includes(element.getAttribute("rel")) &&
|
REL_LIST.includes(element.getAttribute('rel')) &&
|
||||||
element.getAttribute("href"),
|
element.getAttribute('href'),
|
||||||
);
|
);
|
||||||
|
|
||||||
return favicon?.getAttribute("href") || undefined;
|
return favicon?.getAttribute('href') || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidHttpUrl(urlParam: string) {
|
export function isValidHttpUrl(urlParam: string) {
|
||||||
@@ -57,5 +57,5 @@ export function isValidHttpUrl(urlParam: string) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return url.protocol === "http:" || url.protocol === "https:";
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
import { Profile } from "next-auth";
|
import { Profile } from 'next-auth';
|
||||||
|
|
||||||
export default async function createUser(profile: Profile) {
|
export default async function createUser(profile: Profile) {
|
||||||
return await prisma.user.create({
|
return await prisma.user.create({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Session } from "next-auth";
|
import { Session } from 'next-auth';
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default async function getUser(session: Session) {
|
export default async function getUser(session: Session) {
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Profile } from "next-auth";
|
import { Profile } from 'next-auth';
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default async function getUserByProfileProvider(profile: Profile) {
|
export default async function getUserByProfileProvider(profile: Profile) {
|
||||||
return await prisma.user.findFirst({
|
return await prisma.user.findFirst({
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Session } from "next-auth";
|
import { Session } from 'next-auth';
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default async function getUserOrThrow(session: Session) {
|
export default async function getUserOrThrow(session: Session) {
|
||||||
if (!session || session === null) {
|
if (!session) {
|
||||||
throw new Error("You must be connected");
|
throw new Error('You must be connected');
|
||||||
}
|
}
|
||||||
|
|
||||||
return await prisma.user.findFirstOrThrow({
|
return await prisma.user.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
email: session?.user?.email,
|
email: session?.user?.email,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import prisma from "../../utils/prisma";
|
import prisma from '../../utils/prisma';
|
||||||
import { Profile } from "next-auth";
|
import { Profile } from 'next-auth';
|
||||||
|
|
||||||
export default async function updateUser(profile: Profile) {
|
export default async function updateUser(profile: Profile) {
|
||||||
return await prisma.user.update({
|
return await prisma.user.update({
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import PageTransition from "components/PageTransition";
|
import PageTransition from 'components/PageTransition';
|
||||||
import { NextSeo } from "next-seo";
|
import { NextSeo } from 'next-seo';
|
||||||
import styles from "styles/error-page.module.scss";
|
import styles from 'styles/error-page.module.scss';
|
||||||
import NavbarUntranslated from "../components/Navbar/NavbarUntranslated";
|
import NavbarUntranslated from '../components/Navbar/NavbarUntranslated';
|
||||||
|
|
||||||
export default function Custom404() {
|
export default function Custom404() {
|
||||||
return (
|
return (
|
||||||
<PageTransition className={styles["App"]} hideLangageSelector>
|
<PageTransition
|
||||||
<NextSeo title="Page not found" />
|
className={styles['App']}
|
||||||
|
hideLangageSelector
|
||||||
|
>
|
||||||
|
<NextSeo title='Page not found' />
|
||||||
<NavbarUntranslated />
|
<NavbarUntranslated />
|
||||||
<header>
|
<header>
|
||||||
<h1>404</h1>
|
<h1>404</h1>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import PageTransition from "components/PageTransition";
|
import PageTransition from 'components/PageTransition';
|
||||||
import { NextSeo } from "next-seo";
|
import { NextSeo } from 'next-seo';
|
||||||
import styles from "styles/error-page.module.scss";
|
import styles from 'styles/error-page.module.scss';
|
||||||
import NavbarUntranslated from "../components/Navbar/NavbarUntranslated";
|
import NavbarUntranslated from '../components/Navbar/NavbarUntranslated';
|
||||||
|
|
||||||
export default function Custom500() {
|
export default function Custom500() {
|
||||||
return (
|
return (
|
||||||
<PageTransition className={styles["App"]} hideLangageSelector>
|
<PageTransition
|
||||||
<NextSeo title="Internal server error" />
|
className={styles['App']}
|
||||||
|
hideLangageSelector
|
||||||
|
>
|
||||||
|
<NextSeo title='Internal server error' />
|
||||||
<NavbarUntranslated />
|
<NavbarUntranslated />
|
||||||
<header>
|
<header>
|
||||||
<h1>500</h1>
|
<h1>500</h1>
|
||||||
|
|||||||
@@ -1,41 +1,44 @@
|
|||||||
import ErrorBoundary from "components/ErrorBoundary/ErrorBoundary";
|
import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
|
||||||
import * as Keys from "constants/keys";
|
import * as Keys from 'constants/keys';
|
||||||
import PATHS from "constants/paths";
|
import PATHS from 'constants/paths';
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from 'next-auth/react';
|
||||||
import { appWithTranslation } from "next-i18next";
|
import { appWithTranslation } from 'next-i18next';
|
||||||
import { DefaultSeo } from "next-seo";
|
import { DefaultSeo } from 'next-seo';
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from 'next/router';
|
||||||
import nProgress from "nprogress";
|
import nProgress from 'nprogress';
|
||||||
import "nprogress/nprogress.css";
|
import 'nprogress/nprogress.css';
|
||||||
import { useEffect } from "react";
|
import { useEffect } from 'react';
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import "styles/globals.scss";
|
import 'styles/globals.scss';
|
||||||
import nextI18nextConfig from "../../next-i18next.config";
|
import nextI18nextConfig from '../../next-i18next.config';
|
||||||
|
|
||||||
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
|
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useHotkeys(Keys.CLOSE_SEARCH_KEY, () => router.push(PATHS.HOME), {
|
useHotkeys(Keys.CLOSE_SEARCH_KEY, () => router.push(PATHS.HOME), {
|
||||||
enabled: router.pathname !== PATHS.HOME,
|
enabled: router.pathname !== PATHS.HOME,
|
||||||
enableOnFormTags: ["INPUT"],
|
enableOnFormTags: ['INPUT'],
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Page loading events
|
// Page loading events
|
||||||
router.events.on("routeChangeStart", nProgress.start);
|
router.events.on('routeChangeStart', nProgress.start);
|
||||||
router.events.on("routeChangeComplete", nProgress.done);
|
router.events.on('routeChangeComplete', nProgress.done);
|
||||||
router.events.on("routeChangeError", nProgress.done);
|
router.events.on('routeChangeError', nProgress.done);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
router.events.off("routeChangeStart", nProgress.start);
|
router.events.off('routeChangeStart', nProgress.start);
|
||||||
router.events.off("routeChangeComplete", nProgress.done);
|
router.events.off('routeChangeComplete', nProgress.done);
|
||||||
router.events.off("routeChangeError", nProgress.done);
|
router.events.off('routeChangeError', nProgress.done);
|
||||||
};
|
};
|
||||||
}, [router.events]);
|
}, [router.events]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionProvider session={session}>
|
<SessionProvider session={session}>
|
||||||
<DefaultSeo titleTemplate="MyLinks — %s" defaultTitle="MyLinks" />
|
<DefaultSeo
|
||||||
|
titleTemplate='MyLinks — %s'
|
||||||
|
defaultTitle='MyLinks'
|
||||||
|
/>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { Head, Html, Main, NextScript } from "next/document";
|
import { Head, Html, Main, NextScript } from 'next/document';
|
||||||
|
|
||||||
const Document = () => (
|
const Document = () => (
|
||||||
<Html lang="fr">
|
<Html lang='fr'>
|
||||||
<Head>
|
<Head>
|
||||||
<meta name="theme-color" content="#f0eef6" />
|
<meta
|
||||||
<link
|
name='theme-color'
|
||||||
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=Rubik:ital,wght@0,400;0,700;1,400;1,700&display=swap"
|
content='#f0eef6'
|
||||||
rel="stylesheet"
|
/>
|
||||||
|
<link
|
||||||
|
href='https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=Rubik:ital,wght@0,400;0,700;1,400;1,700&display=swap'
|
||||||
|
rel='stylesheet'
|
||||||
|
/>
|
||||||
|
<meta charSet='UTF-8' />
|
||||||
|
<link
|
||||||
|
rel='shortcut icon'
|
||||||
|
href='/favicon.png'
|
||||||
|
type='image/png'
|
||||||
/>
|
/>
|
||||||
<meta charSet="UTF-8" />
|
|
||||||
<link rel="shortcut icon" href="/favicon.png" type="image/png" />
|
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>Vous devez activer JavaScript pour utiliser ce site</noscript>
|
<noscript>Vous devez activer JavaScript pour utiliser ce site</noscript>
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import PATHS from "constants/paths";
|
import PATHS from 'constants/paths';
|
||||||
import NextAuth, { NextAuthOptions, Profile } from "next-auth";
|
import NextAuth, { NextAuthOptions, Profile } from 'next-auth';
|
||||||
import GoogleProvider from "next-auth/providers/google";
|
import GoogleProvider from 'next-auth/providers/google';
|
||||||
import getUserByProfileProvider from "lib/user/getUserByProfileProvider";
|
import getUserByProfileProvider from 'lib/user/getUserByProfileProvider';
|
||||||
import createUser from "lib/user/createUser";
|
import createUser from 'lib/user/createUser';
|
||||||
import updateUser from "lib/user/updateUser";
|
import updateUser from 'lib/user/updateUser';
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
const authLogger = (profile: Profile, ...args: any[]) =>
|
const authLogger = (profile: Profile, ...args: any[]) =>
|
||||||
console.log(
|
console.log(
|
||||||
"[AUTH]",
|
'[AUTH]',
|
||||||
profile.email,
|
profile.email,
|
||||||
`(${profile.name} - ${profile.sub})`,
|
`(${profile.name} - ${profile.sub})`,
|
||||||
...args,
|
...args,
|
||||||
);
|
);
|
||||||
const redirectUser = (errorKey: string) => `${PATHS.LOGIN}?error=${errorKey}`;
|
const redirectUser = (errorKey: string) => `${PATHS.LOGIN}?error=${errorKey}`;
|
||||||
|
|
||||||
const checkProvider = (provider: string) => provider === "google";
|
const checkProvider = (provider: string) => provider === 'google';
|
||||||
const checkAccountDataReceived = (profile: Profile) =>
|
const checkAccountDataReceived = (profile: Profile) =>
|
||||||
!!profile?.sub && !!profile?.email;
|
!!profile?.sub && !!profile?.email;
|
||||||
|
|
||||||
const cookieOptions = {
|
const cookieOptions = {
|
||||||
sameSite: "None",
|
sameSite: 'None',
|
||||||
path: "/",
|
path: '/',
|
||||||
secure: true,
|
secure: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,9 +32,9 @@ export const authOptions = {
|
|||||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
authorization: {
|
authorization: {
|
||||||
params: {
|
params: {
|
||||||
prompt: "consent",
|
prompt: 'consent',
|
||||||
access_type: "offline",
|
access_type: 'offline',
|
||||||
response_type: "code",
|
response_type: 'code',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -49,30 +49,30 @@ export const authOptions = {
|
|||||||
},
|
},
|
||||||
async signIn({ account: accountParam, profile }) {
|
async signIn({ account: accountParam, profile }) {
|
||||||
if (!checkProvider(accountParam.provider)) {
|
if (!checkProvider(accountParam.provider)) {
|
||||||
authLogger(profile, "rejected : forbidden provider");
|
authLogger(profile, 'rejected : forbidden provider');
|
||||||
return redirectUser("AUTH_REQUIRED");
|
return redirectUser('AUTH_REQUIRED');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!checkAccountDataReceived(profile)) {
|
if (!checkAccountDataReceived(profile)) {
|
||||||
authLogger(profile, "rejected : missing data from provider", profile);
|
authLogger(profile, 'rejected : missing data from provider', profile);
|
||||||
return redirectUser("MISSING_PROVIDER_VALUES");
|
return redirectUser('MISSING_PROVIDER_VALUES');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isUserExists = await getUserByProfileProvider(profile);
|
const isUserExists = await getUserByProfileProvider(profile);
|
||||||
if (isUserExists) {
|
if (isUserExists) {
|
||||||
await updateUser(profile);
|
await updateUser(profile);
|
||||||
authLogger(profile, "success : user updated");
|
authLogger(profile, 'success : user updated');
|
||||||
} else {
|
} else {
|
||||||
await createUser(profile);
|
await createUser(profile);
|
||||||
authLogger(profile, "success : user created");
|
authLogger(profile, 'success : user created');
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
authLogger(profile, "rejected : unhandled error");
|
authLogger(profile, 'rejected : unhandled error');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return redirectUser("AUTH_ERROR");
|
return redirectUser('AUTH_ERROR');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -83,15 +83,15 @@ export const authOptions = {
|
|||||||
},
|
},
|
||||||
cookies: {
|
cookies: {
|
||||||
sessionToken: {
|
sessionToken: {
|
||||||
name: "next-auth.session-token",
|
name: 'next-auth.session-token',
|
||||||
options: cookieOptions,
|
options: cookieOptions,
|
||||||
},
|
},
|
||||||
callbackUrl: {
|
callbackUrl: {
|
||||||
name: "next-auth.callback-url",
|
name: 'next-auth.callback-url',
|
||||||
options: cookieOptions,
|
options: cookieOptions,
|
||||||
},
|
},
|
||||||
csrfToken: {
|
csrfToken: {
|
||||||
name: "next-auth.csrf-token",
|
name: 'next-auth.csrf-token',
|
||||||
options: cookieOptions,
|
options: cookieOptions,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { apiHandler } from "lib/api/handler";
|
import { apiHandler } from 'lib/api/handler';
|
||||||
import {
|
import {
|
||||||
CategorieBodySchema,
|
CategoryBodySchema,
|
||||||
CategorieQuerySchema,
|
CategoryQuerySchema,
|
||||||
} from "lib/category/categoryValidationSchema";
|
} from 'lib/category/categoryValidationSchema';
|
||||||
import getUserCategory from "lib/category/getUserCategory";
|
import getUserCategory from 'lib/category/getUserCategory';
|
||||||
import getUserCategoryByName from "lib/category/getUserCategoryByName";
|
import getUserCategoryByName from 'lib/category/getUserCategoryByName';
|
||||||
|
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default apiHandler({
|
export default apiHandler({
|
||||||
put: editCategory,
|
put: editCategory,
|
||||||
@@ -14,21 +14,21 @@ export default apiHandler({
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function editCategory({ req, res, user }) {
|
async function editCategory({ req, res, user }) {
|
||||||
const { cid } = await CategorieQuerySchema.validate(req.query);
|
const { cid } = await CategoryQuerySchema.validate(req.query);
|
||||||
const { name } = await CategorieBodySchema.validate(req.body);
|
const { name } = await CategoryBodySchema.validate(req.body);
|
||||||
|
|
||||||
const category = await getUserCategory(user, cid);
|
const category = await getUserCategory(user, cid);
|
||||||
if (!category) {
|
if (!category) {
|
||||||
throw new Error("Unable to find category " + cid);
|
throw new Error('Unable to find category ' + cid);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCategoryNameAlreadyused = await getUserCategoryByName(user, name);
|
const isCategoryNameAlreadyUsed = await getUserCategoryByName(user, name);
|
||||||
if (isCategoryNameAlreadyused) {
|
if (isCategoryNameAlreadyUsed) {
|
||||||
throw new Error("Category name already used");
|
throw new Error('Category name already used');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category.name === name) {
|
if (category.name === name) {
|
||||||
throw new Error("New category name must be different");
|
throw new Error('New category name must be different');
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.category.update({
|
await prisma.category.update({
|
||||||
@@ -36,28 +36,28 @@ async function editCategory({ req, res, user }) {
|
|||||||
data: { name },
|
data: { name },
|
||||||
});
|
});
|
||||||
return res.send({
|
return res.send({
|
||||||
success: "Category successfully updated",
|
success: 'Category successfully updated',
|
||||||
categoryId: category.id,
|
categoryId: category.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCategory({ req, res, user }) {
|
async function deleteCategory({ req, res, user }) {
|
||||||
const { cid } = await CategorieQuerySchema.validate(req.query);
|
const { cid } = await CategoryQuerySchema.validate(req.query);
|
||||||
|
|
||||||
const category = await getUserCategory(user, cid);
|
const category = await getUserCategory(user, cid);
|
||||||
if (!category) {
|
if (!category) {
|
||||||
throw new Error("Unable to find category " + cid);
|
throw new Error('Unable to find category ' + cid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category.links.length !== 0) {
|
if (category.links.length !== 0) {
|
||||||
throw new Error("You cannot remove category with links");
|
throw new Error('You cannot remove category with links');
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.category.delete({
|
await prisma.category.delete({
|
||||||
where: { id: cid },
|
where: { id: cid },
|
||||||
});
|
});
|
||||||
return res.send({
|
return res.send({
|
||||||
success: "Category successfully deleted",
|
success: 'Category successfully deleted',
|
||||||
categoryId: category.id,
|
categoryId: category.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { apiHandler } from "lib/api/handler";
|
import { apiHandler } from 'lib/api/handler';
|
||||||
import { CategorieBodySchema } from "lib/category/categoryValidationSchema";
|
import { CategoryBodySchema } from 'lib/category/categoryValidationSchema';
|
||||||
import getUserCategories from "lib/category/getUserCategories";
|
import getUserCategories from 'lib/category/getUserCategories';
|
||||||
import getUserCategoryByName from "lib/category/getUserCategoryByName";
|
import getUserCategoryByName from 'lib/category/getUserCategoryByName';
|
||||||
|
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default apiHandler({
|
export default apiHandler({
|
||||||
get: getCatgories,
|
get: getCategories,
|
||||||
post: createCategory,
|
post: createCategory,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getCatgories({ res, user }) {
|
async function getCategories({ res, user }) {
|
||||||
const categories = await getUserCategories(user);
|
const categories = await getUserCategories(user);
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
categories,
|
categories,
|
||||||
@@ -18,18 +18,18 @@ async function getCatgories({ res, user }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createCategory({ req, res, user }) {
|
async function createCategory({ req, res, user }) {
|
||||||
const { name } = await CategorieBodySchema.validate(req.body);
|
const { name } = await CategoryBodySchema.validate(req.body);
|
||||||
|
|
||||||
const category = await getUserCategoryByName(user, name);
|
const category = await getUserCategoryByName(user, name);
|
||||||
if (category) {
|
if (category) {
|
||||||
throw new Error("Category name already used");
|
throw new Error('Category name already used');
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryCreated = await prisma.category.create({
|
const categoryCreated = await prisma.category.create({
|
||||||
data: { name, authorId: user.id },
|
data: { name, authorId: user.id },
|
||||||
});
|
});
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
success: "Category successfully created",
|
success: 'Category successfully created',
|
||||||
categoryId: categoryCreated.id,
|
categoryId: categoryCreated.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { createReadStream } from "node:fs";
|
import { createReadStream } from 'node:fs';
|
||||||
import { resolve } from "node:path";
|
import { resolve } from 'node:path';
|
||||||
import {
|
import {
|
||||||
downloadImageFromUrl,
|
downloadImageFromUrl,
|
||||||
getFavicon,
|
getFavicon,
|
||||||
makeRequestWithUserAgent,
|
makeRequestWithUserAgent,
|
||||||
} from "lib/request";
|
} from 'lib/request';
|
||||||
import { Favicon } from "types/types";
|
import { Favicon } from 'types/types';
|
||||||
import { buildFaviconUrl, findFaviconPath } from "lib/url";
|
import { buildFaviconUrl, findFaviconPath } from 'lib/url';
|
||||||
import { convertBase64ToBuffer, isBase64Image, isImage } from "lib/image";
|
import { convertBase64ToBuffer, isBase64Image, isImage } from 'lib/image';
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse,
|
res: NextApiResponse,
|
||||||
) {
|
) {
|
||||||
const urlParam = (req.query?.urlParam as string) || "";
|
const urlParam = (req.query?.urlParam as string) || '';
|
||||||
if (!urlParam) {
|
if (!urlParam) {
|
||||||
throw new Error("URL is missing");
|
throw new Error('URL is missing');
|
||||||
}
|
}
|
||||||
|
|
||||||
const faviconRequestUrl = buildFaviconUrl(urlParam, "/favicon.ico");
|
const faviconRequestUrl = buildFaviconUrl(urlParam, '/favicon.ico');
|
||||||
try {
|
try {
|
||||||
return sendImage(res, await getFavicon(faviconRequestUrl));
|
return sendImage(res, await getFavicon(faviconRequestUrl));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
"[Favicon]",
|
'[Favicon]',
|
||||||
`[first: ${faviconRequestUrl}]`,
|
`[first: ${faviconRequestUrl}]`,
|
||||||
"Unable to retrieve favicon from favicon.ico url",
|
'Unable to retrieve favicon from favicon.ico url',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,9 +36,9 @@ export default async function handler(
|
|||||||
const faviconPath = findFaviconPath(documentAsText);
|
const faviconPath = findFaviconPath(documentAsText);
|
||||||
if (!faviconPath) {
|
if (!faviconPath) {
|
||||||
console.error(
|
console.error(
|
||||||
"[Favicon]",
|
'[Favicon]',
|
||||||
`[first: ${faviconRequestUrl}]`,
|
`[first: ${faviconRequestUrl}]`,
|
||||||
"No link/href attribute found",
|
'No link/href attribute found',
|
||||||
);
|
);
|
||||||
return sendDefaultImage(res);
|
return sendDefaultImage(res);
|
||||||
}
|
}
|
||||||
@@ -46,50 +46,50 @@ export default async function handler(
|
|||||||
const finalUrl = buildFaviconUrl(requestDocument.url, faviconPath);
|
const finalUrl = buildFaviconUrl(requestDocument.url, faviconPath);
|
||||||
try {
|
try {
|
||||||
if (!faviconPath) {
|
if (!faviconPath) {
|
||||||
throw new Error("Unable to find favicon path");
|
throw new Error('Unable to find favicon path');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBase64Image(faviconPath)) {
|
if (isBase64Image(faviconPath)) {
|
||||||
console.log(
|
console.log(
|
||||||
"[Favicon]",
|
'[Favicon]',
|
||||||
`[second: ${faviconRequestUrl}]`,
|
`[second: ${faviconRequestUrl}]`,
|
||||||
"info: base64, convert it to buffer",
|
'info: base64, convert it to buffer',
|
||||||
);
|
);
|
||||||
const buffer = convertBase64ToBuffer(faviconPath);
|
const buffer = convertBase64ToBuffer(faviconPath);
|
||||||
return sendImage(res, {
|
return sendImage(res, {
|
||||||
buffer,
|
buffer,
|
||||||
type: "image/x-icon",
|
type: 'image/x-icon',
|
||||||
size: buffer.length,
|
size: buffer.length,
|
||||||
url: faviconPath,
|
url: faviconPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalUrl = faviconPath.startsWith("http")
|
const finalUrl = faviconPath.startsWith('http')
|
||||||
? faviconPath
|
? faviconPath
|
||||||
: buildFaviconUrl(requestDocument.url, faviconPath);
|
: buildFaviconUrl(requestDocument.url, faviconPath);
|
||||||
const favicon = await downloadImageFromUrl(finalUrl);
|
const favicon = await downloadImageFromUrl(finalUrl);
|
||||||
if (!isImage(favicon.type)) {
|
if (!isImage(favicon.type)) {
|
||||||
throw new Error("Favicon path does not return an image");
|
throw new Error('Favicon path does not return an image');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[Favicon]", `[second: ${finalUrl}]`, "success: image found");
|
console.log('[Favicon]', `[second: ${finalUrl}]`, 'success: image found');
|
||||||
return sendImage(res, favicon);
|
return sendImage(res, favicon);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error?.message || "Unable to retrieve favicon";
|
const errorMessage = error?.message || 'Unable to retrieve favicon';
|
||||||
console.log("[Favicon]", `[second: ${finalUrl}], error:`, errorMessage);
|
console.log('[Favicon]', `[second: ${finalUrl}], error:`, errorMessage);
|
||||||
return sendDefaultImage(res);
|
return sendDefaultImage(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendImage(res: NextApiResponse, { buffer, type, size }: Favicon) {
|
function sendImage(res: NextApiResponse, { buffer, type, size }: Favicon) {
|
||||||
res.setHeader("Content-Type", type);
|
res.setHeader('Content-Type', type);
|
||||||
res.setHeader("Content-Length", size);
|
res.setHeader('Content-Length', size);
|
||||||
res.send(buffer);
|
res.send(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendDefaultImage(res: NextApiResponse) {
|
function sendDefaultImage(res: NextApiResponse) {
|
||||||
const readStream = createReadStream(
|
const readStream = createReadStream(
|
||||||
resolve(process.cwd(), "./public/empty-image.png"),
|
resolve(process.cwd(), './public/empty-image.png'),
|
||||||
);
|
);
|
||||||
res.writeHead(206);
|
res.writeHead(206);
|
||||||
readStream.pipe(res);
|
readStream.pipe(res);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { apiHandler } from "lib/api/handler";
|
import { apiHandler } from 'lib/api/handler';
|
||||||
import getUserLink from "lib/link/getUserLink";
|
import getUserLink from 'lib/link/getUserLink';
|
||||||
import { LinkBodySchema, LinkQuerySchema } from "lib/link/linkValidationSchema";
|
import { LinkBodySchema, LinkQuerySchema } from 'lib/link/linkValidationSchema';
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default apiHandler({
|
export default apiHandler({
|
||||||
put: editLink,
|
put: editLink,
|
||||||
@@ -16,7 +16,7 @@ async function editLink({ req, res, user }) {
|
|||||||
|
|
||||||
const link = await getUserLink(user, lid);
|
const link = await getUserLink(user, lid);
|
||||||
if (!link) {
|
if (!link) {
|
||||||
throw new Error("Unable to find link " + lid);
|
throw new Error('Unable to find link ' + lid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -25,7 +25,7 @@ async function editLink({ req, res, user }) {
|
|||||||
link.favorite === favorite &&
|
link.favorite === favorite &&
|
||||||
link.categoryId === categoryId
|
link.categoryId === categoryId
|
||||||
) {
|
) {
|
||||||
throw new Error("You must update at least one field");
|
throw new Error('You must update at least one field');
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.link.update({
|
await prisma.link.update({
|
||||||
@@ -40,7 +40,7 @@ async function editLink({ req, res, user }) {
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
.status(200)
|
.status(200)
|
||||||
.send({ success: "Link successfully updated", categoryId });
|
.send({ success: 'Link successfully updated', categoryId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteLink({ req, res, user }) {
|
async function deleteLink({ req, res, user }) {
|
||||||
@@ -48,7 +48,7 @@ async function deleteLink({ req, res, user }) {
|
|||||||
|
|
||||||
const link = await getUserLink(user, lid);
|
const link = await getUserLink(user, lid);
|
||||||
if (!link) {
|
if (!link) {
|
||||||
throw new Error("Unable to find link " + lid);
|
throw new Error('Unable to find link ' + lid);
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.link.delete({
|
await prisma.link.delete({
|
||||||
@@ -56,7 +56,7 @@ async function deleteLink({ req, res, user }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return res.send({
|
return res.send({
|
||||||
success: "Link successfully deleted",
|
success: 'Link successfully deleted',
|
||||||
categoryId: link.categoryId,
|
categoryId: link.categoryId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { apiHandler } from "lib/api/handler";
|
import { apiHandler } from 'lib/api/handler';
|
||||||
import getUserCategory from "lib/category/getUserCategory";
|
import getUserCategory from 'lib/category/getUserCategory';
|
||||||
import getUserLinkByName from "lib/link/getLinkFromCategoryByName";
|
import getUserLinkByName from 'lib/link/getLinkFromCategoryByName';
|
||||||
import { LinkBodySchema } from "lib/link/linkValidationSchema";
|
import { LinkBodySchema } from 'lib/link/linkValidationSchema';
|
||||||
|
|
||||||
import prisma from "utils/prisma";
|
import prisma from 'utils/prisma';
|
||||||
|
|
||||||
export default apiHandler({
|
export default apiHandler({
|
||||||
post: createLink,
|
post: createLink,
|
||||||
@@ -16,12 +16,12 @@ async function createLink({ req, res, user }) {
|
|||||||
|
|
||||||
const link = await getUserLinkByName(user, name, categoryId);
|
const link = await getUserLinkByName(user, name, categoryId);
|
||||||
if (link) {
|
if (link) {
|
||||||
throw new Error("Link name is already used in this category");
|
throw new Error('Link name is already used in this category');
|
||||||
}
|
}
|
||||||
|
|
||||||
const category = await getUserCategory(user, categoryId);
|
const category = await getUserCategory(user, categoryId);
|
||||||
if (!category) {
|
if (!category) {
|
||||||
throw new Error("Unable to find category " + categoryId);
|
throw new Error('Unable to find category ' + categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.link.create({
|
await prisma.link.create({
|
||||||
@@ -34,5 +34,5 @@ async function createLink({ req, res, user }) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.send({ success: "Link successfully created", categoryId });
|
return res.send({ success: 'Link successfully created', categoryId });
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user