mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 22:53:25 +00:00
Add translation (#9)
* feat(wip): translation * fix: some i18n errors + use ssr translation * fix: i18next implementation * fix: tsc errors * feat: i18n middleware * feat: translate link view home page * feat: translate quick actions * feat: translate search modal * feat: translate side menu * feat: native error boundary + translation * feat: translate error pages * feat: translate category forms * feat: translate link forms * refactor: LangSelector is no longer absolute by default
This commit is contained in:
47
middleware.ts
Normal file
47
middleware.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import acceptLanguage from "accept-language";
|
||||
import { NextResponse } from "next/server";
|
||||
import { i18n } from "./next-i18next.config";
|
||||
|
||||
acceptLanguage.languages(i18n.locales);
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)"],
|
||||
};
|
||||
|
||||
const cookieName = "i18next";
|
||||
|
||||
// Source : https://github.com/i18next/next-app-dir-i18next-example/blob/3d653a46ae33f46abc011b6186f7a4595b84129f/middleware.js
|
||||
export function middleware(req) {
|
||||
if (
|
||||
req.nextUrl.pathname.indexOf("icon") > -1 ||
|
||||
req.nextUrl.pathname.indexOf("chrome") > -1
|
||||
)
|
||||
return NextResponse.next();
|
||||
let lng;
|
||||
if (req.cookies.has(cookieName))
|
||||
lng = acceptLanguage.get(req.cookies.get(cookieName).value);
|
||||
if (!lng) lng = acceptLanguage.get(req.headers.get("Accept-Language"));
|
||||
if (!lng) lng = i18n.defaultLocale;
|
||||
|
||||
// Redirect if lng in path is not supported
|
||||
if (
|
||||
!i18n.locales.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
|
||||
!req.nextUrl.pathname.startsWith("/_next")
|
||||
) {
|
||||
return NextResponse.redirect(
|
||||
new URL(`/${lng}${req.nextUrl.pathname}`, req.url)
|
||||
);
|
||||
}
|
||||
|
||||
if (req.headers.has("referer")) {
|
||||
const refererUrl = new URL(req.headers.get("referer"));
|
||||
const lngInReferer = i18n.locales.find((l) =>
|
||||
refererUrl.pathname.startsWith(`/${l}`)
|
||||
);
|
||||
const response = NextResponse.next();
|
||||
if (lngInReferer) response.cookies.set(cookieName, lngInReferer);
|
||||
return response;
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
10
next-i18next.config.js
Normal file
10
next-i18next.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('next-i18next').UserConfig} */
|
||||
module.exports = {
|
||||
// debug: process.env.NODE_ENV === "development",
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en", "fr"],
|
||||
},
|
||||
reloadOnPrerender: process.env.NODE_ENV === "development",
|
||||
returnNull: false,
|
||||
};
|
||||
@@ -1,5 +1,8 @@
|
||||
const { i18n } = require("./next-i18next.config");
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const config = {
|
||||
i18n,
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
|
||||
416
package-lock.json
generated
416
package-lock.json
generated
@@ -8,22 +8,24 @@
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.5.2",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"accept-language": "^3.0.18",
|
||||
"axios": "^1.6.1",
|
||||
"framer-motion": "^10.16.4",
|
||||
"next": "^14.0.1",
|
||||
"i18next": "^23.7.1",
|
||||
"next": "^14.0.2",
|
||||
"next-auth": "^4.24.4",
|
||||
"next-i18next": "^15.0.0",
|
||||
"next-seo": "^6.4.0",
|
||||
"node-html-parser": "^6.1.11",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-i18next": "^13.3.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-select": "^5.8.0",
|
||||
"sass": "^1.69.5",
|
||||
"sharp": "^0.32.6",
|
||||
"toastr": "^2.1.4",
|
||||
"yup": "^1.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -33,7 +35,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-next": "14.0.1",
|
||||
"eslint-config-next": "14.0.2",
|
||||
"prisma": "^5.5.2",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
@@ -60,11 +62,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.21.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz",
|
||||
"integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==",
|
||||
"version": "7.22.13",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
|
||||
"integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
|
||||
"dependencies": {
|
||||
"@babel/highlight": "^7.18.6"
|
||||
"@babel/highlight": "^7.22.13",
|
||||
"chalk": "^2.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -108,11 +111,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.21.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz",
|
||||
"integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==",
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
|
||||
"integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.21.4",
|
||||
"@babel/types": "^7.23.0",
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
"jsesc": "^2.5.1"
|
||||
@@ -241,9 +244,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-environment-visitor": {
|
||||
"version": "7.18.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
|
||||
"integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==",
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
|
||||
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -260,23 +263,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-function-name": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz",
|
||||
"integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==",
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
|
||||
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.20.7",
|
||||
"@babel/types": "^7.21.0"
|
||||
"@babel/template": "^7.22.15",
|
||||
"@babel/types": "^7.23.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-hoist-variables": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
|
||||
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
|
||||
"integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.18.6"
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -397,28 +400,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-split-export-declaration": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
|
||||
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
|
||||
"version": "7.22.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
|
||||
"integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.18.6"
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.19.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz",
|
||||
"integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==",
|
||||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
|
||||
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
|
||||
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==",
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
|
||||
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -459,12 +462,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
|
||||
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
|
||||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
|
||||
"integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.18.6",
|
||||
"chalk": "^2.0.0",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"chalk": "^2.4.2",
|
||||
"js-tokens": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -472,9 +475,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.21.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz",
|
||||
"integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==",
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
|
||||
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
@@ -1659,42 +1662,42 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
|
||||
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz",
|
||||
"integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.11"
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.20.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz",
|
||||
"integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==",
|
||||
"version": "7.22.15",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
|
||||
"integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/parser": "^7.20.7",
|
||||
"@babel/types": "^7.20.7"
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/parser": "^7.22.15",
|
||||
"@babel/types": "^7.22.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.21.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz",
|
||||
"integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==",
|
||||
"version": "7.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
|
||||
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.21.4",
|
||||
"@babel/generator": "^7.21.4",
|
||||
"@babel/helper-environment-visitor": "^7.18.9",
|
||||
"@babel/helper-function-name": "^7.21.0",
|
||||
"@babel/helper-hoist-variables": "^7.18.6",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6",
|
||||
"@babel/parser": "^7.21.4",
|
||||
"@babel/types": "^7.21.4",
|
||||
"@babel/code-frame": "^7.22.13",
|
||||
"@babel/generator": "^7.23.0",
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
"@babel/helper-function-name": "^7.23.0",
|
||||
"@babel/helper-hoist-variables": "^7.22.5",
|
||||
"@babel/helper-split-export-declaration": "^7.22.6",
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@babel/types": "^7.23.0",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
@@ -1703,12 +1706,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.21.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz",
|
||||
"integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==",
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
|
||||
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.19.4",
|
||||
"@babel/helper-validator-identifier": "^7.19.1",
|
||||
"@babel/helper-string-parser": "^7.22.5",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2012,23 +2015,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.1.tgz",
|
||||
"integrity": "sha512-Ms8ZswqY65/YfcjrlcIwMPD7Rg/dVjdLapMcSHG26W6O67EJDF435ShW4H4LXi1xKO1oRc97tLXUpx8jpLe86A=="
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.2.tgz",
|
||||
"integrity": "sha512-HAW1sljizEaduEOes/m84oUqeIDAUYBR1CDwu2tobNlNDFP3cSm9d6QsOsGeNlIppU1p/p1+bWbYCbvwjFiceA=="
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.0.1.tgz",
|
||||
"integrity": "sha512-bLjJMwXdzvhnQOnxvHoTTUh/+PYk6FF/DCgHi4BXwXCINer+o1ZYfL9aVeezj/oI7wqGJOqwGIXrlBvPbAId3w==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.0.2.tgz",
|
||||
"integrity": "sha512-APrYFsXfAhnysycqxHcpg6Y4i7Ukp30GzVSZQRKT3OczbzkqGjt33vNhScmgoOXYBU1CfkwgtXmNxdiwv1jKmg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"glob": "7.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.1.tgz",
|
||||
"integrity": "sha512-JyxnGCS4qT67hdOKQ0CkgFTp+PXub5W1wsGvIq98TNbF3YEIN7iDekYhYsZzc8Ov0pWEsghQt+tANdidITCLaw==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.2.tgz",
|
||||
"integrity": "sha512-i+jQY0fOb8L5gvGvojWyZMfQoQtDVB2kYe7fufOEiST6sicvzI2W5/EXo4lX5bLUjapHKe+nFxuVv7BA+Pd7LQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2041,9 +2044,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.1.tgz",
|
||||
"integrity": "sha512-625Z7bb5AyIzswF9hvfZWa+HTwFZw+Jn3lOBNZB87lUS0iuCYDHqk3ujuHCkiyPtSC0xFBtYDLcrZ11mF/ap3w==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.2.tgz",
|
||||
"integrity": "sha512-zRCAO0d2hW6gBEa4wJaLn+gY8qtIqD3gYd9NjruuN98OCI6YyelmhWVVLlREjS7RYrm9OUQIp/iVJFeB6kP1hg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2056,9 +2059,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.1.tgz",
|
||||
"integrity": "sha512-iVpn3KG3DprFXzVHM09kvb//4CNNXBQ9NB/pTm8LO+vnnnaObnzFdS5KM+w1okwa32xH0g8EvZIhoB3fI3mS1g==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.2.tgz",
|
||||
"integrity": "sha512-tSJmiaon8YaKsVhi7GgRizZoV0N1Sx5+i+hFTrCKKQN7s3tuqW0Rov+RYdPhAv/pJl4qiG+XfSX4eJXqpNg3dA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2071,9 +2074,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.1.tgz",
|
||||
"integrity": "sha512-mVsGyMxTLWZXyD5sen6kGOTYVOO67lZjLApIj/JsTEEohDDt1im2nkspzfV5MvhfS7diDw6Rp/xvAQaWZTv1Ww==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.2.tgz",
|
||||
"integrity": "sha512-dXJLMSEOwqJKcag1BeX1C+ekdPPJ9yXbWIt3nAadhbLx5CjACoB2NQj9Xcqu2tmdr5L6m34fR+fjGPs+ZVPLzA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2086,9 +2089,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.1.tgz",
|
||||
"integrity": "sha512-wMqf90uDWN001NqCM/auRl3+qVVeKfjJdT9XW+RMIOf+rhUzadmYJu++tp2y+hUbb6GTRhT+VjQzcgg/QTD9NQ==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.2.tgz",
|
||||
"integrity": "sha512-WC9KAPSowj6as76P3vf1J3mf2QTm3Wv3FBzQi7UJ+dxWjK3MhHVWsWUo24AnmHx9qDcEtHM58okgZkXVqeLB+Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2101,9 +2104,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.1.tgz",
|
||||
"integrity": "sha512-ol1X1e24w4j4QwdeNjfX0f+Nza25n+ymY0T2frTyalVczUmzkVD7QGgPTZMHfR1aLrO69hBs0G3QBYaj22J5GQ==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.2.tgz",
|
||||
"integrity": "sha512-KSSAwvUcjtdZY4zJFa2f5VNJIwuEVnOSlqYqbQIawREJA+gUI6egeiRu290pXioQXnQHYYdXmnVNZ4M+VMB7KQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2116,9 +2119,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.1.tgz",
|
||||
"integrity": "sha512-WEmTEeWs6yRUEnUlahTgvZteh5RJc4sEjCQIodJlZZ5/VJwVP8p2L7l6VhzQhT4h7KvLx/Ed4UViBdne6zpIsw==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.2.tgz",
|
||||
"integrity": "sha512-2/O0F1SqJ0bD3zqNuYge0ok7OEWCQwk55RPheDYD0va5ij7kYwrFkq5ycCRN0TLjLfxSF6xI5NM6nC5ux7svEQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2131,9 +2134,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.1.tgz",
|
||||
"integrity": "sha512-oFpHphN4ygAgZUKjzga7SoH2VGbEJXZa/KL8bHCAwCjDWle6R1SpiGOdUdA8EJ9YsG1TYWpzY6FTbUA+iAJeww==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.2.tgz",
|
||||
"integrity": "sha512-vJI/x70Id0oN4Bq/R6byBqV1/NS5Dl31zC+lowO8SDu1fHmUxoAdILZR5X/sKbiJpuvKcCrwbYgJU8FF/Gh50Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -2146,9 +2149,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.1.tgz",
|
||||
"integrity": "sha512-FFp3nOJ/5qSpeWT0BZQ+YE1pSMk4IMpkME/1DwKBwhg4mJLB9L+6EXuJi4JEwaJdl5iN+UUlmUD3IsR1kx5fAg==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.2.tgz",
|
||||
"integrity": "sha512-Ut4LXIUvC5m8pHTe2j0vq/YDnTEyq6RSR9vHYPqnELrDapPhLNz9Od/L5Ow3J8RNDWpEnfCiQXuVdfjlNEJ7ug==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2570,6 +2573,15 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
|
||||
"integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
|
||||
"dependencies": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -2876,6 +2888,15 @@
|
||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/accept-language": {
|
||||
"version": "3.0.18",
|
||||
"resolved": "https://registry.npmjs.org/accept-language/-/accept-language-3.0.18.tgz",
|
||||
"integrity": "sha512-sUofgqBPzgfcF20sPoBYGQ1IhQLt2LSkxTnlQSuLF3n5gPEqd5AimbvOvHEi0T1kLMiGVqPWzI5a9OteBRth3A==",
|
||||
"dependencies": {
|
||||
"bcp47": "^1.1.2",
|
||||
"stable": "^0.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.11.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
|
||||
@@ -3229,6 +3250,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/bcp47": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz",
|
||||
"integrity": "sha512-JnkkL4GUpOvvanH9AZPX38CxhiLsXMBicBY2IAtqiVN8YulGDQybUydWA4W6yAMtw6iShtw+8HEF6cfrTHU+UQ==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
@@ -3537,6 +3566,16 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.33.2",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.2.tgz",
|
||||
"integrity": "sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ==",
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.27.2",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.27.2.tgz",
|
||||
@@ -4105,12 +4144,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-next": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.0.1.tgz",
|
||||
"integrity": "sha512-QfIFK2WD39H4WOespjgf6PLv9Bpsd7KGGelCtmq4l67nGvnlsGpuvj0hIT+aIy6p5gKH+lAChYILsyDlxP52yg==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.0.2.tgz",
|
||||
"integrity": "sha512-CasWThlsyIcg/a+clU6KVOMTieuDhTztsrqvniP6AsRki9v7FnojTa7vKQOYM8QSOsQdZ/aElLD1Y2Oc8/PsIg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "14.0.1",
|
||||
"@next/eslint-plugin-next": "14.0.2",
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0",
|
||||
"eslint-import-resolver-node": "^0.3.6",
|
||||
@@ -5114,6 +5153,41 @@
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "23.7.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.7.1.tgz",
|
||||
"integrity": "sha512-lD2lZkdhb9jnIGGc2ja8ER6cGStgJ+jFVL336Sa1C37//2Q8odC617ek2oafYbbs0/a+BbUqKe5JPST2r88UEQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-fs-backend": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.2.0.tgz",
|
||||
"integrity": "sha512-VOPHhdDX0M/csRqEw+9Ectpf6wvTIg1MZDfAHxc3JKnAlJz7fcZSAKAeyDohOq0xuLx57esYpJopIvBaRb0Bag=="
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -5591,11 +5665,6 @@
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz",
|
||||
"integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw=="
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -5873,11 +5942,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.0.1.tgz",
|
||||
"integrity": "sha512-s4YaLpE4b0gmb3ggtmpmV+wt+lPRuGtANzojMQ2+gmBpgX9w5fTbjsy6dXByBuENsdCX5pukZH/GxdFgO62+pA==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.0.2.tgz",
|
||||
"integrity": "sha512-jsAU2CkYS40GaQYOiLl9m93RTv2DA/tTJ0NRlmZIBIL87YwQ/xR8k796z7IqgM3jydI8G25dXvyYMC9VDIevIg==",
|
||||
"dependencies": {
|
||||
"@next/env": "14.0.1",
|
||||
"@next/env": "14.0.2",
|
||||
"@swc/helpers": "0.5.2",
|
||||
"busboy": "1.6.0",
|
||||
"caniuse-lite": "^1.0.30001406",
|
||||
@@ -5892,15 +5961,15 @@
|
||||
"node": ">=18.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "14.0.1",
|
||||
"@next/swc-darwin-x64": "14.0.1",
|
||||
"@next/swc-linux-arm64-gnu": "14.0.1",
|
||||
"@next/swc-linux-arm64-musl": "14.0.1",
|
||||
"@next/swc-linux-x64-gnu": "14.0.1",
|
||||
"@next/swc-linux-x64-musl": "14.0.1",
|
||||
"@next/swc-win32-arm64-msvc": "14.0.1",
|
||||
"@next/swc-win32-ia32-msvc": "14.0.1",
|
||||
"@next/swc-win32-x64-msvc": "14.0.1"
|
||||
"@next/swc-darwin-arm64": "14.0.2",
|
||||
"@next/swc-darwin-x64": "14.0.2",
|
||||
"@next/swc-linux-arm64-gnu": "14.0.2",
|
||||
"@next/swc-linux-arm64-musl": "14.0.2",
|
||||
"@next/swc-linux-x64-gnu": "14.0.2",
|
||||
"@next/swc-linux-x64-musl": "14.0.2",
|
||||
"@next/swc-win32-arm64-msvc": "14.0.2",
|
||||
"@next/swc-win32-ia32-msvc": "14.0.2",
|
||||
"@next/swc-win32-x64-msvc": "14.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
@@ -5944,6 +6013,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-i18next": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-15.0.0.tgz",
|
||||
"integrity": "sha512-9iGEU4dt1YCC5CXh6H8YHmDpmeWKjxES6XfoABxy9mmfaLLJcqS92F56ZKmVuZUPXEOLtgY/JtsnxsHYom9J4g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2",
|
||||
"@types/hoist-non-react-statics": "^3.3.4",
|
||||
"core-js": "^3",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"i18next-fs-backend": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": "^23.6.0",
|
||||
"next": ">= 12.0.0",
|
||||
"react": ">= 17.0.2",
|
||||
"react-i18next": "^13.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/next-seo": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/next-seo/-/next-seo-6.4.0.tgz",
|
||||
@@ -5975,9 +6079,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi/node_modules/semver": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz",
|
||||
"integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
@@ -6549,17 +6653,6 @@
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-error-boundary": {
|
||||
"version": "4.0.11",
|
||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz",
|
||||
"integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hotkeys-hook": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz",
|
||||
@@ -6569,6 +6662,27 @@
|
||||
"react-dom": ">=16.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "13.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.3.1.tgz",
|
||||
"integrity": "sha512-JAtYREK879JXaN9GdzfBI4yJeo/XyLeXWUsRABvYXiFUakhZJ40l+kaTo+i+A/3cKIED41kS/HAbZ5BzFtq/Og==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.22.5",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "4.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.11.0.tgz",
|
||||
@@ -6678,9 +6792,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
||||
},
|
||||
"node_modules/regenerator-transform": {
|
||||
"version": "0.15.1",
|
||||
@@ -7098,6 +7212,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stable": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
||||
"integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
|
||||
"deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility"
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
@@ -7414,14 +7534,6 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toastr": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/toastr/-/toastr-2.1.4.tgz",
|
||||
"integrity": "sha512-LIy77F5n+sz4tefMmFOntcJ6HL0Fv3k1TDnNmFZ0bU/GcvIIfy6eG2v7zQmMiYgaalAiUv75ttFrPn5s0gyqlA==",
|
||||
"dependencies": {
|
||||
"jquery": ">=1.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||
@@ -7698,6 +7810,14 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
|
||||
10
package.json
10
package.json
@@ -10,22 +10,24 @@
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.5.2",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"accept-language": "^3.0.18",
|
||||
"axios": "^1.6.1",
|
||||
"framer-motion": "^10.16.4",
|
||||
"next": "^14.0.1",
|
||||
"i18next": "^23.7.1",
|
||||
"next": "^14.0.2",
|
||||
"next-auth": "^4.24.4",
|
||||
"next-i18next": "^15.0.0",
|
||||
"next-seo": "^6.4.0",
|
||||
"node-html-parser": "^6.1.11",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-i18next": "^13.3.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-select": "^5.8.0",
|
||||
"sass": "^1.69.5",
|
||||
"sharp": "^0.32.6",
|
||||
"toastr": "^2.1.4",
|
||||
"yup": "^1.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -35,7 +37,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-config-next": "14.0.1",
|
||||
"eslint-config-next": "14.0.2",
|
||||
"prisma": "^5.5.2",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
|
||||
33
public/locales/en/common.json
Normal file
33
public/locales/en/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"back-home": "← Back to home page",
|
||||
"logout": "Logout",
|
||||
"login": "Login",
|
||||
"link": {
|
||||
"links": "Links",
|
||||
"link": "Link",
|
||||
"name": "Link name",
|
||||
"create": "Create a link",
|
||||
"edit": "Edit a link",
|
||||
"remove": "Delete a link",
|
||||
"remove-confirm": "Confirm deletion?"
|
||||
},
|
||||
"category": {
|
||||
"categories": "Categories",
|
||||
"category": "Category",
|
||||
"name": "Category name",
|
||||
"create": "Create a category",
|
||||
"edit": "Edit a category",
|
||||
"remove": "Delete a category",
|
||||
"remove-confirm": "Confirm deletion?",
|
||||
"remove-description": "You must delete all links in this category before you can delete this category."
|
||||
},
|
||||
"favorite": "Favorite",
|
||||
"no-item-found": "No item found",
|
||||
"search": "Search",
|
||||
"avatar": "{{name}}'s avatar",
|
||||
"generic-error": "Something went wrong",
|
||||
"generic-error-description": "An error has occurred, if this happens again please <a href=\"https://github.com/Sonny93/my-links\" target=\"_blank\">create an issue</a> with as much detail as possible.",
|
||||
"retry": "Retry"
|
||||
}
|
||||
5
public/locales/en/home.json
Normal file
5
public/locales/en/home.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"select-categorie": "Please select a category",
|
||||
"or-create-one": "or create one",
|
||||
"no-link": "No link for <b>{{name}}</b>"
|
||||
}
|
||||
5
public/locales/en/login.json
Normal file
5
public/locales/en/login.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Authentication",
|
||||
"informative-text": "Authentication required to use MyLinks",
|
||||
"continue-with": "Continue with {{provider}}"
|
||||
}
|
||||
33
public/locales/fr/common.json
Normal file
33
public/locales/fr/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"confirm": "Confirmer",
|
||||
"cancel": "Annuler",
|
||||
"back-home": "← Revenir à l'accueil",
|
||||
"logout": "Déconnexion",
|
||||
"login": "Connexion",
|
||||
"link": {
|
||||
"links": "Liens",
|
||||
"link": "Lien",
|
||||
"name": "Nom du lien",
|
||||
"create": "Créer un lien",
|
||||
"edit": "Modifier un lien",
|
||||
"remove": "Supprimer un lien",
|
||||
"remove-confirm": "Confirmer la suppression ?"
|
||||
},
|
||||
"category": {
|
||||
"categories": "Catégories",
|
||||
"category": "Catégorie",
|
||||
"name": "Nom de la catégorie",
|
||||
"create": "Créer une catégorie",
|
||||
"edit": "Modifier une catégorie",
|
||||
"remove": "Supprimer une categorie",
|
||||
"remove-confirm": "Confirmer la suppression ?",
|
||||
"remove-description": "Vous devez supprimer tous les liens de cette catégorie avant de pouvoir supprimer cette catégorie"
|
||||
},
|
||||
"favorite": "Favoris",
|
||||
"no-item-found": "Aucun élément trouvé",
|
||||
"search": "Rechercher",
|
||||
"avatar": "Avatar de {{name}}",
|
||||
"generic-error": "Une erreur est survenue",
|
||||
"generic-error-description": "Une erreur est survenue, si cela se reproduit merci de <a href=\"https://github.com/Sonny93/my-links\" target=\"_blank\">créer une issue</a> avec le maximum de détails.",
|
||||
"retry": "Recommencer"
|
||||
}
|
||||
5
public/locales/fr/home.json
Normal file
5
public/locales/fr/home.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"select-categorie": "Veuillez séléctionner une categorie",
|
||||
"or-create-one": "ou en créer une",
|
||||
"no-link": "Aucun lien pour <b>{{name}}</b>"
|
||||
}
|
||||
5
public/locales/fr/login.json
Normal file
5
public/locales/fr/login.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Authentification",
|
||||
"informative-text": "Authentification requise pour utiliser ce service",
|
||||
"continue-with": "Continuer avec {{provider}}"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
|
||||
function fallbackRender({ error, resetErrorBoundary }) {
|
||||
return (
|
||||
<div role="alert">
|
||||
<p>Something went wrong:</p>
|
||||
<pre style={{ color: "red" }}>{error.message}</pre>
|
||||
<button onClick={resetErrorBoundary}>retry</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AppErrorBoundary({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ErrorBoundary fallbackRender={fallbackRender}>{children}</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
}
|
||||
|
||||
& ul {
|
||||
flex: 1;
|
||||
animation: fadein 0.3s both;
|
||||
}
|
||||
|
||||
|
||||
48
src/components/ErrorBoundary/ErrorBoundary.jsx
Normal file
48
src/components/ErrorBoundary/ErrorBoundary.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { withTranslation } from 'next-i18next';
|
||||
import React from 'react';
|
||||
import LangSelector from '../LangSelector';
|
||||
import styles from './error-boundary.module.scss';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { error: false, errorInfo: null };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
// Catch errors in any components below and re-render with error message
|
||||
this.setState({
|
||||
error: error,
|
||||
errorInfo: errorInfo
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.errorInfo) return this.props.children;
|
||||
|
||||
return (
|
||||
<div className={styles["error-boundary"]}>
|
||||
<div className={styles["boundary-content"]}>
|
||||
<h1>{this.props.t('common:generic-error')}</h1>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.props.t('common:generic-error-description')
|
||||
}}
|
||||
/>
|
||||
<button onClick={() => window.location.reload()}>
|
||||
{this.props.t('common:retry')}
|
||||
</button>
|
||||
<details>
|
||||
<summary>{this.state.error && this.state.error.toString()}</summary>
|
||||
<code>{this.state.errorInfo.componentStack}</code>
|
||||
</details>
|
||||
<div className='lang-selector'>
|
||||
<LangSelector />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation()(ErrorBoundary);
|
||||
29
src/components/ErrorBoundary/error-boundary.module.scss
Normal file
29
src/components/ErrorBoundary/error-boundary.module.scss
Normal file
@@ -0,0 +1,29 @@
|
||||
.error-boundary {
|
||||
padding: 3em 2em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
& .boundary-content {
|
||||
height: 100%;
|
||||
width: 600px;
|
||||
gap: 2em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& button {
|
||||
width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.error-boundary .boundary-content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import MessageManager from "components/MessageManager/MessageManager";
|
||||
import { i18n, useTranslation } from "next-i18next";
|
||||
import { NextSeo } from "next-seo";
|
||||
import Link from "next/link";
|
||||
|
||||
import MessageManager from "components/MessageManager/MessageManager";
|
||||
|
||||
interface FormProps {
|
||||
title: string;
|
||||
|
||||
@@ -29,11 +29,13 @@ export default function Form({
|
||||
infoMessage,
|
||||
canSubmit,
|
||||
handleSubmit,
|
||||
textBtnConfirm = "Valider",
|
||||
textBtnConfirm = i18n.t("common:confirm"),
|
||||
classBtnConfirm = "",
|
||||
children,
|
||||
disableHomeLink = false,
|
||||
}: FormProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo title={title} />
|
||||
@@ -46,7 +48,7 @@ export default function Form({
|
||||
</form>
|
||||
{!disableHomeLink && (
|
||||
<Link href={categoryId ? `/?categoryId=${categoryId}` : "/"}>
|
||||
← Revenir à l'accueil
|
||||
{t("common:back-home")}
|
||||
</Link>
|
||||
)}
|
||||
<MessageManager
|
||||
|
||||
31
src/components/LangSelector.tsx
Normal file
31
src/components/LangSelector.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function LangSelector() {
|
||||
const router = useRouter();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const onToggleLanguageClick = (newLocale: string) => {
|
||||
const { pathname, asPath, query } = router;
|
||||
i18n.changeLanguage(newLocale);
|
||||
router.push({ pathname, query }, asPath, { locale: newLocale });
|
||||
};
|
||||
const languages = ["en", "fr"];
|
||||
|
||||
return (
|
||||
<select
|
||||
name="lng-select"
|
||||
id="lng-select"
|
||||
onChange={(event) => {
|
||||
onToggleLanguageClick(event.target.value);
|
||||
}}
|
||||
value={i18n.language}
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<option key={lang} value={lang}>
|
||||
{lang}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import { motion } from "framer-motion";
|
||||
import LinkTag from "next/link";
|
||||
|
||||
import { Category, Link } from "types";
|
||||
|
||||
import EditItem from "components/QuickActions/EditItem";
|
||||
import RemoveItem from "components/QuickActions/RemoveItem";
|
||||
import LinkItem from "./LinkItem";
|
||||
|
||||
import ButtonLink from "components/ButtonLink";
|
||||
import CreateItem from "components/QuickActions/CreateItem";
|
||||
import EditItem from "components/QuickActions/EditItem";
|
||||
import RemoveItem from "components/QuickActions/RemoveItem";
|
||||
import QuickActionSearch from "components/QuickActions/Search";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import LinkTag from "next/link";
|
||||
import { RxHamburgerMenu } from "react-icons/rx";
|
||||
import { Category, Link } from "types";
|
||||
import { TFunctionParam } from "types/i18next";
|
||||
import LinkItem from "./LinkItem";
|
||||
import styles from "./links.module.scss";
|
||||
|
||||
export default function Links({
|
||||
@@ -26,11 +25,13 @@ export default function Links({
|
||||
openMobileModal: () => void;
|
||||
openSearchModal: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation("home");
|
||||
|
||||
if (category === null) {
|
||||
return (
|
||||
<div className={styles["no-category"]}>
|
||||
<p>Veuillez séléctionner une categorié</p>
|
||||
<LinkTag href="/category/create">ou en créer une</LinkTag>
|
||||
<p>{t("home:select-categorie")}</p>
|
||||
<LinkTag href="/category/create">{t("home:or-create-one")}</LinkTag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -85,11 +86,14 @@ export default function Links({
|
||||
damping: 20,
|
||||
duration: 0.01,
|
||||
}}
|
||||
>
|
||||
Aucun lien pour <b>{name}</b>
|
||||
</motion.p>
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("home:no-link", { name } as TFunctionParam, {
|
||||
interpolation: { escapeValue: false },
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<LinkTag href={`/link/create?categoryId=${id}`}>
|
||||
Créer un lien
|
||||
{t("common:link.create")}
|
||||
</LinkTag>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { motion } from "framer-motion";
|
||||
import useIsMobile from "hooks/useIsMobile";
|
||||
import { CSSProperties, ReactNode } from "react";
|
||||
import LangSelector from "./LangSelector";
|
||||
|
||||
export default function PageTransition({
|
||||
className,
|
||||
children,
|
||||
style = {},
|
||||
hideLangageSelector = false,
|
||||
}: {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
style?: CSSProperties;
|
||||
hideLangageSelector?: boolean;
|
||||
}) {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
@@ -23,6 +29,11 @@ export default function PageTransition({
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
{!hideLangageSelector && !isMobile && (
|
||||
<div className="lang-selector">
|
||||
<LangSelector />
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
import LinkTag from "next/link";
|
||||
import { IoAddOutline } from "react-icons/io5";
|
||||
|
||||
import { Category } from "types";
|
||||
|
||||
import styles from "./quickactions.module.scss";
|
||||
|
||||
export default function CreateItem({
|
||||
@@ -12,12 +11,14 @@ export default function CreateItem({
|
||||
}: {
|
||||
type: "category" | "link";
|
||||
categoryId?: Category["id"];
|
||||
onClick?: (event: any) => void; // FIXME: find good event type
|
||||
onClick?: (event: any) => void;
|
||||
}) {
|
||||
const { t } = useTranslation("home");
|
||||
|
||||
return (
|
||||
<LinkTag
|
||||
href={`/${type}/create${categoryId && `?categoryId=${categoryId}`}`}
|
||||
title={`Create ${type}`}
|
||||
title={t(`common:${type}.create`)}
|
||||
className={styles["action"]}
|
||||
onClick={onClick && onClick}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import LinkTag from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { AiOutlineEdit } from "react-icons/ai";
|
||||
|
||||
import { Category, Link } from "types";
|
||||
|
||||
import styles from "./quickactions.module.scss";
|
||||
|
||||
export default function EditItem({
|
||||
@@ -13,13 +12,15 @@ export default function EditItem({
|
||||
}: {
|
||||
type: "category" | "link";
|
||||
id: Link["id"] | Category["id"];
|
||||
onClick?: (event: any) => void; // FIXME: find good event type
|
||||
onClick?: (event: any) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const { t } = useTranslation("home");
|
||||
|
||||
return (
|
||||
<LinkTag
|
||||
href={`/${type}/edit/${id}`}
|
||||
title={`Edit ${type}`}
|
||||
title={t(`common:${type}.edit`)}
|
||||
className={`${styles["action"]} ${className ? className : ""}`}
|
||||
onClick={onClick && onClick}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
import LinkTag from "next/link";
|
||||
import { CgTrashEmpty } from "react-icons/cg";
|
||||
|
||||
import { Category, Link } from "types";
|
||||
|
||||
import styles from "./quickactions.module.scss";
|
||||
|
||||
export default function RemoveItem({
|
||||
@@ -12,12 +11,14 @@ export default function RemoveItem({
|
||||
}: {
|
||||
type: "category" | "link";
|
||||
id: Link["id"] | Category["id"];
|
||||
onClick?: (event: any) => void; // FIXME: find good event type
|
||||
onClick?: (event: any) => void;
|
||||
}) {
|
||||
const { t } = useTranslation("home");
|
||||
|
||||
return (
|
||||
<LinkTag
|
||||
href={`/${type}/remove/${id}`}
|
||||
title={`Remove ${type}`}
|
||||
title={t(`common:${type}.remove`)}
|
||||
className={styles["action"]}
|
||||
onClick={onClick && onClick}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import ButtonLink from "components/ButtonLink";
|
||||
import { BiSearchAlt } from "react-icons/bi";
|
||||
|
||||
import styles from "./quickactions.module.scss";
|
||||
|
||||
export default function QuickActionSearch({
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import * as Keys from "constants/keys";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { ReactNode, useEffect, useMemo } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
import { SearchItem } from "types";
|
||||
import { groupItemBy } from "utils/array";
|
||||
import SearchListItem from "./SearchListItem";
|
||||
|
||||
import * as Keys from "constants/keys";
|
||||
|
||||
import styles from "./search.module.scss";
|
||||
|
||||
const isActiveItem = (item: SearchItem, otherItem: SearchItem) =>
|
||||
@@ -65,16 +63,18 @@ export default function SearchList({
|
||||
<ul className={styles["search-list"]}>
|
||||
{groupedItems.length > 0 ? (
|
||||
groupedItems.map(([key, items]) => (
|
||||
<li key={key + "-" + key}>
|
||||
<li>{typeof key === "undefined" ? "-" : key}</li>
|
||||
{items.map((item) => (
|
||||
<SearchListItem
|
||||
item={item}
|
||||
selected={isActiveItem(item, selectedItem)}
|
||||
closeModal={closeModal}
|
||||
key={item.id}
|
||||
/>
|
||||
))}
|
||||
<li key={`${key}-${key}`}>
|
||||
<span>{typeof key === "undefined" ? "-" : key}</span>
|
||||
<ul>
|
||||
{items.map((item) => (
|
||||
<SearchListItem
|
||||
item={item}
|
||||
selected={isActiveItem(item, selectedItem)}
|
||||
closeModal={closeModal}
|
||||
key={item.id}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))
|
||||
) : noItem ? (
|
||||
@@ -87,5 +87,6 @@ export default function SearchList({
|
||||
}
|
||||
|
||||
function LabelNoItem() {
|
||||
return <i className={styles["no-item"]}>Aucun élément trouvé</i>;
|
||||
const { t } = useTranslation("home");
|
||||
return <i className={styles["no-item"]}>{t("common:no-item-found")}</i>;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function SearchListItem({
|
||||
}
|
||||
ref={ref}
|
||||
key={id}
|
||||
title={name}
|
||||
>
|
||||
<LinkTag
|
||||
href={url}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { FormEvent, useCallback, useMemo, useState } from "react";
|
||||
import { BsSearch } from "react-icons/bs";
|
||||
|
||||
import useAutoFocus from "hooks/useAutoFocus";
|
||||
|
||||
import Modal from "components/Modal/Modal";
|
||||
import TextBox from "components/TextBox";
|
||||
import { GOOGLE_SEARCH_URL } from "constants/search-urls";
|
||||
import useAutoFocus from "hooks/useAutoFocus";
|
||||
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { FormEvent, useCallback, useMemo, useState } from "react";
|
||||
import { BsSearch } from "react-icons/bs";
|
||||
import { Category, SearchItem } from "types";
|
||||
import LabelSearchWithGoogle from "./LabelSearchWithGoogle";
|
||||
import SearchList from "./SearchList";
|
||||
|
||||
import { GOOGLE_SEARCH_URL } from "constants/search-urls";
|
||||
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||
import { Category, SearchItem } from "types";
|
||||
|
||||
import styles from "./search.module.scss";
|
||||
|
||||
export default function SearchModal({
|
||||
@@ -27,6 +24,7 @@ export default function SearchModal({
|
||||
items: SearchItem[];
|
||||
noHeader?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const autoFocusRef = useAutoFocus();
|
||||
|
||||
const [canSearchLink, setCanSearchLink] = useLocalStorage(
|
||||
@@ -110,7 +108,7 @@ export default function SearchModal({
|
||||
name="search"
|
||||
onChangeCallback={handleSearchInputChange}
|
||||
value={search}
|
||||
placeholder="Rechercher"
|
||||
placeholder={t("common:search")}
|
||||
innerRef={autoFocusRef}
|
||||
fieldClass={styles["search-input-field"]}
|
||||
inputClass={"reset"}
|
||||
@@ -132,7 +130,7 @@ export default function SearchModal({
|
||||
/>
|
||||
)}
|
||||
<button type="submit" disabled={!canSubmit} style={{ display: "none" }}>
|
||||
Valider
|
||||
{t("common:confirm")}
|
||||
</button>
|
||||
</form>
|
||||
</Modal>
|
||||
@@ -150,6 +148,8 @@ function SearchFilter({
|
||||
canSearchCategory: boolean;
|
||||
setCanSearchCategory: (value: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -160,7 +160,6 @@ function SearchFilter({
|
||||
marginBottom: "1em",
|
||||
}}
|
||||
>
|
||||
{/* à remplacer par des Chips Checkbox */}
|
||||
<div style={{ display: "flex", gap: ".25em" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -169,7 +168,7 @@ function SearchFilter({
|
||||
onChange={({ target }) => setCanSearchLink(target.checked)}
|
||||
checked={canSearchLink}
|
||||
/>
|
||||
<label htmlFor="filter-link">liens</label>
|
||||
<label htmlFor="filter-link">{t("common:link.links")}</label>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: ".25em" }}>
|
||||
<input
|
||||
@@ -179,7 +178,9 @@ function SearchFilter({
|
||||
onChange={({ target }) => setCanSearchCategory(target.checked)}
|
||||
checked={canSearchCategory}
|
||||
/>
|
||||
<label htmlFor="filter-category">categories</label>
|
||||
<label htmlFor="filter-category">
|
||||
{t("common:category.categories")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useMemo } from "react";
|
||||
import { Category } from "types";
|
||||
import CategoryItem from "./CategoryItem";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import styles from "./categories.module.scss";
|
||||
|
||||
interface CategoriesProps {
|
||||
@@ -14,13 +14,17 @@ export default function Categories({
|
||||
categoryActive,
|
||||
handleSelectCategory,
|
||||
}: CategoriesProps) {
|
||||
const { t } = useTranslation();
|
||||
const linksCount = useMemo(
|
||||
() => categories.reduce((acc, current) => (acc += current.links.length), 0),
|
||||
[categories]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles["categories"]}>
|
||||
<h4>Catégories • {linksCount}</h4>
|
||||
<h4>
|
||||
{t("common:category.categories")} • {linksCount}
|
||||
</h4>
|
||||
<ul className={styles["items"]}>
|
||||
{categories.map((category, index) => (
|
||||
<CategoryItem
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { AiFillFolderOpen, AiOutlineFolder } from "react-icons/ai";
|
||||
|
||||
import { Category } from "types";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import styles from "./categories.module.scss";
|
||||
|
||||
interface CategoryItemProps {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { Link } from "types";
|
||||
import FavoriteItem from "./FavoriteItem";
|
||||
|
||||
import styles from "./favorites.module.scss";
|
||||
|
||||
export default function Favorites({ favorites }: { favorites: Link[] }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
favorites.length !== 0 && (
|
||||
<div className={styles["favorites"]}>
|
||||
<h4>Favoris</h4>
|
||||
<h4>{t("common:favorite")}</h4>
|
||||
<ul className={styles["items"]}>
|
||||
{favorites.map((link) => (
|
||||
<FavoriteItem link={link} key={link.id} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PATHS from "constants/paths";
|
||||
import { SideMenuProps } from "./SideMenu";
|
||||
|
||||
import ButtonLink from "components/ButtonLink";
|
||||
import PATHS from "constants/paths";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { SideMenuProps } from "./SideMenu";
|
||||
import styles from "./sidemenu.module.scss";
|
||||
|
||||
export default function NavigationLinks({
|
||||
@@ -11,25 +11,25 @@ export default function NavigationLinks({
|
||||
categoryActive: SideMenuProps["categoryActive"];
|
||||
openSearchModal: SideMenuProps["openSearchModal"];
|
||||
}) {
|
||||
const handleOpenSearchModal = (event) => {
|
||||
event.preventDefault();
|
||||
openSearchModal();
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={styles["menu-controls"]}>
|
||||
<div className={styles["action"]}>
|
||||
<ButtonLink onClick={openSearchModal}>Rechercher</ButtonLink>
|
||||
<ButtonLink onClick={openSearchModal}>{t("common:search")}</ButtonLink>
|
||||
<kbd>S</kbd>
|
||||
</div>
|
||||
<div className={styles["action"]}>
|
||||
<ButtonLink href={PATHS.CATEGORY.CREATE}>Créer categorie</ButtonLink>
|
||||
<ButtonLink href={PATHS.CATEGORY.CREATE}>
|
||||
{t("common:category.create")}
|
||||
</ButtonLink>
|
||||
<kbd>C</kbd>
|
||||
</div>
|
||||
<div className={styles["action"]}>
|
||||
<ButtonLink
|
||||
href={`${PATHS.LINK.CREATE}?categoryId=${categoryActive.id}`}
|
||||
>
|
||||
Créer lien
|
||||
{t("common:link.create")}
|
||||
</ButtonLink>
|
||||
<kbd>L</kbd>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
import BlockWrapper from "components/BlockWrapper/BlockWrapper";
|
||||
import Categories from "./Categories/Categories";
|
||||
import NavigationLinks from "./NavigationLinks";
|
||||
import Favorites from "./Favorites/Favorites";
|
||||
import UserCard from "./UserCard/UserCard";
|
||||
|
||||
import * as Keys from "constants/keys";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Category, Link } from "types";
|
||||
|
||||
import Categories from "./Categories/Categories";
|
||||
import Favorites from "./Favorites/Favorites";
|
||||
import NavigationLinks from "./NavigationLinks";
|
||||
import UserCard from "./UserCard/UserCard";
|
||||
import styles from "./sidemenu.module.scss";
|
||||
|
||||
export interface SideMenuProps {
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import PATHS from "constants/paths";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import Image from "next/image";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
|
||||
import PATHS from "constants/paths";
|
||||
import { TFunctionParam } from "types/i18next";
|
||||
import styles from "./user-card.module.scss";
|
||||
|
||||
export default function UserCard() {
|
||||
const { data } = useSession({ required: true });
|
||||
const { t } = useTranslation();
|
||||
|
||||
const avatarLabel = t("common:avatar", {
|
||||
name: data.user.name,
|
||||
} as TFunctionParam);
|
||||
return (
|
||||
<div className={styles["user-card-wrapper"]}>
|
||||
<div className={styles["user-card"]}>
|
||||
@@ -14,13 +20,15 @@ export default function UserCard() {
|
||||
src={data.user.image}
|
||||
width={28}
|
||||
height={28}
|
||||
alt={`${data.user.name}'s avatar`}
|
||||
alt={avatarLabel}
|
||||
title={avatarLabel}
|
||||
/>
|
||||
{data.user.name}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: PATHS.LOGIN })}
|
||||
className="reset"
|
||||
title={t("common:logout")}
|
||||
>
|
||||
<FiLogOut size={24} />
|
||||
</button>
|
||||
|
||||
8
src/hooks/useIsMobile.tsx
Normal file
8
src/hooks/useIsMobile.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
const MOBILE_SCREEN_SIZE = 768;
|
||||
|
||||
export default function useIsMobile() {
|
||||
return (
|
||||
typeof window !== "undefined" &&
|
||||
window.matchMedia(`screen and (max-width: ${MOBILE_SCREEN_SIZE}px)`).matches
|
||||
);
|
||||
}
|
||||
12
src/i18n/index.ts
Normal file
12
src/i18n/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import nextI18NextConfig from "../../next-i18next.config";
|
||||
|
||||
async function getServerSideTranslation(locale: string = "en") {
|
||||
return await serverSideTranslations(
|
||||
locale,
|
||||
["common", "login", "home"],
|
||||
nextI18NextConfig
|
||||
);
|
||||
}
|
||||
|
||||
export { getServerSideTranslation };
|
||||
11
src/i18n/resources.ts
Normal file
11
src/i18n/resources.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import common from "../../public/locales/en/common.json";
|
||||
import home from "../../public/locales/en/home.json";
|
||||
import login from "../../public/locales/en/login.json";
|
||||
|
||||
const resources = {
|
||||
common,
|
||||
login,
|
||||
home,
|
||||
} as const;
|
||||
|
||||
export default resources;
|
||||
@@ -1,15 +1,18 @@
|
||||
import PageTransition from "components/PageTransition";
|
||||
import PATHS from "constants/paths";
|
||||
import { NextSeo } from "next-seo";
|
||||
|
||||
import Link from "next/link";
|
||||
import styles from "styles/error-page.module.scss";
|
||||
|
||||
export default function Custom404() {
|
||||
return (
|
||||
<>
|
||||
<NextSeo title="Page introuvable" />
|
||||
<PageTransition hideLangageSelector>
|
||||
<NextSeo title="Page not found" />
|
||||
<div className={styles["App"]}>
|
||||
<h1>404</h1>
|
||||
<h2>Cette page est introuvable.</h2>
|
||||
<h2>Page not found</h2>
|
||||
</div>
|
||||
</>
|
||||
<Link href={PATHS.HOME}>← Back to home page</Link>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import PageTransition from "components/PageTransition";
|
||||
import PATHS from "constants/paths";
|
||||
import { NextSeo } from "next-seo";
|
||||
|
||||
import Link from "next/link";
|
||||
import styles from "styles/error-page.module.scss";
|
||||
|
||||
export default function Custom500() {
|
||||
return (
|
||||
<>
|
||||
<NextSeo title="Une erreur est survenue" />
|
||||
<PageTransition hideLangageSelector>
|
||||
<NextSeo title="Internal server error" />
|
||||
<div className={styles["App"]}>
|
||||
<h1>500</h1>
|
||||
<h2>Une erreur côté serveur est survenue.</h2>
|
||||
<h2>An internal server error has occurred</h2>
|
||||
</div>
|
||||
</>
|
||||
<Link href={PATHS.HOME}>← Back to home page</Link>
|
||||
</PageTransition>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import ErrorBoundary from "components/ErrorBoundary/ErrorBoundary";
|
||||
import * as Keys from "constants/keys";
|
||||
import PATHS from "constants/paths";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { appWithTranslation } from "next-i18next";
|
||||
import { DefaultSeo } from "next-seo";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
import { useEffect } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
import AppErrorBoundary from "components/AppErrorBoundary";
|
||||
|
||||
import * as Keys from "constants/keys";
|
||||
import PATHS from "constants/paths";
|
||||
|
||||
import "nprogress/nprogress.css";
|
||||
import "styles/globals.scss";
|
||||
import nextI18nextConfig from "../../next-i18next.config";
|
||||
|
||||
function MyApp({ Component, pageProps: { session, ...pageProps } }) {
|
||||
const router = useRouter();
|
||||
@@ -22,7 +21,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Chargement pages
|
||||
// Page loading events
|
||||
router.events.on("routeChangeStart", nProgress.start);
|
||||
router.events.on("routeChangeComplete", nProgress.done);
|
||||
router.events.on("routeChangeError", nProgress.done);
|
||||
@@ -37,11 +36,11 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) {
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<DefaultSeo titleTemplate="MyLinks — %s" defaultTitle="MyLinks" />
|
||||
<AppErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
<Component {...pageProps} />
|
||||
</AppErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
export default appWithTranslation(MyApp, nextI18nextConfig);
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import axios from "axios";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import PATHS from "constants/paths";
|
||||
import useAutoFocus from "hooks/useAutoFocus";
|
||||
import { getServerSideTranslation } from "i18n";
|
||||
import getUserCategoriesCount from "lib/category/getUserCategoriesCount";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
import styles from "styles/form.module.scss";
|
||||
import { redirectWithoutClientCache } from "utils/client";
|
||||
import { HandleAxiosError } from "utils/front";
|
||||
import { withAuthentication } from "utils/session";
|
||||
|
||||
import styles from "styles/form.module.scss";
|
||||
|
||||
export default function PageCreateCategory({
|
||||
categoriesCount,
|
||||
}: {
|
||||
categoriesCount: number;
|
||||
}) {
|
||||
const autoFocusRef = useAutoFocus();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const autoFocusRef = useAutoFocus();
|
||||
const info = useRouter().query?.info as string;
|
||||
|
||||
const [name, setName] = useState<string>("");
|
||||
@@ -57,7 +57,7 @@ export default function PageCreateCategory({
|
||||
return (
|
||||
<PageTransition className={styles["form-container"]}>
|
||||
<FormLayout
|
||||
title="Créer une catégorie"
|
||||
title={t("common:category.create")}
|
||||
errorMessage={error}
|
||||
infoMessage={info}
|
||||
canSubmit={canSubmit}
|
||||
@@ -66,11 +66,11 @@ export default function PageCreateCategory({
|
||||
>
|
||||
<TextBox
|
||||
name="name"
|
||||
label="Nom de la catégorie"
|
||||
label={t("common:category.name")}
|
||||
onChangeCallback={(value) => setName(value)}
|
||||
value={name}
|
||||
fieldClass={styles["input-field"]}
|
||||
placeholder="Nom..."
|
||||
placeholder={t("common:category.name")}
|
||||
innerRef={autoFocusRef}
|
||||
/>
|
||||
</FormLayout>
|
||||
@@ -79,12 +79,14 @@ export default function PageCreateCategory({
|
||||
}
|
||||
|
||||
export const getServerSideProps = withAuthentication(
|
||||
async ({ session, user }) => {
|
||||
async ({ session, user, locale }) => {
|
||||
const categoriesCount = await getUserCategoriesCount(user);
|
||||
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
categoriesCount,
|
||||
...(await getServerSideTranslation(locale)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import axios from "axios";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import PATHS from "constants/paths";
|
||||
import useAutoFocus from "hooks/useAutoFocus";
|
||||
import { getServerSideTranslation } from "i18n";
|
||||
import getUserCategory from "lib/category/getUserCategory";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
import styles from "styles/form.module.scss";
|
||||
import { Category } from "types";
|
||||
import { HandleAxiosError } from "utils/front";
|
||||
import { withAuthentication } from "utils/session";
|
||||
|
||||
import styles from "styles/form.module.scss";
|
||||
|
||||
export default function PageEditCategory({ category }: { category: Category }) {
|
||||
const autoFocusRef = useAutoFocus();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const autoFocusRef = useAutoFocus();
|
||||
|
||||
const [name, setName] = useState<string>(category.name);
|
||||
|
||||
@@ -53,18 +53,18 @@ export default function PageEditCategory({ category }: { category: Category }) {
|
||||
return (
|
||||
<PageTransition className={styles["form-container"]}>
|
||||
<FormLayout
|
||||
title="Modifier une catégorie"
|
||||
title={t("common:category.edit")}
|
||||
errorMessage={error}
|
||||
canSubmit={canSubmit}
|
||||
handleSubmit={handleSubmit}
|
||||
>
|
||||
<TextBox
|
||||
name="name"
|
||||
label="Nom"
|
||||
label={t("common:category.name")}
|
||||
onChangeCallback={(value) => setName(value)}
|
||||
value={name}
|
||||
fieldClass={styles["input-field"]}
|
||||
placeholder={`Nom original : ${category.name}`}
|
||||
placeholder={`${t("common:category.name")} : ${category.name}`}
|
||||
innerRef={autoFocusRef}
|
||||
/>
|
||||
</FormLayout>
|
||||
@@ -73,7 +73,7 @@ export default function PageEditCategory({ category }: { category: Category }) {
|
||||
}
|
||||
|
||||
export const getServerSideProps = withAuthentication(
|
||||
async ({ query, session, user }) => {
|
||||
async ({ query, session, user, locale }) => {
|
||||
const { cid } = query;
|
||||
|
||||
const category = await getUserCategory(user, Number(cid));
|
||||
@@ -89,6 +89,7 @@ export const getServerSideProps = withAuthentication(
|
||||
props: {
|
||||
session,
|
||||
category: JSON.parse(JSON.stringify(category)),
|
||||
...(await getServerSideTranslation(locale)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
import axios from "axios";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Checkbox from "components/Checkbox";
|
||||
import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import PATHS from "constants/paths";
|
||||
import { getServerSideTranslation } from "i18n";
|
||||
import getUserCategory from "lib/category/getUserCategory";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import styles from "styles/form.module.scss";
|
||||
import { Category } from "types";
|
||||
import { HandleAxiosError } from "utils/front";
|
||||
import { withAuthentication } from "utils/session";
|
||||
|
||||
import styles from "styles/form.module.scss";
|
||||
|
||||
export default function PageRemoveCategory({
|
||||
category,
|
||||
}: {
|
||||
category: Category;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string | null>(
|
||||
category.links.length > 0
|
||||
? "Vous devez supprimer tous les liens de cette catégorie avant de pouvoir supprimer cette catégorie"
|
||||
: null
|
||||
);
|
||||
|
||||
const [error, setError] = useState<string>(null);
|
||||
const [confirmDelete, setConfirmDelete] = useState<boolean>(false);
|
||||
const [submitted, setSubmitted] = useState<boolean>(false);
|
||||
|
||||
@@ -53,10 +50,16 @@ export default function PageRemoveCategory({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setError(
|
||||
category.links.length > 0 ? t("common:category.remove-description") : null
|
||||
);
|
||||
}, [category.links.length, i18n.language, t]);
|
||||
|
||||
return (
|
||||
<PageTransition className={styles["form-container"]}>
|
||||
<FormLayout
|
||||
title="Supprimer une catégorie"
|
||||
title={t("common:category.remove")}
|
||||
categoryId={category.id.toString()}
|
||||
errorMessage={error}
|
||||
canSubmit={canSubmit}
|
||||
@@ -66,14 +69,14 @@ export default function PageRemoveCategory({
|
||||
>
|
||||
<TextBox
|
||||
name="name"
|
||||
label="Nom"
|
||||
label={t("common:category.name")}
|
||||
value={category.name}
|
||||
fieldClass={styles["input-field"]}
|
||||
disabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
name="confirm-delete"
|
||||
label="Confirmer la suppression ?"
|
||||
label={t("common:category.remove-confirm")}
|
||||
isChecked={confirmDelete}
|
||||
disabled={!!error}
|
||||
onChangeCallback={(checked) => setConfirmDelete(checked)}
|
||||
@@ -84,7 +87,7 @@ export default function PageRemoveCategory({
|
||||
}
|
||||
|
||||
export const getServerSideProps = withAuthentication(
|
||||
async ({ query, session, user }) => {
|
||||
async ({ query, session, user, locale }) => {
|
||||
const { cid } = query;
|
||||
|
||||
const category = await getUserCategory(user, Number(cid));
|
||||
@@ -100,6 +103,7 @@ export const getServerSideProps = withAuthentication(
|
||||
props: {
|
||||
session,
|
||||
category: JSON.parse(JSON.stringify(category)),
|
||||
...(await getServerSideTranslation(locale)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
|
||||
import BlockWrapper from "components/BlockWrapper/BlockWrapper";
|
||||
import ButtonLink from "components/ButtonLink";
|
||||
import LangSelector from "components/LangSelector";
|
||||
import Links from "components/Links/Links";
|
||||
import Modal from "components/Modal/Modal";
|
||||
import PageTransition from "components/PageTransition";
|
||||
@@ -12,14 +8,18 @@ import SearchModal from "components/SearchModal/SearchModal";
|
||||
import Categories from "components/SideMenu/Categories/Categories";
|
||||
import SideMenu from "components/SideMenu/SideMenu";
|
||||
import UserCard from "components/SideMenu/UserCard/UserCard";
|
||||
|
||||
import * as Keys from "constants/keys";
|
||||
import PATHS from "constants/paths";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import { useMediaQuery } from "hooks/useMediaQuery";
|
||||
import useModal from "hooks/useModal";
|
||||
import { getServerSideTranslation } from "i18n";
|
||||
import getUserCategories from "lib/category/getUserCategories";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Category, Link, SearchItem } from "types";
|
||||
import { pushStateVanilla } from "utils/link";
|
||||
import { withAuthentication } from "utils/session";
|
||||
|
||||
interface HomePageProps {
|
||||
@@ -30,6 +30,7 @@ interface HomePageProps {
|
||||
export default function HomePage(props: HomePageProps) {
|
||||
const router = useRouter();
|
||||
const searchModal = useModal();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const mobileModal = useModal();
|
||||
@@ -105,7 +106,7 @@ export default function HomePage(props: HomePageProps) {
|
||||
|
||||
const handleSelectCategory = (category: Category) => {
|
||||
setCategoryActive(category);
|
||||
pushStateVanilla(`${PATHS.HOME}?categoryId=${category.id}`);
|
||||
router.push(`${PATHS.HOME}?categoryId=${category.id}`);
|
||||
mobileModal.close();
|
||||
};
|
||||
|
||||
@@ -142,13 +143,22 @@ export default function HomePage(props: HomePageProps) {
|
||||
<PageTransition className="App">
|
||||
{isMobile ? (
|
||||
<>
|
||||
<UserCard />
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<UserCard />
|
||||
<LangSelector />
|
||||
</span>
|
||||
<AnimatePresence>
|
||||
{mobileModal.isShowing && (
|
||||
<Modal close={mobileModal.close}>
|
||||
<BlockWrapper style={{ minHeight: "0", flex: "1" }}>
|
||||
<ButtonLink href={PATHS.CATEGORY.CREATE}>
|
||||
Créer categorie
|
||||
{t("common:category.create")}
|
||||
</ButtonLink>
|
||||
<Categories
|
||||
categories={categories}
|
||||
@@ -193,7 +203,7 @@ export default function HomePage(props: HomePageProps) {
|
||||
}
|
||||
|
||||
export const getServerSideProps = withAuthentication(
|
||||
async ({ query, session, user }) => {
|
||||
async ({ query, session, user, locale }) => {
|
||||
const queryCategoryId = (query?.categoryId as string) || "";
|
||||
|
||||
const categories = await getUserCategories(user);
|
||||
@@ -215,6 +225,7 @@ export const getServerSideProps = withAuthentication(
|
||||
currentCategory: currentCategory
|
||||
? JSON.parse(JSON.stringify(currentCategory))
|
||||
: null,
|
||||
...(await getServerSideTranslation(locale)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import axios from "axios";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Checkbox from "components/Checkbox";
|
||||
import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import Selector from "components/Selector";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import PATHS from "constants/paths";
|
||||
import useAutoFocus from "hooks/useAutoFocus";
|
||||
import { getServerSideTranslation } from "i18n";
|
||||
import getUserCategories from "lib/category/getUserCategories";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
import styles from "styles/form.module.scss";
|
||||
import { Category, Link } from "types";
|
||||
import { HandleAxiosError, IsValidURL } from "utils/front";
|
||||
import { withAuthentication } from "utils/session";
|
||||
|
||||
import styles from "styles/form.module.scss";
|
||||
|
||||
export default function PageCreateLink({
|
||||
categories,
|
||||
}: {
|
||||
categories: Category[];
|
||||
}) {
|
||||
const autoFocusRef = useAutoFocus();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const autoFocusRef = useAutoFocus();
|
||||
const categoryIdQuery = router.query?.categoryId as string;
|
||||
|
||||
const [name, setName] = useState<Link["name"]>("");
|
||||
@@ -69,7 +69,7 @@ export default function PageCreateLink({
|
||||
return (
|
||||
<PageTransition className={styles["form-container"]}>
|
||||
<FormLayout
|
||||
title="Créer un lien"
|
||||
title={t("common:link.create")}
|
||||
categoryId={categoryIdQuery}
|
||||
errorMessage={error}
|
||||
canSubmit={canSubmit}
|
||||
@@ -77,24 +77,24 @@ export default function PageCreateLink({
|
||||
>
|
||||
<TextBox
|
||||
name="name"
|
||||
label="Nom"
|
||||
label={t("common:link.name")}
|
||||
onChangeCallback={(value) => setName(value)}
|
||||
value={name}
|
||||
fieldClass={styles["input-field"]}
|
||||
placeholder="Nom du lien"
|
||||
placeholder={t("common:link.name")}
|
||||
innerRef={autoFocusRef}
|
||||
/>
|
||||
<TextBox
|
||||
name="url"
|
||||
label="URL"
|
||||
label={t("common:link.link")}
|
||||
onChangeCallback={(value) => setUrl(value)}
|
||||
value={url}
|
||||
fieldClass={styles["input-field"]}
|
||||
placeholder="https://www.example.org/"
|
||||
placeholder="https://www.example.com/"
|
||||
/>
|
||||
<Selector
|
||||
name="category"
|
||||
label="Catégorie"
|
||||
label={t("common:category.category")}
|
||||
value={categoryId}
|
||||
onChangeCallback={(value: number) => setCategoryId(value)}
|
||||
options={categories.map(({ id, name }) => ({
|
||||
@@ -106,7 +106,7 @@ export default function PageCreateLink({
|
||||
name="favorite"
|
||||
isChecked={favorite}
|
||||
onChangeCallback={(value) => setFavorite(value)}
|
||||
label="Favoris"
|
||||
label={t("common:favorite")}
|
||||
/>
|
||||
</FormLayout>
|
||||
</PageTransition>
|
||||
@@ -114,7 +114,7 @@ export default function PageCreateLink({
|
||||
}
|
||||
|
||||
export const getServerSideProps = withAuthentication(
|
||||
async ({ session, user }) => {
|
||||
async ({ session, user, locale }) => {
|
||||
const categories = await getUserCategories(user);
|
||||
if (categories.length === 0) {
|
||||
return {
|
||||
@@ -128,6 +128,7 @@ export const getServerSideProps = withAuthentication(
|
||||
props: {
|
||||
session,
|
||||
categories: JSON.parse(JSON.stringify(categories)),
|
||||
...(await getServerSideTranslation(locale)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import axios from "axios";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Checkbox from "components/Checkbox";
|
||||
import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import Selector from "components/Selector";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import PATHS from "constants/paths";
|
||||
import useAutoFocus from "hooks/useAutoFocus";
|
||||
import { getServerSideTranslation } from "i18n";
|
||||
import getUserCategories from "lib/category/getUserCategories";
|
||||
import getUserLink from "lib/link/getUserLink";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
import styles from "styles/form.module.scss";
|
||||
import { Category, Link } from "types";
|
||||
import { HandleAxiosError, IsValidURL } from "utils/front";
|
||||
import { withAuthentication } from "utils/session";
|
||||
|
||||
import styles from "styles/form.module.scss";
|
||||
|
||||
export default function PageEditLink({
|
||||
link,
|
||||
categories,
|
||||
@@ -26,8 +25,9 @@ export default function PageEditLink({
|
||||
link: Link;
|
||||
categories: Category[];
|
||||
}) {
|
||||
const autoFocusRef = useAutoFocus();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const autoFocusRef = useAutoFocus();
|
||||
|
||||
const [name, setName] = useState<string>(link.name);
|
||||
const [url, setUrl] = useState<string>(link.url);
|
||||
@@ -85,31 +85,31 @@ export default function PageEditLink({
|
||||
return (
|
||||
<PageTransition className={styles["form-container"]}>
|
||||
<FormLayout
|
||||
title="Modifier un lien"
|
||||
title={t("common:link.edit")}
|
||||
errorMessage={error}
|
||||
canSubmit={canSubmit}
|
||||
handleSubmit={handleSubmit}
|
||||
>
|
||||
<TextBox
|
||||
name="name"
|
||||
label="Nom"
|
||||
label={t("common:link.name")}
|
||||
onChangeCallback={(value) => setName(value)}
|
||||
value={name}
|
||||
fieldClass={styles["input-field"]}
|
||||
placeholder={`Nom original : ${link.name}`}
|
||||
placeholder={`${t("common:link.name")} : ${link.name}`}
|
||||
innerRef={autoFocusRef}
|
||||
/>
|
||||
<TextBox
|
||||
name="url"
|
||||
label="URL"
|
||||
label={t("common:link.link")}
|
||||
onChangeCallback={(value) => setUrl(value)}
|
||||
value={url}
|
||||
fieldClass={styles["input-field"]}
|
||||
placeholder={`URL original : ${link.url}`}
|
||||
placeholder="https://example.com/"
|
||||
/>
|
||||
<Selector
|
||||
name="category"
|
||||
label="Catégorie"
|
||||
label={t("common:category.category")}
|
||||
value={categoryId}
|
||||
onChangeCallback={(value: number) => setCategoryId(value)}
|
||||
options={categories.map(({ id, name }) => ({
|
||||
@@ -121,7 +121,7 @@ export default function PageEditLink({
|
||||
name="favorite"
|
||||
isChecked={favorite}
|
||||
onChangeCallback={(value) => setFavorite(value)}
|
||||
label="Favoris"
|
||||
label={t("common:favorite")}
|
||||
/>
|
||||
</FormLayout>
|
||||
</PageTransition>
|
||||
@@ -129,7 +129,7 @@ export default function PageEditLink({
|
||||
}
|
||||
|
||||
export const getServerSideProps = withAuthentication(
|
||||
async ({ query, session, user }) => {
|
||||
async ({ query, session, user, locale }) => {
|
||||
const { lid } = query;
|
||||
|
||||
const categories = await getUserCategories(user);
|
||||
@@ -147,6 +147,7 @@ export const getServerSideProps = withAuthentication(
|
||||
session,
|
||||
link: JSON.parse(JSON.stringify(link)),
|
||||
categories: JSON.parse(JSON.stringify(categories)),
|
||||
...(await getServerSideTranslation(locale)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import axios from "axios";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Checkbox from "components/Checkbox";
|
||||
import FormLayout from "components/FormLayout";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import TextBox from "components/TextBox";
|
||||
|
||||
import PATHS from "constants/paths";
|
||||
import { getServerSideTranslation } from "i18n";
|
||||
import getUserLink from "lib/link/getUserLink";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
import styles from "styles/form.module.scss";
|
||||
import { Link } from "types";
|
||||
import { HandleAxiosError } from "utils/front";
|
||||
import { withAuthentication } from "utils/session";
|
||||
|
||||
import styles from "styles/form.module.scss";
|
||||
|
||||
export default function PageRemoveLink({ link }: { link: Link }) {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -47,7 +47,7 @@ export default function PageRemoveLink({ link }: { link: Link }) {
|
||||
return (
|
||||
<PageTransition className={styles["form-container"]}>
|
||||
<FormLayout
|
||||
title="Supprimer un lien"
|
||||
title={t("common:link.remove")}
|
||||
categoryId={link.category.id.toString()}
|
||||
errorMessage={error}
|
||||
canSubmit={canSubmit}
|
||||
@@ -57,34 +57,34 @@ export default function PageRemoveLink({ link }: { link: Link }) {
|
||||
>
|
||||
<TextBox
|
||||
name="name"
|
||||
label="Nom"
|
||||
label={t("common:link.name")}
|
||||
value={link.name}
|
||||
fieldClass={styles["input-field"]}
|
||||
disabled={true}
|
||||
/>
|
||||
<TextBox
|
||||
name="url"
|
||||
label="URL"
|
||||
label={t("common:link.link")}
|
||||
value={link.url}
|
||||
fieldClass={styles["input-field"]}
|
||||
disabled={true}
|
||||
/>
|
||||
<TextBox
|
||||
name="category"
|
||||
label="Catégorie"
|
||||
label={t("common:category.category")}
|
||||
value={link.category.name}
|
||||
fieldClass={styles["input-field"]}
|
||||
disabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
name="favorite"
|
||||
label="Favoris"
|
||||
label={t("common:favorite")}
|
||||
isChecked={link.favorite}
|
||||
disabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
name="confirm-delete"
|
||||
label="Confirmer la suppression ?"
|
||||
label={t("common:category.remove-confirm")}
|
||||
isChecked={confirmDelete}
|
||||
onChangeCallback={(checked) => setConfirmDelete(checked)}
|
||||
/>
|
||||
@@ -94,7 +94,7 @@ export default function PageRemoveLink({ link }: { link: Link }) {
|
||||
}
|
||||
|
||||
export const getServerSideProps = withAuthentication(
|
||||
async ({ query, session, user }) => {
|
||||
async ({ query, session, user, locale }) => {
|
||||
const { lid } = query;
|
||||
|
||||
const link = await getUserLink(user, Number(lid));
|
||||
@@ -110,6 +110,7 @@ export const getServerSideProps = withAuthentication(
|
||||
props: {
|
||||
session,
|
||||
link: JSON.parse(JSON.stringify(link)),
|
||||
...(await getServerSideTranslation(locale)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
import ButtonLink from "components/ButtonLink";
|
||||
import LangSelector from "components/LangSelector";
|
||||
import MessageManager from "components/MessageManager/MessageManager";
|
||||
import PageTransition from "components/PageTransition";
|
||||
import PATHS from "constants/paths";
|
||||
import { getServerSideTranslation } from "i18n/index";
|
||||
import getUser from "lib/user/getUser";
|
||||
import { Provider } from "next-auth/providers";
|
||||
import { getProviders, signIn } from "next-auth/react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { NextSeo } from "next-seo";
|
||||
import Image from "next/image";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
|
||||
import ButtonLink from "components/ButtonLink";
|
||||
import MessageManager from "components/MessageManager/MessageManager";
|
||||
import PageTransition from "components/PageTransition";
|
||||
|
||||
import PATHS from "constants/paths";
|
||||
import getUser from "lib/user/getUser";
|
||||
import { getSession } from "utils/session";
|
||||
|
||||
import styles from "styles/login.module.scss";
|
||||
import { getSession } from "utils/session";
|
||||
|
||||
interface SignInProps {
|
||||
providers: Provider[];
|
||||
}
|
||||
export default function SignIn({ providers }: SignInProps) {
|
||||
const { t } = useTranslation("login");
|
||||
|
||||
return (
|
||||
<div className={styles["login-page"]}>
|
||||
<PageTransition className={styles["login-container"]}>
|
||||
<NextSeo title="Authentification" />
|
||||
<PageTransition className={styles["login-container"]} hideLangageSelector>
|
||||
<NextSeo title={t("login:title")} />
|
||||
<div className={styles["image-wrapper"]}>
|
||||
<Image
|
||||
src={"/logo-light.png"}
|
||||
@@ -31,24 +33,28 @@ export default function SignIn({ providers }: SignInProps) {
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["form-wrapper"]}>
|
||||
<h1>Authentification</h1>
|
||||
<MessageManager info="Authentification requise pour utiliser ce service" />
|
||||
<h1>{t("login:title")}</h1>
|
||||
<MessageManager info={t("login:informative-text")} />
|
||||
{Object.values(providers).map(({ name, id }) => (
|
||||
<ButtonLink
|
||||
onClick={() => signIn(id, { callbackUrl: PATHS.HOME })}
|
||||
className={styles["login-button"]}
|
||||
key={id}
|
||||
>
|
||||
<FcGoogle size={"1.5em"} /> Continuer avec {name}
|
||||
<FcGoogle size={"1.5em"} />{" "}
|
||||
{t("login:continue-with", { provider: name } as undefined)}
|
||||
</ButtonLink>
|
||||
))}
|
||||
</div>
|
||||
</PageTransition>
|
||||
<div className="lang-selector">
|
||||
<LangSelector />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps({ req, res }) {
|
||||
export async function getServerSideProps({ req, res, locale }) {
|
||||
const session = await getSession(req, res);
|
||||
const user = await getUser(session);
|
||||
if (user) {
|
||||
@@ -61,6 +67,10 @@ export async function getServerSideProps({ req, res }) {
|
||||
|
||||
const providers = await getProviders();
|
||||
return {
|
||||
props: { session, providers },
|
||||
props: {
|
||||
session,
|
||||
providers,
|
||||
...(await getServerSideTranslation(locale)),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
@import "keyframes.scss";
|
||||
|
||||
.App {
|
||||
height: 100%;
|
||||
margin-top: 10em;
|
||||
margin-bottom: 3em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadein 250ms both;
|
||||
|
||||
& h1 {
|
||||
display: inline-block;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.3);
|
||||
margin: 0;
|
||||
margin-right: 20px;
|
||||
padding: 10px 23px 10px 0;
|
||||
font-size: 24px;
|
||||
font-size: 1.75em;
|
||||
font-weight: 500;
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
margin-right: 1em;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.3);
|
||||
padding: 10px 23px 10px 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
& h2 {
|
||||
font-size: 14px;
|
||||
font-size: 1em;
|
||||
font-weight: normal;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
|
||||
@@ -193,6 +193,12 @@ kbd {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.lang-selector {
|
||||
position: absolute;
|
||||
bottom: 4em;
|
||||
right: 2em;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.App {
|
||||
width: 100%;
|
||||
|
||||
22
src/types/i18next.d.ts
vendored
Normal file
22
src/types/i18next.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* If you want to enable locale keys typechecking and enhance IDE experience.
|
||||
*
|
||||
* Requires `resolveJsonModule:true` in your tsconfig.json.
|
||||
*
|
||||
* @link https://www.i18next.com/overview/typescript
|
||||
*/
|
||||
import "i18next";
|
||||
|
||||
// resources.ts file is generated with `npm run toc`
|
||||
import resources from "../i18n/resources";
|
||||
|
||||
declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
defaultNS: "common";
|
||||
resources: typeof resources;
|
||||
returnNull: false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ugly hack because of the above declaration, i cant use "t" function params
|
||||
type TFunctionParam = undefined;
|
||||
0
src/types.d.ts → src/types/types.d.ts
vendored
0
src/types.d.ts → src/types/types.d.ts
vendored
@@ -1,10 +1,13 @@
|
||||
import { i18n } from "next-i18next";
|
||||
|
||||
export function groupItemBy(array: any[], property: string) {
|
||||
const hash = {};
|
||||
const props = property.split(".");
|
||||
|
||||
for (const item of array) {
|
||||
const key = props.reduce((acc, prop) => acc && acc[prop], item);
|
||||
const hashKey = key !== undefined ? key : "catégories";
|
||||
const hashKey =
|
||||
key !== undefined ? key : i18n.t("common:category.categories");
|
||||
|
||||
if (!hash[hashKey]) {
|
||||
hash[hashKey] = [];
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
export function faviconLinkBuilder(origin: string) {
|
||||
return `http://localhost:3000/api/favicon?url=${origin}`;
|
||||
}
|
||||
|
||||
export function pushStateVanilla(newUrl: string) {
|
||||
window.history.replaceState(
|
||||
{ ...window.history.state, as: newUrl, url: newUrl },
|
||||
"",
|
||||
newUrl
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"baseUrl": "./src"
|
||||
"baseUrl": "./src",
|
||||
"typeRoots": ["./src/types"]
|
||||
},
|
||||
"include": ["next-env.d.ts", "@/**.*", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user