Skip to content

Commit

Permalink
Merge pull request #24 from strapi/feat/locale-switcher-logic
Browse files Browse the repository at this point in the history
chore: create a locale switcher logic
  • Loading branch information
Mcastres authored Dec 18, 2024
2 parents 13fcb28 + 1560122 commit 33cc961
Show file tree
Hide file tree
Showing 13 changed files with 346 additions and 214 deletions.
20 changes: 20 additions & 0 deletions next/app/[locale]/(marketing)/ClientSlugHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";

import { useEffect } from "react";
import { useSlugContext } from "@/app/context/SlugContext";

export default function ClientSlugHandler({
localizedSlugs,
}: {
localizedSlugs: Record<string, string>;
}) {
const { dispatch } = useSlugContext();

useEffect(() => {
if (localizedSlugs) {
dispatch({ type: "SET_SLUGS", payload: localizedSlugs });
}
}, [localizedSlugs, dispatch]);

return null; // This component only handles the state and doesn't render anything.
}
15 changes: 14 additions & 1 deletion next/app/[locale]/(marketing)/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Metadata } from 'next';
import PageContent from '@/lib/shared/PageContent';
import fetchContentType from '@/lib/strapi/fetchContentType';
import { generateMetadataObject } from '@/lib/shared/metadata';
import ClientSlugHandler from '../ClientSlugHandler';

