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
}

今回のコミット内容

react-toastify導入・スタイル調整、Material UIの色をカスタマイズ、Alertのスタイル調整

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です