Mantine 入門4  ブログトップページの構成と主要パーツの選定

個人ブログサイトは下記で構成することにします。

  • トップページ

  • コンテンツ表示ページ

  • コンテンツ作成ページ

トップページを作っていく

まずは、今回はトップページの見た目を作っていきます。制御は次回。

トップページのイメージ

  1. 中央コンテンツの他、ヘッダー、フッター、ナビゲーションバー(左)に持ちます。

  2. 中央にはキャッチーな写真とページ概要、記事カード(リンク)を配置します。

  3. ヘッダーにはサイト名を入れます。レスポンシブで見えなくなったサイドバーを開くボタンがあります。

  4. フッターにはコピーライトを表示します。

  5. 左サイドバーは、ブログ記事に付加したタグから、タグ付けされた記事への記事を読み出します。

Mantineコンポーネントの選定

1 を満たすために、AppShellを使います。
2はCenterでレイアウトします。上にはImgのCarouselを入れます。Cardも使います。
3のヘッダーのサイト名表示にはTitleを使います。Burgerでサイドバーを開きます。ダークモード切り替えのアイコンActionIconがあります。
5はタグをChipで表します。個人サイトでタグが少ないため。

AppShellの使い方

公式ページのAppShellのComponent propsのタブを見ると、Appshellはfooter,header,navbar(左サイドバー),aside(右サイドバー)というリアクトコンポーネントを持っています。
つまりこれは、AppShellの1個目のタグの中に上記のコンポーネントを入れ込むということです。
今回は左サイドバーのnavbarとheader,footerしか使いません。公式マニュアルで、これらのpropsも見ていきます。


必須のものは赤いアスタリスクでマークされています。それぞれのコンテンツ(子要素)と、header,footerは高さも必須の設定です。navbarもwidthを設定しないと最大幅となりメインコンテンツが見えなくなりました。

// pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import { AppShell, Navbar, Footer, Header,Text } from '@mantine/core'

const Home: NextPage = () => {
  return (
    <>
      <Head><title>Home</title></Head>
      <AppShell 
        header={<Header height={40}>ヘッダー。高さ設定必須</Header>} 
        navbar={<Navbar width={{base:200}}>幅設定ほぼ必須.型は後述</Navbar>} 
        footer={<Footer height={30}>フッター。高さ設定必須。</Footer>} 
      >
        <Text>メインコンテンツ</Text>
      </AppShell>
    </>
  )
}

export default Home

navbarの幅を動的に変える

// pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import { AppShell, Navbar, Footer, Header,Text } from '@mantine/core'

const nav_width={
  sm:200,//画面幅がテーマのブレークポイントsmを超える時
  md:300,//画面幅がテーマのブレークポイントmdを超える時
  lg:400,//画面幅がテーマのブレークポイントmdを超える時
  base:100//上記以外。デフォルト100%幅
}

const Home: NextPage = () => {
  return (
    <>
      <Head><title>Home</title></Head>
      <AppShell 
        header={<Header height={40}>ヘッダー。高さ設定必須</Header>} 
        navbar={<Navbar width={nav_width}>幅設定ほぼ必須.型は後述</Navbar>} 
        footer={<Footer height={30}>フッター。高さ設定必須。</Footer>} 
      >
        <Text>メインコンテンツ</Text>
      </AppShell>
    </>
  )
}

export default Home

Navbarのプロパティにnav_widthのように設定することで画面幅に応じてNavbarの幅も変えることができます。デフォルトの幅が100%であることもわかりました。

レスポンシブの練習

公式ドキュメントのサンプルを参考にします。
navbarにhidden要素を追加し、表示非表示を制御できるようにします。
ステートでbooleanのopened変数を追加し、Burgerボタンでopened変数を変化させます。openedがtrueの時はnavbarのhiddenがfalse,openedがfalseの時はhiddenがtrueとします。
Burgerはメディアクエリで囲み、smサイズ以上の画面サイズの時は{display:'none' }で非表示にします。

// pages/index.tsx
import type { NextPage } from 'next'
import { useState } from 'react'
import Head from 'next/head'
import { AppShell, Navbar, Footer, Header,Text } from '@mantine/core'
import { Burger, MediaQuery } from '@mantine/core';
const nav_width={
  sm:300,//画面幅がテーマのブレークポイントsmを超える時
  lg:400,//画面幅がテーマのブレークポイントmdを超える時
  base:200//上記以外。デフォルト100%幅
}