export async function generateMetadata({
params,
Expand All @@ -27,7 +28,19 @@ export default async function Page({ params }: { params: { locale: string, slug:
true
);

const localizedSlugs = pageData.localizations?.reduce(
(acc: Record<string, string>, localization: any) => {
acc[localization.locale] = localization.slug;
return acc;
},
{ [params.locale]: params.slug }
);

return (
<PageContent pageData={pageData} />
<>
<ClientSlugHandler localizedSlugs={localizedSlugs} />
<PageContent pageData={pageData} />
</>

);
}
37 changes: 21 additions & 16 deletions next/app/[locale]/(marketing)/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
import { Metadata } from 'next';
import React from "react";

import { BlogLayout } from "@/components/blog-layout";
import fetchContentType from "@/lib/strapi/fetchContentType";
import { BlocksRenderer } from '@strapi/blocks-react-renderer';
import { BlocksRenderer } from "@strapi/blocks-react-renderer";

import { generateMetadataObject } from '@/lib/shared/metadata';
import ClientSlugHandler from "../../ClientSlugHandler";

export async function generateMetadata({
export default async function SingleArticlePage({
params,
}: {
params: { locale: string, slug: string };
}): Promise<Metadata> {
const pageData = await fetchContentType("articles", `filters[slug]=${params?.slug}&filters[locale][$eq]=${params.locale}&populate=seo.metaImage`, true)

const seo = pageData?.seo;
const metadata = generateMetadataObject(seo);
return metadata;
}

export default async function singleArticlePage({ params }: { params: { slug: string, locale: string } }) {
const article = await fetchContentType("articles", `filters[slug]=${params?.slug}&filters[locale][$eq]=${params.locale}`, true)
params: { slug: string; locale: string };
}) {
const article = await fetchContentType(
"articles",
`filters[slug]=${params?.slug}&filters[locale][$eq]=${params.locale}`,
true
);

if (!article) {
return <div>Blog not found</div>;
}

const localizedSlugs = article.localizations?.reduce(
(acc: Record<string, string>, localization: any) => {
acc[localization.locale] = localization.slug;
return acc;
},
{ [params.locale]: params.slug }
);

return (
<BlogLayout article={article} locale={params.locale}>
<ClientSlugHandler localizedSlugs={localizedSlugs} />
<BlocksRenderer content={article.content} />
</BlogLayout>
);
}
}
12 changes: 11 additions & 1 deletion next/app/[locale]/(marketing)/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import fetchContentType from "@/lib/strapi/fetchContentType";
import { Article } from "@/types/types";
import { generateMetadataObject } from '@/lib/shared/metadata';

import ClientSlugHandler from "../ClientSlugHandler";

export async function generateMetadata({
params,
Expand All @@ -27,13 +28,22 @@ export async function generateMetadata({
export default async function Blog({
params,
}: {
params: { locale: string };
params: { locale: string, slug: string };
}) {
const blogPage = await fetchContentType('blog-page', `filters[locale]=${params.locale}`, true)
const articles = await fetchContentType('articles', `filters[locale]=${params.locale}`)

const localizedSlugs = blogPage.localizations?.reduce(
(acc: Record<string, string>, localization: any) => {
acc[localization.locale] = "blog";
return acc;
},
{ [params.locale]: "blog" }
);

return (
<div className="relative overflow-hidden py-20 md:py-0">
<ClientSlugHandler localizedSlugs={localizedSlugs} />
<AmbientColor />
<Container className="flex flex-col items-center justify-between pb-20">
<div className="relative z-20 py-10 md:pt-40">
Expand Down
14 changes: 13 additions & 1 deletion next/app/[locale]/(marketing)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Metadata } from 'next';
import PageContent from '@/lib/shared/PageContent';
import fetchContentType from '@/lib/strapi/fetchContentType';
import { generateMetadataObject } from '@/lib/shared/metadata';
import ClientSlugHandler from './ClientSlugHandler';

export async function generateMetadata({
params,
Expand All @@ -27,5 +28,16 @@ export default async function HomePage({ params }: { params: { locale: string }
true
);

return <PageContent pageData={pageData} />;
const localizedSlugs = pageData.localizations?.reduce(
(acc: Record<string, string>, localization: any) => {
acc[localization.locale] = "";
return acc;
},
{ [params.locale]: "" }
);

return <>
<ClientSlugHandler localizedSlugs={localizedSlugs} />
<PageContent pageData={pageData} />
</>;
}
10 changes: 10 additions & 0 deletions next/app/[locale]/(marketing)/products/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { IconShoppingCartUp } from "@tabler/icons-react";
import fetchContentType from "@/lib/strapi/fetchContentType";
import { generateMetadataObject } from '@/lib/shared/metadata';

import ClientSlugHandler from '../ClientSlugHandler';

export async function generateMetadata({
params,
}: {
Expand All @@ -32,10 +34,18 @@ export default async function Products({
const productPage = await fetchContentType('product-page', `filters[locale]=${params.locale}`, true);
const products = await fetchContentType('products', ``);

const localizedSlugs = productPage.localizations?.reduce(
(acc: Record<string, string>, localization: any) => {
acc[localization.locale] = "products";
return acc;
},
{ [params.locale]: "products" }
);
const featured = products?.data.filter((product: { featured: boolean }) => product.featured);

return (
<div className="relative overflow-hidden w-full">
<ClientSlugHandler localizedSlugs={localizedSlugs} />
<AmbientColor />
<Container className="pt-40 pb-40">
<FeatureIconContainer className="flex justify-center items-center overflow-hidden">
Expand Down
44 changes: 44 additions & 0 deletions next/app/context/SlugContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import React, { createContext, useContext, useReducer } from "react";

type State = {
localizedSlugs: Record<string, string>;
};

type Action = {
type: "SET_SLUGS";
payload: Record<string, string>;
};

const SlugContext = createContext<{
state: State;
dispatch: React.Dispatch<Action>;
} | null>(null);

const slugReducer = (state: State, action: Action): State => {
switch (action.type) {
case "SET_SLUGS":
return { ...state, localizedSlugs: action.payload };
default:
return state;
}
};

export const SlugProvider = ({ children }: { children: React.ReactNode }) => {
const [state, dispatch] = useReducer(slugReducer, { localizedSlugs: {} });

return (
<SlugContext.Provider value={{ state, dispatch }}>
{children}
</SlugContext.Provider>
);
};

export const useSlugContext = () => {
const context = useContext(SlugContext);
if (!context) {
throw new Error("useSlugContext must be used within a SlugProvider");
}
return context;
};
6 changes: 5 additions & 1 deletion next/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Locale, i18n } from '@/i18n.config'

import "./globals.css";

import { SlugProvider } from "./context/SlugContext";

export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#06b6d4" },
Expand All @@ -24,7 +26,9 @@ export default function RootLayout({
return (
<html lang={params.lang}>
<body>
{children}
<SlugProvider>
{children}
</SlugProvider>
</body>
</html>
);
Expand Down
75 changes: 43 additions & 32 deletions next/components/locale-switcher.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,55 @@
"use client";

import React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useSlugContext } from "@/app/context/SlugContext";
import { cn } from "@/lib/utils";

export function LocaleSwitcher({ currentLocale }: { currentLocale: string }) {
const { state } = useSlugContext();
const { localizedSlugs } = state;

import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { i18n } from '@/i18n.config'
const pathname = usePathname(); // Current path
const segments = pathname.split("/"); // Split path into segments

import { cn } from "@/lib/utils";
// Generate localized path for each locale
const generateLocalizedPath = (locale: string): string => {
if (!pathname) return `/${locale}`; // Default to root path for the locale

// Handle homepage (e.g., "/en" -> "/fr")
if (segments.length <= 2) {
return `/${locale}`;
}

export function LocaleSwitcher() {
const pathName = usePathname()
const currentLocale = pathName.split('/')[1]
// Handle dynamic paths (e.g., "/en/blog/[slug]")
if (localizedSlugs[locale]) {
segments[1] = locale; // Replace the locale
segments[segments.length - 1] = localizedSlugs[locale]; // Replace slug if available
return segments.join("/");
}

const redirectedPathName = (locale: string) => {
if (!pathName) return '/'
const segments = pathName.split('/')
segments[1] = locale
return segments.join('/')
}
// Fallback to replace only the locale
segments[1] = locale;
return segments.join("/");
};

return (
<div className="flex gap-2 p-1 rounded-md">
{i18n.locales.map((locale) => (
<Link
key={locale}
href={redirectedPathName(locale)}
>
<React.Fragment >
<div
className={cn(
"flex cursor-pointer items-center justify-center text-sm leading-[110%] w-8 py-1 rounded-md hover:bg-neutral-800 hover:text-white/80 text-white hover:shadow-[0px_1px_0px_0px_var(--neutral-600)_inset] transition duration-200",
locale === currentLocale
? "bg-neutral-800 text-white shadow-[0px_1px_0px_0px_var(--neutral-600)_inset]"
: ""
)}
>
{locale}
</div>
</React.Fragment>
<div className="flex gap-2 p-1 rounded-md">
{!pathname.includes("/products/") && Object.keys(localizedSlugs).map((locale) => (
<Link key={locale} href={generateLocalizedPath(locale)}>
<div
className={cn(
"flex cursor-pointer items-center justify-center text-sm leading-[110%] w-8 py-1 rounded-md hover:bg-neutral-800 hover:text-white/80 text-white hover:shadow-[0px_1px_0px_0px_var(--neutral-600)_inset] transition duration-200",
locale === currentLocale
? "bg-neutral-800 text-white shadow-[0px_1px_0px_0px_var(--neutral-600)_inset]"
: ""
)}
>
{locale}
</div>
</Link>
))}
</div>
);
}
}
2 changes: 1 addition & 1 deletion next/components/navbar/desktop-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const DesktopNavbar = ({ leftNavbarItems, rightNavbarItems, logo, locale
</div>
</div>
<div className="flex space-x-2 items-center">
<LocaleSwitcher />
<LocaleSwitcher currentLocale={locale} />

{rightNavbarItems.map((item, index) => (
<Button key={item.text} variant={index === rightNavbarItems.length - 1 ? 'primary' : 'simple'} as={Link} href={`/${locale}${item.URL}`}>
Expand Down
8 changes: 4 additions & 4 deletions strapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
"typescript": "^5"
},
"dependencies": {
"@strapi/plugin-cloud": "5.5.0",
"@strapi/plugin-cloud": "5.5.1",
"@strapi/plugin-seo": "^2.0.4",
"@strapi/plugin-users-permissions": "5.5.0",
"@strapi/strapi": "5.5.0",
"better-sqlite3": "9.4.3",
"@strapi/plugin-users-permissions": "5.5.1",
"@strapi/strapi": "5.5.1",
"better-sqlite3": "11.7.0",
"patch-package": "^8.0.0",
"pluralize": "^8.0.0",
"react": "^18.0.0",
Expand Down
9 changes: 6 additions & 3 deletions strapi/src/middlewares/deepPopulate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,18 @@ const getDeepPopulate = (uid: UID.Schema, opts: Options = {}) => {

export default (config, { strapi }: { strapi: Core.Strapi }) => {
return async (ctx, next) => {
if (ctx.request.url.startsWith('/api/') && ctx.request.method === 'GET' && !ctx.query.populate) {
if (ctx.request.url.startsWith('/api/') && ctx.request.method === 'GET' && !ctx.query.populate && !ctx.request.url.includes('/api/users')) {
strapi.log.info('Using custom Dynamic-Zone population Middleware...');

const contentType = extractPathSegment(ctx.request.url);
const singular = pluralize.singular(contentType)
const uid = `api::${singular}.${singular}`;

// @ts-ignores
ctx.query.populate = getDeepPopulate(uid);
ctx.query.populate = {
// @ts-ignores
...getDeepPopulate(uid),
...(!ctx.request.url.includes("products") && { localizations: { populate: {} } })
};
}
await next();
};
Expand Down
Loading

0 comments on commit 33cc961

Please sign in to comment.