1
Fork 0
mirror of https://github.com/Steffo99/todocolors.git synced 2024-11-25 17:54:18 +00:00

Setup localization and localize root page

This commit is contained in:
Steffo 2023-08-08 04:06:41 +02:00
parent 567818e95e
commit f6f6cbf9df
Signed by: steffo
GPG key ID: 2A24051445686895
58 changed files with 589 additions and 298 deletions

View file

@ -19,11 +19,17 @@
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"client-only": "^0.0.1", "client-only": "^0.0.1",
"i18next": "^23.4.2",
"i18next-resources-to-backend": "^1.1.4",
"negotiator": "^0.6.3",
"next": "13.4.12", "next": "13.4.12",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"typescript": "5.1.6", "typescript": "5.1.6",
"use-local-storage": "^3.0.0" "use-local-storage": "^3.0.0"
},
"devDependencies": {
"@types/negotiator": "^0.6.1"
} }
} }

View file

@ -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."
}

View file

@ -0,0 +1,3 @@
export const AVAILABLE_LOCALES: string[] = [
"en-US",
]

View file

@ -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<i18n> {
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<i18n | undefined>(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,
}
}

View file

@ -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<i18n> {
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
}
}

View file

@ -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<boolean | null>(null);
useEffect(() => {
setCanCreate(window.isSecureContext)
}, [])
let formContents;
if(canCreate === null) {
formContents = <MightCreateBoardFormContents/>
}
else if(!canCreate) {
formContents = <CannotCreateBoardFormContents/>
}
else {
formContents = <CanCreateBoardFormContents/>
}
return (
<form
className={classNames({
"panel": true,
"box": true,
"form-flex": true,
"fade": canCreate === null,
"red": canCreate === false,
})}
onSubmit={e => {
e.preventDefault();
createBoard(crypto.randomUUID().toString());
}}
>
<h3>
<FontAwesomeIcon icon={faKey} size={"1x"}/>
{" "}
Privato
</h3>
{formContents}
</form>
)
}
function MightCreateBoardFormContents() {
return <>
<p>
Sto verificando se è possibile creare un tabellone privato sul tuo browser...
<br/>
<small>Attendi un attimo...</small>
</p>
</>
}
function CanCreateBoardFormContents() {
return <>
<p>
Crea un nuovo tabellone privato utilizzando un codice segreto autogenerato!
<br/>
<small>Esso sarà accessibile solo da chi ne conosce il link.</small>
</p>
<label className={"float-bottom"}>
<span/>
<button>
Crea
</button>
<span/>
</label>
</>
}
function CannotCreateBoardFormContents() {
return <>
<p>
Questa funzionalità non è disponibile al di fuori di contesti sicuri.
<br/>
<small>Assicurati di stare usando HTTPS!</small>
</p>
</>
}

View file

@ -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 (
<form
className={"panel box form-flex"}
onSubmit={e => {
e.preventDefault();
createBoard(code);
}}
>
<h3>
<FontAwesomeIcon icon={faGlobe}/>
{" "}
Pubblico
</h3>
<p>
Crea un nuovo tabellone pubblico, con un codice personalizzato!
<br/>
<small>Se un tabellone con quel codice esiste già, sarai reindirizzato ad esso.</small>
</p>
<label className={"float-bottom"}>
<span>
Codice
</span>
<input
type={"text"}
placeholder={"garasauto-planning-2023"}
value={code}
onChange={(
e => setCode(e.target.value)
)}
/>
<span/>
</label>
<label>
<span/>
<button
onClick={_ => createBoard(code)}
>
Crea
</button>
<span/>
</label>
</form>
)
}

View file

@ -1,13 +0,0 @@
import style from "@/app/page.module.css"
import {default as React} from "react"
export function RootHeader() {
return (
<header className={style.pageHeader}>
<h1>
Todoblue
</h1>
</header>
)
}

View file

@ -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 (
<main className={style.pageMain}>
<div className={"chapter-2"}>
<h2>
Crea un nuovo tabellone
</h2>
<CreatePublicBoardPanel/>
<CreatePrivateBoardPanel/>
</div>
<div className={"chapter-1"}>
<h2>
Usa un tabellone già esistente
</h2>
<StarredBoardsPanel/>
</div>
</main>
)
}

View file

