Mantine 入門5 トップページに動きをつけよう

前回のトップページに動きをつけます。具体的には下記を実施します。

  • ダークモードの実装

  • スマホ用、タブレット用、PC用の表示

ダークモード

まずはダークモードです。まずは__app.tsxで以下のように手動でダークモードを設定してみます。

import { AppProps } from 'next/app';
import Head from 'next/head';
import { MantineProvider } from '@mantine/core';

export default function App(props: AppProps) {
  const { Component, pageProps } = props;

  return (
    <>
      <Head>
        <title>Page title</title>
        <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
      </Head>

      <MantineProvider
        withGlobalStyles
        withNormalizeCSS
        theme={{
          /** Put your mantine theme override here */
          colorScheme: 'dark',
        }}
      >
        <Component {...pageProps} />
      </MantineProvider>
    </>
  );
}

それっぽくなりました。次に、どうやってコレを動的に変えるかです。
公式ドキュメントの下記に書いてありました。

まず、__app.tsxを下記で置き換えます。

import { GetServerSidePropsContext } from 'next';
import { useState } from 'react';
import { AppProps } from 'next/app';
import { getCookie, setCookie } from 'cookies-next';
import Head from 'next/head';
import { MantineProvider, ColorScheme, ColorSchemeProvider } from '@mantine/core';
import { NotificationsProvider } from '@mantine/notifications';

export default function App(props: AppProps & { colorScheme: ColorScheme }) {
  const { Component, pageProps } = props;
  const [colorScheme, setColorScheme] = useState<ColorScheme>(props.colorScheme);

  const toggleColorScheme = (value?: ColorScheme) => {
    const nextColorScheme = value || (colorScheme === 'dark' ? 'light' : 'dark');
    setColorScheme(nextColorScheme);
    setCookie('mantine-color-scheme', nextColorScheme, { maxAge: 60 * 60 * 24 * 30 });
  };

  return (
    <>
      <Head>
        <title>Mantine next example</title>
        <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
        <link rel="shortcut icon" href="/favicon.svg" />
      </Head>

      <ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}>
        <MantineProvider theme={{ colorScheme }} withGlobalStyles withNormalizeCSS>
          <NotificationsProvider>
            <Component {...pageProps} />
          </NotificationsProvider>
        </MantineProvider>
      </ColorSchemeProvider>
    </>
  );
}

App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({
  colorScheme: getCookie('mantine-color-scheme', ctx) || 'light',
});

クッキーに、mantine-color-schemeのキーでcolorSchemeを保存し、MantineProviderのthemeに保存してあるcolorSchemeを設定しています。
次に、以下のようなアクションアイコンコンポーネントを作成します。


//components/tips/darkOrSunIcon.tsx
import { ActionIcon, useMantineColorScheme } from "@mantine/core"
import { IconSun,IconMoonStars } from "@tabler/icons"

const DarkOrSunIcon = () =>{
    const { colorScheme, toggleColorScheme } = useMantineColorScheme();
    const dark = colorScheme === 'dark';
  
    return (
      <ActionIcon
        onClick={() => toggleColorScheme()}
      >
        {dark ? <IconSun /> : <IconMoonStars />}
      </ActionIcon>
    );

}

export default DarkOrSunIcon


クリックされると、下記で定義したtoggleColorSchemeを介して、先ほどの__app .tsxのcolorSchemeを変更できるようです。

const { colorScheme, toggleColorScheme } = useMantineColorScheme();

最後に、このアクションアイコンコンポーネントをホームヘッダに設定します。

・・・・
const HomeHeader= ()=>{

    const {classes} = useStyles()

    return(
        <Header p="sm" height={50}>
            <SimpleGrid cols={3}>
                <div className={classes.burgerArea}>
                    <Burger opened={false}/>
                </div>
                <div className={classes.titleArea}>
                    <Title order={3}>毎日ブログ</Title>
                </div>
                <div className={classes.iconArea}>
                    <DarkOrSunIcon/>
                </div>
            </SimpleGrid>
        </Header>
    )
}
export default HomeHeader

レスポンシブ

ナビゲーションバー

