ちゃなべの備忘録

ほぼ備忘録です。

openapiとそれ関連いろいろ実装してみるンゴ (後編)【備忘録】

はじめに

これの続きな?

ayumu1212.hatenablog.com

以下実装

設計(再掲)

Style周りの環境構築

アプリ作っていく前にUIライブラリとcss-in-jsの導入する。

UIライブラリの選定

まぁデザイン力ないからね、こういうの使うほうが速い。

参考

note.com

いろいろあるもんだなぁ。
だけどなんかおしゃれなのあったーー!笑

nextui.org

これ、nextjsとの互換性強そう。

CSS-in-JSの選定

ライブラリ入れるにしても、その土壌がないとだめだわ。

これみてみる。

2022.stateofcss.com

ピックアップするとこれらが気になる。

  • Styled Component
  • CSS Modules
  • Styled JSX
  • vanilla extract
  • Stitches
  • Emotion

どれを使おうかなと悩んだ。
悩んだ記事はこちら

ayumu1212.hatenablog.com

結論は、Stitchesを使ってみようかな。 styled componentsを使おうかと思ったけど、なんかこっちのほうが新しいし、nextUIもこれ使ってるぽいからね。

stitches.dev

Stitchesの導入

公式でnextjsに導入しているやつがあったからこれみてやろう

stitches.dev

まずinstall

# uiレポジトリに導入
$ yarn workspace ui add @stitches/react

# cdsアプリにも導入
$ yarn workspace cds add @stitches/react

次に、stiches.config.tsを作らないと行けないんだけど、たぶんこれ共通の設定をapps/cdsapps/cdmpackages/ui にinstallしたいから、packagesにフォルダを作ろう。

$ mkdir packages/stitches-config
$ cd packages/stitches-config

$ yarn init
yarn init v1.22.19
question name (stitches-config): 
question version (1.0.0): 
question description: package of stiches config files.
question entry point (index.js): 
question repository url: 
question author: 
question license (MIT): 
question private: true
success Saved package.json
Done in 61.99s.

$ cd ../../

$ yarn workspace stitches-config add @stitches/react

できて、編集を加えたファイルがこちら。

{
  "name": "stitches-config",
  "version": "1.0.0",
  "description": "package of stiches config files.",
  "main": "index.ts",
  "private": true,
  "dependencies": {
    "@stitches/react": "^1.2.8"
  }
}

じゃあstitches.config.ts を作る。

import { createStitches } from '@stitches/react'

export const { styled, getCssText } = createStitches({
  theme: {
    fonts: {
      system: 'system-ui',
    },
    colors: {
      hiContrast: 'hsl(206,10%,5%)',
      loContrast: 'white',
    },
    fontSizes: {
      1: '13px',
      2: '15px',
      3: '17px',
    },
  },
})

とりあえずこれにする。 これを、index.tsでexportする。

export { styled, getCssText } from './stitches.config'

そしたらこれをuiのレポジトリで使う。

import * as React from "react"
import { styled } from 'stitches-config'

export const Button = styled('button', {
  fontFamily: '$system',
  color: '$hiContrast',

  variants: {
    size: {
      1: {
        fontSize: '$1',
      },
      2: {
        fontSize: '$2',
      },
      3: {
        fontSize: '$3',
      },
    },
  },
})

使えてそーー! じゃあ、これをcdsレポジトリで使って表示してみる。

import { Button } from "ui"

export default function View() {
  return (
    <div>
      <p>this is cds</p>
      <Button size={3}>ボタン</Button>
    </div>
  )
}

あとSSRの設定もするみたい。 新たにpagesの下に_document.tsxを作っておく

import React from 'react'
import NextDocument, { Html, Head, Main, NextScript } from 'next/document'
import { getCssText } from 'stitches-config'