@ -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<true | null>(null);
const {starred} = useManagedStarred()
useEffect(() => setIsClient(true), [])
let content;
if(!isClient) {
content = <>
<p>
Sto recuperando i dati salvati sul tuo browser...
</p>
</>
}
else {
content = <>
<p>
Puoi stellare un tabellone cliccando sulla stellina una volta che ci sei dentro.
</p>
{starred.length > 0 ?
<ul>
{starred.map(s => <li key={s}><Link href={`/board/${s}`}><code>{s}</code></Link></li>)}
</ul>
:
<p className={"fade"}>
Non hai ancora stellato nessun tabellone.
</p>
}
</>
}
return (
<div className={cn({
"panel": true,
"box": true,
"fade": !isClient,
})}>
<h3>
Tabelloni stellati
</h3>
{content}
</div>
)
}

View file

@ -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<boolean | null>(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 = <>
<p>
{t("createPrivateBoardLoadingDescription")}
<br/>
<small>{t("createPrivateBoardLoadingSmall")}</small>
</p>
</>
}
else if(!canCreate) {
formContents = <>
<p>
{t("createPrivateBoardUnavailableDescription")}
<br/>
<small>{t("createPrivateBoardUnavailableSmall")}</small>
</p>
</>
}
else {
formContents = <>
<p>
{t("createPrivateBoardDescription")}
<br/>
<small>{t("createPrivateBoardSmall")}</small>
</p>
<label className={"float-bottom"}>
<span/>
<button onClick={createBoardValidated}>
{t("createPrivateBoardSubmitText")}
</button>
<span/>
</label>
</>
}
return (
<form
className={classNames({
"panel": true,
"box": true,
"form-flex": true,
"red": canCreate === false,
})}
onSubmit={createBoardValidated}
>
<h3>
<FontAwesomeIcon icon={faLock} size={"1x"}/>
{" "}
{t("createPrivateBoardTitle")}
</h3>
{formContents}
</form>
)
}

View file

@ -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 (
<form
className={"panel box form-flex"}
onSubmit={createBoardValidated}
>
<h3>
<FontAwesomeIcon icon={faGlobe}/>
{" "}
{t("createPublicBoardTitle")}
</h3>
<p>
{t("createPublicBoardDescription")}
<br/>
<small>{t("createPublicBoardSmall")}</small>
</p>
<label className={"float-bottom"}>
<span>
{t("createPublicBoardCodeLeft")}
</span>
<input
type={"text"}
placeholder={t("createPublicBoardCodePlaceholder")}
value={code}
onChange={e => setCode(e.target.value)}
/>
<span/>
</label>
<label>
<span/>
<button
onClick={createBoardValidated}
className={cn({"fade": !codeIsValid})}
>
{t("createPublicBoardSubmitText")}
</button>
<span/>
</label>
</form>
)
}

View file

@ -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 (
<form
className={"panel box form-flex"}
onSubmit={moveToBoardValidated}
>
<h3>
<FontAwesomeIcon icon={faKey}/>
{" "}
{t("existingKnownBoardsTitle")}
</h3>
<p>
{t("existingKnownBoardsDescription")}
<br/>
<small>{t("existingKnownBoardsSmall")}</small>
</p>
<label className={"float-bottom"}>
<span>
{t("existingKnownBoardsCodeLeft")}
</span>
<input
type={"text"}
placeholder={t("existingKnownBoardsCodePlaceholder")}
value={code}
onChange={e => setCode(e.target.value)}
/>
<span/>
</label>
<label>
<span/>
<button
onClick={moveToBoardValidated}
className={cn({"fade": !codeIsValid})}
>
{t("existingKnownBoardsSubmitText")}
</button>
<span/>
</label>
</form>
)
}

View file

