diff --git a/client/i18n.js b/client/i18n.js new file mode 100644 index 0000000..cccf061 --- /dev/null +++ b/client/i18n.js @@ -0,0 +1,27 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +import Backend from 'i18next-http-backend'; +// don't want to use this? +// have a look at the Quick start guide +// for passing in lng and translations on init + +i18n + .use(Backend) + // pass the i18n instance to react-i18next. + .use(initReactI18next) + // init i18next + .init({ + lng: 'en', + fallbackLng: 'en', + debug: true, + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: `${process.env.NEXT_PUBLIC_FRONT_URL}/static/locales/{{lng}}/translation.json`, + }, + }); + + +export default i18n; \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index dd8c134..4231a00 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,13 +14,18 @@ "@uidotdev/usehooks": "^2.4.1", "axios": "^1.6.7", "dayjs": "^1.11.10", + "i18next": "^23.11.5", + "i18next-http-backend": "^2.5.2", "jwt-decode": "^4.0.0", "next": "14.1.0", "react": "^18", "react-big-calendar": "^1.10.1", "react-dom": "^18", + "react-error-boundary": "^4.0.13", "react-hot-toast": "^2.4.1", + "react-i18next": "^14.1.2", "react-icons": "^5.0.1", + "react-responsive": "^10.0.0", "sass": "^1.70.0", "uuidv4": "^6.2.13" }, @@ -1503,6 +1508,11 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, + "node_modules/css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -2893,6 +2903,14 @@ "node": ">= 0.4" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-link-header": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.1.tgz", @@ -2901,6 +2919,49 @@ "node": ">=6.0.0" } }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==" + }, + "node_modules/i18next": { + "version": "23.11.5", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz", + "integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.5.2.tgz", + "integrity": "sha512-+K8HbDfrvc1/2X8jpb7RLhI9ZxBDpx3xogYkQwGKlWAUXLSEGXzgdt3EcUjLlBCdMwdQY+K+EUF6oh8oB6rwHw==", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, + "node_modules/i18next-http-backend/node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -3648,6 +3709,14 @@ "semver": "bin/semver.js" } }, + "node_modules/matchmediaquery": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.4.2.tgz", + "integrity": "sha512-wrZpoT50ehYOudhDjt/YvUJc6eUzcdFPdmbizfgvswCKNHD1/OBOHYJpHie+HXpu6bSkEGieFMYk6VuutaiRfA==", + "dependencies": { + "css-mediaquery": "^0.1.2" + } + }, "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", @@ -4561,6 +4630,17 @@ "react": "^18.2.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", + "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-hot-toast": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", @@ -4576,6 +4656,27 @@ "react-dom": ">=16" } }, + "node_modules/react-i18next": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.2.tgz", + "integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-icons": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", @@ -4613,6 +4714,23 @@ "react-dom": ">=16.3.0" } }, + "node_modules/react-responsive": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-10.0.0.tgz", + "integrity": "sha512-N6/UiRLGQyGUqrarhBZmrSmHi2FXSD++N5VbSKsBBvWfG0ZV7asvUBluSv5lSzdMyEVjzZ6Y8DL4OHABiztDOg==", + "dependencies": { + "hyphenate-style-name": "^1.0.0", + "matchmediaquery": "^0.4.2", + "prop-types": "^15.6.1", + "shallow-equal": "^3.1.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4941,6 +5059,11 @@ "node": ">= 0.4" } }, + "node_modules/shallow-equal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz", + "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5650,6 +5773,14 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", diff --git a/client/package.json b/client/package.json index ca3130c..3a1e33f 100644 --- a/client/package.json +++ b/client/package.json @@ -15,13 +15,18 @@ "@uidotdev/usehooks": "^2.4.1", "axios": "^1.6.7", "dayjs": "^1.11.10", + "i18next": "^23.11.5", + "i18next-http-backend": "^2.5.2", "jwt-decode": "^4.0.0", "next": "14.1.0", "react": "^18", "react-big-calendar": "^1.10.1", "react-dom": "^18", + "react-error-boundary": "^4.0.13", "react-hot-toast": "^2.4.1", + "react-i18next": "^14.1.2", "react-icons": "^5.0.1", + "react-responsive": "^10.0.0", "sass": "^1.70.0", "uuidv4": "^6.2.13" }, diff --git a/client/pages/_app.tsx b/client/pages/_app.tsx index 100d261..8eac919 100644 --- a/client/pages/_app.tsx +++ b/client/pages/_app.tsx @@ -11,6 +11,9 @@ import { SessionProvider } from "@/src/components/Context/SolidContext"; import { useEffect, useState } from "react"; import { useRouter } from "next/router"; import Loader from "@/src/components/Loading/Loading"; +import '../i18n'; +import { ErrorBoundary } from "react-error-boundary"; +import Fallback from "@/src/components/Error/Error"; export default function MyApp({ Component, pageProps }: AppProps) { @@ -33,26 +36,28 @@ export default function MyApp({ Component, pageProps }: AppProps) { }, [router]); return ( - - - - - + (location.href = '/')}> + + + + + - - - {loading ? - - : - - } - - - - - - - - + + + {loading ? + + : + + } + + + + + + + + + ); } diff --git a/client/pages/api/github.ts b/client/pages/api/github.ts index 715c1c4..63d7c3d 100644 --- a/client/pages/api/github.ts +++ b/client/pages/api/github.ts @@ -26,10 +26,10 @@ export function useGithubHandler() { if (response) { window.location.assign(`${process.env.NEXT_PUBLIC_BACK_URL}/github/auth`); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } catch (error) { - toast.error('Error when connecting to the server'); + toast.error(t('toast.serverError')); } } @@ -50,10 +50,10 @@ export function useGithubHandler() { localStorage.removeItem('githubLoggedIn'); toast.success(data.data); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } catch (error) { - toast.error('Error when logging out'); + toast.error(t('toast.loggedOutError')); } } @@ -76,7 +76,7 @@ export function useGithubHandler() { throw Error(data.data); } } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } catch (error:any) { console.error(error.message); @@ -97,7 +97,7 @@ export function useGithubHandler() { const data = await issues.json(); return data.data; } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } catch (error) { console.error(error); @@ -121,12 +121,12 @@ export function useGithubHandler() { }); const data = await issues.json(); if (data.data.state === "closed") { - toast.success("Updated issue on GitHub!"); + toast.success(t('toast.updated')); } else { - toast.error("There has been a problem updating the issue on GitHub"); + toast.error(t('toast.errorUpdating')); } } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } catch (error) { console.error(error); @@ -150,12 +150,12 @@ export function useGithubHandler() { }); const data = await issues.json(); if (data.data.state === "open") { - toast.success("Updated issue on GitHub!"); + toast.success(t('toast.updated')); } else { - toast.error("There has been a problem updating the issue on GitHub"); + toast.error(t('toast.errorUpdating')); } } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } catch (error) { console.error(error); @@ -181,12 +181,12 @@ export function useGithubHandler() { }); const data = await issues.json(); if (data.data.title === title && data.data.body === body) { - toast.success("Updated issue on GitHub!"); + toast.success(t('toast.updated')); } else { - toast.error("There has been a problem updating the issue on GitHub"); + toast.error(t('toast.errorUpdating')); } } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } catch (error) { console.error(error); diff --git a/client/pages/api/google.ts b/client/pages/api/google.ts index 3aca8c8..06bb55f 100644 --- a/client/pages/api/google.ts +++ b/client/pages/api/google.ts @@ -2,9 +2,10 @@ import toast from "react-hot-toast"; import { useGoogleContext } from "../../src/components/Context/GoogleContext"; import { useEventContext } from "../../src/components/Context/EventContext"; import { CalendarItem, Event } from "../../src/model/Scheme"; +import { useTranslation } from "react-i18next"; export function useGoogleHandler() { - + const { t } = useTranslation(); const { setCalendars, setSelectedCalendarId, setLoggedIn, setAuthUrl, loggedIn } = useGoogleContext(); const { events, setEvents } = useEventContext(); @@ -44,12 +45,12 @@ export function useGoogleHandler() { } setCalendars([]); setSelectedCalendarId(""); - toast.success("Logged out!", { + toast.success(t('toast.loggedOut'), { position: "top-center" }) setLoggedIn(false); } else { - toast.error("Failed to log out!", { + toast.error(t('toast.loggedOutError'), { position: "top-center" }) } @@ -57,7 +58,7 @@ export function useGoogleHandler() { console.error('Error when logging out'); }); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } }) } @@ -82,7 +83,7 @@ export function useGoogleHandler() { console.error('Error al obtener la URL de autorización', error); }) } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } }) } @@ -101,7 +102,7 @@ export function useGoogleHandler() { setLoggedIn(false); } } catch (error) { - toast.error("There has been an error in the authentication. Please, try again.") + toast.error(t('toast.errorAuthn')) setLoggedIn(false); } } @@ -114,7 +115,7 @@ export function useGoogleHandler() { localStorage.removeItem('googleLoggedIn'); } } catch (error) { - toast.error("There has been an error in the authentication. Please, try again.") + toast.error(t('toast.errorAuthn')) localStorage.removeItem('googleLoggedIn'); setLoggedIn(false); } @@ -142,7 +143,7 @@ export function useGoogleHandler() { setCalendars(calendars); resolve(true); } else { - toast.error("Couldn't import the calendars.\nPlease log in with your Google account and try again.", { + toast.error(t('toast.errorGoogle'), { position: "top-center", duration: 6000, icon: "", @@ -154,13 +155,13 @@ export function useGoogleHandler() { } }) .catch(error => { - toast.error(`Error when fetching the calendars`); + toast.error(t('toast.errorRetrieving')); resolve(false); }); }) } else { - toast.error("Server appears to be down"); + toast.error(t('toast.serverDown')); return Promise.resolve(false); } }); @@ -195,7 +196,7 @@ export function useGoogleHandler() { }); }); } else { - toast.error("Server appears to be down"); + toast.error(t('toast.serverDown')); return Promise.resolve(false); } }); @@ -238,7 +239,7 @@ export function useGoogleHandler() { return [...existingEvents, ...updatedEvents, ...newEvents]; }); } else { - toast.error("Server appears to be down"); + toast.error(t('toast.serverDown')); } }) } @@ -268,7 +269,7 @@ export function useGoogleHandler() { .then(response => response.json()) .then(data => { addGoogleInformation(data.googleId, data.googleHTML, selectedIndex); - toast.success('Event exported to Google Calendar!', {position: "bottom-center"}); + toast.success(t('toast.exported')); resolve(); }) .catch(error => { @@ -277,7 +278,7 @@ export function useGoogleHandler() { }); }); } else { - toast.error("Server appears to be down"); + toast.error(t('toast.serverDown')); } }) return Promise.resolve(false); @@ -343,7 +344,7 @@ export function useGoogleHandler() { reject(error); // Reject with any error occurred }); } else { - toast.error("Server appears to be down"); + toast.error(t('toast.serverDown')); reject(new Error("Server appears to be down")); } }) diff --git a/client/pages/api/inrupt.ts b/client/pages/api/inrupt.ts index e19fbd1..d19241a 100644 --- a/client/pages/api/inrupt.ts +++ b/client/pages/api/inrupt.ts @@ -4,10 +4,11 @@ import { useTaskContext } from "@/src/components/Context/TaskContext"; import { useRouter } from "next/router"; import toast from "react-hot-toast"; import { Event, Task, TaskList } from "@/src/model/Scheme"; +import { useTranslation } from "react-i18next"; export function useInruptHandler() { - + const { t } = useTranslation(); const router = useRouter(); const { setSolidSession, setUserName } = useSessionContext(); const { setListNames, setLabels, setBoardColumns, setshowTasksInCalendar, setTasks } = useTaskContext(); @@ -35,7 +36,7 @@ export function useInruptHandler() { if (response) { window.location.assign(`${process.env.NEXT_PUBLIC_BACK_URL}/solid/login`); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } }) } @@ -58,11 +59,11 @@ export function useInruptHandler() { if (window.location.pathname !== "/") { router.push("/"); } - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } catch (error) { console.error(error); - toast.error('There has been a problem fetching your session!'); + toast.error(t('toast.errorSession')); } } @@ -83,7 +84,7 @@ export function useInruptHandler() { }) } else { setSolidSession(null) - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } }) } @@ -106,7 +107,7 @@ export function useInruptHandler() { console.error(data.data); } } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -119,10 +120,10 @@ export function useInruptHandler() { }) const data = await fetchResponse.json() if (!data.status) { - toast.error('Could not check the configuration'); + toast.error(t('toast.errorRetrieving')); } } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -143,11 +144,11 @@ export function useInruptHandler() { setEventView(data.config.calendarView); return false; } else if (data.status === "created") { - toast.success("Your POD has been initialized!"); + toast.success(t('toast.podInit')); return true; } } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } return true; // the server is not up so we dont want to get any data from the pod } @@ -172,13 +173,13 @@ export function useInruptHandler() { }).then((response) => response.json()) .then((data) => { if (data.status) { - toast.success("Preferences saved in your POD!"); + toast.success(t('toast.updated')); } else { - toast.error("Preferences could not be saved in your POD"); + toast.error(t('toast.errorUpdating')); } }); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } }) } @@ -201,10 +202,10 @@ export function useInruptHandler() { await getTasks(); } } else { - toast.error('There has been a problem fetching your data'); + toast.error(t('toast.errorRetrieving')); } } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -246,10 +247,10 @@ export function useInruptHandler() { setTasks([]); setListNames([]); } else { - toast.error('There has been a problem fetching your data :('); + toast.error(t('toast.errorRetrieving')); } } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -273,11 +274,11 @@ export function useInruptHandler() { } else if (data.status === "empty") { ; } else { - toast.error('There has been a problem fetching your data :('); + toast.error(t('toast.errorRetrieving')); } setEvents(events); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -294,10 +295,10 @@ export function useInruptHandler() { setBoardColumns(data.data.boardColumns ? data.data.boardColumns : []); } else { - toast.error('There has been a problem fetching your columns :('); + toast.error(t('toast.errorRetrieving')); } } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -315,10 +316,10 @@ export function useInruptHandler() { } else if (data.status === "empty") { setLabels([]); } else { - toast.error('There has been a problem fetching your labels :('); + toast.error(t('toast.errorRetrieving')); } } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -338,9 +339,9 @@ export function useInruptHandler() { }).then((response) => response.json()) .then((data) => { if (data.status) { - toast.success("Data correctly stored in your pod"); + toast.success(t('toast.updated')); } else { - toast.error("There has been a problem while storing data in your pod"); + toast.error(t('toast.errorUpdating')); } }) } @@ -362,9 +363,9 @@ export function useInruptHandler() { }).then((response) => response.json()) .then((data) => { if (data.status) { - toast.success("List correctly deleted"); + toast.success(t('toast.deleted')); } else { - toast.error("There has been a problem while deleting the list"); + toast.error(t('toast.errorDeleting')); } }) } @@ -412,16 +413,16 @@ export function useInruptHandler() { }); } } else if (data.status === "empty"){ - toast.success("There's no data to fetch!"); + toast.success(t('toast.noDataToFetch')); } else { ; - toast.error("There has been a problem fetching the data in your pod"); + toast.error(t('toast.errorRetrieving')); } setListNames(names); setTasks(taskLists); setEvents(events); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } catch (error) { console.error(error); @@ -443,10 +444,10 @@ export function useInruptHandler() { }) const data = await fetchResponse.json(); data.status - ? toast.success('Pod updated!') - :toast.error('There has been a problem storing your data :('); + ? toast.success(t('toast.updated')) + :toast.error(t('toast.errorUpdating')); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -465,10 +466,10 @@ export function useInruptHandler() { }) const data = await fetchResponse.json(); data.status - ? toast.success('Pod updated!') - : toast.error('There has been a problem storing your data :('); + ? toast.success(t('toast.updated')) + : toast.error(t('toast.errorUpdating')); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -487,10 +488,10 @@ export function useInruptHandler() { }) const data = await fetchResponse.json(); data.status - ? toast.success('Task updated!') - :toast.error('There has been a problem fetching your data :('); + ? toast.success(t('toast.updated')) + :toast.error(t('toast.errorUpdating')); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -509,10 +510,10 @@ export function useInruptHandler() { }) const data = await fetchResponse.json(); data.status - ? toast.success('Task updated!') - :toast.error('There has been a problem updating your data :('); + ? toast.success(t('toast.updated')) + :toast.error(t('toast.errorUpdating')); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -531,10 +532,10 @@ export function useInruptHandler() { }) const data = await fetchResponse.json(); data.status - ? toast.success('Event updated!') - : toast.error('There has been a problem updating your data :('); + ? toast.success(t('toast.updated')) + : toast.error(t('toast.errorUpdating')); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -553,10 +554,10 @@ export function useInruptHandler() { }) const data = await fetchResponse.json(); data.status - ? toast.success('Task updated!') - :toast.error('There has been a problem updating your data :('); + ? toast.success(t('toast.updated')) + :toast.error(t('toast.errorUpdating')); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -575,10 +576,10 @@ export function useInruptHandler() { }) const data = await fetchResponse.json(); data.status - ? toast.success('Task deleted!') - :toast.error('There has been a problem deleting your data :('); + ? toast.success(t('toast.deleted')) + :toast.error(t('toast.errorDeleting')); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -597,10 +598,10 @@ export function useInruptHandler() { }) const data = await fetchResponse.json(); data.status - ? toast.success('Event deleted!') - : toast.error('There has been a problem deleting your data :('); + ? toast.success(t('toast.deleted')) + : toast.error(t('toast.errorDeleting')); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } } @@ -619,10 +620,10 @@ export function useInruptHandler() { }) const data = await fetchResponse.json(); data.status - ? toast.success('Columns updated!') - :toast.error('There has been a problem updating your data :('); + ? toast.success(t('toast.updated')) + :toast.error(t('toast.errorUpdating')); } else { - toast.error('Server appears to be down'); + toast.error(t('toast.serverDown')); } }; diff --git a/client/pages/board/board.scss b/client/pages/board/board.scss index 1bc4327..39570a0 100644 --- a/client/pages/board/board.scss +++ b/client/pages/board/board.scss @@ -1,9 +1,14 @@ -@use "../../styles/variables" as var; +@use "../../styles/variables" as *; .board-container { - width: var.$view-width; + width: $view-width; height: 100%; - padding: var.$view-padding; + padding: $view-padding; box-sizing: border-box; width: 100%; +} + +body.dark-mode .board-container { + background-color: $background-dark-3; + color: $white; } \ No newline at end of file diff --git a/client/pages/calendar/calendar.scss b/client/pages/calendar/calendar.scss index 8346d68..166468b 100644 --- a/client/pages/calendar/calendar.scss +++ b/client/pages/calendar/calendar.scss @@ -10,6 +10,11 @@ height: calc(100% - 2*var.$view-padding); } +body.dark-mode .calendar-container { + background-color: var.$background-dark-3; + color: var.$white; +} + .calendar-menu { display: flex; justify-content: space-between; @@ -26,6 +31,14 @@ } } +body.dark-mode .calendar-menu-icon { + background-color: var.$background-dark-3; + color: var.$white; + &:hover { + background-color: var.$hover-green; + } +} + // needs to be more specific than the default class .rbc-toolbar span button { border: 1px solid var.$primary-green; @@ -44,6 +57,14 @@ } } +body.dark-mode .rbc-toolbar span button { + &:hover { + color: var.$black; + border: 1px solid var.$primary-green; + background-color: var.$primary-grey; + } +} + .rbc-toolbar span button.rbc-active { background-color: var.$primary-green; color: var.$white; @@ -54,6 +75,11 @@ } } +body.dark-mode .rbc-toolbar span button.rbc-active { + background-color: var.$background-dark-1; + color: var.$white; +} + .rbc-time-slot span.rbc-label { font-size: 1em; } @@ -63,6 +89,11 @@ div.rbc-today { border-color: var.$white; } +body.dark-mode div.rbc-today { + background-color: var.$medium-green; + color: var.$background-dark-1; +} + .rbc-agenda-table { font-size: .8rem } @@ -86,4 +117,9 @@ div.rbc-event.rbc-selected { color: var.$black; background-color: transparent; } +} + +body.dark-mode div.rbc-day-bg.rbc-off-range-bg { + background-color: var.$light-grey; + color: var.$white; } \ No newline at end of file diff --git a/client/pages/calendar/index.tsx b/client/pages/calendar/index.tsx index 19b5f2f..1be510d 100644 --- a/client/pages/calendar/index.tsx +++ b/client/pages/calendar/index.tsx @@ -1,6 +1,6 @@ import CalendarComponent from "../../src/components/Calendar/Calendar" import LoginGoogleCalendar from "../../src/components/Login/GoogleCalendarLogin" -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import CalendarMenu from "../../src/components/Menu/CalendarMenu"; import { useGoogleHandler } from "../api/google"; import { useSessionContext } from "@/src/components/Context/SolidContext"; @@ -8,7 +8,6 @@ import { useRouter } from "next/router"; import { useInruptHandler } from "../api/inrupt"; import Loader from "@/src/components/Loading/Loading"; - export default function Calendar() { const { solidSession } = useSessionContext(); diff --git a/client/pages/index.scss b/client/pages/index.scss index 38dddee..d8ffbbc 100644 --- a/client/pages/index.scss +++ b/client/pages/index.scss @@ -22,9 +22,21 @@ font-size: 1.1rem; } } +} +body.dark-mode { + .index-container { + background-color: $background-dark-3; + } -} + .index-container .welcome { + color: $white; + } + + .inrupt-info { + background-color: $primary-grey; + } +} .inrupt-info { display: flex; @@ -32,6 +44,7 @@ padding: 1rem 2rem; box-shadow: 0px 4px 20px 0px rgba(0,0,0,0.4); border-radius: .4rem; + color: $black; @media screen and (min-width: 320px) and (max-width: 768px) { padding: 1rem; diff --git a/client/pages/index.tsx b/client/pages/index.tsx index 93ef4d1..9878283 100644 --- a/client/pages/index.tsx +++ b/client/pages/index.tsx @@ -5,9 +5,10 @@ import Inrupt from "@/src/components/Login/InruptLogin"; import StatisticsPanel from "@/src/components/Panel/StatisticsPanel/StatisticsPanel"; import Loader from "@/src/components/Loading/Loading"; import LogoutInrupt from "@/src/components/Login/LogoutInruptBtn"; +import { useTranslation } from 'react-i18next'; export default function MainPage() { - + const { t } = useTranslation(); const { getSession, getProfile, getAllConfiguration, getApplicationData } = useInruptHandler(); const { solidSession, userName } = useSessionContext(); @@ -30,7 +31,6 @@ export default function MainPage() { } const podWasInitialized = await getAllConfiguration(); if (!podWasInitialized) { // if the pod was initialized, there are no lists/events to fetch - // TODO: fetch events await getApplicationData(); } setLoading(false); @@ -52,9 +52,9 @@ export default function MainPage() { solidSession?.info.isLoggedIn ?
-