export default class Document extends NextDocument {
  render() {
    return (
      <Html lang="ja">
        <Head>
          <style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

さぁ表示!

完璧か?

NextUIの導入

次はこれの導入。結構わくわくしてる。

$ yarn workspace cds add @nextui-org/react
import React from 'react'
+ import NextDocument, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
import { getCssText } from 'stitches-config'
+ import { CssBaseline } from "@nextui-org/react";

export default class Document extends NextDocument {
+  static async getInitialProps(ctx: DocumentContext) {
+    const initialProps = await NextDocument.getInitialProps(ctx)
+    return {
+      ...initialProps,
+      styles: React.Children.toArray(initialProps.styles),
+    }
;  }

  render() {
    return (
      <Html lang="ja">
        <Head>
+          {CssBaseline.flush()}
          <style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

Providerの設置。

import { NextUIProvider } from '@nextui-org/react'
import type { AppProps } from "next/app";

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <NextUIProvider>
      <Component {...pageProps} />
    </NextUIProvider>
  )
}

これで準備ができた。 じゃあやってみるべ!!

- import { Button } from "ui"
+ import { Button } from "@nextui-org/react";

export default function View() {
  return (
    <div>
      <p>this is cds</p>
-      <Button size={3}>ボタン</Button>
+      <Button color="gradient" auto shadow>いい感じのボタン</Button>
    </div>
  )
}

えーーー、、きれぇーーー。。ww

準備オーケー

CDS作成

じゃあついにCDS作っていくか。

一覧画面

ホームページの一覧画面から。コンセプトは「イメージで感じ取るブログ」。本屋の背表紙で買うような感じで。

Cardコンポーネント

まずはCard作るべ。 そのためにuiにNextUIいれる。

あと、このプロジェクトの日付管理はdate-fnsにしよ。cdateも気になるけども。

qiita.com

$ yarn workspace ui add @nextui-org/react date-fns
$ yarn workspace cds add date-fns
import { Button, Card, Col, Row, Text } from "@nextui-org/react"
import { format } from 'date-fns'

type Props = {
  userName: string
  date: Date
  imageUrl: string
  isNew?: boolean
  title: string
}

export const BlogCard = ({
  userName = 'unknown',
  date = new Date(),
  imageUrl = 'https://nextui.org/images/card-example-1.jpeg',
  isNew = false,
  title = 'unknown'
}: Props) => {
  return (
    <Card css={{ w: "100%", h: "400px" }}>
      <Card.Header css={{ position: "absolute", zIndex: 1, top: 5 }}>
        <Col>
          <Text size={12} weight="bold" transform="uppercase" color="#ffffffAA">
            {isNew ? 'New' : ''}
          </Text>
          <Text h3 color="black">
            {title}
          </Text>
        </Col>
      </Card.Header>
      <Card.Body css={{ p: 0 }}>
        <Card.Image
          src={imageUrl}
          width="100%"
          height="100%"
          objectFit="cover"
          alt="Card example background"
        />
      </Card.Body>
      <Card.Footer
        isBlurred
        css={{
          position: "absolute",
          bgBlur: "#ffffff66",
          borderTop: "$borderWeights$light solid rgba(255, 255, 255, 0.2)",
          bottom: 0,
          zIndex: 1,
        }}
      >
        <Row>
          <Col>
            <Text color="#000" size={12}>
              {format(date, 'yyyy/MM/dd')}
            </Text>
            <Text color="#000" size={14}>
              @{userName}
            </Text>
          </Col>
          <Col>
            <Row justify="flex-end">
              <Button flat auto rounded color="secondary">
                <Text
                  css={{ color: "inherit" }}
                  size={12}
                  weight="bold"
                  transform="uppercase"
                >
                  Show
                </Text>
              </Button>
            </Row>
          </Col>
        </Row>
      </Card.Footer>
    </Card>
  )
};

これで、アプリ側から呼ぶとエラーが。

エラー内容と解決策はこれ。

zenn.dev

なんか他のレポジトリからimportするときはこの設定が必要っぽい...?ほんとか?
まぁけどこれは入れた。

module.exports = {
  experimental: {
    externalDir: true,
  },
}

いい感じに表示されてきた。

headerコンポーネント

とりあえずHeaderコンポーネントを作った。

import { Button, Link, Navbar } from "@nextui-org/react"

export const Header = () => {
  return (
    <Navbar isBordered variant='static' color="secondary">
      <Navbar.Content hideIn="xs">
        <Navbar.Link href="#">Features</Navbar.Link>
        <Navbar.Link isActive href="#">Customers</Navbar.Link>
        <Navbar.Link href="#">Pricing</Navbar.Link>
        <Navbar.Link href="#">Company</Navbar.Link>
      </Navbar.Content>
      <Navbar.Content>
        <Navbar.Link color="inherit" href="#">
          Login
        </Navbar.Link>
        <Navbar.Item>
          <Button auto flat as={Link} href="#">
            Sign Up
          </Button>
        </Navbar.Item>
      </Navbar.Content>
    </Navbar>
  )
}

これを全ページで適用したい。どうやら、Layoutというものを定義したほうが良いらしい。

nextjs-ja-translation-docs.vercel.app

なので作ってみた。

import { ReactNode } from 'react';
import { Header } from 'ui'

type Props = {
  children: ReactNode
}

export const Layout = ({ children }: Props) => {
  return (
    <>
      <Header />
      <main>
        {children}
      </main>
    </>
  );
}

んでどうやら子要素に指定するのはReactNodeReactElement があるが、用途が違うらしい。

  • ReactNode : まじで何でもいい。
  • ReactElement : 一個の要素をよこせ。

zenn.dev

んで、作ったLayoutを_app.tsx に配置

import { NextUIProvider, useSSR } from '@nextui-org/react'
import type { AppProps } from "next/app";
+ import { Layout } from '../components/Layout';

export default function MyApp({ Component, pageProps }: AppProps) {
  const { isBrowser } = useSSR()
  return (
    isBrowser && (
      <NextUIProvider>
+      <Layout>
          <Component {...pageProps} />
+      </Layout>
      </NextUIProvider>
    )
  )
}

これでいい感じに表示された。

footerの作成

NextUIにfooterがなかったから、stitchesで作った。
とりあえずこんな感じ。

import { styled } from "stitches-config"
import { theme } from "@nextui-org/react"

export const FOOTER_HEIGHT = '4rem'

export const Footer = () => {
  return (
    <FooterWrapper>
      <FooterContent>
        © Chanabe Corp. All rights reserved.
      </FooterContent>
    </FooterWrapper>
  )
}

const FooterWrapper = styled('footer', {
  position: 'absolute',
  bottom: 0,
  width: '100%',
  flexGrow: 0,
  flexShrink: 0,
})

const FooterContent = styled('div', {
  width: '100%',
  height: FOOTER_HEIGHT,
  lineHeight: FOOTER_HEIGHT,
  textAlign: 'center',
  backgroundColor: theme.colors.blue100,
})

NextUIはthemeも提供していてありがたいなぁ。
デザイナーさんがやるところを全部やってくれる。

nextui.org

これをそのまんまLayoutの一番下にやると、こんな感じに。

みたらわかるようにFooterが上になっちゃう。

Layoutを変えて、コンテンツが画面高さよりも少ない場合も最下部にいくようにしよう

import { ReactNode } from 'react';
import { Header, Footer, FOOTER_HEIGHT } from 'ui'
import { styled, css } from 'stitches-config';
import { theme } from '@nextui-org/react';

type Props = {
  children: ReactNode
}

export const Layout = ({ children }: Props) => {
  return (
    <Container>
      <Header />
      <Main>
        {children}
      </Main>
      <Footer />
    </Container>
  )
}

const Container = styled('div', {
  minHeight: '100vh',
  display: 'flex',
  flexDirection: 'column',
})

const Main = styled('main', {
  flexGrow: 1,
  marginBottom: FOOTER_HEIGHT,
  backgroundColor: theme.colors.gray50
})

flexを使って最下部に持っていった。
完成したのはこんな感じ。

これやりながらわかったんだけど、Stitchesってすごい可変的な引数のとり方( 例:width: $propsWidth ) できないっぽい?マジ?
まぁやらないほうがいいのはわかるし、思想も理解できるけど、ここまでの制限があると後々少し嫌になりそう。

style整え

いろいろstyleを整えて、最終調整

完成したらこんな感じになった。

詳細ページの作成

次は詳細ページを作っていこう。

どんなブログがいいだろうか。「本屋を背表紙で買った人はおそらく作者に興味がない」という想像から、作者情報は文末にしよう。

けど目次は横がいい!(個人的欲求)

あともしできるんだったら、ブログを進んでいくごとに夕焼けになるようにしたい。(なんか思いつき)

...かな?とりあえずここまで作ってみよう。

ページの枠の作成

とりあえずページの枠を作成しよう。

routingはこれを参考に。

nextjs-ja-translation-docs.vercel.app

これでfile作成

import { Text } from "@nextui-org/react";

export default function View() {
  return (
    <Text>詳細ページです</Text>
  )
}

よし、表示もできるっぽい

ざっくりとレイアウト作成

そういえば、「何かを囲う時のclassの接尾字の命名規則」をchatGPTに作ってもらったので記載。

Q. 何かを囲う時は、class名の末尾に何を使うのが適切? A. 何かを囲む要素の場合、一般的には以下のような接尾辞がclass名に使われることが多いです。 1. -wrapper(ラッパー) - 例: content-wrapper 2. -container(コンテナ) - 例: header-container 3. -box(ボックス) - 例: search-box 4. -area(エリア) - 例: footer-area 5. -section(セクション) - 例: banner-section ただし、命名規則はプロジェクトやチームによって異なる場合があるため、一貫性を保つためにコーディング規約やガイドラインを設定することが推奨されます。このような規約やガイドラインに従ってclass名を選択することが適切です。
引用元:ChatGPT
Q. じゃあいま出した5つのうち、最初の3つだけの使用規則を作ってください。 A. 以下に、1-3の接尾辞を使った改訂された命名規則の使用例を示します。 1. -wrapper(ラッパー) 使用例: content-wrapper, image-wrapper 使用目的: 他の要素を包み、レイアウトやスタイリングの目的で使用される。余白やサイズ調整などのスタイルを適用する場合に適している。 2. -container(コンテナ) 使用例: header-container, form-container 使用目的: 関連する要素をグループ化し、共通のスタイリングやレイアウトを適用する。親要素に対してグリッドシステムやフレックスボックスを適用する場合に適している。 3. -box(ボックス) 使用例: search-box, info-box 使用目的: 独立した機能を持つコンポーネントを表現する。そのコンポーネント内で独自のスタイリングやレイアウトが必要な場合に適している。 これらの命名規則は、一貫性を保つためにプロジェクトやチームで共有されるべきです。また、プロジェクトに応じてカスタマイズされることがあります。
引用元:ChatGPT

あとこれめっちゃめちゃ参考になった。

blog.ojisan.io

いやーけど、style関数できないのきつくないかい?選択肢でしかpropsを与えられないのは辛い。imageUrlとか与えたいもん。
無理なのでcssで与えるようにしました。

こんな感じで設定しました。

import { Col, Container, Image, Row, Text, User } from "@nextui-org/react";
import { css, styled } from "stitches-config";

export default function View() {
  return (
    <Container gap={0} md>
      <Row gap={0} justify="center">
        <Col span={2}>
          <BlogThumbnailImageStyled css={{ backgroundImage: `url(https://nextui.org/images/card-example-${6}.jpeg)` }} />
        </Col>
      </Row>
      <Row gap={0} justify="center">
        <Col span={8}>
          <BlogTitleWrapperStyled>
            <Text h2>羅生門</Text>
          </BlogTitleWrapperStyled>
        </Col>
      </Row>
      <Row>
        <Text>
          ある日の暮方の事である。一人の下人げにんが、羅生門らしょうもんの下で雨やみを待っていた。
          「おのれ、どこへ行く。」
          下人は、老婆が死骸につまずきながら、慌てふためいて逃げようとする行手を塞ふさいで、こう罵ののしった。老婆は、それでも下人をつきのけて行こうとする。下人はまた、それを行かすまいとして、押しもどす。二人は死骸の中で、しばらく、無言のまま、つかみ合った。しかし勝敗は、はじめからわかっている。下人はとうとう、老婆の腕をつかんで、無理にそこへ※(「てへん+丑」、第4水準2-12-93)ねじ倒した。丁度、鶏にわとりの脚のような、骨と皮ばかりの腕である。
          「何をしていた。云え。云わぬと、これだぞよ。」
          下人は、老婆をつき放すと、いきなり、太刀の鞘さやを払って、白い鋼はがねの色をその眼の前へつきつけた。けれども、老婆は黙っている。両手をわなわなふるわせて、肩で息を切りながら、眼を、眼球めだまが※(「目+匡」、第3水準1-88-81)まぶたの外へ出そうになるほど、見開いて、唖のように執拗しゅうねく黙っている。これを見ると、下人は始めて明白にこの老婆の生死が、全然、自分の意志に支配されていると云う事を意識した。そうしてこの意識は、今までけわしく燃えていた憎悪の心を、いつの間にか冷ましてしまった。後あとに残ったのは、ただ、ある仕事をして、それが円満に成就した時の、安らかな得意と満足とがあるばかりである。そこで、下人は、老婆を見下しながら、少し声を柔らげてこう云った。
          「己おれは検非違使けびいしの庁の役人などではない。今し方この門の下を通りかかった旅の者だ。だからお前に縄なわをかけて、どうしようと云うような事はない。ただ、今時分この門の上で、何をして居たのだか、それを己に話しさえすればいいのだ。」
          すると、老婆は、見開いていた眼を、一層大きくして、じっとその下人の顔を見守った。※(「目+匡」、第3水準1-88-81)まぶたの赤くなった、肉食鳥のような、鋭い眼で見たのである。それから、皺で、ほとんど、鼻と一つになった唇を、何か物でも噛んでいるように動かした。細い喉で、尖った喉仏のどぼとけの動いているのが見える。その時、その喉から、鴉からすの啼くような声が、喘あえぎ喘ぎ、下人の耳へ伝わって来た。
          「この髪を抜いてな、この髪を抜いてな、鬘かずらにしようと思うたのじゃ。」
          下人は、老婆の答が存外、平凡なのに失望した。そうして失望すると同時に、また前の憎悪が、冷やかな侮蔑ぶべつと一しょに、心の中へはいって来た。すると、その気色けしきが、先方へも通じたのであろう。老婆は、片手に、まだ死骸の頭から奪った長い抜け毛を持ったなり、蟇ひきのつぶやくような声で、口ごもりながら、こんな事を云った。
          「成程な、死人しびとの髪の毛を抜くと云う事は、何ぼう悪い事かも知れぬ。じゃが、ここにいる死人どもは、皆、そのくらいな事を、されてもいい人間ばかりだぞよ。現在、わしが今、髪を抜いた女などはな、蛇を四寸しすんばかりずつに切って干したのを、干魚ほしうおだと云うて、太刀帯たてわきの陣へ売りに往いんだわ。疫病えやみにかかって死ななんだら、今でも売りに往んでいた事であろ。それもよ、この女の売る干魚は、味がよいと云うて、太刀帯どもが、欠かさず菜料さいりように買っていたそうな。わしは、この女のした事が悪いとは思うていぬ。せねば、饑死をするのじゃて、仕方がなくした事であろ。されば、今また、わしのしていた事も悪い事とは思わぬぞよ。これとてもやはりせねば、饑死をするじゃて、仕方がなくする事じゃわいの。じゃて、その仕方がない事を、よく知っていたこの女は、大方わしのする事も大目に見てくれるであろ。」
          老婆は、大体こんな意味の事を云った。
        </Text>
      </Row>
      <Row gap={1}>
        <BlogDetailFooterCss>
          <User src={`https://i.pravatar.cc/150?u=a042581f4e29026024d`} name={`@ayumu-1212`} size='xl' />
          <Text size="$xl">作成日:2023/05/07 15:09</Text>
        </BlogDetailFooterCss>
      </Row>
    </Container>
  )
}

const BlogDetailFooterCss = styled('div', {
  padding: '2rem 1rem',
  display: 'flex',
  flexDirection: 'column',
  gap: '1rem'
})

const BlogThumbnailImageStyled = styled('div', {
  width: '100%',
  paddingBottom: '100%',
  borderRadius: '50%',
  backgroundPosition: 'center center',
  margin: '1rem 0 0'
})

const BlogTitleWrapperStyled = styled('div', {
  width: '100%',
  textAlign: 'center',
  padding: '2rem 0'
})

表示したらこんな感じ。

リファクタリング

ちょっと立ち止まってリファクタリング

Styleコンポーネントからcss propsに。

どうやらStitchesはcss propsも使えるらしい。
そっちがいいので変更。

だけどなんかStyledコンポーネントにしたほうが良さげなものと、CssPropsのほうが良さげのものがあるな。

たぶんこんな感じ。

  • Styledコンポーネント:そのタグ自体に意味がある場合
    • 例:FooterContainer, FooterContent
  • CssProps:そのタグ自体に意味がない場合
    • 例:余白を付けたいがためのStyleなど
prettierの設定

開発していてやりづらい!!
自動で自分の間違いを修正してほしい。

ということで環境作りから。

$ mkdir packages/prettier-config-custom

$ cd packages/prettier-config-custom/

$ yarn init
yarn init v1.22.19
question name (prettier-config-custom): 
question version (1.0.0): 
question description: package of prettier config files.
question entry point (index.js): 
question repository url: 
question author: 
question license (MIT): 
question private: true
success Saved package.json
Done in 32.38s.

$ cd ../..

$ yarn workspace prettier-config-custom add -D prettier

んで、ファイルも生成。こちらを参考。

zenn.dev

module.exports = {
  printWidth: 100,
  tabWidth: 2,
  useTabs: false,
  semi: false,
  singleQuote: true,
  quoteProps: "as-needed",
  jsxSingleQuote: false,
  trailingComma: "es5",
  bracketSpacing: true,
  bracketSameLine: false,
  proseWrap: "always",
  endOfLine: "lf",
  embeddedLanguageFormatting: "off",
}

じゃあこれをappsとuiに導入する。

まず諸々のinstallから。

$ yarn workspace cds add -D prettier eslint-config-prettier
$ yarn workspace ui add -D prettier eslint-config-prettier

突然だけど...

一旦ここまで!!これはWIPにします。