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:
parent
567818e95e
commit
f6f6cbf9df
58 changed files with 589 additions and 298 deletions
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
32
todoblue/src/app/(i18n)/(locales)/(root)/en-US.json
Normal file
32
todoblue/src/app/(i18n)/(locales)/(root)/en-US.json
Normal 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."
|
||||||
|
}
|
3
todoblue/src/app/(i18n)/(locales)/index.ts
Normal file
3
todoblue/src/app/(i18n)/(locales)/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const AVAILABLE_LOCALES: string[] = [
|
||||||
|
"en-US",
|
||||||
|
]
|
41
todoblue/src/app/(i18n)/client.ts
Normal file
41
todoblue/src/app/(i18n)/client.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
27
todoblue/src/app/(i18n)/server.ts
Normal file
27
todoblue/src/app/(i18n)/server.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
80
todoblue/src/app/[lang]/(page)/CreatePrivateBoardPanel.tsx
Normal file
80
todoblue/src/app/[lang]/(page)/CreatePrivateBoardPanel.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
68
todoblue/src/app/[lang]/(page)/CreatePublicBoardPanel.tsx
Normal file
68
todoblue/src/app/[lang]/(page)/CreatePublicBoardPanel.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
68
todoblue/src/app/[lang]/(page)/KnownBoardsPanel.tsx
Normal file
68
todoblue/src/app/[lang]/(page)/KnownBoardsPanel.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
16
todoblue/src/app/[lang]/(page)/RootHeader.tsx
Normal file
16
todoblue/src/app/[lang]/(page)/RootHeader.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
31
todoblue/src/app/[lang]/(page)/RootMain.tsx
Normal file
31
todoblue/src/app/[lang]/(page)/RootMain.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
76
todoblue/src/app/[lang]/(page)/StarredBoardsPanel.tsx
Normal file
76
todoblue/src/app/[lang]/(page)/StarredBoardsPanel.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
|
@ -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"
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
@ -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"
|
|
@ -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,
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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}.
|
|
@ -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}}) {
|
|
@ -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 {
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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 = {
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
|
@ -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"
|
||||||
|
|
17
todoblue/src/app/[lang]/page.tsx
Normal file
17
todoblue/src/app/[lang]/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {useHttpBaseURL} from "@/app/useHttpBaseURL"
|
import {useHttpBaseURL} from "@/app/[lang]/useHttpBaseURL"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
23
todoblue/src/middleware.ts
Normal file
23
todoblue/src/middleware.ts
Normal 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).*)',
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue