feat: privacy page with translation

refactor: apply prettier conf to all files

feat: terms of use with translation
This commit is contained in:
Sonny
2023-11-19 18:58:49 +01:00
parent 5bdd42afdb
commit 6d5afee4f4
55 changed files with 725 additions and 118 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

58
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,58 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
<option name="HTML_ENFORCE_QUOTES" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="Remove" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="80" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

5
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/my-links.iml" filepath="$PROJECT_DIR$/.idea/my-links.iml" />
</modules>
</component>
</project>

9
.idea/my-links.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/prettier.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,4 +1,4 @@
{
"tabWidth": 2,
"useTabs": false
}
}

View File

@@ -1,3 +1,3 @@
{
"discord.enabled": false
}
"discord.enabled": false
}

View File

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

View File

@@ -29,14 +29,14 @@ export function middleware(req) {
!req.nextUrl.pathname.startsWith("/_next")
) {
return NextResponse.redirect(
new URL(`/${lng}${req.nextUrl.pathname}`, req.url)
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}`)
refererUrl.pathname.startsWith(`/${l}`),
);
const response = NextResponse.next();
if (lngInReferer) response.cookies.set(cookieName, lngInReferer);

100
package-lock.json generated
View File

@@ -9,6 +9,8 @@
"@prisma/client": "^5.5.2",
"@svgr/webpack": "^8.1.0",
"accept-language": "^3.0.18",
"axios": "^1.6.1",
"clsx": "^2.0.0",
"framer-motion": "^10.16.4",
"i18next": "^23.7.3",
"next": "^14.0.2",
@@ -3124,6 +3126,11 @@
"has-symbols": "^1.0.3"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/available-typed-arrays": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
@@ -3145,6 +3152,16 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -3459,6 +3476,14 @@
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clsx": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
"engines": {
"node": ">=6"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -3509,6 +3534,17 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
@@ -3761,6 +3797,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -4720,6 +4764,25 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -4729,6 +4792,19 @@
"is-callable": "^1.1.3"
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/framer-motion": {
"version": "10.16.4",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.4.tgz",
@@ -5789,6 +5865,25 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
@@ -6474,6 +6569,11 @@
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz",
"integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA=="
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",

View File

@@ -11,6 +11,8 @@
"@prisma/client": "^5.5.2",
"@svgr/webpack": "^8.1.0",
"accept-language": "^3.0.18",
"axios": "^1.6.1",
"clsx": "^2.0.0",
"framer-motion": "^10.16.4",
"i18next": "^23.7.3",
"next": "^14.0.2",

View File

@@ -29,5 +29,7 @@
"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"
"retry": "Retry",
"privacy": "Privacy",
"terms": "Terms of use"
}

View File

@@ -0,0 +1,45 @@
{
"title": "Privacy Policy of MyLinks",
"edited_at": "Last updated: {{date}}",
"welcome": "Welcome to MyLinks, a free and open-source bookmark manager focused on privacy and self-hosting. This privacy policy aims to inform you about how we collect, use, and protect your data.",
"collect": {
"title": "1. Data Collection",
"cookie": {
"title": "1.1 Cookies",
"description": "Cookies used on MyLinks are essential to ensure the proper functioning of the site. By continuing to use our service, you consent to the use of these cookies."
},
"user": {
"title": "1.2 User Data",
"description": "To create personalized categories and links and associate them with their author, we collect the following information:",
"fields": ["Google ID", "Lastname", "Firstname", "Email", "Avatar"]
}
},
"data_use": {
"title": "2. Data Use",
"description": "The collected data is neither resold nor used for purposes other than initially intended, namely the management of categories and links created by the user."
},
"data_storage": {
"title": "3. Data Storage",
"description": "Data is stored securely to protect your privacy.",
"data_retention": {
"title": "3.1 Data Retention Period",
"description": "Functional data is retained until the user requests deletion. Once this request is made, the data will be permanently deleted."
}
},
"user_rights": {
"title": "4. User Rights",
"description": "The user has the right to retrieve all their data at any time and/or request the complete deletion of their data."
},
"rgpd": {
"title": "5. GDPR Compliance",
"description": "MyLinks complies with the General Data Protection Regulation (GDPR) of the European Union."
},
"contact": {
"title": "6. Contact",
"description": "If you have any questions or concerns about our privacy policy, feel free to contact us at the following address:"
},
"footer": {
"changes": "We reserve the right to update this privacy policy. We encourage you to regularly check this page to stay informed of any changes.",
"thanks": "Thank you for using MyLinks!"
}
}

View File

View File

@@ -29,5 +29,7 @@
"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"
"retry": "Recommencer",
"privacy": "Confidentialité",
"terms": "CGU"
}

View File

@@ -0,0 +1,51 @@
{
"title": "Politique de confidentialité de MyLinks",
"edited_at": "Dernière mise à jour : {{date}}",
"welcome": "Bienvenue sur MyLinks, un gestionnaire de favoris gratuit et open source axé sur la privacy et le self hosting. Cette politique de confidentialité vise à vous informer sur la manière dont nous collectons, utilisons et protégeons vos données.",
"collect": {
"title": "1. Collecte de données",
"cookie": {
"title": "1.1 Cookies",
"description": "Les cookies utilisés sur MyLinks sont indispensables pour assurer le bon fonctionnement du site. En continuant à utiliser notre service, vous consentez à l'utilisation de ces cookies."
},
"user": {
"title": "1.2 Données utilisateur",
"description": "Pour créer des catégories et liens personnalisés et les associer à leur auteur, nous collectons les informations suivantes :",
"fields": [
"Identifiant Google",
"Nom",
"Prénom",
"Adresse e-mail",
"Avatar"
]
}
},
"data_use": {
"title": "2. Utilisation des données",
"description": "Les données collectées ne sont ni revendues ni utilisées à d'autres fins que celles prévues initialement, à savoir la gestion des catégories et des liens créés par l'utilisateur."
},
"data_storage": {
"title": "3. Stockage des données",
"description": "Les données sont stockées de manière sécurisée afin de protéger votre confidentialité.",
"data_retention": {
"title": "3.1 Durée de conservation",
"description": "Les données fonctionnelles sont conservées jusqu'à ce que l'utilisateur fasse une demande de suppression. Une fois cette demande effectuée, les données seront définitivement supprimées."
}
},
"user_rights": {
"title": "4. Droits de l'utilisateur",
"description": "L'utilisateur a le droit de récupérer l'ensemble de ses données à tout moment et/ou de demander la suppression complète de ses données."
},
"rgpd": {
"title": "5. Conformité au RGPD",
"description": "MyLinks est conforme au Règlement Général sur la Protection des Données (RGPD) de l'Union européenne."
},
"contact": {
"title": "6. Contact",
"description": "Si vous avez des questions ou des préoccupations concernant notre politique de confidentialité, n'hésitez pas à nous contacter à l'adresse suivante :"
},
"footer": {
"changes": "Nous nous réservons le droit de mettre à jour cette politique de confidentialité. Nous vous encourageons à consulter régulièrement cette page pour rester informé des changements éventuels.",
"thanks": "Merci d'utiliser MyLinks !"
}
}

View File

View File

@@ -1,7 +1,7 @@
import { withTranslation } from 'next-i18next';
import React from 'react';
import LangSelector from '../LangSelector';
import styles from './error-boundary.module.scss';
import { withTranslation } from "next-i18next";
import React from "react";
import LangSelector from "../LangSelector";
import styles from "./error-boundary.module.scss";
class ErrorBoundary extends React.Component {
constructor(props) {
@@ -13,7 +13,7 @@ class ErrorBoundary extends React.Component {
// Catch errors in any components below and re-render with error message
this.setState({
error: error,
errorInfo: errorInfo
errorInfo: errorInfo,
});
}
@@ -23,20 +23,20 @@ class ErrorBoundary extends React.Component {
return (
<div className={styles["error-boundary"]}>
<div className={styles["boundary-content"]}>
<h1>{this.props.t('common:generic-error')}</h1>
<h1>{this.props.t("common:generic-error")}</h1>
<p
dangerouslySetInnerHTML={{
__html: this.props.t('common:generic-error-description')
__html: this.props.t("common:generic-error-description"),
}}
/>
<button onClick={() => window.location.reload()}>
{this.props.t('common:retry')}
{this.props.t("common:retry")}
</button>
<details>
<summary>{this.state.error && this.state.error.toString()}</summary>
<code>{this.state.errorInfo.componentStack}</code>
</details>
<div className='lang-selector'>
<div className="lang-selector">
<LangSelector />
</div>
</div>

View File

@@ -11,6 +11,7 @@ import { Category, Link } from "types";
import { TFunctionParam } from "types/i18next";
import LinkItem from "./LinkItem";
import styles from "./links.module.scss";
import clsx from "clsx";
export default function Links({
category,
@@ -64,7 +65,7 @@ export default function Links({
</span>
</h2>
{links.length !== 0 ? (
<ul className={styles["links"]}>
<ul className={clsx(styles["links"], "reset")}>
{links.map((link, index) => (
<LinkItem
link={link}

View File

@@ -57,6 +57,6 @@ export default function Modal({
<div className={styles["modal-body"]}>{children}</div>
</motion.div>
</motion.div>,
document.body
document.body,
);
}

View File

@@ -0,0 +1,35 @@
import LinkTag from "next/link";
import { useSession } from "next-auth/react";
import PATHS from "constants/paths";
import styles from "./navbar.module.scss";
import { useTranslation } from "next-i18next";
export default function Navbar() {
const { status } = useSession();
const { t } = useTranslation();
return (
<nav className={styles["navbar"]}>
<ul className="reset">
<li>
<LinkTag href={PATHS.HOME}>MyLinks</LinkTag>
</li>
<li>
<LinkTag href={PATHS.PRIVACY}>{t("common:privacy")}</LinkTag>
</li>
<li>
<LinkTag href={PATHS.TERMS}>{t("common:terms")}</LinkTag>
</li>
{status === "authenticated" ? (
<li>
<LinkTag href={PATHS.LOGOUT}>{t("common:logout")}</LinkTag>
</li>
) : (
<li>
<LinkTag href={PATHS.LOGIN}>{t("common:login")}</LinkTag>
</li>
)}
</ul>
</nav>
);
}

View File

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

View File

@@ -0,0 +1,10 @@
.navbar {
width: 100%;
padding: 0.75em;
}
.navbar ul {
display: flex;
gap: 1.5em;
justify-content: center;
}

View File

@@ -24,16 +24,16 @@ export default function SearchList({
}) {
const searchItemsGrouped = useMemo(
() => groupItemBy(items, "category.name"),
[items]
[items],
);
const groupedItems = useMemo<any>(
() => Object.entries(searchItemsGrouped),
[searchItemsGrouped]
[searchItemsGrouped],
);
const selectedItemIndex = useMemo<number>(
() => items.findIndex((item) => isActiveItem(item, selectedItem)),
[items, selectedItem]
[items, selectedItem],
);
useHotkeys(
@@ -43,7 +43,7 @@ export default function SearchList({
enableOnFormTags: ["INPUT"],
enabled: items.length > 1 && selectedItemIndex !== 0,
preventDefault: true,
}
},
);
useHotkeys(
Keys.ARROW_DOWN,
@@ -52,7 +52,7 @@ export default function SearchList({
enableOnFormTags: ["INPUT"],
enabled: items.length > 1 && selectedItemIndex !== items.length - 1,
preventDefault: true,
}
},
);
useEffect(() => {

View File

@@ -29,11 +29,11 @@ export default function SearchModal({
const [canSearchLink, setCanSearchLink] = useLocalStorage(
"search-link",
true
true,
);
const [canSearchCategory, setCanSearchCategory] = useLocalStorage(
"search-category",
false
false,
);
const [search, setSearch] = useState<string>("");
@@ -52,9 +52,9 @@ export default function SearchModal({
(item.type === "link" && canSearchLink)) &&
item.name
.toLocaleLowerCase()
.includes(search.toLocaleLowerCase().trim())
.includes(search.toLocaleLowerCase().trim()),
),
[canSearchCategory, canSearchLink, items, search]
[canSearchCategory, canSearchLink, items, search],
);
const resetForm = useCallback(() => {
@@ -64,7 +64,7 @@ export default function SearchModal({
const handleSearchInputChange = useCallback(
(value: string) => setSearch(value),
[]
[],
);
const handleCanSearchLink = (checked: boolean) => setCanSearchLink(checked);
@@ -94,7 +94,7 @@ export default function SearchModal({
resetForm,
search,
selectedItem,
]
],
);
return (

View File

@@ -3,12 +3,14 @@ import { useMemo } from "react";
import { Category } from "types";
import CategoryItem from "./CategoryItem";
import styles from "./categories.module.scss";
import clsx from "clsx";
interface CategoriesProps {
categories: Category[];
categoryActive: Category;
handleSelectCategory: (category: Category) => void;
}
export default function Categories({
categories,
categoryActive,
@@ -17,7 +19,7 @@ export default function Categories({
const { t } = useTranslation();
const linksCount = useMemo(
() => categories.reduce((acc, current) => (acc += current.links.length), 0),
[categories]
[categories],
);
return (
@@ -25,7 +27,7 @@ export default function Categories({
<h4>
{t("common:category.categories")} {linksCount}
</h4>
<ul className={styles["items"]}>
<ul className={clsx(styles["items"], "reset")}>
{categories.map((category, index) => (
<CategoryItem
category={category}

View File

@@ -29,20 +29,20 @@ export default function SideMenu({
Keys.ARROW_UP,
() => {
const currentCategoryIndex = categories.findIndex(
({ id }) => id === categoryActive.id
({ id }) => id === categoryActive.id,
);
if (currentCategoryIndex === -1 || currentCategoryIndex === 0) return;
handleSelectCategory(categories[currentCategoryIndex - 1]);
},
{ enabled: !isModalShowing }
{ enabled: !isModalShowing },
);
useHotkeys(
Keys.ARROW_DOWN,
() => {
const currentCategoryIndex = categories.findIndex(
({ id }) => id === categoryActive.id
({ id }) => id === categoryActive.id,
);
if (
currentCategoryIndex === -1 ||
@@ -52,7 +52,7 @@ export default function SideMenu({
handleSelectCategory(categories[currentCategoryIndex + 1]);
},
{ enabled: !isModalShowing }
{ enabled: !isModalShowing },
);
return (

View File

@@ -2,6 +2,8 @@ const PATHS = {
LOGIN: "/login",
LOGOUT: "/logout",
HOME: "/",
PRIVACY: "/privacy",
TERMS: "/terms",
CATEGORY: {
CREATE: "/category/create",
EDIT: "/category/edit",

View File

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

View File

@@ -1,11 +1,15 @@
import common from "../../public/locales/en/common.json";
import home from "../../public/locales/en/home.json";
import login from "../../public/locales/en/login.json";
import privacy from "../../public/locales/en/privacy.json";
import terms from "../../public/locales/en/terms.json";
const resources = {
common,
login,
home,
privacy,
terms,
} as const;
export default resources;

View File

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

View File

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

View File

@@ -1,18 +1,17 @@
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";
import NavbarUntranslated from "../components/Navbar/NavbarUntranslated";
export default function Custom404() {
return (
<PageTransition hideLangageSelector>
<PageTransition className={styles["App"]} hideLangageSelector>
<NextSeo title="Page not found" />
<div className={styles["App"]}>
<NavbarUntranslated />
<header>
<h1>404</h1>
<h2>Page not found</h2>
</div>
<Link href={PATHS.HOME}> Back to home page</Link>
</header>
</PageTransition>
);
}

View File

@@ -1,18 +1,17 @@
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";
import NavbarUntranslated from "../components/Navbar/NavbarUntranslated";
export default function Custom500() {
return (
<PageTransition hideLangageSelector>
<PageTransition className={styles["App"]} hideLangageSelector>
<NextSeo title="Internal server error" />
<div className={styles["App"]}>
<NavbarUntranslated />
<header>
<h1>500</h1>
<h2>An internal server error has occurred</h2>
</div>
<Link href={PATHS.HOME}> Back to home page</Link>
</header>
</PageTransition>
);
}

View File

@@ -70,4 +70,4 @@ export const authOptions = {
signOut: PATHS.LOGOUT
}
} as NextAuthOptions;
export default NextAuth(authOptions);
export default NextAuth(authOptions);

View File

@@ -15,7 +15,7 @@ interface Favicon {
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
res: NextApiResponse,
) {
const urlParam = (req.query?.url as string) || "";
if (!urlParam) {

View File

@@ -12,7 +12,7 @@ export default apiHandler({
async function editLink({ req, res, user }) {
const { lid } = await LinkQuerySchema.validate(req.query);
const { name, url, favorite, categoryId } = await LinkBodySchema.validate(
req.body
req.body,
);
const link = await getUserLink(user, lid);

View File

@@ -11,7 +11,7 @@ export default apiHandler({
async function createLink({ req, res, user }) {
const { name, url, favorite, categoryId } = await LinkBodySchema.validate(
req.body
req.body,
);
const link = await getUserLinkByName(user, name, categoryId);

View File

@@ -29,7 +29,7 @@ export default function PageCreateCategory({
const canSubmit = useMemo<boolean>(
() => name.length !== 0 && !submitted,
[name.length, submitted]
[name.length, submitted],
);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
@@ -82,5 +82,5 @@ export const getServerSideProps = withAuthentication(
...(await getServerSideTranslation(locale))
}
};
}
},
);

View File

@@ -25,7 +25,7 @@ export default function PageEditCategory({ category }: { category: Category }) {
const canSubmit = useMemo<boolean>(
() => name !== category.name && name !== "" && !submitted,
[category.name, name, submitted]
[category.name, name, submitted],
);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
@@ -85,5 +85,5 @@ export const getServerSideProps = withAuthentication(
...(await getServerSideTranslation(locale))
}
};
}
},
);

View File

@@ -27,7 +27,7 @@ export default function PageRemoveCategory({
const canSubmit = useMemo<boolean>(
() => category.links.length === 0 && confirmDelete && !submitted,
[category.links.length, confirmDelete, submitted]
[category.links.length, confirmDelete, submitted],
);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
@@ -46,7 +46,9 @@ export default function PageRemoveCategory({
useEffect(() => {
setError(
category.links.length > 0 ? t("common:category.remove-description") : null
category.links.length > 0
? t("common:category.remove-description")
: null,
);
}, [category.links.length, i18n.language, t]);
@@ -100,5 +102,5 @@ export const getServerSideProps = withAuthentication(
...(await getServerSideTranslation(locale))
}
};
}
},
);

View File

@@ -37,23 +37,23 @@ export default function HomePage(props: HomePageProps) {
const [categories, setCategories] = useState<Category[]>(props.categories);
const [categoryActive, setCategoryActive] = useState<Category | null>(
props.currentCategory || categories?.[0]
props.currentCategory || categories?.[0],
);
const favorites = useMemo<Link[]>(
() =>
categories.reduce((acc, category) => {
category.links.forEach((link) =>
link.favorite ? acc.push(link) : null
link.favorite ? acc.push(link) : null,
);
return acc;
}, [] as Link[]),
[categories]
[categories],
);
const searchItemBuilder = (
item: Category | Link,
type: SearchItem["type"]
type: SearchItem["type"],
): SearchItem => ({
id: item.id,
name: item.name,
@@ -66,15 +66,13 @@ export default function HomePage(props: HomePageProps) {
});
const itemsSearch = useMemo<SearchItem[]>(() => {
const items = categories.reduce((acc, category) => {
return categories.reduce((acc, category) => {
const categoryItem = searchItemBuilder(category, "category");
const items: SearchItem[] = category.links.map((link) =>
searchItemBuilder(link, "link")
searchItemBuilder(link, "link"),
);
return [...acc, ...items, categoryItem];
}, [] as SearchItem[]);
return items;
}, [categories]);
// TODO: refacto
@@ -101,7 +99,7 @@ export default function HomePage(props: HomePageProps) {
setCategoryActive(categories[categoryIndex]);
}
},
[categories, categoryActive.id]
[categories, categoryActive.id],
);
const handleSelectCategory = (category: Category) => {
@@ -117,7 +115,7 @@ export default function HomePage(props: HomePageProps) {
event.preventDefault();
searchModal.open();
},
areHokeysEnabled
areHokeysEnabled,
);
useHotkeys(Keys.CLOSE_SEARCH_KEY, searchModal.close, {
enabled: searchModal.isShowing,
@@ -129,14 +127,14 @@ export default function HomePage(props: HomePageProps) {
() => {
router.push(`${PATHS.LINK.CREATE}?categoryId=${categoryActive.id}`);
},
areHokeysEnabled
areHokeysEnabled,
);
useHotkeys(
Keys.OPEN_CREATE_CATEGORY_KEY,
() => {
router.push("/category/create");
},
areHokeysEnabled
areHokeysEnabled,
);
return (
@@ -216,7 +214,7 @@ export const getServerSideProps = withAuthentication(
}
const currentCategory = categories.find(
({ id }) => id === Number(queryCategoryId)
({ id }) => id === Number(queryCategoryId),
);
return {
props: {
@@ -225,8 +223,8 @@ export const getServerSideProps = withAuthentication(
currentCategory: currentCategory
? JSON.parse(JSON.stringify(currentCategory))
: null,
...(await getServerSideTranslation(locale)),
...(await getServerSideTranslation(locale, ["home"])),
},
};
}
},
);

View File

@@ -30,7 +30,7 @@ export default function PageCreateLink({
const [url, setUrl] = useState<Link["url"]>("");
const [favorite, setFavorite] = useState<Link["favorite"]>(false);
const [categoryId, setCategoryId] = useState<Link["category"]["id"]>(
Number(categoryIdQuery) || categories?.[0].id || null
Number(categoryIdQuery) || categories?.[0].id || null,
);
const [error, setError] = useState<string>(null);
@@ -43,7 +43,7 @@ export default function PageCreateLink({
favorite !== null &&
categoryId !== null &&
!submitted,
[name, url, favorite, categoryId, submitted]
[name, url, favorite, categoryId, submitted],
);
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
@@ -126,5 +126,5 @@ export const getServerSideProps = withAuthentication(
...(await getServerSideTranslation(locale))
}
};
}
},
);

View File

@@ -32,7 +32,7 @@ export default function PageEditLink({
const [url, setUrl] = useState<string>(link.url);
const [favorite, setFavorite] = useState<boolean>(link.favorite);
const [categoryId, setCategoryId] = useState<number | null>(
link.category?.id || null
link.category?.id || null,
);
const [error, setError] = useState<string | null>(null);
@@ -145,5 +145,5 @@ export const getServerSideProps = withAuthentication(
...(await getServerSideTranslation(locale))
}
};
}
},
);

View File

@@ -23,7 +23,7 @@ export default function PageRemoveLink({ link }: { link: Link }) {
const canSubmit = useMemo<boolean>(
() => confirmDelete && !submitted,
[confirmDelete, submitted]
[confirmDelete, submitted],
);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
@@ -109,5 +109,5 @@ export const getServerSideProps = withAuthentication(
...(await getServerSideTranslation(locale))
}
};
}
},
);

View File

@@ -3,7 +3,7 @@ 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 { getServerSideTranslation } from "../i18n";
import getUser from "lib/user/getUser";
import { Provider } from "next-auth/providers";
import { getProviders, signIn } from "next-auth/react";
@@ -17,6 +17,7 @@ import { getSession } from "utils/session";
interface SignInProps {
providers: Provider[];
}
export default function SignIn({ providers }: SignInProps) {
const { t } = useTranslation("login");
@@ -70,7 +71,7 @@ export async function getServerSideProps({ req, res, locale }) {
props: {
session,
providers,
...(await getServerSideTranslation(locale)),
...(await getServerSideTranslation(locale, ["login"])),
},
};
}

76
src/pages/privacy.tsx Normal file
View File

@@ -0,0 +1,76 @@
import LinkTag from "next/link";
import PageTransition from "components/PageTransition";
import styles from "styles/legal-pages.module.scss";
import clsx from "clsx";
import Navbar from "../components/Navbar/Navbar";
import { getServerSideTranslation } from "../i18n";
import { useTranslation } from "next-i18next";
import { TFunctionParam } from "../types/i18next";
export default function Privacy() {
const { t } = useTranslation("privacy");
return (
<PageTransition className={clsx("App", styles["privacy"])}>
<Navbar />
<main>
<h1>{t("privacy:title")}</h1>
<p>
{t("privacy:edited_at", { date: "19/11/2023" } as TFunctionParam)}
</p>
<p>{t("privacy:welcome")}</p>
<h2>{t("privacy:collect.title")}</h2>
<h3>{t("privacy:collect.cookie.title")}</h3>
<p>{t("privacy:collect.cookie.description")}</p>
<h3>{t("privacy:collect.user.title")}</h3>
<p>{t("privacy:collect.user.description")}</p>
<ul>
{(
t("privacy:collect.user.fields", {
returnObjects: true,
} as TFunctionParam) as Array<string>
).map((field) => (
<li key={field}>{field}</li>
))}
</ul>
<h2>{t("privacy:data_use.title")}</h2>
<p>{t("privacy:data_use.description")}</p>
<h2>{t("privacy:data_storage.title")}</h2>
<p>{t("privacy:data_storage.description")}</p>
<h3>{t("privacy:data_storage.data_retention.title")}</h3>
<p>{t("privacy:data_storage.data_retention.description")}</p>
<h2>{t("privacy:user_rights.title")}</h2>
<p>{t("privacy:user_rights.description")}</p>
<h2>{t("privacy:rgpd.title")}</h2>
<p>{t("privacy:rgpd.description")}</p>
<h2>{t("privacy:contact.title")}</h2>
<p>
{t("privacy:contact.description")}{" "}
<LinkTag href="mailto:sonnyasdev@gmail.com" target="_blank">
sonnyasdev[at]gmail.com
</LinkTag>
.
</p>
<p>{t("privacy:footer.changes")}</p>
<p>{t("privacy:footer.thanks")}</p>
</main>
</PageTransition>
);
}
export async function getServerSideProps({ locale }) {
return {
props: {
...(await getServerSideTranslation(locale, ["privacy"])),
},
};
}

111
src/pages/terms.tsx Normal file
View File

@@ -0,0 +1,111 @@
import PageTransition from "components/PageTransition";
import styles from "styles/legal-pages.module.scss";
import clsx from "clsx";
import LinkTag from "next/link";
import Navbar from "components/Navbar/Navbar";
import { getServerSideTranslation } from "../i18n";
export default function Terms() {
return (
<PageTransition className={clsx("App", styles["privacy"])}>
<Navbar />
<main>
<h1>Conditions Générales d'Utilisation de MyLinks</h1>
<p>Dernière mise à jour : 19/11/2023</p>
<p>
Bienvenue sur MyLinks, un gestionnaire de favoris gratuit et open
source axé sur la privacy et le self hosting. En utilisant ce service,
vous acceptez les conditions générales d'utilisation énoncées
ci-dessous. Veuillez les lire attentivement.
</p>
<h2>1. Acceptation des Conditions</h2>
<p>
En accédant à MyLinks et en utilisant nos services, vous acceptez de
vous conformer à ces Conditions Générales d'Utilisation.
</p>
<h2>2. Utilisation du Service</h2>
<h3>2.1 Compte Utilisateur</h3>
<p>
Pour accéder à certaines fonctionnalités de MyLinks, vous devrez créer
un compte utilisateur. Vous êtes responsable de la confidentialité de
votre compte et de vos informations d'identification.
</p>
<h3>2.2 Utilisation Autorisée</h3>
<p>
Vous vous engagez à utiliser MyLinks conformément aux lois en vigueur
et à ne pas violer les droits de tiers.
</p>
<h3>2.3 Contenu Utilisateur</h3>
<p>
En publiant du contenu sur MyLinks, vous accordez à MyLinks une
licence mondiale, non exclusive, transférable et gratuite pour
utiliser, reproduire, distribuer et afficher ce contenu.
</p>
<h2>3. Données Personnelles</h2>
<h3>3.1 Collecte et Utilisation</h3>
<p>
Les données personnelles collectées sont utilisées conformément à
notre <LinkTag href="/privacy">Politique de Confidentialité</LinkTag>.
En utilisant MyLinks, vous consentez à cette collecte et utilisation.
</p>
<h3>3.2 Suppression de Compte</h3>
<p>
Vous pouvez demander la suppression de votre compte à tout moment
conformément à notre Politique de Confidentialité.
</p>
<h2>4. Responsabilités et Garanties</h2>
<h3>4.1 Responsabilité</h3>
<p>
MyLinks ne peut être tenu responsable des dommages directs ou
indirects découlant de l'utilisation de nos services.
</p>
<h3>4.2 Garanties</h3>
<p>
MyLinks ne garantit pas que le service sera exempt d'erreurs ou de
interruptions.
</p>
<h2>5. Modifications des Conditions</h2>
<p>
MyLinks se réserve le droit de modifier ces Conditions Générales
d'Utilisation à tout moment. Les utilisateurs seront informés des
changements par le biais d'une notification sur le site.
</p>
<h2>6. Résiliation</h2>
<p>
MyLinks se réserve le droit de résilier ou de suspendre votre accès au
service, avec ou sans préavis, en cas de violation de ces Conditions
Générales d'Utilisation.
</p>
<h2>7. Contact</h2>
<p>
Pour toute question ou préoccupation concernant ces Conditions
Générales d'Utilisation, veuillez nous contacter à l'adresse suivante
:{" "}
<LinkTag href="mailto:sonnyasdev@gmail.com" target="_blank">
sonnyasdev[at]gmail.com
</LinkTag>
.
</p>
</main>
</PageTransition>
);
}
export async function getServerSideProps({ locale }) {
return {
props: {
...(await getServerSideTranslation(locale)),
},
};
}

View File

@@ -1,28 +1,30 @@
@import "keyframes.scss";
.App {
margin-top: 10em;
margin-bottom: 3em;
display: flex;
align-items: center;
justify-content: center;
animation: fadein 250ms both;
margin-top: 3em;
& h1 {
font-size: 1.75em;
font-weight: 500;
margin: 0;
margin-right: 1em;
border-right: 1px solid rgba(0, 0, 0, 0.3);
padding: 10px 23px 10px 0;
display: inline-block;
}
& header {
margin-top: 3em;
display: flex;
align-items: center;
justify-content: center;
& h2 {
font-size: 1em;
font-weight: normal;
line-height: inherit;
margin: 0;
padding: 0;
& h1 {
font-size: 1.75em;
font-weight: 500;
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: 1em;
font-weight: normal;
line-height: inherit;
margin: 0;
padding: 0;
}
}
}

View File

@@ -1,7 +1,7 @@
@import "keyframes.scss";
@import "colors.scss";
* {
*:not(ul) {
box-sizing: border-box;
outline: 0;
margin: 0;
@@ -33,7 +33,6 @@ body {
width: 1280px;
padding: 10px;
display: flex;
justify-content: center;
}
a {
@@ -59,11 +58,6 @@ h6 {
color: $blue;
}
ul,
li {
list-style: none;
}
/* width */
::-webkit-scrollbar {
height: 0.45em;
@@ -106,6 +100,7 @@ button:not(.reset) {
color: $white;
}
}
button.red-btn {
cursor: pointer;
width: 100%;
@@ -180,6 +175,7 @@ select:not(.nostyle) {
padding: 0;
margin: 0;
border: 0;
list-style: none;
}
kbd {
@@ -189,7 +185,9 @@ kbd {
padding: 0.25em 0.5em;
border-radius: 3px;
border: 1px solid rgb(204, 204, 204);
box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;
box-shadow:
0 1px 0 rgba(0, 0, 0, 0.2),
0 0 0 2px #ffffff inset;
display: inline-block;
}

View File

@@ -0,0 +1,24 @@
.privacy {
height: 100%;
flex-direction: column;
}
.privacy main {
overflow: auto;
}
.privacy h1 {
margin-top: 0.5em;
}
.privacy h2 {
margin-top: 1.5em;
}
.privacy h3 {
margin-top: 1em;
}
.privacy p {
margin-top: 0.5em;
}

View File

@@ -19,7 +19,7 @@ export function withAuthentication(serverSidePropsFunc) {
const session = await getSession(
req as NextApiRequest,
res as NextApiResponse
res as NextApiResponse,
);
const user = await getUser(session);
if (!session || !user) {