mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 14:43:24 +00:00
feat: privacy page with translation
refactor: apply prettier conf to all files feat: terms of use with translation
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
58
.idea/codeStyles/Project.xml
generated
Normal 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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
5
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
9
.idea/my-links.iml
generated
Normal 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
6
.idea/prettier.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
}
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"discord.enabled": false
|
||||
}
|
||||
"discord.enabled": false
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const config = {
|
||||
siteName: "My Links",
|
||||
siteName: "My Links",
|
||||
};
|
||||
|
||||
@@ -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
100
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
45
public/locales/en/privacy.json
Normal file
45
public/locales/en/privacy.json
Normal 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!"
|
||||
}
|
||||
}
|
||||
0
public/locales/en/terms.json
Normal file
0
public/locales/en/terms.json
Normal 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"
|
||||
}
|
||||
|
||||
51
public/locales/fr/privacy.json
Normal file
51
public/locales/fr/privacy.json
Normal 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 !"
|
||||
}
|
||||
}
|
||||
0
public/locales/fr/terms.json
Normal file
0
public/locales/fr/terms.json
Normal 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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -57,6 +57,6 @@ export default function Modal({
|
||||
<div className={styles["modal-body"]}>{children}</div>
|
||||
</motion.div>
|
||||
</motion.div>,
|
||||
document.body
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
35
src/components/Navbar/Navbar.tsx
Normal file
35
src/components/Navbar/Navbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
33
src/components/Navbar/NavbarUntranslated.tsx
Normal file
33
src/components/Navbar/NavbarUntranslated.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
10
src/components/Navbar/navbar.module.scss
Normal file
10
src/components/Navbar/navbar.module.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.navbar {
|
||||
width: 100%;
|
||||
padding: 0.75em;
|
||||
}
|
||||
|
||||
.navbar ul {
|
||||
display: flex;
|
||||
gap: 1.5em;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -2,6 +2,8 @@ const PATHS = {
|
||||
LOGIN: "/login",
|
||||
LOGOUT: "/logout",
|
||||
HOME: "/",
|
||||
PRIVACY: "/privacy",
|
||||
TERMS: "/terms",
|
||||
CATEGORY: {
|
||||
CREATE: "/category/create",
|
||||
EDIT: "/category/edit",
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,4 +70,4 @@ export const authOptions = {
|
||||
signOut: PATHS.LOGOUT
|
||||
}
|
||||
} as NextAuthOptions;
|
||||
export default NextAuth(authOptions);
|
||||
export default NextAuth(authOptions);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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"])),
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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
76
src/pages/privacy.tsx
Normal 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
111
src/pages/terms.tsx
Normal 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)),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
24
src/styles/legal-pages.module.scss
Normal file
24
src/styles/legal-pages.module.scss
Normal 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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user