diff --git a/todoblue/package.json b/todoblue/package.json index 802d7b6..b0beac1 100644 --- a/todoblue/package.json +++ b/todoblue/package.json @@ -19,11 +19,17 @@ "@types/react-dom": "18.2.7", "classnames": "^2.3.2", "client-only": "^0.0.1", + "i18next": "^23.4.2", + "i18next-resources-to-backend": "^1.1.4", + "negotiator": "^0.6.3", "next": "13.4.12", "react": "18.2.0", "react-dom": "18.2.0", "server-only": "^0.0.1", "typescript": "5.1.6", "use-local-storage": "^3.0.0" + }, + "devDependencies": { + "@types/negotiator": "^0.6.1" } } diff --git a/todoblue/src/app/(i18n)/(locales)/(root)/en-US.json b/todoblue/src/app/(i18n)/(locales)/(root)/en-US.json new file mode 100644 index 0000000..4ed3a38 --- /dev/null +++ b/todoblue/src/app/(i18n)/(locales)/(root)/en-US.json @@ -0,0 +1,32 @@ +{ + "title": "Todocolors", + "createBoardTitle": "Create a new board", + "createPublicBoardTitle": "Public", + "createPublicBoardDescription": "When creating a public board, you have to choose a code that others can use to access your board!", + "createPublicBoardSmall": "If a board with the given code already exists, you'll be redirected to it.", + "createPublicBoardCodeLeft": "Code", + "createPublicBoardCodePlaceholder": "my-new-board", + "createPublicBoardSubmitText": "Create public board", + "createPrivateBoardTitle": "Private", + "createPrivateBoardLoadingDescription": "Your connection is being checked to verify if it can support a private board.", + "createPrivateBoardLoadingSmall": "Just a moment...", + "createPrivateBoardUnavailableDescription": "Your connection does not support private boards.", + "createPrivateBoardUnavailableSmall": "Are you using HTTPS?", + "createPrivateBoardDescription": "When creating a private board, a random secure code will be assigned to it, which you can share privately with others to give them access.", + "createPrivateBoardSmall": "Don't share this code with anyone who you don't want to give access to your board!", + "createPrivateBoardSubmitText": "Create private board", + "existingBoardTitle": "Use an existing board", + "existingKnownBoardsTitle": "Access with code", + "existingKnownBoardsDescription": "If you know the code of a board, you can access it by entering the code here.", + "existingKnownBoardsSmall": "Entering an invalid code will create a new public board with the given code.", + "existingKnownBoardsCodeLeft": "Code", + "existingKnownBoardsCodePlaceholder": "your-new-board", + "existingKnownBoardsSubmitText": "Access board with code", + "existingStarredBoardsTitle": "Starred boards", + "existingStarredBoardsLoadingDescription": "Your browser is loading the list of boards you've starred.", + "existingStarredBoardsLoadingSmall": "Just a moment...", + "existingStarredBoardsEmptyDescription": "You haven't starred any boards yet. Find or create a board, then come back here!", + "existingStarredBoardsEmptySmall": "Once opened, you can star any board by pressing the second button from the top left.", + "existingStarredBoardsDescription": "These are the codes for the boards your starred. Click on one of them to access the corresponding board.", + "existingStarredBoardsSmall": "Once opened, you can unstar any board by pressing again the second button from the top left." +} diff --git a/todoblue/src/app/(i18n)/(locales)/index.ts b/todoblue/src/app/(i18n)/(locales)/index.ts new file mode 100644 index 0000000..4e344f4 --- /dev/null +++ b/todoblue/src/app/(i18n)/(locales)/index.ts @@ -0,0 +1,3 @@ +export const AVAILABLE_LOCALES: string[] = [ + "en-US", +] diff --git a/todoblue/src/app/(i18n)/client.ts b/todoblue/src/app/(i18n)/client.ts new file mode 100644 index 0000000..1971493 --- /dev/null +++ b/todoblue/src/app/(i18n)/client.ts @@ -0,0 +1,41 @@ +"use client"; +import "client-only"; +import {createInstance, i18n} from "i18next" +import resourcesToBackend from "i18next-resources-to-backend" +import {useEffect, useState} from "react" + + +async function init(lng: string, ns: string): Promise { + const instance = createInstance() + await instance + .use(resourcesToBackend((language: string, namespace: string) => import(`./(locales)/(${namespace})/${language}.json`))) + .init({ + supportedLngs: ["en-US"], + fallbackLng: "en-US", + lng, + fallbackNS: "common", + defaultNS: "common", + ns, + }) + return instance +} + +export function useTranslation(lng: string, ns: string) { + const [instance, setInstance] = useState(undefined); + + useEffect( + () => { + console.debug("[useTranslation] Initializing translation with:", lng, ":", ns) + init(lng, ns).then((v: i18n) => { + console.debug("[useTranslation] Initialized i18n:", v) + return setInstance(v) + }) + }, + [lng, ns] + ) + + return { + t: instance?.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns) ?? ((...args) => `${args}`), + i18n: instance, + } +} diff --git a/todoblue/src/app/(i18n)/server.ts b/todoblue/src/app/(i18n)/server.ts new file mode 100644 index 0000000..6d03135 --- /dev/null +++ b/todoblue/src/app/(i18n)/server.ts @@ -0,0 +1,27 @@ +import "server-only"; +import {createInstance, i18n} from "i18next" +import resourcesToBackend from "i18next-resources-to-backend" + + +async function init(lng: string, ns: string): Promise { + const instance = createInstance() + await instance + .use(resourcesToBackend((language: string, namespace: string) => import(`./(locales)/(${namespace})/${language}.json`))) + .init({ + supportedLngs: ["en-US"], + fallbackLng: "en-US", + lng, + fallbackNS: "common", + defaultNS: "common", + ns, + }) + return instance +} + +export async function useTranslation(lng: string, ns: string) { + const instance = await init(lng, ns) + return { + t: instance.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns), + i18n: instance + } +} diff --git a/todoblue/src/app/CreatePrivateBoardPanel.tsx b/todoblue/src/app/CreatePrivateBoardPanel.tsx deleted file mode 100644 index dbdd63b..0000000 --- a/todoblue/src/app/CreatePrivateBoardPanel.tsx +++ /dev/null @@ -1,87 +0,0 @@ -"use client"; - -import {useBoardCreator} from "@/app/useBoardCreator" -import {faKey} from "@fortawesome/free-solid-svg-icons" -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" -import classNames from "classnames" -import {default as React, useEffect, useState} from "react" - -export function CreatePrivateBoardPanel() { - const {createBoard} = useBoardCreator(); - const [canCreate, setCanCreate] = useState(null); - - useEffect(() => { - setCanCreate(window.isSecureContext) - }, []) - - let formContents; - if(canCreate === null) { - formContents = - } - else if(!canCreate) { - formContents = - } - else { - formContents = - } - - return ( -
{ - e.preventDefault(); - createBoard(crypto.randomUUID().toString()); - }} - > -

