Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions CleanArchitecture.Presentation/admin/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { ThemeProvider } from "@/context/ThemeContext";
import { LanguageProvider } from "@/context/LanguageContext";
import { Outfit } from "next/font/google";
import localFont from "next/font/local";
import { cookies } from "next/headers";
import { defaultLocale, isLocale, isRtl } from "@/i18n";
import "./globals.css";
import "flatpickr/dist/flatpickr.css";

Expand Down Expand Up @@ -38,17 +40,23 @@ interface RootLayoutProps {
children: ReactNode;
}

export default function RootLayout({ children }: Readonly<RootLayoutProps>) {
export default async function RootLayout({ children }: Readonly<RootLayoutProps>) {
const cookieStore = await cookies();
const localeCookie = cookieStore.get("admin-locale")?.value;
const initialLocale = isLocale(localeCookie) ? localeCookie : defaultLocale;
const htmlDirection = isRtl(initialLocale) ? "rtl" : "ltr";

return (
<html
lang="en"
lang={initialLocale}
dir={htmlDirection}
suppressHydrationWarning
className={`${outfit.variable} ${vazirmatn.variable}`}
>
<body className="antialiased dark:bg-gray-900">
<AuthProvider>
<QueryProvider>
<LanguageProvider>
<LanguageProvider initialLocale={initialLocale}>
<ThemeProvider>
<SidebarProvider>{children}</SidebarProvider>
</ThemeProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ import { DropdownItem } from "../ui/dropdown/DropdownItem";
import { useSession, signOut } from "next-auth/react";
import { useLanguage } from "@/context/LanguageContext";

function resolveSafeRedirectUrl(logoutUrl: string | undefined): string {
if (!logoutUrl) {
return "/";
}

if (typeof window === "undefined") {
return "/";
}

try {
const parsedLogoutUrl = new URL(logoutUrl, window.location.origin);
const isSameOrigin = parsedLogoutUrl.origin === window.location.origin;
const isSecureExternalUrl = parsedLogoutUrl.protocol === "https:";
const isTrustedRedirect = isSameOrigin || isSecureExternalUrl;

return isTrustedRedirect ? parsedLogoutUrl.toString() : "/";
} catch {
return "/";
}
}

export default function UserDropdown() {
const { data: session } = useSession();
const [isOpen, setIsOpen] = useState(false);
Expand Down Expand Up @@ -34,14 +55,15 @@ export default function UserDropdown() {
}

const { logoutUrl } = (await response.json()) as { logoutUrl?: string };
const safeLogoutUrl = resolveSafeRedirectUrl(logoutUrl);

await signOut({
redirect: false,
callbackUrl: "/",
});

setTimeout(() => {
window.location.assign(logoutUrl || "/");
window.location.assign(safeLogoutUrl);
}, 100);
} catch {
await signOut({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"use client";

import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { Locale, defaultLocale, isRtl, dictionaries } from "@/i18n";
import { Locale, defaultLocale, isLocale, isRtl, dictionaries } from "@/i18n";

const localeStorageKey = "locale";
const localeCookieName = "admin-locale";

type LanguageContextType = {
locale: Locale;
Expand All @@ -12,19 +15,30 @@ type LanguageContextType = {

const LanguageContext = createContext<LanguageContextType | undefined>(undefined);

export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [locale, setLocaleState] = useState<Locale>(defaultLocale);
type LanguageProviderProps = {
children: React.ReactNode;
initialLocale?: Locale;
};

export const LanguageProvider: React.FC<LanguageProviderProps> = ({
children,
initialLocale = defaultLocale,
}) => {
const [locale, setLocaleState] = useState<Locale>(initialLocale);

useEffect(() => {
const savedLocale = localStorage.getItem("locale") as Locale;
if (savedLocale && dictionaries[savedLocale]) {
setLocaleState(savedLocale);
}
}, []);
const persistedLocale = isLocale(window.localStorage.getItem(localeStorageKey))
? locale
: initialLocale;

window.localStorage.setItem(localeStorageKey, persistedLocale);
document.cookie = `${localeCookieName}=${persistedLocale}; path=/; max-age=31536000; samesite=lax`;
}, [initialLocale, locale]);

const setLocale = useCallback((newLocale: Locale) => {
setLocaleState(newLocale);
localStorage.setItem("locale", newLocale);
window.localStorage.setItem(localeStorageKey, newLocale);
document.cookie = `${localeCookieName}=${newLocale}; path=/; max-age=31536000; samesite=lax`;
}, []);

const t = useCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import dynamic from "next/dynamic";
import type { PropsWithChildren } from "react";
import { useState } from "react";
import { isApiError } from "@/lib/utils/api-error";

const ReactQueryDevtools = dynamic(
() => import("@tanstack/react-query-devtools").then((module) => module.ReactQueryDevtools),
{ ssr: false },
);

const queryStaleTime = 60_000;
const queryGcTime = 5 * 60_000;
const maxQueryRetries = 2;
Expand Down
5 changes: 5 additions & 0 deletions CleanArchitecture.Presentation/admin/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ export const dictionaries = {
};

export type Locale = keyof typeof dictionaries;
export const supportedLocales = Object.keys(dictionaries) as Locale[];

export const defaultLocale: Locale = 'en';

export const isRtl = (locale: Locale) => {
return locale === 'fa' || locale === 'ar';
};

export function isLocale(value: string | null | undefined): value is Locale {
return Boolean(value) && supportedLocales.includes(value as Locale);
}
Loading