Create toast provider
This commit is contained in:
@@ -29,6 +29,10 @@
|
|||||||
"signin": "Sign in",
|
"signin": "Sign in",
|
||||||
"signout": "Sign out"
|
"signout": "Sign out"
|
||||||
},
|
},
|
||||||
|
"Toast": {
|
||||||
|
"need_signed_in": "You need signed in",
|
||||||
|
"session_expired": "Session expired"
|
||||||
|
},
|
||||||
"Auth": {
|
"Auth": {
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"signup": "Sign up",
|
"signup": "Sign up",
|
||||||
|
|||||||
@@ -29,6 +29,10 @@
|
|||||||
"signin": "サインイン",
|
"signin": "サインイン",
|
||||||
"signout": "サインアウト"
|
"signout": "サインアウト"
|
||||||
},
|
},
|
||||||
|
"Toast": {
|
||||||
|
"need_signed_in": "サインインが必要です",
|
||||||
|
"session_expired": "セッションが切れました"
|
||||||
|
},
|
||||||
"Auth": {
|
"Auth": {
|
||||||
"account": "アカウント",
|
"account": "アカウント",
|
||||||
"signup": "新規登録",
|
"signup": "新規登録",
|
||||||
|
|||||||
19
frontend/package-lock.json
generated
19
frontend/package-lock.json
generated
@@ -32,6 +32,7 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-apexcharts": "^1.4.1",
|
"react-apexcharts": "^1.4.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-toastify": "^10.0.5",
|
||||||
"tailwind-variants": "^0.1.18",
|
"tailwind-variants": "^0.1.18",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4"
|
||||||
@@ -3672,9 +3673,9 @@
|
|||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
||||||
},
|
},
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "2.0.0",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -6173,6 +6174,18 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-apexcharts": "^1.4.1",
|
"react-apexcharts": "^1.4.1",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-toastify": "^10.0.5",
|
||||||
"tailwind-variants": "^0.1.18",
|
"tailwind-variants": "^0.1.18",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Providers } from './providers';
|
|||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export async function generateMetadata({ params: { locale } }) {
|
export async function generateMetadata({ params: { locale } }) {
|
||||||
const t = await getTranslations({ locale, namespace: 'Header' });
|
const t = await getTranslations({ locale, namespace: 'Header' });
|
||||||
@@ -25,11 +26,20 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: { locale: string };
|
params: { locale: string };
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations('Toast');
|
||||||
|
const toastMessages = {
|
||||||
|
needSignedIn: t('need_signed_in'),
|
||||||
|
sessionExpired: t('session_expired'),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} suppressHydrationWarning>
|
<html lang={locale} suppressHydrationWarning>
|
||||||
<head />
|
<head />
|
||||||
<body className={clsx('min-h-[calc(100vh-64px)] bg-background font-sans antialiased', fontSans.variable)}>
|
<body className={clsx('min-h-[calc(100vh-64px)] bg-background font-sans antialiased', fontSans.variable)}>
|
||||||
<Providers themeProps={{ attribute: 'class', defaultTheme: 'light' }} tokenProps={{ locale: locale }}>
|
<Providers
|
||||||
|
themeProps={{ attribute: 'class', defaultTheme: 'light' }}
|
||||||
|
tokenProps={{ toastMessages: toastMessages, locale: locale }}
|
||||||
|
>
|
||||||
<div className="relative flex flex-col min-h-screen light:bg-neutral-50 dark:bg-neutral-800">
|
<div className="relative flex flex-col min-h-screen light:bg-neutral-50 dark:bg-neutral-800">
|
||||||
<Header locale={locale} />
|
<Header locale={locale} />
|
||||||
<main>{children}</main>
|
<main>{children}</main>
|
||||||
|
|||||||
@@ -5,22 +5,27 @@ import { NextUIProvider } from '@nextui-org/react';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||||
import { ThemeProviderProps } from 'next-themes/dist/types';
|
import { ThemeProviderProps } from 'next-themes/dist/types';
|
||||||
|
import ToastProvider from '@/utils/ToastProvider';
|
||||||
|
import { ToastProps } from '@/types/toast';
|
||||||
import TokenProvider from '@/utils/TokenProvider';
|
import TokenProvider from '@/utils/TokenProvider';
|
||||||
import { TokenProps } from '@/types/user';
|
import { TokenProps } from '@/types/user';
|
||||||
|
|
||||||
export interface ProvidersProps {
|
export interface ProvidersProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
themeProps?: ThemeProviderProps;
|
themeProps?: ThemeProviderProps;
|
||||||
|
toastProps?: ToastProps;
|
||||||
tokenProps?: TokenProps;
|
tokenProps?: TokenProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Providers({ children, themeProps, tokenProps }: ProvidersProps) {
|
export function Providers({ children, themeProps, toastProps, tokenProps }: ProvidersProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NextUIProvider navigate={router.push}>
|
<NextUIProvider navigate={router.push}>
|
||||||
<NextThemesProvider {...themeProps}>
|
<NextThemesProvider {...themeProps}>
|
||||||
|
<ToastProvider {...toastProps}>
|
||||||
<TokenProvider {...tokenProps}>{children}</TokenProvider>
|
<TokenProvider {...tokenProps}>{children}</TokenProvider>
|
||||||
|
</ToastProvider>
|
||||||
</NextThemesProvider>
|
</NextThemesProvider>
|
||||||
</NextUIProvider>
|
</NextUIProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
12
frontend/types/toast.ts
Normal file
12
frontend/types/toast.ts
Normal file
@@ -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) => {};
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { ToastMessages } from './toast';
|
||||||
|
|
||||||
export type UserType = {
|
export type UserType = {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -8,6 +10,7 @@ export type UserType = {
|
|||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
export type TokenProps = {
|
export type TokenProps = {
|
||||||
|
toastMessages: ToastMessages;
|
||||||
locale: string;
|
locale: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|||||||
31
frontend/utils/ToastProvider.tsx
Normal file
31
frontend/utils/ToastProvider.tsx
Normal file
@@ -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<ToastContextType>(defaultContext);
|
||||||
|
|
||||||
|
const ToastProvider = ({ children }: ToastProps) => {
|
||||||
|
const showToast = (text: string, mode: string) => {
|
||||||
|
toast(text, { theme: 'light' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const toastContext = {
|
||||||
|
showToast,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={toastContext}>
|
||||||
|
<ToastContainer hideProgressBar={true} />
|
||||||
|
{children}
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ToastContext };
|
||||||
|
export default ToastProvider;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { createContext, useState, useEffect } from 'react';
|
import { createContext, useState, useEffect, useContext } from 'react';
|
||||||
import { TokenContextType, TokenType } from '@/types/user';
|
import { TokenContextType, TokenType } from '@/types/user';
|
||||||
import { TokenProps } from '@/types/user';
|
import { TokenProps } from '@/types/user';
|
||||||
import { useRouter, usePathname } from '@/src/navigation';
|
import { useRouter, usePathname } from '@/src/navigation';
|
||||||
import { roles } from '@/config/selection';
|
import { roles } from '@/config/selection';
|
||||||
|
import { ToastContext } from './ToastProvider';
|
||||||
const LOCAL_STORAGE_KEY = 'testplat-auth-token';
|
const LOCAL_STORAGE_KEY = 'testplat-auth-token';
|
||||||
|
|
||||||
function storeTokenToLocalStorage(token: TokenType) {
|
function storeTokenToLocalStorage(token: TokenType) {
|
||||||
@@ -27,9 +28,10 @@ const defaultContext = {
|
|||||||
};
|
};
|
||||||
const TokenContext = createContext<TokenContextType>(defaultContext);
|
const TokenContext = createContext<TokenContextType>(defaultContext);
|
||||||
|
|
||||||
const TokenProvider = ({ locale, children }: TokenProps) => {
|
const TokenProvider = ({ toastMessages, locale, children }: TokenProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const toastContext = useContext(ToastContext);
|
||||||
|
|
||||||
const [hasRestoreFinished, setHasRestoreFinished] = useState(false);
|
const [hasRestoreFinished, setHasRestoreFinished] = useState(false);
|
||||||
const [token, setToken] = useState<TokenType>({
|
const [token, setToken] = useState<TokenType>({
|
||||||
@@ -38,34 +40,39 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
|
|||||||
user: null,
|
user: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isSignedIn = () => {
|
const tokenExists = () => {
|
||||||
// check token
|
|
||||||
if (token && token.user && token.user.username) {
|
if (token && token.user && token.user.username) {
|
||||||
// check expire date
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTokenValid = () => {
|
||||||
if (Date.now() < token.expires_at) {
|
if (Date.now() < token.expires_at) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
console.error('session expired');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSignedIn = () => {
|
||||||
|
if (tokenExists() && isTokenValid()) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAdmin = () => {
|
const isAdmin = () => {
|
||||||
// check token
|
if (tokenExists() && isTokenValid()) {
|
||||||
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');
|
const adminRoleIndex = roles.findIndex((entry) => entry.uid === 'administrator');
|
||||||
if (token.user.role === adminRoleIndex) {
|
if (token.user.role === adminRoleIndex) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenContext = {
|
const tokenContext = {
|
||||||
@@ -91,9 +98,11 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// check current path is private. pravate path is '/account' or '/projects/*'
|
// pravate paths are '/account', '/admin', '/projects/*'
|
||||||
const isPrivatePath = (pathname: string) => {
|
const isPrivatePath = (pathname: string) => {
|
||||||
return /^\/account(\/)?$/.test(pathname) || /^\/projects(\/)?$/.test(pathname);
|
return (
|
||||||
|
/^\/account(\/)?$/.test(pathname) || /^\/admin(\/)?$/.test(pathname) || /^\/projects(\/.*)?$/.test(pathname)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkSignInPage = () => {
|
const checkSignInPage = () => {
|
||||||
@@ -101,7 +110,16 @@ const TokenProvider = ({ locale, children }: TokenProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isPrivatePath(pathname) && !isSignedIn()) {
|
if (isPrivatePath(pathname) && !isSignedIn()) {
|
||||||
|
if (tokenExists()) {
|
||||||
|
toastContext.showToast(toastMessages.needSignedIn, 'default');
|
||||||
router.push(`/account/signin`, { locale: locale });
|
router.push(`/account/signin`, { locale: locale });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isTokenValid()) {
|
||||||
|
toastContext.showToast(toastMessages.sessionExpired, 'default');
|
||||||
|
router.push(`/account/signin`, { locale: locale });
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user