- - {" "} - Privato -

- {formContents} -
- ) -} - -function MightCreateBoardFormContents() { - return <> -

- Sto verificando se è possibile creare un tabellone privato sul tuo browser... -
- Attendi un attimo... -

- -} - -function CanCreateBoardFormContents() { - return <> -

- Crea un nuovo tabellone privato utilizzando un codice segreto autogenerato! -
- Esso sarà accessibile solo da chi ne conosce il link. -

- - -} - -function CannotCreateBoardFormContents() { - return <> -

- Questa funzionalità non è disponibile al di fuori di contesti sicuri. -
- Assicurati di stare usando HTTPS! -

- -} diff --git a/todoblue/src/app/CreatePublicBoardPanel.tsx b/todoblue/src/app/CreatePublicBoardPanel.tsx deleted file mode 100644 index 07aa60e..0000000 --- a/todoblue/src/app/CreatePublicBoardPanel.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; - -import {useBoardCreator} from "@/app/useBoardCreator" -import {useLowerKebabState} from "@/app/useKebabState" -import {faGlobe} from "@fortawesome/free-solid-svg-icons" -import {default as React} from "react" -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" - - -export function CreatePublicBoardPanel() { - const [code, setCode] = useLowerKebabState("") - const {createBoard} = useBoardCreator(); - - return ( -
{ - e.preventDefault(); - createBoard(code); - }} - > -