ナビゲーションバーを画面幅に応じて自動で出したり、選択的にトグルできるようにします。

// pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import { AppShell} from '@mantine/core'
import HomeHeader from '../components/homeHeader'
import HomeNavbar from '../components/homeNavbar'
import HomeFooter from '../components/homeFooter'
import HomeMain from '../components/homeMain'
import { useState , useEffect} from 'react';
import { useViewportSize } from '@mantine/hooks';
import { useToggle } from '@mantine/hooks';


const Home: NextPage = () => {

  const { height, width } = useViewportSize();
  const [navbarOpened, toggleNavbarOpened] = useToggle([true, false]);
  const onoffNavbar = () =>{toggleNavbarOpened()}

  useEffect(()=>{
    if(width < 600){
      toggleNavbarOpened(false)
    }
    else{
      toggleNavbarOpened(true)
    }
    //document.title = `${width}${navbarOpened}`
  },[width])

  return (
      <>
        <Head><title>Home</title></Head>
        <AppShell 
          header={
          <HomeHeader 
            navbarOpened={navbarOpened}
            onoffNavbar={onoffNavbar}
          />} 
          navbar={<HomeNavbar navbarOpened={navbarOpened}/>} 
          footer={<HomeFooter/>} 
        >
          <HomeMain/>
        </AppShell>
      </>
    )
}

export default Home

まず、横幅は以下のhookで取得できます。

import { useViewportSize } from '@mantine/hooks';
・・・ 
const { height, width } = useViewportSize();

次にwidthが変化したときに、コレを検知して600px以上だったら強制的にナブバーON,狭い場合はナブバーOFFとします。
2値のStateは下記で生成すると、自分でONの時OFFにして、OFFの時ONにしてという処理を書かなくていいので便利です。

import { useToggle } from '@mantine/hooks';
・・・
  const [navbarOpened, toggleNavbarOpened] = useToggle([true, false]);
  const onoffNavbar = () =>{toggleNavbarOpened()}

定石ですが、oggleNavbarOpenedの型は複雑なので、ただの関数onoffNavbarでラップして、子コンポーネントに渡しやすいようにします。
以下のように、navbarOpenedを直接編集しても大丈夫です。

  useEffect(()=>{
    if(width < 600){
      toggleNavbarOpened(false)
    }
    else{
      toggleNavbarOpened(true)
    }
    //document.title = `${width}${navbarOpened}`
  },[width])

コレで、画面幅600px以上だったらON,そうでなければOFFのフラグができました。
コレをNavBarコンポーネントに渡してあげています。

navbar={<HomeNavbar navbarOpened={navbarOpened}/>} 

受け取るHomeNavbar側は次のように改造しています。

import {  Navbar, ScrollArea, Chip, Title, Divider} from "@mantine/core"
 
const nav_width={
    sm:200,//画面幅がテーマのブレークポイントsmを超える時
    base:150//上記以外。デフォルト100%幅
}

export type HomeNavbarProps = {
    navbarOpened:boolean
}

const HomeNavbar = ({navbarOpened}:HomeNavbarProps)=>{

    if(navbarOpened){
        nav_width.base=150
    }else{
        nav_width.base=1
    }

    return(
        <Navbar hidden={!navbarOpened} p="sm" width={nav_width}>
                      ・・・・
        </Navbar>
    )

}
export default HomeNavbar

コツはNavbarのhidden属性に渡すだけでなく、幅を1pxにしているところです。コレがなければナブバーが隠れるだけで、baseの幅が残っていました。

次に、バーガーメニューで600px以下の時でもナブバーを表示できるように
します。バーガーメニューはヘッダの子コンポーネントなので、ヘッダを介してONOFFフラグnavbarOpenedとコレをトグルする関数onoffNavbarを渡してあげます。

     <AppShell 
          header={
          <HomeHeader 
            navbarOpened={navbarOpened}
            onoffNavbar={onoffNavbar}
          />} 

バーガーメニューは以下の用に実装しました。

//components/tips/menuBurger.tsx
import { Burger } from "@mantine/core"
import { createStyles } from '@mantine/core'