@ -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" import {default as React} from "react"
export function RootFooter() { export async function RootFooter({lng}: {lng: string}) {
const {t} = await useTranslation(lng, "root")
return ( return (
<footer className={style.pageFooter}> <footer className={style.pageFooter}>
<p> <p>

View file

@ -0,0 +1,16 @@
import {useTranslation} from "@/app/(i18n)/server"
import style from "@/app/[lang]/page.module.css"
import {default as React} from "react"
export async function RootHeader({lng}: {lng: string}) {
const {t} = await useTranslation(lng, "root")
return (
<header className={style.pageHeader}>
<h1>
{t("title")}
</h1>
</header>
)
}

View file

@ -0,0 +1,31 @@
import {useTranslation} from "@/app/(i18n)/server"
import {CreatePrivateBoardPanel} from "@/app/[lang]/(page)/CreatePrivateBoardPanel"
import {CreatePublicBoardPanel} from "@/app/[lang]/(page)/CreatePublicBoardPanel"
import {KnownBoardsPanel} from "@/app/[lang]/(page)/KnownBoardsPanel"
import style from "@/app/[lang]/page.module.css"
import {StarredBoardsPanel} from "@/app/[lang]/(page)/StarredBoardsPanel"
import {default as React} from "react"
export async function RootMain({lng}: {lng: string}) {
const {t} = await useTranslation(lng, "root")
return (
<main className={style.pageMain}>
<div className={"chapter-2"}>
<h2>
{t("createBoardTitle")}
</h2>
<CreatePublicBoardPanel lng={lng}/>
<CreatePrivateBoardPanel lng={lng}/>
</div>
<div className={"chapter-2"}>
<h2>
{t("existingBoardTitle")}
</h2>
<KnownBoardsPanel lng={lng}/>
<StarredBoardsPanel lng={lng}/>
</div>
</main>
)
}

View file

@ -0,0 +1,76 @@
"use client";
import {useTranslation} from "@/app/(i18n)/client"
import {useManagedStarred} from "@/app/[lang]/StarContext"
import {faStar} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames"
import Link from "next/link"
import {useEffect, useState} from "react"
export function StarredBoardsPanel({lng}: {lng: string}) {
const {t} = useTranslation(lng, "root")
const [isClient, setIsClient] = useState<true | null>(null);
const {starred} = useManagedStarred()
useEffect(() => setIsClient(true), [])
let content;
if(!isClient) {
content = <>
<p>
{t("existingStarredBoardsLoadingDescription")}
<br/>
<small>
{t("existingStarredBoardsLoadingSmall")}
</small>
</p>
</>
}
if(starred.length === 0) {
content = <>
<p>
{t("existingStarredBoardsEmptyDescription")}
<br/>
<small>
{t("existingStarredBoardsEmptySmall")}
</small>
</p>
</>
}
else {
content = <>
<p>
{t("existingStarredBoardsDescription")}
<br/>
<small>
{t("existingStarredBoardsSmall")}
</small>
</p>
{starred.length > 0 ?
<ul>
{starred.map(s => <li key={s}><Link href={`/board/${s}`}><code>{s}</code></Link></li>)}
</ul>
:
<p className={"fade"}>
Non hai ancora stellato nessun tabellone.
</p>
}
</>
}
return (
<div className={cn({
"panel": true,
"box": true,
})}>
<h3>
<FontAwesomeIcon icon={faStar}/>
{" "}
{t("existingStarredBoardsTitle")}
</h3>
{content}
</div>
)
}

View file

@ -33,7 +33,7 @@ export function StarredManager({children}: {children: ReactNode}) {
} }
else { else {
const result = [...prev] const result = [...prev]
delete result[result.indexOf(value)] result.splice(result.indexOf(value), 1)
return result return result
} }
}) })

View file

@ -1,8 +1,8 @@
import {useManagedStarred} from "@/app/StarContext" import {useManagedStarred} from "@/app/[lang]/StarContext"
import {useRouter} from "next/navigation" import {useRouter} from "next/navigation"
import {ReactNode, useCallback} from "react" import {ReactNode, useCallback} from "react"
import style from "./BoardHeader.module.css" import style from "./BoardHeader.module.css"
import {useManagedBoard} from "@/app/board/[board]/BoardManager" import {useManagedBoard} from "@/app/[lang]/board/[board]/BoardManager"
import {faArrowDownWideShort, faHouse, faPencil, faObjectGroup, faTableColumns, faStar as faStarSolid} from "@fortawesome/free-solid-svg-icons" import {faArrowDownWideShort, faHouse, faPencil, faObjectGroup, faTableColumns, faStar as faStarSolid} from "@fortawesome/free-solid-svg-icons"
import {faStar as faStarRegular} from "@fortawesome/free-regular-svg-icons" import {faStar as faStarRegular} from "@fortawesome/free-regular-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"