{userName && userName !== 'No user name' ? `Welcome back, ${userName}!` : 'Welcome back!' }

+

{userName && userName !== 'No user name' ? `${t('home.welcome')}, ${userName}!` : `${t('home.welcome')}!` }

-

Currenlty logged in as {solidSession.info.webId}

+

{t('home.loggedIn')} {solidSession.info.webId}

diff --git a/client/pages/list/index.tsx b/client/pages/list/index.tsx index cfd1ca6..d8748f1 100644 --- a/client/pages/list/index.tsx +++ b/client/pages/list/index.tsx @@ -13,8 +13,10 @@ import { useSessionContext } from "@/src/components/Context/SolidContext"; import { useRouter } from "next/router"; import { useInruptHandler } from "../api/inrupt"; import Loader from "@/src/components/Loading/Loading"; +import { useTranslation } from "react-i18next"; export default function List() { + const { t } = useTranslation(); const [reRender, setRerender] = useState(Math.random()); const [firstRender, setFirstRender] = useState(true); const [isEditingTaskModalOpen, setIsEditingTaskModalOpen] = useState(false); @@ -73,10 +75,10 @@ export default function List() { } } catch (error:any) { if (error.message === "Failed to fetch") { - toast.error("Error when connecting to the server"); + toast.error(t('toast.serverError')); } else { if (error.message.includes('access not found') && githubLoggedIn) { - toast.error("Please reconnect to GitHub"); + toast.error(t('toast.reconnectGitHub')); } } } @@ -150,12 +152,12 @@ export default function List() {
{tasks && tasks.length > 0 && }
diff --git a/client/pages/list/list.scss b/client/pages/list/list.scss index 373a767..dfb08b2 100644 --- a/client/pages/list/list.scss +++ b/client/pages/list/list.scss @@ -10,6 +10,11 @@ box-sizing: border-box; } +body.dark-mode .list-container { + background-color: $background-dark-3; + color: $white; +} + .list-header-section { padding-inline: 3rem; padding-bottom: 3rem; @@ -52,7 +57,7 @@ column-gap: 1rem; left: 1rem; width: 90vw; - bottom: 8rem; + bottom: 6rem; } @media screen and (min-width: 769px) and (max-width: 1200px) { diff --git a/client/pages/settings/index.tsx b/client/pages/settings/index.tsx index 6049ff6..6b5abfa 100644 --- a/client/pages/settings/index.tsx +++ b/client/pages/settings/index.tsx @@ -11,9 +11,10 @@ import { useRouter } from "next/router"; import { useSessionContext } from "@/src/components/Context/SolidContext"; import { useInruptHandler } from "../api/inrupt"; import Loader from "@/src/components/Loading/Loading"; +import { useTranslation } from 'react-i18next'; export default function Settings() { - + const { t } = useTranslation(); const { checkAuthentication } = useGoogleHandler(); const { getUserData } = useGithubHandler(); const { githubLoggedIn, userData } = useGithubContext(); @@ -41,10 +42,10 @@ export default function Settings() { } } catch (error:any) { if (error.message === "Failed to fetch") { - toast.error("Error when connecting to the server"); + toast.error(t('toast.serverError')); } else { if (error.message.includes('access not found') && githubLoggedIn) { - toast.error("Please reconnect to GitHub"); + toast.error(t('toast.reconnectGitHub')); } } } @@ -83,11 +84,15 @@ export default function Settings() { : solidSession?.info.isLoggedIn &&
- + - +
) } \ No newline at end of file diff --git a/client/pages/settings/settings.scss b/client/pages/settings/settings.scss index f55461d..9a53154 100644 --- a/client/pages/settings/settings.scss +++ b/client/pages/settings/settings.scss @@ -11,13 +11,29 @@ overflow-y: auto; } +body.dark-mode .settings-container { + background-color: $background-dark-3; + color: $white; +} + +body.dark-mode .save-preferences-button { + background-color: $background-dark-0; + color: $white; + &:hover { + color: $background-dark-0; + background-color: $hover-green; + } +} + .save-preferences-button { + min-height: 3rem; display: flex; justify-content: center; outline: none; border: none; - background-color: $light-blue; + background-color: $hover-green; color: $black; + margin-top: 2rem; &:hover { background-color: $medium-green; } diff --git a/client/pages/tidier/index.tsx b/client/pages/tidier/index.tsx index 17bcd5c..d40a315 100644 --- a/client/pages/tidier/index.tsx +++ b/client/pages/tidier/index.tsx @@ -9,11 +9,11 @@ import { RxClock } from "react-icons/rx"; import CheckableTaskList, { TasksPreview } from "@/src/components/List/CheckableTaskList"; import { earliestDeadlineFirst, mostDifficultyFirst } from "../../src/algorithms/tidier"; import { Icon } from "../../src/components/Icon/Icon"; -import { GiFlatPlatform } from "react-icons/gi"; -import { GrFormNext } from "react-icons/gr"; +import { useTranslation } from "react-i18next"; +import toast from "react-hot-toast"; export default function Tidier() { - + const { t } = useTranslation(); const { solidSession } = useSessionContext(); const { getSession, getTasks } = useInruptHandler(); const { tasks, listNames } = useTaskContext(); @@ -69,6 +69,10 @@ export default function Tidier() { }; const handleGeneratePlan = ({ variant = 'dueDate' } : {variant: 'dueDate' | 'difficulty'}) => { + if (availableTime === "") { + toast.error(t('toast.availableTime')); + return; + } const [hoursStr, minutesStr] = availableTime.split(':'); const hours = parseInt(hoursStr, 10); const minutes = parseInt(minutesStr, 10); @@ -93,11 +97,11 @@ export default function Tidier() {
-

What shall get done today...

+

{t('tidier.title')}

-

Available time:

+

{t('tidier.availableTime')}

@@ -110,11 +114,11 @@ export default function Tidier() {
diff --git a/client/pages/tidier/tidier.scss b/client/pages/tidier/tidier.scss index ac1d5f1..1304790 100644 --- a/client/pages/tidier/tidier.scss +++ b/client/pages/tidier/tidier.scss @@ -11,6 +11,11 @@ overflow-y: auto; } +body.dark-mode .tidier-container { + background-color: $background-dark-3; + color: $white; +} + .tidier-header { margin-left: 1rem; grid-column: 1/-1; @@ -38,6 +43,13 @@ } } +body.dark-mode .tidier-generate { + button { + background-color: $background-dark-0; + border-color: $medium-grey; + } +} + .tidier-generate { grid-row: 1/1; display: flex; @@ -125,6 +137,10 @@ } } +body.dark-mode .line, body.dark-mode .circle { + background-color: $white; +} + .line { position: absolute; width: .3rem; /* Grosor de la línea */ @@ -147,6 +163,12 @@ transform: translateY(-50%); } +body.dark-mode .available-time { + .input-container .clock-from { + color: $background-dark-0; + } +} + .available-time { grid-column: 1/1; grid-row: 4/4; diff --git a/client/public/error.svg b/client/public/error.svg new file mode 100644 index 0000000..7cf8b47 --- /dev/null +++ b/client/public/error.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/client/public/favicon.ico b/client/public/favicon.ico index 718d6fe..a66f78c 100644 Binary files a/client/public/favicon.ico and b/client/public/favicon.ico differ diff --git a/client/public/static/locales/en/translation.json b/client/public/static/locales/en/translation.json new file mode 100644 index 0000000..4280b76 --- /dev/null +++ b/client/public/static/locales/en/translation.json @@ -0,0 +1,227 @@ +{ + "loginPage": { + "title": "Welcome to TidyTime!", + "desc": "TidyTime works with Solid PODs, ensuring your data is only controlled by you. Log in to your Inrupt Solid POD and start using the app!", + "loginButton": "LOG IN", + "learnMore": "Learn more about ", + "inruptESS": "Inrupt's ESS", + "solidProject": "Solid Project" + }, + "sidemenu": { + "list": "List", + "board": "Board", + "calendar": "Calendar", + "tidier": "Tidier", + "settings": "Preferences" + }, + "deletePanel": { + "title": "Are you sure you want to delete it? This action can't be undone", + "cancel": "Cancel", + "delete": "Delete", + "deleting": "Deleting..." + }, + "home": { + "welcome": "Welcome back", + "loggedIn": "Currenlty logged in as", + "logoutButton": "Log out", + "upcomingEvents": { + "title": "Upcoming events", + "today": "Today", + "dayLeft": "day left", + "daysLeft": "days left", + "emptyEvents": "No upcoming events!" + }, + "taskStat": "Completed tasks" + }, + "list": { + "search": "Search", + "github": "Connect to GitHub", + "addList": "Add new list", + "filterPanel": { + "done": "done", + "todo": "to do", + "filter": "Filter...", + "dueDateSorting": "Sort by due date", + "difficultySorting": "Sort by difficulty" + }, + "showDoneTask": "Show ", + "hideDoneTasks": "Hide ", + "doneTasks": "done tasks", + "syncIssues": "Sync issues", + "syncIssuesTitle": "Connect to GitHub and sync your issues", + "newTask": "New task...", + "emptyLists": "Add task lists!", + "editTaskPanel": { + "todo": "To do", + "done": "Done", + "urgent": "Urgent:", + "desc": "Add a description...", + "difficulty": "Difficulty:", + "endDate": "End-date:", + "labels": "Labels:", + "selectLabels": "Select options", + "delete": "Delete", + "reset": "Reset", + "save": "Save" + }, + "editListPanel": { + "title": "What do you want to do with this list?", + "rename": "Rename", + "remove": "Remove" + } + }, + "board": { + "newColumn": "Add new column", + "emptyBoard": "No columns yet... add a new one", + "movePanel": "Move to:", + "editColumnPanel": { + "title": "What do you want to do with this column?", + "rename": "Rename", + "remove": "Remove" + } + }, + "calendar": { + "connectGoogle": "Connect to Google", + "calendarMenu": { + "addEvent": { + "title": "Add event", + "desc": "Create a new event and add it to your calendar.", + "newEventButton": "New event", + "createButton": "Create", + "addTitle": "Add a title...", + "addNotes": "Notes...", + "from": "From:", + "to": "To:", + "color": "Color:" + }, + "googleCalendar": { + "title": "Google Calendar", + "desc": "Import your calendars and sync them to import their events.", + "importButton": "Import calendars" + } + }, + "messages": { + "allDay": "All day", + "previous": "Previous", + "next": "Next", + "today": "Today", + "month": "Month", + "week": "Week", + "day": "Day", + "agenda": "Agenda", + "date": "Date", + "time": "Time", + "event": "Event", + "noEventsInRange": "No events" + }, + "weekDays": { + "monday": { + "day": "Monday", + "dim": "M" + }, + "tuesday": { + "day": "Tuesday", + "dim": "Tue" + }, + "wednesday": { + "day": "Wednesday", + "dim": "W" + }, + "thursday": { + "day": "Thursday", + "dim": "Th" + }, + "friday": { + "day": "Friday", + "dim": "F" + }, + "saturday": { + "day": "Saturday", + "dim": "Sat" + }, + "sunday": { + "day": "Sunday", + "dim": "Sun" + } + }, + "newEventPanel": { + "title": "New event", + "cancel": "Cancel", + "create": "Create", + "creating": "Creating..." + }, + "eventPanel": { + "buttons": { + "google": "See in Google", + "save": "Save", + "delete": "Delete", + "share": "Share" + }, + "title": "New title...", + "notes": "Add notes...", + "color": "Color:" + } + }, + "tidier": { + "title": "What shall get done today...", + "tasksToGetDone": "Tasks to get done:", + "noLists": "No lists to show", + "dueDatePrior": "Prioritize due date", + "difficultyPrior": "Prioritize difficulty", + "availableTime": "Available time:" + }, + "preferences": { + "savePreferences": "Save preferences", + "list": { + "title": "List preferences", + "desc": "Create, delete, and customize tags for your tasks.", + "showTasks": "Show tasks with due date in the calendar?", + "showYes": "Yes", + "showNo": "No" + }, + "board": { + "title": "Board preferences", + "desc": "Modify, delete, or adapt the columns of the board to better suit your needs." + }, + "calendar": { + "title": "Calendar preferences", + "desc": "Adjust the starting day of the week and choose the default calendar view.", + "weekDay": "Starting day of week:", + "calendarView": "Default calendar view:" + }, + "applications": { + "title": "Connect to applications", + "desc": "TidyTime is interoperable with the following applications, allowing you to log in and access the information you need from them.", + "google": "Connect to Google to manage your events and calendars.", + "github": "Connect to GitHub to manage your issues." + } + }, + "errorPage": { + "title": "Oops.. something went wrong", + "desc": "Try refreshing the page to reload the application. If the error persists, feel free to open an issue in ", + "link": "TidyTime", + "refreshButton": "Refresh page" + }, + "toast": { + "loggedOut": "Logged out!", + "loggedOutError": "Error when logging out", + "deleting": "Deleting...", + "deleted": "Data succesfully deleted!", + "updating": "Saving...", + "updated": "Data successfully saved!", + "exported": "Data succesfully exported!", + "googleSays": "Google says: '", + "availableTime": "Enter your available time", + "serverError": "Error when connecting to the server", + "reconnectGitHub": "Please, connect to Github", + "serverDown": "Server appears to be down", + "errorUpdating": "Error when saving", + "errorDeleting": "Error when deleting", + "errorRetrieving": "Data could not be retrieved", + "errorSession": "Session could not be restored", + "errorAuthn": "Error in the authentication. Please, try again", + "podInit": "Your POD has been initialized!", + "noDataToFetch": "No data to retrieve!", + "errorGoogle": "Data could not be fetched. Please, connect to Google" + } +} \ No newline at end of file diff --git a/client/public/static/locales/es/translation.json b/client/public/static/locales/es/translation.json new file mode 100644 index 0000000..3c57459 --- /dev/null +++ b/client/public/static/locales/es/translation.json @@ -0,0 +1,232 @@ +{ + "loginPage": { + "title": "¡Bienvenido a TidyTime!", + "desc": "TidyTime funciona con Solid PODs, asegurando que tus datos sean controlados únicamente por ti. ¡Inicia sesión en tu cuenta de Inrupt y comienza a usar la aplicación!", + "loginButton": "INICIAR SESIÓN", + "learnMore": "Saber más sobre ", + "inruptESS": "ESS de Inrupt", + "solidProject": "Solid Project" + }, + "sidemenu": { + "list": "Lista", + "board": "Tablero", + "calendar": "Calendario", + "tidier": "Tidier", + "settings": "Configuración" + }, + "deletePanel": { + "title": "¿Estás seguro de que deseas eliminarlo? Esta acción no se puede deshacer", + "cancel": "Cancelar", + "delete": "Eliminar", + "deleting": "Eliminando..." + }, + "home": { + "welcome": "¡Hola de nuevo", + "loggedIn": "Sesión iniciada como", + "logoutButton": "Cerrar sessión", + "upcomingEvents": { + "title": "Próximos eventos", + "today": "Hoy", + "dayLeft": "día", + "daysLeft": "días", + "emptyEvents": "¡No hay eventos próximos!" + }, + "taskStat": "Tareas completadas" + }, + "list": { + "search": "Buscar", + "github": "Conectar con GitHub", + "addList": "Nueva lista", + "filterPanel": { + "done": "hechas", + "todo": "por hacer", + "filter": "Filtrar...", + "dueDateSorting": "Ordenar por fecha", + "difficultySorting": "Ordenar por dificultad" + }, + "showDoneTask": "Ver ", + "hideDoneTasks": "Esconder ", + "doneTasks": "tareas hechas", + "syncIssues": "Sincronizar issues", + "syncIssuesTitle": "Conéctate a GitHub y sincroniza tus issues", + "newTask": "Nueva tarea...", + "emptyLists": "¡Añade listas de tareas!", + "editTaskPanel": { + "todo": "Por hacer", + "done": "Hecha", + "urgent": "Urgente:", + "desc": "Añadir descripción...", + "difficulty": "Dificultad:", + "endDate": "Fecha:", + "labels": "Etiquetas:", + "selectLabels": "Seleccionar", + "delete": "Eliminar", + "reset": "Reestablecer", + "save": "Guardar" + }, + "editListPanel": { + "title": "¿Qué quieres hacer con esta lista?", + "rename": "Renombrar", + "remove": "Eliminar" + } + }, + "board": { + "newColumn": "Nueva columna", + "emptyBoard": "No hay columnas... añade una nueva", + "movePanel": "Mover a:", + "editColumnPanel": { + "title": "¿Qué quieres hacer con esta columna?", + "rename": "Renombrar", + "remove": "Eliminar" + } + }, + "calendar": { + "connectGoogle": "Conectar con Google", + "calendarModal": { + "title": "Nuevo evento", + "cancel": "Cancelar", + "create": "Crear" + }, + "calendarMenu": { + "addEvent": { + "title": "Añadir evento", + "desc": "Crea un nuevo evento y añádelo al calendario.", + "newEventButton": "Nuevo evento", + "createButton": "Crear", + "addTitle": "Añadir un título...", + "addNotes": "Notas...", + "from": "Desde:", + "to": "Hasta:", + "color": "Color:" + }, + "googleCalendar": { + "title": "Google Calendar", + "desc": "Importa tus calendarios y sincronízalos para importar sus eventos.", + "importButton": "Importar calendarios" + } + }, + "messages": { + "allDay": "Todo el día", + "previous": "Anterior", + "next": "Siguiente", + "today": "Hoy", + "month": "Mes", + "week": "Semana", + "day": "Día", + "agenda": "Agenda", + "date": "Fecha", + "time": "Hora", + "event": "Evento", + "noEventsInRange": "Sin eventos" + }, + "weekDays": { + "monday": { + "day": "Lunes", + "dim": "L" + }, + "tuesday": { + "day": "Martes", + "dim": "M" + }, + "wednesday": { + "day": "Miércoles", + "dim": "X" + }, + "thursday": { + "day": "Jueves", + "dim": "J" + }, + "friday": { + "day": "Viernes", + "dim": "V" + }, + "saturday": { + "day": "Sábado", + "dim": "S" + }, + "sunday": { + "day": "Domingo", + "dim": "D" + } + }, + "newEventPanel": { + "title": "Nuevo evento", + "cancel": "Cancelar", + "create": "Crear", + "creating": "Creando..." + }, + "eventPanel": { + "buttons": { + "google": "Ver en Google", + "save": "Guardar", + "delete": "Eliminar", + "share": "Compartir" + }, + "title": "Nuevo título...", + "notes": "Añadir notas...", + "color": "Color:" + } + }, + "tidier": { + "title": "Qué debería hacer hoy...", + "tasksToGetDone": "Tareas a completar:", + "noLists": "No hay listas que mostrar", + "dueDatePrior": "Priorizar fecha", + "difficultyPrior": "Priorizar dificultad", + "availableTime": "Tiempo disponible:" + }, + "preferences": { + "savePreferences": "Guardar configuración", + "list": { + "title": "Configurar Lista", + "desc": "Crea, elimina, y personaliza etiquetas para tus tareas.", + "showTasks": "¿Mostrar tareas con fecha de vencimiento en el calendario?", + "showYes": "Sí", + "showNo": "No" + }, + "board": { + "title": "Configurar Tablero", + "desc": "Modifica, elimina o adapta las columnas del tablero para que se ajusten mejor a tus necesidades." + }, + "calendar": { + "title": "Configurar Calendario", + "desc": "Ajusta el día de inicio de la semana y elige la vista predeterminada del calendario.", + "weekDay": "Comienzo de la semana:", + "calendarView": "Vista del calendario por defecto:" + }, + "applications": { + "title": "Conectar aplicaciones", + "desc": "TidyTime es interoperable con las siguientes aplicaciones, permitiéndote iniciar sesión y acceder a la información que necesites de ellas.", + "google": "Conéctate a Google para gestionar tus eventos y calendarios.", + "github": "Conéctate a GitHub para gestionar tus issues." + } + }, + "errorPage": { + "title": "Oops.. algo ha ido mal", + "desc": "Prueba a recargar la página para volver a cargar la aplicación. Si el error persiste, abre una incidencia en ", + "link": "TidyTime", + "refreshButton": "Recargar" + }, + "toast": { + "loggedOut": "¡Se ha cerrado la sesión!", + "loggedOutError": "Error al cerrar la sesión", + "deleting": "Eliminando...", + "deleted": "¡Datos eliminados!", + "updating": "Guardando...", + "updated": "¡Guardado!", + "exported": "¡Datos exportados!", + "googleSays": "Google dice: '", + "availableTime": "Introduce tu tiempo disponible", + "serverError": "Error al conectar con el servidor", + "reconnectGitHub": "Por favor, conéctate a GitHub", + "serverDown": "El servidor parece estar caído", + "errorUpdating": "Error al guardar", + "errorDeleting": "Error al eliminar", + "errorRetrieving": "No se pudieron cargar los datos", + "errorSession": "No se pudo cargar la sesión", + "errorAuthn": "No se pudo autenticar. Por favor, prueba otra vez", + "podInit": "¡Tu POD ha sido inicializado!", + "noDataToFetch": "¡No hay datos a cargar!", + "errorGoogle": "No se pudieron cargar los datos. Por favor, inicia sesión en Google" + } +} \ No newline at end of file diff --git a/client/src/components/Auth/GitHubAuth.scss b/client/src/components/Auth/GitHubAuth.scss index 88baab9..7660cb8 100644 --- a/client/src/components/Auth/GitHubAuth.scss +++ b/client/src/components/Auth/GitHubAuth.scss @@ -19,4 +19,8 @@ @media screen and (min-width: 769px) and (max-width: 1200px) { font-size: .7rem; } +} + +body.dark-mode .auth-button { + border-color: $white; } \ No newline at end of file diff --git a/client/src/components/Auth/GitHubAuth.tsx b/client/src/components/Auth/GitHubAuth.tsx index 35be7ca..2e93835 100644 --- a/client/src/components/Auth/GitHubAuth.tsx +++ b/client/src/components/Auth/GitHubAuth.tsx @@ -1,9 +1,10 @@ import { Icon } from "../Icon/Icon"; import { useGithubHandler } from "@/pages/api/github"; import { useGithubContext } from "../Context/GithubContext"; +import { useTranslation } from "react-i18next"; export default function GitHubAuthButton() { - + const { t } = useTranslation(); const { getUserData, loginWithGithub, logoutGithub } = useGithubHandler(); const { githubLoggedIn, userData } = useGithubContext(); @@ -30,14 +31,14 @@ export default function GitHubAuthButton() { className="auth-button" onClick={handleLogin}> - Connect with Github + {t('list.github')} : ) } \ No newline at end of file diff --git a/client/src/components/Board/Board.scss b/client/src/components/Board/Board.scss index db2fed3..1394e50 100644 --- a/client/src/components/Board/Board.scss +++ b/client/src/components/Board/Board.scss @@ -1,5 +1,22 @@ @use "../../../styles/variables" as *; +body.dark-mode .board-section { + .board-button-section { + background-color: $background-dark-3; + color: $white; + } + + .add-column-button { + background-color: $background-dark-1; + color: $white; + + &:hover { + background-color: $primary-grey; + color: $background-dark-0 + } + } +} + .board-section { display: flex; flex-direction: column; @@ -24,10 +41,6 @@ gap: .5rem; } - p { - text-decoration: underline; - } - .add-column-button { display: flex; gap: .5rem; @@ -41,7 +54,7 @@ } @media screen and (min-width: 320px) and (max-width: 768px) { - justify-content: flex-end; + justify-content: center; } } } diff --git a/client/src/components/Board/Board.tsx b/client/src/components/Board/Board.tsx index 6774529..554e617 100644 --- a/client/src/components/Board/Board.tsx +++ b/client/src/components/Board/Board.tsx @@ -5,6 +5,7 @@ import MoveModal from "../Modal/AbsoluteModal/AbsoluteModal"; import { BiAddToQueue } from "react-icons/bi"; import { Task } from "@/src/model/Scheme"; import { useInruptHandler } from "@/pages/api/inrupt"; +import { useTranslation } from "react-i18next"; export interface Props { @@ -12,7 +13,7 @@ export interface Props { } export default function Board({handleCardClick} : Props) { - + const { t } = useTranslation(); const {tasks, boardColumns, setBoardColumns, setTasks, selectedListId, selectedTaskId} = useTaskContext(); const {updateTaskStatus, storeBoardColumns} = useInruptHandler(); @@ -79,14 +80,12 @@ export default function Board({handleCardClick} : Props) { return (
-

See your progress using the board! -

0 ? "board-board" : "board-board-empty"}> @@ -106,7 +105,7 @@ export default function Board({handleCardClick} : Props) { ) }) : -

No columns yet... add a new one

+

{t('board.emptyBoard')}

}
{ isMovingTask && diff --git a/client/src/components/Board/Column.tsx b/client/src/components/Board/Column.tsx index 4450ee9..f56989e 100644 --- a/client/src/components/Board/Column.tsx +++ b/client/src/components/Board/Column.tsx @@ -6,6 +6,7 @@ import PromptModal from "../Modal/PromptModal/PromptModal"; import {v4 as uuid} from 'uuid'; import Card from "./Card"; import { useInruptHandler } from "@/pages/api/inrupt"; +import { useTranslation } from "react-i18next"; export interface ColumnProps { sectionWidth: number, @@ -17,7 +18,7 @@ export interface ColumnProps { } export default function Column({sectionWidth, content, index, name, handleMoveTask, handleCardClick} : ColumnProps) { - + const { t } = useTranslation(); const {listNames, setBoardColumns, boardColumns, tasks, setTasks} = useTaskContext(); const {storeBoardColumns} = useInruptHandler(); @@ -86,7 +87,7 @@ export default function Column({sectionWidth, content, index, name, handleMoveTa />{" "} {managingListIndex === index && ( setConfirmationDeleteModalOpen(true)} onRenameAction={renameList} onInputChange={handleInputChange} @@ -116,10 +117,10 @@ export default function Column({sectionWidth, content, index, name, handleMoveTa
{isConfirmationDeleteModalOpen && ( await deleteColumn()} - primaryActionText='Delete' - secondaryActionText='Cancel' + primaryActionText={t('deletePanel.delete')} + secondaryActionText={t('deletePanel.cancel')} onSecondaryAction={() => setConfirmationDeleteModalOpen(false)} variant='confirmation-modal' backdrop diff --git a/client/src/components/Calendar/Calendar.tsx b/client/src/components/Calendar/Calendar.tsx index 99b7fd0..efd41b5 100644 --- a/client/src/components/Calendar/Calendar.tsx +++ b/client/src/components/Calendar/Calendar.tsx @@ -2,7 +2,8 @@ import { Calendar, View, dayjsLocalizer } from "react-big-calendar"; import "react-big-calendar/lib/css/react-big-calendar.css"; import dayjs from "dayjs"; import enLocale from 'dayjs/locale/en'; -import { useRef, useState } from "react"; +import esLocale from 'dayjs/locale/es'; +import { useEffect, useRef, useState } from "react"; import PromptModal from "../Modal/PromptModal/PromptModal"; import NewEventForm from "../Event/NewEventForm"; import {v4 as uuid} from 'uuid'; @@ -11,6 +12,7 @@ import { useEventContext } from "../Context/EventContext"; import { Event } from "@/src/model/Scheme"; import SeeTaskModal from "../Modal/EditModal/SeeTaskModal"; import { useInruptHandler } from "@/pages/api/inrupt"; +import { useTranslation } from "react-i18next"; // custom event for calendar (all views) const components = { @@ -36,11 +38,14 @@ const eventStyleGetter = (event:any, view:any) => { }; export default function CalendarComponent() { + const { t, i18n } = useTranslation(); const { events, setEvents, setSelectedEventId, weekStart, eventView } = useEventContext(); const { createEvent } = useInruptHandler(); + const [languageLocalizer, setLanguageLocalizer] = useState(enLocale); + dayjs.locale('en-custom', { - ...enLocale, + ...languageLocalizer, weekStart: weekStart }); const localizer = dayjsLocalizer(dayjs); @@ -51,6 +56,7 @@ export default function CalendarComponent() { const [isOpenEditEventModal, setOpenEditEventModal] = useState(false); const [isOpenSeeTaskModal, setOpenSeeTaskModal] = useState(false); const [selectedColor, setSelectedColor] = useState(""); + const [creatingEvent, setIsCreatingEvent] = useState(false); const titleRef = useRef(null); const infoRef = useRef(null); @@ -104,9 +110,11 @@ export default function CalendarComponent() { } const handleCreateEvent = async () => { + setIsCreatingEvent(true); if (await createNewEvent()) { setOpenNewEventModal(false); } + setIsCreatingEvent(false); } const handleColorChange = (color:string) => { @@ -127,21 +135,28 @@ export default function CalendarComponent() { return false; } -/* Para poner los botones de la cabecera en español, pasar el const messages como prop de Calendar - const messages = { - allDay: "Todo el día", - previous: "Anterior", - next: "Siguiente", - today: "Hoy", - month: "Mes", - week: "Semana", - day: "Día", - agenda: "Agenda", - date: "Fecha", - time: "Hora", - event: "Evento", - noEventsInRange: "Sin eventos", - }; */ + useEffect(() => { + if (i18n.language === 'es') { + setLanguageLocalizer(esLocale); + } else if (i18n.language === 'en') { + setLanguageLocalizer(enLocale); + } + }, [i18n.language]) + + const messages = { + allDay: `${t('calendar.messages.allDay')}`, + previous: `${t('calendar.messages.previous')}`, + next: `${t('calendar.messages.next')}`, + today: `${t('calendar.messages.today')}`, + month: `${t('calendar.messages.month')}`, + week: `${t('calendar.messages.week')}`, + day: `${t('calendar.messages.day')}`, + agenda: `${t('calendar.messages.agenda')}`, + date: `${t('calendar.messages.date')}`, + time: `${t('calendar.messages.time')}`, + event: `${t('calendar.messages.event')}`, + noEventsInRange: `${t('calendar.messages.noEventsInRange')}`, + }; return ( <> @@ -154,13 +169,14 @@ export default function CalendarComponent() { onSelectEvent={(data) => handleSelectEvent(data)} selectable defaultView={eventView as View} + messages={messages} > { isOpenNewEventModal && ( await handleCreateEvent()} onSecondaryAction={() => setOpenNewEventModal(false)} backdrop={false} diff --git a/client/src/components/ComboBox/CheckableComboBox.scss b/client/src/components/ComboBox/CheckableComboBox.scss index 0f4757b..5ac8256 100644 --- a/client/src/components/ComboBox/CheckableComboBox.scss +++ b/client/src/components/ComboBox/CheckableComboBox.scss @@ -10,6 +10,10 @@ background-color: $primary-grey; } +body.dark-mode .combobox-select { + color: $black; +} + .dropdown-header { display: flex; gap: .4rem; @@ -64,6 +68,10 @@ } } +body.dark-mode .dropdown-content { + background-color: $primary-grey; +} + .dropdown-option { display: flex; align-items: center; diff --git a/client/src/components/ComboBox/CheckableComboBox.tsx b/client/src/components/ComboBox/CheckableComboBox.tsx index 6c9523a..40e8d43 100644 --- a/client/src/components/ComboBox/CheckableComboBox.tsx +++ b/client/src/components/ComboBox/CheckableComboBox.tsx @@ -1,5 +1,5 @@ import { Label } from "@/src/model/Scheme"; -import { ReactNode, useContext, useState } from "react"; +import { ReactNode, useState } from "react"; import { useTaskContext } from "../Context/TaskContext"; import { useClickAway } from "@uidotdev/usehooks"; @@ -11,7 +11,6 @@ export interface Props{ } export default function CheckableComboBox({checkedLabels, onChange, text, variant} : Props) { - const [isOpen, setIsOpen] = useState(false); const [selectedOptions, setSelectedOptions] = useState(checkedLabels); const {labels} = useTaskContext(); diff --git a/client/src/components/ComboBox/ComboBox.tsx b/client/src/components/ComboBox/ComboBox.tsx index a009560..5edfcf9 100644 --- a/client/src/components/ComboBox/ComboBox.tsx +++ b/client/src/components/ComboBox/ComboBox.tsx @@ -31,7 +31,7 @@ export default function ComboBox({text, options, colors, checkedOption, onOption ? : text } - +
{isOpen && colors && ( // color combobox
diff --git a/client/src/components/DonutChart/DonutChart.tsx b/client/src/components/DonutChart/DonutChart.tsx index 4477348..311ff48 100644 --- a/client/src/components/DonutChart/DonutChart.tsx +++ b/client/src/components/DonutChart/DonutChart.tsx @@ -11,7 +11,7 @@ export default function DonutChart({ total, value }: {total:any, value:any}) { cx="60" cy="60" r={radius} - stroke="#d3d3d3" + stroke="#d8d8d8" strokeWidth="10" fill="transparent" /> @@ -19,7 +19,7 @@ export default function DonutChart({ total, value }: {total:any, value:any}) { cx="60" cy="60" r={radius} - stroke="#879C89" + stroke="#000000" strokeWidth="10" fill="transparent" strokeDasharray={circumference} diff --git a/client/src/components/Error/Error.scss b/client/src/components/Error/Error.scss new file mode 100644 index 0000000..27c3384 --- /dev/null +++ b/client/src/components/Error/Error.scss @@ -0,0 +1,29 @@ +@use "../../../styles/variables" as *; + +.alert { + display: flex; + flex-direction: column; + height: 100vh; + justify-content: center; + align-items: center; + background-color: $hover-green; +} + +.error-img { + background-image: url("../../../public/error.svg"); + height: 20rem; + width: 20rem; + background-size: contain; + background-repeat: no-repeat; +} + +.error-p { + width: 80%; + text-align: center; +} + +.refresh-page { + display: flex; + padding-inline: .5rem; + border: .1rem solid $primary-grey; +} \ No newline at end of file diff --git a/client/src/components/Error/Error.tsx b/client/src/components/Error/Error.tsx new file mode 100644 index 0000000..1f2c620 --- /dev/null +++ b/client/src/components/Error/Error.tsx @@ -0,0 +1,18 @@ +import { useTranslation } from "react-i18next"; + + +export default function Fallback({error, resetErrorBoundary}: {error: Error, resetErrorBoundary: any}) { + + const { t } = useTranslation(); + + return ( +
+
+

{t('errorPage.title')}

+

{t('errorPage.desc')} + {t('errorPage.link')} +

+ +
+ ); + } \ No newline at end of file diff --git a/client/src/components/Event/NewEventForm.tsx b/client/src/components/Event/NewEventForm.tsx index 105ec54..752c0d8 100644 --- a/client/src/components/Event/NewEventForm.tsx +++ b/client/src/components/Event/NewEventForm.tsx @@ -1,5 +1,6 @@ +import { useTranslation } from "react-i18next"; import ComboBox from "../ComboBox/ComboBox"; -import { MutableRefObject, useEffect, useRef, useState } from "react"; +import { MutableRefObject, useState } from "react"; export interface Props { startDate?: Date, //yyyy-mm-ddThh:mm @@ -12,6 +13,7 @@ export interface Props { } export default function NewEventForm({startDate, endDate, titleRef, infoRef, fromDateRef, toDateRef, onColorChange} : Props) { + const { t } = useTranslation(); const start = startDate ? new Date(startDate.getTime() - (startDate.getTimezoneOffset() * 60000)).toISOString().slice(0, 16) : ""; @@ -27,12 +29,12 @@ export default function NewEventForm({startDate, endDate, titleRef, infoRef, fro return (
- -