From f98658b4410510ed74c79d3943933edf9ba679b9 Mon Sep 17 00:00:00 2001 From: Etienne Soulard-Geoffrion Date: Mon, 11 Mar 2024 09:35:31 -0400 Subject: [PATCH] feat: i18n router --- messages/en.json | 6 +++++- package.json | 1 + pnpm-lock.yaml | 14 ++++++++++++++ src/app/[locale]/i18n-provider.tsx | 3 ++- src/app/[locale]/page.tsx | 12 ++++++++++-- src/app/[locale]/search/page.tsx | 9 +++++++++ src/components/searchbox.tsx | 24 ++++++++++++++++++++---- src/i18n.ts | 30 ++++++++++++++++++++++-------- src/lib/use-i18n-router.ts | 15 +++++++++++++++ 9 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 src/app/[locale]/search/page.tsx create mode 100644 src/lib/use-i18n-router.ts diff --git a/messages/en.json b/messages/en.json index 76ccd21..f433c75 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1,5 +1,9 @@ { - "title": "Home of the Clinia Health Grade Search Sandbox", + "home": { + "questions": { + "title": "Try asking..." + } + }, "searchbox": { "placeholder": "Search for words and phrases, or ask a question...", "groups": { diff --git a/package.json b/package.json index 9e585f4..da80772 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@clinia-ui/react": "^0.1.8", "@clinia/search-sdk-core": "^0.1.0", "@clinia/search-sdk-react": "^0.1.0", + "@uidotdev/usehooks": "^2.4.1", "clsx": "^2.1.0", "lucide-react": "^0.354.0", "next": "14.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e470039..64f7712 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@clinia/search-sdk-react': specifier: ^0.1.0 version: 0.1.0(react@18.2.0) + '@uidotdev/usehooks': + specifier: ^2.4.1 + version: 2.4.1(react-dom@18.2.0)(react@18.2.0) clsx: specifier: ^2.1.0 version: 2.1.0 @@ -3952,6 +3955,17 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@uidotdev/usehooks@2.4.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==} + engines: {node: '>=16'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true diff --git a/src/app/[locale]/i18n-provider.tsx b/src/app/[locale]/i18n-provider.tsx index 913810d..a1858e4 100644 --- a/src/app/[locale]/i18n-provider.tsx +++ b/src/app/[locale]/i18n-provider.tsx @@ -1,3 +1,4 @@ +import { getMessages } from '@/i18n'; import type { PropsWithChildren } from 'react'; import { NextIntlClientProvider } from 'next-intl'; import type { useTimeZone } from 'next-intl'; @@ -12,7 +13,7 @@ export default async function I18nProvider({ locale, timeZone, }: I18nProviderProps) { - const messages = (await import(`../../../messages/${locale}.json`)).default; + const messages = await getMessages(locale); return ( -
+
+
+
+

+ {t('home.questions.title')} +

+ + +
); } diff --git a/src/app/[locale]/search/page.tsx b/src/app/[locale]/search/page.tsx new file mode 100644 index 0000000..ddb896d --- /dev/null +++ b/src/app/[locale]/search/page.tsx @@ -0,0 +1,9 @@ +import { Searchbox } from '@/components/searchbox'; + +export default function Search() { + return ( +
+ +
+ ); +} diff --git a/src/components/searchbox.tsx b/src/components/searchbox.tsx index bc18bd4..2dccb01 100644 --- a/src/components/searchbox.tsx +++ b/src/components/searchbox.tsx @@ -1,6 +1,9 @@ 'use client'; +import { useI18nRouter } from '@/lib/use-i18n-router'; +import { useClickAway } from '@uidotdev/usehooks'; import { Search, Sparkles } from 'lucide-react'; +import { twMerge } from 'tailwind-merge'; import { useMemo, useState } from 'react'; import { useTranslations } from 'next-intl'; import { @@ -12,10 +15,13 @@ import { CommandList, } from '@clinia-ui/react'; -export const Searchbox = () => { +type SearchBoxProps = React.HTMLAttributes; + +export const Searchbox = ({ className, ...props }: SearchBoxProps) => { const t = useTranslations(); const [value, setValue] = useState(''); const [open, setOpen] = useState(false); + const router = useI18nRouter(); const groups = useMemo( () => [ @@ -37,14 +43,24 @@ export const Searchbox = () => { [t] ); + const ref = useClickAway(() => setOpen(false)); + + const handleSearch = (v: string) => { + router.push(`/search?q=${v}`); + }; + return ( - + setOpen(true)} - onBlur={() => setOpen(false)} onValueChange={(v) => setValue(v)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleSearch(value); + } + }} /> @@ -64,7 +80,7 @@ export const Searchbox = () => { console.log(v)} + onSelect={(v) => handleSearch(v)} > {group.icon} {item} diff --git a/src/i18n.ts b/src/i18n.ts index 663964b..02d6e97 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,14 +1,28 @@ -import {notFound} from 'next/navigation'; -import {getRequestConfig} from 'next-intl/server'; - +import { getRequestConfig } from 'next-intl/server'; +import { notFound } from 'next/navigation'; + // Can be imported from a shared config const locales = ['en', 'de']; - -export default getRequestConfig(async ({locale}) => { + +export default getRequestConfig(async ({ locale }) => { // Validate that the incoming `locale` parameter is valid if (!locales.includes(locale as any)) notFound(); - + return { - messages: (await import(`../messages/${locale}.json`)).default + messages: (await import(`../messages/${locale}.json`)).default, }; -}); \ No newline at end of file +}); + +const messages = { + en: (): Record => + import('../messages/en.json').then((module) => module.default), + fr: (): Record => + import('../messages/fr.json').then((module) => module.default), +}; + +type Locales = keyof typeof messages; + +export const getMessages = async (locale: string) => { + const key = locale as Locales; + return messages[key](); +}; diff --git a/src/lib/use-i18n-router.ts b/src/lib/use-i18n-router.ts new file mode 100644 index 0000000..fc37ac7 --- /dev/null +++ b/src/lib/use-i18n-router.ts @@ -0,0 +1,15 @@ +import { useParams, useRouter } from 'next/navigation'; + +export const useI18nRouter = (): ReturnType => { + const { locale } = useParams(); + const router = useRouter(); + + return { + push: (url: string) => router.push(`/${locale}${url}`), + forward: () => router.forward(), + back: () => router.back(), + replace: (url: string) => router.replace(`/${locale}${url}`), + refresh: () => router.refresh(), + prefetch: (url: string) => router.prefetch(`/${locale}${url}`), + }; +};