View file

@ -1,6 +1,6 @@
import {BoardMainIcon} from "@/app/board/[board]/BoardMainIcon" import {BoardMainIcon} from "@/app/[lang]/board/[board]/BoardMainIcon"
import {BoardMainTaskGroups} from "@/app/board/[board]/BoardMainTaskGroups" import {BoardMainTaskGroups} from "@/app/[lang]/board/[board]/BoardMainTaskGroups"
import {useManagedBoard} from "@/app/board/[board]/BoardManager" import {useManagedBoard} from "@/app/[lang]/board/[board]/BoardManager"
import {faLink, faLinkSlash, faGear} from "@fortawesome/free-solid-svg-icons" import {faLink, faLinkSlash, faGear} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"

View file

@ -1,6 +1,6 @@
import {useManagedBoard} from "@/app/board/[board]/BoardManager" import {useManagedBoard} from "@/app/[lang]/board/[board]/BoardManager"
import {TaskDisplay} from "@/app/board/[board]/TaskDisplay" import {TaskDisplay} from "@/app/[lang]/board/[board]/TaskDisplay"
import {TaskGroup} from "@/app/board/[board]/useBoardTaskArranger" import {TaskGroup} from "@/app/[lang]/board/[board]/useBoardTaskArranger"
import cn from "classnames" import cn from "classnames"
import style from "./BoardMainTaskGroups.module.css" import style from "./BoardMainTaskGroups.module.css"

View file

@ -1,4 +1,4 @@
import {useBoard, UseBoardReturns} from "@/app/board/[board]/useBoard" import {useBoard, UseBoardReturns} from "@/app/[lang]/board/[board]/useBoard"
import {createContext, ReactNode, useContext} from "react" import {createContext, ReactNode, useContext} from "react"

View file

@ -1,4 +1,4 @@
import {useManagedBoard} from "@/app/board/[board]/BoardManager" import {useManagedBoard} from "@/app/[lang]/board/[board]/BoardManager"
import {FormEvent, useCallback} from "react" import {FormEvent, useCallback} from "react"

View file

@ -1,4 +1,4 @@
import {useManagedBoard} from "@/app/board/[board]/BoardManager" import {useManagedBoard} from "@/app/[lang]/board/[board]/BoardManager"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {FormEvent, useCallback} from "react" import {FormEvent, useCallback} from "react"

View file

@ -1,8 +1,8 @@
"use client"; "use client";
import {TaskIconEl} from "@/app/board/[board]/TaskIconEl" import {TaskIconEl} from "@/app/[lang]/board/[board]/TaskIconEl"
import {TaskWithId} from "@/app/board/[board]/Types" import {TaskWithId} from "@/app/[lang]/board/[board]/Types"
import {useManagedBoard} from "@/app/board/[board]/BoardManager" import {useManagedBoard} from "@/app/[lang]/board/[board]/BoardManager"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import {useCallback, useState, MouseEvent} from "react" import {useCallback, useState, MouseEvent} from "react"
import {faTrashCanArrowUp} from "@fortawesome/free-solid-svg-icons" import {faTrashCanArrowUp} from "@fortawesome/free-solid-svg-icons"

View file

