Nuxt.js(Vue)でも導入したtoastを導入します。
→ Nuxt BridgeをNuxt3に移行。vue-toast-notificationかvue-toastificationを導入
やはり、背景色はMaterial UIのとは異なるので、同じになるように調整しました。
序でにMaterial UIのもカスタマイズできるように、定義を持ってきました。参照しやすいように。
※元のコードは長いので、結果的に同じになるように短くしています。
react-toastify導入
% npm install react-toastify
src/app/[locale]/layout.tsx
ページ遷移でtoastが消えないように、layoutに定義します。
- import { theme } from '@/theme'
+ import { theme, toastStyle } from '@/theme'
+ import { ToastContainer, Slide } from 'react-toastify'
+ import 'react-toastify/dist/ReactToastify.min.css'
+ <ToastContainer
+ position="bottom-right"
+ theme="colored"
+ transition={Slide}
+ style={toastStyle}
+ />
<main>{children}</main>
src/theme.ts
'use client'
import { createTheme } from '@mui/material/styles'
+ import amber from '@mui/material/colors/amber'
const theme = createTheme({
palette: {
mode: 'dark',
+ accent: { main: amber[800], light: amber[500], dark: amber[900], contrastText: 'rgba(0, 0, 0, 0.87)' } // <- 追加
}
})
+ const toastStyle = { // https://fkhadra.github.io/react-toastify/how-to-style/
+ // TODO: 後ほど追加します
+ }
+ const alertStyle = {
+ // TODO: 後ほど追加します
+ }
export {
theme,
+ toastStyle,
+ alertStyle
}
確認ページ+Client Components作成
下記で作成したのと同じようなページを作成します。
→ Nuxt BridgeをNuxt3に移行。Vuetify3のテーマと色を設定+確認ページ作成
完成イメージ。デフォルトだとalertとtoastの色が違うので、後ほど調整します。
src/app/[locale]/development/color/page.tsx
import React from 'react'
import { notFound } from 'next/navigation'
// eslint-disable-next-line camelcase
import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Box from '@mui/material/Box'
import Link from '@mui/material/Link'
import { config } from '@/config'
import AppToastClient from '@/components/app/ToastClient'
import AppAlertClient from '@/components/app/AlertClient'
import ButtonClient from './components/ButtonClient'
export const metadata: any = async () => {
const t = await getTranslations()
return {
title: t('テーマカラー確認')
}
}
export default function Page({ params }: { params: { locale: string } }) {
// Enable static rendering
unstable_setRequestLocale(params.locale)
/* c8 ignore next */
if (!config.developmentMenu) { notFound() }
return (
<>
<AppToastClient target="error" message="error" />
<AppToastClient target="info" message="info" />
<AppToastClient target="warning" message="warning" />
<AppToastClient target="success" message="success" />
<AppAlertClient severity="error" sx={{ mb: 1 }}>error</AppAlertClient>
<AppAlertClient severity="info" sx={{ mb: 1 }}>info</AppAlertClient>
<AppAlertClient severity="warning" sx={{ mb: 1 }}>warning</AppAlertClient>
<AppAlertClient severity="success" sx={{ mb: 1 }}>success</AppAlertClient>
<Card>
<CardContent>
<Box>
<ButtonClient color="primary" sx={{ mb: 1, mr: 1 }} text="primary">primary</ButtonClient>
<ButtonClient color="secondary" sx={{ mb: 1, mr: 1 }} text="secondary">secondary</ButtonClient>
<ButtonClient color="success" sx={{ mb: 1, mr: 1 }} text="success">success</ButtonClient>
<ButtonClient color="error" sx={{ mb: 1, mr: 1 }} text="error">error</ButtonClient>
<ButtonClient color="info" sx={{ mb: 1, mr: 1 }} text="info">info</ButtonClient>
<ButtonClient color="warning" sx={{ mb: 1, mr: 1 }} text="warning">warning</ButtonClient>
<ButtonClient sx={{ mb: 1, mr: 1 }}>undefined(=primary)</ButtonClient>
<ButtonClient color="accent" sx={{ mb: 1, mr: 1 }} text="accent">accent(custom)</ButtonClient>
</Box>
<Box>
<ButtonClient color="primary" sx={{ mb: 1, mr: 1 }} disabled>primary</ButtonClient>
<ButtonClient color="secondary" sx={{ mb: 1, mr: 1 }} disabled>secondary</ButtonClient>
<ButtonClient color="success" sx={{ mb: 1, mr: 1 }} disabled>success</ButtonClient>
<ButtonClient color="error" sx={{ mb: 1, mr: 1 }} disabled>error</ButtonClient>
<ButtonClient color="info" sx={{ mb: 1, mr: 1 }} disabled>info</ButtonClient>
<ButtonClient color="warning" sx={{ mb: 1, mr: 1 }} disabled>warning</ButtonClient>
<ButtonClient sx={{ mb: 1, mr: 1 }} disabled>undefined(=primary)</ButtonClient>
<ButtonClient color="accent" sx={{ mb: 1, mr: 1 }} disabled>accent(custom)</ButtonClient>
</Box>
</CardContent>
</Card>
</>
)
}
src/components/app/ToastClient.tsx
toastはClient Componentsじゃないと動かないので、ファイルを分けます。
汎用的に使えるので、src/componentsに入れました。
'use client'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
export default function Component({ target, message }: { target: string, message: string }) {
useEffect(() => {
(toast as any)[target](message)
}, [target, message])
return true
}
src/components/app/AlertClient.tsx
alert自体はClient Componentsじゃなくても表示できますが、
閉じるボタン入れたい場合、関数を呼び出す+状態を持つので、Client Componentsにします。
こちらも汎用的に使えるので、src/componentsに入れました。
'use client'
import React, { ReactNode, useState } from 'react'
import Alert from '@mui/material/Alert'
import ErrorIcon from '@mui/icons-material/Error'
import InfoIcon from '@mui/icons-material/Info'
import WarningIcon from '@mui/icons-material/Warning'
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import { alertStyle } from '@/theme'
export default function Component({
children,
severity,
variant = 'filled',
sx = {}
}: {
children: ReactNode,
severity: any,
variant?: any,
sx?: any
}) {
const [visible, setVisible] = useState(true)
const closeAlert = () => { setVisible(false) }
return (
<>
{visible &&
<Alert
iconMapping={{
error: <ErrorIcon />,
info: <InfoIcon />,
warning: <WarningIcon />,
success: <CheckCircleIcon />
}}
severity={severity}
variant={variant}
sx={{ ...alertStyle, py: 1.5, ...sx }}
onClose={closeAlert}
>
{children}
</Alert>
}
</>
)
}
src/app/[locale]/development/color/components/ButtonClient.tsx
ボタン押したらクリップボードにコピー。これもClient Components。
これは専用なので、page.tsxと同じパスのcomponentsに入れる事にしました。
'use client'
import React, { ReactNode } from 'react'
import { useTranslations } from 'next-intl'
import { toast } from 'react-toastify'
import Button from '@mui/material/Button'
import { config } from '@/config'
export default function Component({
children,
color,
variant = 'contained',
sx = {},
disabled = false,
text
}: {
children: ReactNode,
color?: any,
variant?: any,
sx?: any
disabled?: boolean,
text?: string
}) {
const t = useTranslations()
return (
<Button
color={color}
variant={variant}
sx={{ textTransform: 'none', ...sx }}
disabled={disabled}
onClick={() => copyText(t, text)}
>
{children}
</Button>
)
}
// クリップボードにコピー
async function copyText (t: any, text?: string) {
if (text == null) { return }
try {
await navigator.clipboard.writeText(text)
toast.success(t('クリップボードコピー成功', { text }))
/* c8 ignore start */
} catch (error) {
// eslint-disable-next-line no-console
if (config.debug) { console.log(error) }
toast.error(t('クリップボードコピー失敗'))
}
/* c8 ignore stop */
}
Material UIやtoastの色やサイズをカスタマイズ
完成イメージ。alertとtoastの色を揃えたり、メッセージが長くてもサイズが大きくならなかったので、調整も入れました。変更箇所は元のを値をコメントで残しています。
src/theme.ts
このファイルに定義を集約できました。
ここだけ見れば良いので見通しが良くなっていると思います。
'use client'
import { createTheme } from '@mui/material/styles'
import blue from '@mui/material/colors/blue'
// import purple from '@mui/material/colors/purple'
import grey from '@mui/material/colors/grey'
import red from '@mui/material/colors/red'
import lightBlue from '@mui/material/colors/lightBlue'
import orange from '@mui/material/colors/orange'
import green from '@mui/material/colors/green'
import amber from '@mui/material/colors/amber'
const paletteLite = { // https://mui.com/material-ui/customization/color/
mode: 'light',
primary: { main: blue[700], light: blue[400], dark: blue[800], contrastText: '#fff' },
secondary: { main: grey[700], light: grey[400], dark: grey[800], contrastText: '#fff' }, // <- main: purple[500], light: purple[300], dark: purple[700]
error: { main: red[700], light: red[400], dark: red[800], contrastText: '#fff' },
info: { main: lightBlue[700], light: lightBlue[500], dark: lightBlue[900], contrastText: '#fff' },
warning: { main: '#ed6c02', light: orange[500], dark: orange[900], contrastText: '#fff' },
success: { main: green[800], light: green[500], dark: green[900], contrastText: '#fff' },
accent: { main: amber[800], light: amber[500], dark: amber[900], contrastText: '#fff' } // <- 追加
}
const paletteDark = {
mode: 'dark',
primary: { main: blue[200], light: blue[50], dark: blue[400], contrastText: 'rgba(0, 0, 0, 0.87)' },
secondary: { main: grey[500], light: grey[300], dark: grey[700], contrastText: 'rgba(0, 0, 0, 0.87)' }, // <- main: purple[200], light: purple[50], dark: purple[400]
error: { main: red[500], light: red[300], dark: red[700], contrastText: 'rgba(0, 0, 0, 0.87)' }, // <- contrastText: '#fff'
info: { main: lightBlue[400], light: lightBlue[300], dark: lightBlue[700], contrastText: 'rgba(0, 0, 0, 0.87)' },
warning: { main: orange[400], light: orange[300], dark: orange[700], contrastText: 'rgba(0, 0, 0, 0.87)' },
success: { main: green[400], light: green[300], dark: green[700], contrastText: 'rgba(0, 0, 0, 0.87)' },
accent: { main: amber[800], light: amber[500], dark: amber[900], contrastText: 'rgba(0, 0, 0, 0.87)' } // <- 追加
}
const mode: any = 'dark' // TODO: 切り替え
const palette: any = mode === 'light' ? paletteLite : paletteDark
const theme = createTheme({ palette })
const toastStyle = { // https://fkhadra.github.io/react-toastify/how-to-style/
width: 'auto', // <- 320px
minWidth: '320px', // <- 64px
maxWidth: '570px', // <- 追加
'--toastify-color-error': mode === 'light' ? palette.error.main : palette.error.dark, // <- '#e74c3c'
'--toastify-color-info': mode === 'light' ? palette.info.main : palette.info.dark, // <- '#3498db'
'--toastify-color-warning': mode === 'light' ? palette.warning.main : palette.warning.dark, // <- '#f1c40f'
'--toastify-color-success': mode === 'light' ? palette.success.main : palette.success.dark // <- '#07bc0c'
}
const alertStyle = {
color: '#fff', // <- contrastText
fontSize: '1rem' // <- '0.875rem'
}
export {
theme,
toastStyle,
alertStyle
}