const useStyles = createStyles((theme) => ({

    burger:{
        [theme.fn.largerThan(600)]:{
            display:"none"
        }
    }
}))

export type MenuBurgerProps = {
    navbarOpened:boolean
    onoffNavbar:()=>void
}

const MenuBurger = ({navbarOpened,onoffNavbar}:MenuBurgerProps) =>{
    const {classes} = useStyles()

    return(
        <>
        <Burger className={classes.burger} 
            opened={navbarOpened}
            onClick={()=>{onoffNavbar()}}/>
        </>
    )
}

export default MenuBurger

ポイントは、Burgerのタグにpropsとして、先ほどのフラグと関数を受け取るところと、下記により600px以下ではメニューを隠しているところです。

[theme.fn.largerThan(600)]:{
            display:"none"
        }

中継するpropsを中継するヘッダーは下記の通りです。


import { Header,  Title , SimpleGrid} from "@mantine/core"
import { createStyles } from '@mantine/core'
import DarkOrSunIcon from "./tips/darkOrSunIcon"
import MenuBurger from "./tips/menuBurger"
import { MenuBurgerProps } from "./tips/menuBurger"

・・・・

const HomeHeader = ({navbarOpened, onoffNavbar}:MenuBurgerProps)=>{

    const {classes} = useStyles()

    return(
        <Header p="sm" height={50}>
            <SimpleGrid cols={3}>
                <div className={classes.burgerArea}>
                    <MenuBurger 
                        navbarOpened={navbarOpened}
                        onoffNavbar={onoffNavbar}/>
                </div>
                <div className={classes.titleArea}>
                    <Title order={3}>毎日ブログ</Title>
                </div>
                <div className={classes.iconArea}>
                    <DarkOrSunIcon/>
                </div>
            </SimpleGrid>
        </Header>
    )
}
export default HomeHeader

カード群表示

現在は、下記の通り、シンプルグリッドを使ってどんな時でも2列でカードを並べていました。

//components/homeMainCards.tsx
import { SimpleGrid } from "@mantine/core"
import BlogCard from "./tips/blogCard"
import type { BlogCardProps } from "./tips/blogCard"

......

const HomeMainCards = () =>{

    const list_jsx_Cards = article_headers_obj.map((item,index) => (
        <BlogCard 
            title={item.title} 
            date={item.date} 
            abstract={item.abstract}
            tags={item.tags}
        />
    ))

    return(
        <SimpleGrid m="md" cols={2}>
            {list_jsx_Cards}
        </SimpleGrid>
    
    )
}

export default HomeMainCards

通常のGridでは、ブレークポイントサイズごとに幅(最大12)を設定できます。smより小さい時は幅12(1列)、sm以上で幅6(2列)、lg以上で幅3(4列)にします。


const HomeMainCards = () =>{

    const list_jsx_Cards = article_headers_obj.map((item,index) => (
        <Grid.Col sm={6} lg={4}>
            <BlogCard 
                title={item.title} 
                date={item.date} 
                abstract={item.abstract}
                tags={item.tags}
            />
        </Grid.Col>
    ))

    return(
        <Grid m="md">
            {list_jsx_Cards}
        </Grid>
    
    )
}

export default HomeMainCards


あとは、カードのコンテンツによってカードサイズがあまり変化しないようにしたいと思います。カードはタイトル、日付、概要、タグからなりますが、概要のテキストをSpoilerで囲って高さを制限しました。


    return (
        <Card className={classes.cardArea} shadow="sm" p="lg" radius="md">
            <Title order={5}>{title}</Title>

            <Group position="right">
                <Text size="xs">{date}</Text>
            </Group>
            <Spoiler maxHeight={40} showLabel="もっと見る" hideLabel="隠す" transitionDuration={0}>
                <Text weight={500} size="sm">{abstract}</Text>
            </Spoiler>
  
            <SimpleGrid mt="sm" cols={2}>
                {list_jsx_Badge}
            </SimpleGrid>

        </Card>
    );
}

コレでだいぶレスポンシブっぽくなりました。

この記事が気に入ったらサポートをしてみませんか?