const Home: NextPage = () => {

  const [opened,setOpened] = useState(false)

  return (
    <>
      <Head><title>Home</title></Head>
      <AppShell 
        header={
        <Header height={40}>
          <MediaQuery largerThan="sm" styles={{ display: 'none' }}>
            <Burger 
              opened={opened} 
              onClick={() => setOpened((o) => !o)}
            />
          </MediaQuery>
        </Header>
        } 
        navbar={
        <Navbar width={nav_width} hidden={!opened}>
          hidden属性の追加
        </Navbar>
        } 
        footer={<Footer height={30}>フッター。高さ設定必須。</Footer>} 
      >
        <Text>メインコンテンツ</Text>
      </AppShell>
    </>
  )
}

export default Home

これでもそれっぽく動きます。ただ、ナビゲーションバーを閉じた状態で画面幅を広げていくとバーガーボタンが消えてしまい、開けなくなるので、useViewportSizeなどを使い、画面幅を監視するなどの処理が必要かもしれません。ざっくりとAppShellの使い方がわかったので、コンポーネントを作っていこうと思います。

ホーム

ホームは、後述するリアクトコンポーネントを呼び出しているだけです。これだけPagesディレクトリに配置しています。

// 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'


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

export default Home

ヘッダー

ヘッダーには、ナビゲーションバーを制御するバーガーボタン、タイトル、ダークモード対応のためのアイコンボタンを配置します。

import { Burger, Header, Title ,ActionIcon, SimpleGrid} from "@mantine/core"
import { IconSun } from "@tabler/icons"
import { createStyles } from '@mantine/core'

const useStyles = createStyles((theme, _params, getRef) => ({
    burgerArea:{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'start',
    },
    titleArea:{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
    },
    iconArea:{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'end',
    },
}))

const HomeHeader= ()=>{

    const { classes } = useStyles();

    return(
        <Header m="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}>
                    <ActionIcon>
                        <IconSun/>
                    </ActionIcon>
                </div>
            </SimpleGrid>
        </Header>
    )
}
export default HomeHeader

まずはレイアウトを定めました。今後アクションアイコンが増える可能性もあるので、バーガー、タイトル、アクションアイコンの領域を均等のシンプルグリッドとしてそれぞれ作成し、それぞれ左よせ、中央よせ、右よせとしました。
もちろん、上下は中央よせです。


アイコンは、tablerアイコンを使いました。下記のコード補完と硬式ホームページを頼っています。

フッター

フッターはコピーライトを入れているだけです。テキストをセンターで中央に表示しています。文字サイズとCenterのmtを調整しました。

import { Center, Footer, Text} from "@mantine/core"

const HomeFooter = () =>{
    return(
        <Footer height={40}>
            <Center mt={10}>
                <Text size="xs">Copyright © Tomy All Rights Reserved.</Text>
            </Center>
        </Footer>

    )
}
export default HomeFooter

ナビゲーションバー

ナビゲーションバーは、記事のタグを選択するイメージで作りました。現在はこれだけですが、セクション分けできるので、今後のためにセクションを切っておきました。また、セクション内はスクロールエリアとして、タグが増えても対応できるようにしました。

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

const HomeNavbar = ()=>{
    return(
        <Navbar p="sm" width={nav_width}>
            <Navbar.Section>
                <Title mt ="sm" order={5}>検索したいタグ</Title>
                <ScrollArea mt="sm" style={{ height: 300 }}>
                    <Chip mt="sm">python</Chip>
                    <Chip mt="sm">react</Chip>
                    <Chip mt="sm">flask</Chip>
                    <Chip mt="sm">Nextjs</Chip>
                    <Chip mt="sm">pytorch</Chip>
                    <Chip mt="sm">opencv</Chip>
                    <Chip mt="sm">numpy</Chip>
                    <Chip mt="sm">pandas</Chip>
                </ScrollArea>
            </Navbar.Section>
        </Navbar>

    )
}
export default HomeNavbar


メイン

メインの部分は、キャッチー画像と説明文とブログへのリンクとなるカード群をCenterで積み上げていくイメージです。
キャッチー画像とリンクとなるカードは部品扱いのリアクトコンポーネントにしました。
カード群は別のリアクトコンポーネントとしました。

import { Center, Text, Title} from "@mantine/core"
import EmblaCarousel from "./tips/emblaCarousel"
import BlogCard from "./tips/blogCard"
import HomeMainCards from "./homeMainCards"

