From cfd6a9b3392f77a1d52c843576976d2f0de40a60 Mon Sep 17 00:00:00 2001 From: Takeshi Kimata <117462761+kimatata@users.noreply.github.com> Date: Mon, 27 May 2024 23:15:30 +0900 Subject: [PATCH] Create toast provider --- frontend/messages/en.json | 4 ++ frontend/messages/ja.json | 4 ++ frontend/package-lock.json | 19 +++++-- frontend/package.json | 1 + frontend/src/app/[locale]/layout.tsx | 12 ++++- frontend/src/app/[locale]/providers.tsx | 9 +++- frontend/types/toast.ts | 12 +++++ frontend/types/user.ts | 3 ++ frontend/utils/ToastProvider.tsx | 31 +++++++++++ frontend/utils/TokenProvider.tsx | 68 ++++++++++++++++--------- 10 files changed, 132 insertions(+), 31 deletions(-) create mode 100644 frontend/types/toast.ts create mode 100644 frontend/utils/ToastProvider.tsx diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 4e517d3..f1b749e 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -29,6 +29,10 @@ "signin": "Sign in", "signout": "Sign out" }, + "Toast": { + "need_signed_in": "You need signed in", + "session_expired": "Session expired" + }, "Auth": { "account": "Account", "signup": "Sign up", diff --git a/frontend/messages/ja.json b/frontend/messages/ja.json index d290e65..b98b987 100644 --- a/frontend/messages/ja.json +++ b/frontend/messages/ja.json @@ -29,6 +29,10 @@ "signin": "サインイン", "signout": "サインアウト" }, + "Toast": { + "need_signed_in": "サインインが必要です", + "session_expired": "セッションが切れました" + }, "Auth": { "account": "アカウント", "signup": "新規登録", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2e214f8..1c45227 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,6 +32,7 @@ "react": "^18.3.1", "react-apexcharts": "^1.4.1", "react-dom": "18.2.0", + "react-toastify": "^10.0.5", "tailwind-variants": "^0.1.18", "tailwindcss": "^3.4.3", "typescript": "5.0.4" @@ -3672,9 +3673,9 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, "node_modules/clsx": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", - "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "engines": { "node": ">=6" } @@ -6173,6 +6174,18 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5abc551..e380034 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "react": "^18.3.1", "react-apexcharts": "^1.4.1", "react-dom": "18.2.0", + "react-toastify": "^10.0.5", "tailwind-variants": "^0.1.18", "tailwindcss": "^3.4.3", "typescript": "5.0.4" diff --git a/frontend/src/app/[locale]/layout.tsx b/frontend/src/app/[locale]/layout.tsx index 180375f..74d1992 100644 --- a/frontend/src/app/[locale]/layout.tsx +++ b/frontend/src/app/[locale]/layout.tsx @@ -4,6 +4,7 @@ import { Providers } from './providers'; import Header from './Header'; import clsx from 'clsx'; import { getTranslations } from 'next-intl/server'; +import { useTranslations } from 'next-intl'; export async function generateMetadata({ params: { locale } }) { const t = await getTranslations({ locale, namespace: 'Header' }); @@ -25,11 +26,20 @@ export default function RootLayout({ children: React.ReactNode; params: { locale: string }; }) { + const t = useTranslations('Toast'); + const toastMessages = { + needSignedIn: t('need_signed_in'), + sessionExpired: t('session_expired'), + }; + return ( - +
{children}
diff --git a/frontend/src/app/[locale]/providers.tsx b/frontend/src/app/[locale]/providers.tsx index 26e7adf..cd2e573 100644 --- a/frontend/src/app/[locale]/providers.tsx +++ b/frontend/src/app/[locale]/providers.tsx @@ -5,22 +5,27 @@ import { NextUIProvider } from '@nextui-org/react'; import { useRouter } from 'next/navigation'; import { ThemeProvider as NextThemesProvider } from 'next-themes'; import { ThemeProviderProps } from 'next-themes/dist/types'; +import ToastProvider from '@/utils/ToastProvider'; +import { ToastProps } from '@/types/toast'; import TokenProvider from '@/utils/TokenProvider'; import { TokenProps } from '@/types/user'; export interface ProvidersProps { children: React.ReactNode; themeProps?: ThemeProviderProps; + toastProps?: ToastProps; tokenProps?: TokenProps; } -export function Providers({ children, themeProps, tokenProps }: ProvidersProps) { +export function Providers({ children, themeProps, toastProps, tokenProps }: ProvidersProps) { const router = useRouter(); return ( - {children} + + {children} + ); diff --git a/frontend/types/toast.ts b/frontend/types/toast.ts new file mode 100644 index 0000000..973610a --- /dev/null +++ b/frontend/types/toast.ts @@ -0,0 +1,12 @@ +export type ToastProps = { + children?: React.ReactNode; +}; + +export type ToastMessages = { + needSignedIn: string; + sessionExpired: string; +}; + +export type ToastContextType = { + showToast: (text: string, mode: string) => {}; +}; diff --git a/frontend/types/user.ts b/frontend/types/user.ts index 970d6c5..ec4cb9b 100644 --- a/frontend/types/user.ts +++ b/frontend/types/user.ts @@ -1,3 +1,5 @@ +import { ToastMessages } from './toast'; + export type UserType = { id: number | null; email: string; @@ -8,6 +10,7 @@ export type UserType = { } | null; export type TokenProps = { + toastMessages: ToastMessages; locale: string; children?: React.ReactNode; }; diff --git a/frontend/utils/ToastProvider.tsx b/frontend/utils/ToastProvider.tsx new file mode 100644 index 0000000..c5ba500 --- /dev/null +++ b/frontend/utils/ToastProvider.tsx @@ -0,0 +1,31 @@ +import { createContext } from 'react'; +import { ToastContextType, ToastProps } from '@/types/toast'; + +// TODO Temporary use until NextUI's Toast is released. +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +const defaultContext = { + showToast: (text: string, mode: string) => {}, +}; +const ToastContext = createContext(defaultContext); + +const ToastProvider = ({ children }: ToastProps) => { + const showToast = (text: string, mode: string) => { + toast(text, { theme: 'light' }); + }; + + const toastContext = { + showToast, + }; + + return ( + + + {children} + + ); +}; + +export { ToastContext }; +export default ToastProvider; diff --git a/frontend/utils/TokenProvider.tsx b/frontend/utils/TokenProvider.tsx index 031f7a8..8fa21ef 100644 --- a/frontend/utils/TokenProvider.tsx +++ b/frontend/utils/TokenProvider.tsx @@ -1,9 +1,10 @@ 'use client'; -import { createContext, useState, useEffect } from 'react'; +import { createContext, useState, useEffect, useContext } from 'react'; import { TokenContextType, TokenType } from '@/types/user'; import { TokenProps } from '@/types/user'; import { useRouter, usePathname } from '@/src/navigation'; import { roles } from '@/config/selection'; +import { ToastContext } from './ToastProvider'; const LOCAL_STORAGE_KEY = 'testplat-auth-token'; function storeTokenToLocalStorage(token: TokenType) { @@ -27,9 +28,10 @@ const defaultContext = { }; const TokenContext = createContext(defaultContext); -const TokenProvider = ({ locale, children }: TokenProps) => { +const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => { const router = useRouter(); const pathname = usePathname(); + const toastContext = useContext(ToastContext); const [hasRestoreFinished, setHasRestoreFinished] = useState(false); const [token, setToken] = useState({ @@ -38,34 +40,39 @@ const TokenProvider = ({ locale, children }: TokenProps) => { user: null, }); - const isSignedIn = () => { - // check token + const tokenExists = () => { if (token && token.user && token.user.username) { - // check expire date - if (Date.now() < token.expires_at) { - return true; - } else { - console.error('session expired'); - } + return true; + } else { + return false; } + }; - return false; + const isTokenValid = () => { + if (Date.now() < token.expires_at) { + return true; + } else { + return false; + } + }; + + const isSignedIn = () => { + if (tokenExists() && isTokenValid()) { + return true; + } else { + return false; + } }; const isAdmin = () => { - // check token - if (token && token.user && token.user.username) { - // check expire date - if (Date.now() < token.expires_at) { - // check role - const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator'); - if (token.user.role === adminRoleIndex) { - return true; - } + if (tokenExists() && isTokenValid()) { + const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator'); + if (token.user.role === adminRoleIndex) { + return true; } + } else { + return false; } - - return false; }; const tokenContext = { @@ -91,9 +98,11 @@ const TokenProvider = ({ locale, children }: TokenProps) => { }, []); useEffect(() => { - // check current path is private. pravate path is '/account' or '/projects/*' + // pravate paths are '/account', '/admin', '/projects/*' const isPrivatePath = (pathname: string) => { - return /^\/account(\/)?$/.test(pathname) || /^\/projects(\/)?$/.test(pathname); + return ( + /^\/account(\/)?$/.test(pathname) || /^\/admin(\/)?$/.test(pathname) || /^\/projects(\/.*)?$/.test(pathname) + ); }; const checkSignInPage = () => { @@ -101,7 +110,16 @@ const TokenProvider = ({ locale, children }: TokenProps) => { return; } if (isPrivatePath(pathname) && !isSignedIn()) { - router.push(`/account/signin`, { locale: locale }); + if (tokenExists()) { + toastContext.showToast(toastMessages.needSignedIn, 'default'); + router.push(`/account/signin`, { locale: locale }); + return; + } + if (isTokenValid()) { + toastContext.showToast(toastMessages.sessionExpired, 'default'); + router.push(`/account/signin`, { locale: locale }); + return; + } } };