- - {" "} - Pubblico -

-

- Crea un nuovo tabellone pubblico, con un codice personalizzato! -
- Se un tabellone con quel codice esiste già, sarai reindirizzato ad esso. -

- - -
- ) -} diff --git a/todoblue/src/app/RootHeader.tsx b/todoblue/src/app/RootHeader.tsx deleted file mode 100644 index d9f2d01..0000000 --- a/todoblue/src/app/RootHeader.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import style from "@/app/page.module.css" -import {default as React} from "react" - - -export function RootHeader() { - return ( -
-

- Todoblue -

-
- ) -} diff --git a/todoblue/src/app/RootMain.tsx b/todoblue/src/app/RootMain.tsx deleted file mode 100644 index ec2e95e..0000000 --- a/todoblue/src/app/RootMain.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import {CreatePrivateBoardPanel} from "@/app/CreatePrivateBoardPanel" -import {CreatePublicBoardPanel} from "@/app/CreatePublicBoardPanel" -import style from "@/app/page.module.css" -import {StarredBoardsPanel} from "@/app/StarredBoardsPanel" -import {default as React} from "react" - - -export function RootMain() { - return ( -
-
-

- Crea un nuovo tabellone -

- - -
-
-

- Usa un tabellone già esistente -

- -
-
- ) -} diff --git a/todoblue/src/app/StarredBoardsPanel.tsx b/todoblue/src/app/StarredBoardsPanel.tsx deleted file mode 100644 index c387b4f..0000000 --- a/todoblue/src/app/StarredBoardsPanel.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; - -import {useManagedStarred} from "@/app/StarContext" -import cn from "classnames" -import Link from "next/link" -import {useEffect, useState} from "react" - - -export function StarredBoardsPanel() { - const [isClient, setIsClient] = useState(null); - const {starred} = useManagedStarred() - - useEffect(() => setIsClient(true), []) - - let content; - if(!isClient) { - content = <> -

- Sto recuperando i dati salvati sul tuo browser... -

- - } - else { - content = <> -

- Puoi stellare un tabellone cliccando sulla stellina una volta che ci sei dentro. -

- {starred.length > 0 ? -
    - {starred.map(s =>
  • {s}
  • )} -
- : -

- Non hai ancora stellato nessun tabellone. -

- } - - } - - return ( -
-

- Tabelloni stellati -

- {content} -
- ) -} diff --git a/todoblue/src/app/[lang]/(page)/CreatePrivateBoardPanel.tsx b/todoblue/src/app/[lang]/(page)/CreatePrivateBoardPanel.tsx new file mode 100644 index 0000000..f676c52 --- /dev/null +++ b/todoblue/src/app/[lang]/(page)/CreatePrivateBoardPanel.tsx @@ -0,0 +1,80 @@ +"use client"; + +import {useTranslation} from "@/app/(i18n)/client" +import {faLock} from "@fortawesome/free-solid-svg-icons" +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" +import classNames from "classnames" +import {useRouter} from "next/navigation" +import {default as React, SyntheticEvent, useCallback, useEffect, useState} from "react" + +export function CreatePrivateBoardPanel({lng}: {lng: string}) { + const {t} = useTranslation(lng, "root") + const router = useRouter(); + const [canCreate, setCanCreate] = useState(null); + + useEffect(() => { + setCanCreate(window.isSecureContext) + }, []) + + const createBoardValidated = useCallback((e: SyntheticEvent) => { + e.preventDefault(); + const code = crypto.randomUUID().toString(); + console.debug("[CreatePrivateBoardPanel] Creating private board..."); + router.push(`/board/${code}`); + }, [router]) + + let formContents; + if(canCreate === null) { + formContents = <> +

+ {t("createPrivateBoardLoadingDescription")} +
+ {t("createPrivateBoardLoadingSmall")} +