@ -1,4 +1,4 @@
import {TaskIcon, TaskStatus} from "@/app/board/[board]/Types" import {TaskIcon, TaskStatus} from "@/app/[lang]/board/[board]/Types"
import {SizeProp} from "@fortawesome/fontawesome-svg-core" import {SizeProp} from "@fortawesome/fontawesome-svg-core"
import { import {
faUser as faUserSolid, faUser as faUserSolid,

View file

@ -1,6 +1,6 @@
import {TaskIconEl} from "@/app/board/[board]/TaskIconEl" import {TaskIconEl} from "@/app/[lang]/board/[board]/TaskIconEl"
import {TaskIcon, TaskImportance, TaskPriority, TaskStatus, TaskWithId} from "@/app/board/[board]/Types" import {TaskIcon, TaskImportance, TaskPriority, TaskStatus, TaskWithId} from "@/app/[lang]/board/[board]/Types"
import {TaskGroupTitleComponent, TaskGroupComparer, TaskGroup, TaskCategorizer} from "@/app/board/[board]/useBoardTaskArranger" import {TaskGroupTitleComponent, TaskGroupComparer, TaskGroup, TaskCategorizer} from "@/app/[lang]/board/[board]/useBoardTaskArranger"
import {ReactNode} from "react" import {ReactNode} from "react"

View file

@ -1,5 +1,5 @@
import {TaskWithId} from "@/app/board/[board]/Types" import {TaskWithId} from "@/app/[lang]/board/[board]/Types"
import {TaskSortingFunction} from "@/app/board/[board]/useBoardTaskArranger" import {TaskSortingFunction} from "@/app/[lang]/board/[board]/useBoardTaskArranger"
/** /**
* **Mapping** from {@link TaskImportance} to a {@link number}. * **Mapping** from {@link TaskImportance} to a {@link number}.

View file

@ -1,9 +1,9 @@
"use client"; "use client";
import {BoardMain} from "@/app/board/[board]/BoardMain" import {BoardMain} from "@/app/[lang]/board/[board]/BoardMain"
import {BoardManager} from "@/app/board/[board]/BoardManager" import {BoardManager} from "@/app/[lang]/board/[board]/BoardManager"
import {BoardHeader} from "@/app/board/[board]/BoardHeader" import {BoardHeader} from "@/app/[lang]/board/[board]/BoardHeader"
import {BoardTaskEditor} from "@/app/board/[board]/BoardTaskEditor" import {BoardTaskEditor} from "@/app/[lang]/board/[board]/BoardTaskEditor"
import style from "./page.module.css" import style from "./page.module.css"
export default function Page({params: {board}}: {params: {board: string}}) { export default function Page({params: {board}}: {params: {board: string}}) {

View file

@ -1,13 +1,13 @@
"use client"; "use client";
import {TASK_GROUPERS} from "@/app/board/[board]/doTaskGrouping" import {TASK_GROUPERS} from "@/app/[lang]/board/[board]/doTaskGrouping"
import {TASK_SORTERS} from "@/app/board/[board]/doTaskSorting" import {TASK_SORTERS} from "@/app/[lang]/board/[board]/doTaskSorting"
import {BoardAction, Task} from "@/app/board/[board]/Types" import {BoardAction, Task} from "@/app/[lang]/board/[board]/Types"
import {useBoardTaskEditor} from "@/app/board/[board]/useBoardTaskEditor" import {useBoardTaskEditor} from "@/app/[lang]/board/[board]/useBoardTaskEditor"
import {useBoardWs} from "@/app/board/[board]/useBoardWs" import {useBoardWs} from "@/app/[lang]/board/[board]/useBoardWs"
import {TaskGroup, useBoardTaskArranger} from "@/app/board/[board]/useBoardTaskArranger" import {TaskGroup, useBoardTaskArranger} from "@/app/[lang]/board/[board]/useBoardTaskArranger"
import {useBoardTitleEditor} from "@/app/board/[board]/useBoardTitleEditor" import {useBoardTitleEditor} from "@/app/[lang]/board/[board]/useBoardTitleEditor"
import {useCycleState} from "@/app/useCycleState" import {useCycleState} from "@/app/[lang]/useCycleState"
import {Dispatch, SetStateAction, useState} from "react" import {Dispatch, SetStateAction, useState} from "react"
export interface UseBoardReturns { export interface UseBoardReturns {

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import {BoardAction, Task, TaskBoardAction, TitleBoardAction} from "@/app/board/[board]/Types" import {BoardAction, Task, TaskBoardAction, TitleBoardAction} from "@/app/[lang]/board/[board]/Types"
import {Reducer, useReducer} from "react" import {Reducer, useReducer} from "react"

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import {Task, TaskWithId} from "@/app/board/[board]/Types" import {Task, TaskWithId} from "@/app/[lang]/board/[board]/Types"
import {ReactNode, useMemo} from "react" import {ReactNode, useMemo} from "react"
export type TaskGroup = { export type TaskGroup = {

View file

@ -1,4 +1,4 @@
import {Task, TaskIcon, TaskImportance, TaskPriority} from "@/app/board/[board]/Types" import {Task, TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/Types"
import {useCallback, useMemo, useState} from "react" import {useCallback, useMemo, useState} from "react"

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import {BoardAction} from "@/app/board/[board]/Types" import {BoardAction} from "@/app/[lang]/board/[board]/Types"
import {useCallback, useState} from "react" import {useCallback, useState} from "react"

View file

@ -1,10 +1,10 @@
'use client'; 'use client';
import {BoardAction} from "@/app/board/[board]/Types" import {BoardAction} from "@/app/[lang]/board/[board]/Types"
import {useBoardState} from "@/app/board/[board]/useBoardState" import {useBoardState} from "@/app/[lang]/board/[board]/useBoardState"
import {useWsBaseURL} from "@/app/useWsBaseURL" import {useWsBaseURL} from "@/app/[lang]/useWsBaseURL"
import {useCallback, useMemo} from "react" import {useCallback, useMemo} from "react"
import {useWs, WebSocketHandlerParams} from "@/app/useWs" import {useWs, WebSocketHandlerParams} from "@/app/[lang]/useWs"
export function useBoardWs(name: string) { export function useBoardWs(name: string) {

View file

@ -1,8 +1,8 @@
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
import "./layout.css"; import "./layout.css";
import {AppBody} from "@/app/AppBody" import {AppBody} from "@/app/[lang]/AppBody"
import {StarredManager} from "@/app/StarContext" import {StarredManager} from "@/app/[lang]/StarContext"
import type {Metadata as NextMetadata} from "next" import type {Metadata as NextMetadata} from "next"
import {default as React, ReactNode} from "react" import {default as React, ReactNode} from "react"

View file

@ -0,0 +1,17 @@
import {RootFooter} from "@/app/[lang]/(page)/RootFooter"
import {RootHeader} from "@/app/[lang]/(page)/RootHeader"
import {RootMain} from "@/app/[lang]/(page)/RootMain"
import {default as React} from "react";
import style from "./page.module.css"
export default async function page({params: {lng}}: {params: {lng: string}}) {
return (
<div className={style.pageRoot}>
<RootHeader lng={lng}/>
<RootMain lng={lng}/>
<RootFooter lng={lng}/>
</div>
)
}

View file

@ -2,8 +2,16 @@
import {useCallback, useState} from "react" import {useCallback, useState} from "react"
/**
* **Regex** identifying the characters to be replaced with dashes in {@link useAnyKebabState}, {@link useLowerKebabState}, and {@link useUpperKebabState}.
*/
const KEBABIFIER = /[^a-zA-Z0-9-]/g const KEBABIFIER = /[^a-zA-Z0-9-]/g
/**
* **Hook** similar to {@link useState}, but replace non-alphanumeric characters with dashes.
* @param initial
*/
export function useAnyKebabState(initial: string): [string, (inputString: string) => void] { export function useAnyKebabState(initial: string): [string, (inputString: string) => void] {
const [state, setInnerState] = useState<string>(initial); const [state, setInnerState] = useState<string>(initial);
@ -15,6 +23,10 @@ export function useAnyKebabState(initial: string): [string, (inputString: string
return [state, setState] return [state, setState]
} }
/**
* **Hook** similar to {@link useState}, but replaces non-alphanumeric characters with dashes and converts to lowercase the whole string.
* @param initial
*/
export function useLowerKebabState(initial: string): [string, (inputString: string) => void] { export function useLowerKebabState(initial: string): [string, (inputString: string) => void] {
const [state, setInnerState] = useState<string>(initial); const [state, setInnerState] = useState<string>(initial);
@ -26,6 +38,10 @@ export function useLowerKebabState(initial: string): [string, (inputString: stri
return [state, setState] return [state, setState]
} }
/**
* **Hook** similar to {@link useState}, but replaces non-alphanumeric characters with dashes and converts to uppercase the whole string.
* @param initial
*/
export function useUpperKebabState(initial: string): [string, (inputString: string) => void] { export function useUpperKebabState(initial: string): [string, (inputString: string) => void] {
const [state, setInnerState] = useState<string>(initial); const [state, setInnerState] = useState<string>(initial);

View file

@ -1,4 +1,4 @@
import {useHttpBaseURL} from "@/app/useHttpBaseURL" import {useHttpBaseURL} from "@/app/[lang]/useHttpBaseURL"
/** /**

View file

@ -1,17 +0,0 @@
import {RootFooter} from "@/app/RootFooter"
import {RootHeader} from "@/app/RootHeader"
import {RootMain} from "@/app/RootMain"
import {default as React} from "react";
import style from "./page.module.css"
export default function page() {
return (
<div className={style.pageRoot}>
<RootHeader/>
<RootMain/>
<RootFooter/>
</div>
)
}

View file

@ -0,0 +1,23 @@
import {AVAILABLE_LOCALES} from "@/app/(i18n)/(locales)"
import Negotiator from 'negotiator'
import {NextRequest, NextResponse} from "next/server"
export function middleware(request: NextRequest) {
// https://nextjs.org/docs/app/building-your-application/routing/internationalization
const pathname = request.nextUrl.pathname
const pathnameIsMissingLocale = AVAILABLE_LOCALES.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
)
if(!pathnameIsMissingLocale) {
return NextResponse.next()
}
const negotiator = new Negotiator(request as any)
const bestLocale = negotiator.language(AVAILABLE_LOCALES)
return NextResponse.rewrite(`${request.nextUrl.protocol}//${request.nextUrl.host}/${bestLocale}${pathname}`)
}
export const config = {
matcher: [
'/((?!api|_next|manifest.json|logo-wbg-[0-9]*.png|favicon.ico).*)',
]
}

View file

@ -2,6 +2,13 @@
# yarn lockfile v1 # yarn lockfile v1
"@babel/runtime@^7.21.5", "@babel/runtime@^7.22.5":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682"
integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==
dependencies:
regenerator-runtime "^0.14.0"
"@fortawesome/fontawesome-common-types@6.4.0": "@fortawesome/fontawesome-common-types@6.4.0":
version "6.4.0" version "6.4.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz#88da2b70d6ca18aaa6ed3687832e11f39e80624b" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz#88da2b70d6ca18aaa6ed3687832e11f39e80624b"
@ -97,6 +104,11 @@
dependencies: dependencies:
tslib "^2.4.0" tslib "^2.4.0"
"@types/negotiator@^0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@types/negotiator/-/negotiator-0.6.1.tgz#4c75543f6ef87f427f4705e731a933595b7397f5"
integrity sha512-c4mvXFByghezQ/eVGN5HvH/jI63vm3B7FiE81BUzDAWmuiohRecCO6ddU60dfq29oKUMiQujsoB2h0JQC7JHKA==
"@types/node@20.4.5": "@types/node@20.4.5":
version "20.4.5" version "20.4.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.5.tgz#9dc0a5cb1ccce4f7a731660935ab70b9c00a5d69" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.5.tgz#9dc0a5cb1ccce4f7a731660935ab70b9c00a5d69"
@ -174,6 +186,20 @@ graceful-fs@^4.1.2:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
i18next-resources-to-backend@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/i18next-resources-to-backend/-/i18next-resources-to-backend-1.1.4.tgz#d139ca0cacc270dcc90b7926e192f4cd5aa4db60"
integrity sha512-hMyr9AOmIea17AOaVe1srNxK/l3mbk81P7Uf3fdcjlw3ehZy3UNTd0OP3EEi6yu4J02kf9jzhCcjokz6AFlEOg==
dependencies:
"@babel/runtime" "^7.21.5"
i18next@^23.4.2:
version "23.4.2"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.4.2.tgz#e68108be82287114e027afc5402bb7830d7f45c9"
integrity sha512-hkVPHKFLtn9iewdqHDiU+MGVIBk+bVFn5usw7CIeCn/SBcVKGTItGdjNPm2B8Lnz42CeHUlnSOTgsr5vbITjhA==
dependencies:
"@babel/runtime" "^7.22.5"
"js-tokens@^3.0.0 || ^4.0.0": "js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -191,6 +217,11 @@ nanoid@^3.3.4:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
negotiator@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
next@13.4.12: next@13.4.12:
version "13.4.12" version "13.4.12"
resolved "https://registry.yarnpkg.com/next/-/next-13.4.12.tgz#809b21ea0aabbe88ced53252c88c4a5bd5af95df" resolved "https://registry.yarnpkg.com/next/-/next-13.4.12.tgz#809b21ea0aabbe88ced53252c88c4a5bd5af95df"
@ -263,6 +294,11 @@ react@18.2.0:
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
scheduler@^0.23.0: scheduler@^0.23.0:
version "0.23.0" version "0.23.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"