const HomeMain = () =>{
    return(
        <>
        <Center mt="lg">
            <EmblaCarousel/>
        </Center>
        <Center mt="lg">
            <Title order={2}>
                プログラムのメモを書いていきます。
            </Title>
        </Center>
        <Center mt="sm">
            <Text>
                MantineとNextjsを使ったブログサイトです。
            </Text>
        </Center>

        <Center mt="sm">
            <HomeMainCards/>
        </Center>
        </>
            
    )
}
export default HomeMain

メイン上部:画像のカルーセル

ほぼ、公式サイトのカルーセルのサンプルのままです。embla-carousel-autoplayをインストールして利用しています。

import { useRef } from 'react';
import Autoplay from 'embla-carousel-autoplay';
import Image from 'next/image';
import { Carousel } from '@mantine/carousel';
import { createStyles } from '@mantine/core'

const useStyles = createStyles((theme, _params, getRef) => ({
    imageArea:{
        borderRadius: '10px',
        overflow: 'hidden'
    }
}))

const image1 = "/imgs/einar-ingi-sigmundsson-QO_XjRt_Hkk-unsplash.jpg"
const image2 = "/imgs/honza-reznik-5nQllMuU8Pw-unsplash.jpg"
const image3 = "/imgs/masahiro-miyagi-MFUn9CApJmg-unsplash.jpg"


const EmblaCarousel = ()=> {
    const {classes} = useStyles()

    const autoplay = useRef(Autoplay({ delay: 3000 }));
    return (
        <Carousel
            sx={{ maxWidth: 400 }}
            mx="auto"
            //withIndicators
            height={250}
            plugins={[autoplay.current]}
            onMouseEnter={autoplay.current.stop}
            onMouseLeave={autoplay.current.reset}
        >
            <Carousel.Slide key={image1}>
                <Image className={classes.imageArea} src={image1} width={400} height={250}/>
            </Carousel.Slide>
            <Carousel.Slide key={image2}>
                <Image className={classes.imageArea} src={image2} width={400} height={250}/>
            </Carousel.Slide>
            <Carousel.Slide key={image3}>
                <Image className={classes.imageArea} src={image3} width={400} height={250}/>
            </Carousel.Slide>

        </Carousel>
 
    );
    }

    export default EmblaCarousel

画像はpublicフォルダのImgsフォルダ内に格納しました。Nextjsを利用しているため、Imageコンポーネントがmantineのものと衝突します。このような場合はNextjsのImageを使うようです。mantineは見た目(だけ)のライブラリですが、Nextjsは最適化やHTMLへの変換を行うライブラリです。


メイン下部:カード群のカード

カードは、propsとして与えられたタイトル、日付、概要、タグを表示します。useStylesで色をつけてみました。

import { Card, Title, Text, Badge,SimpleGrid ,Group} from '@mantine/core';
import { createStyles } from '@mantine/core'

const useStyles = createStyles((theme, _params, getRef) => ({
    cardArea:{
        backgroundColor: '#FFFDFA',
        width: '100%'
    }
}))

export type BlogCardProps = {
    title:string,
    date:string,
    abstract:string,
    tags:string[],
}

const BlogCard = ({title,date,abstract,tags}:BlogCardProps)=> {
    const {classes} = useStyles()

    const list_jsx_Badge = tags.map((item,index) => 
       <Badge color="pink" variant="light">{item}</Badge>
    )

    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>
        
            <Text weight={500} size="sm">{abstract}</Text>

            <SimpleGrid mt="sm" cols={2}>
                {list_jsx_Badge}
            </SimpleGrid>

        </Card>
    );
}

export default BlogCard

メイン下部:カード群

コンテンツを読み込み、先ほどのカードを2列に並べるコンポーネントを想定しました。

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

const article_headers_obj:BlogCardProps[] = [ 
    {
        title:"バックエンドをpythonで",
        date:"2022/10/10",
        abstract:"ブラウザの高機能化に従いweb開発はフロントエンドがかなりの比率を占めてきた。しかし", 
        tags:["python","flask","numpy"],
    },
    {
        title:"jsonとオブジェクトの区別",
        date:"2022/10/10",
        abstract:"Jsonとオブジェクトのハードコーディングで、はまった", 
        tags:["javascript","JSON"], 
    },
    {
        title:"jsonとオブジェクトの区別",
        date:"2022/10/10",
        abstract:"Jsonとオブジェクトのハードコーディングで、はまった", 
        tags:["javascript","JSON"], 
    }
]


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

見た目が大体できたので、次回は制御部分を作っていこうと思います。

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