+ + } + else if(!canCreate) { + formContents = <> +

+ {t("createPrivateBoardUnavailableDescription")} +
+ {t("createPrivateBoardUnavailableSmall")} +

+ + } + else { + formContents = <> +

+ {t("createPrivateBoardDescription")} +
+ {t("createPrivateBoardSmall")} +

+ + + } + + return ( +
+

+ + {" "} + {t("createPrivateBoardTitle")} +

+ {formContents} +
+ ) +} diff --git a/todoblue/src/app/[lang]/(page)/CreatePublicBoardPanel.tsx b/todoblue/src/app/[lang]/(page)/CreatePublicBoardPanel.tsx new file mode 100644 index 0000000..11c76ee --- /dev/null +++ b/todoblue/src/app/[lang]/(page)/CreatePublicBoardPanel.tsx @@ -0,0 +1,68 @@ +"use client"; + +import {useTranslation} from "@/app/(i18n)/client" +import {useLowerKebabState} from "@/app/[lang]/useKebabState" +import {faGlobe} from "@fortawesome/free-solid-svg-icons" +import cn from "classnames" +import {useRouter} from "next/navigation" +import {default as React, SyntheticEvent, useCallback} from "react" +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" + + +export function CreatePublicBoardPanel({lng}: {lng: string}) { + const {t} = useTranslation(lng, "root") + const [code, setCode] = useLowerKebabState("") + const router = useRouter(); + + const codeIsValid = code.length >= 1 + + const createBoardValidated = useCallback((e: SyntheticEvent) => { + e.preventDefault(); + if(!codeIsValid) { + console.debug("[CreatePublicBoardPanel] Code is not valid, refusing to create board."); + return + } + console.debug("[CreatePublicBoardPanel] Creating public board with code:", code); + router.push(`/board/${code}`); + }, [code, codeIsValid, router]) + + return ( +
+

+ + {" "} + {t("createPublicBoardTitle")} +

+

+ {t("createPublicBoardDescription")} +
+ {t("createPublicBoardSmall")} +

+ + +
+ ) +} diff --git a/todoblue/src/app/[lang]/(page)/KnownBoardsPanel.tsx b/todoblue/src/app/[lang]/(page)/KnownBoardsPanel.tsx new file mode 100644 index 0000000..110dd26 --- /dev/null +++ b/todoblue/src/app/[lang]/(page)/KnownBoardsPanel.tsx @@ -0,0 +1,68 @@ +"use client"; + +import {useTranslation} from "@/app/(i18n)/client" +import {useLowerKebabState} from "@/app/[lang]/useKebabState" +import {faKey} from "@fortawesome/free-solid-svg-icons" +import cn from "classnames" +import {useRouter} from "next/navigation" +import {default as React, SyntheticEvent, useCallback} from "react" +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" + + +export function KnownBoardsPanel({lng}: {lng: string}) { + const {t} = useTranslation(lng, "root") + const [code, setCode] = useLowerKebabState("") + const router = useRouter(); + + const codeIsValid = code.length >= 1 + + const moveToBoardValidated = useCallback((e: SyntheticEvent) => { + e.preventDefault(); + if(!codeIsValid) { + console.debug("[KnownBoardsPanel] Code is not valid, refusing to move to board."); + return + } + console.debug("[KnownBoardsPanel] Moving to board with given code..."); + router.push(`/board/${code}`); + }, [code, codeIsValid, router]) + + return ( +
+

+ + {" "} + {t("existingKnownBoardsTitle")} +

+

+ {t("existingKnownBoardsDescription")} +
+ {t("existingKnownBoardsSmall")} +

+ + +
+ ) +} diff --git a/todoblue/src/app/RootFooter.tsx b/todoblue/src/app/[lang]/(page)/RootFooter.tsx similarity index 60% rename from todoblue/src/app/RootFooter.tsx rename to todoblue/src/app/[lang]/(page)/RootFooter.tsx index 41b84e4..2f1e4ef 100644 --- a/todoblue/src/app/RootFooter.tsx +++ b/todoblue/src/app/[lang]/(page)/RootFooter.tsx @@ -1,8 +1,11 @@ -import style from "@/app/page.module.css" +import {useTranslation} from "@/app/(i18n)/server" +import style from "@/app/[lang]/page.module.css" import {default as React} from "react" -export function RootFooter() { +export async function RootFooter({lng}: {lng: string}) { + const {t} = await useTranslation(lng, "root") + return (