まるで名刺のようなポートフォリオサイトを作れるCardfolio!を公開しました！



こちらの記事は**[Gatsby.js Advent Calendar 2019 ](https://qiita.com/advent-calendar/2019/gatsby)24日目**の記事です。(メリークリスマスイヴ :santa_tone1: )
昨日は@mmnsさんの[Gatsby, TypeScript, Emotion, Tailwind, MDXでブログを作っている](https://qiita.com/mmns/items/a6a8400cf9f5f2ce488f)でした！

# Cardfolio!とは
<img width="480" alt="アセット 7@2x.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71154/515b385c-53e9-818d-3ccf-e87613a31f08.png">
Cardfolio!とはまるで名刺のようなポートフォリオサイトが作れるGatsbyベースのポートフォリオサイトフレームワークです。
「まるで名刺のようなポートフォリオサイトってなんやねん笑」と思った方、↓のgifを見ていただくとイメージがつくかと思います。

![cardfolio.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71154/643a62fc-6859-1f0d-8573-9ffc3802d177.gif)

そうです！
まるで名刺のようなポートフォリオサイトとは
まるで本物の名刺がそのままWebサイトになったかのような、紙の質感、回転を再現したポートフォリオサイトなのです :card_box:
(以下カードフォリオサイトと呼びます。)

そしてこのサイトが真骨頂を発揮するのは実際に名刺を渡したときにあります！
↓の写真が今回自分が作成した名刺です。
誰かにこんな名刺を受け取ったことを想像しながらQRコードを読み取ってみてください。
(スマホで見ていてQRコード読めねーよという方は[こちら](https://matsumotokazuya.io/?fromQR=1)からどうぞ)

<img width="960" alt="cardfolio.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71154/2f3647e8-dd59-4617-567d-49b82e08c1f4.png">

いかがでしょうか？
名刺がそのままポートフォリオに変わったかのような不思議な体験をすることができます。:smiling_imp:

そして今回は誰でも同じようなカードフォリオサイトが作れるようにOSSとしてGithubに公開しています :octocat: 
ロゴも自分でデザインしてみました〜:paintbrush: 

[Cardfolio!](https://github.com/kazuooooo/Cardfolio)
<img width="480" alt="アセット 7@2x.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71154/515b385c-53e9-818d-3ccf-e87613a31f08.png">

# カードフォリオサイトの作り方

一応Githubのレポジトリ内にも英語での記載がありますが、今回は日本語向けのカードフォリオサイトを作る方法を簡単にご紹介します。
個人名刺を持ってる or 作りたい方向けかと思いますが、Reactの経験が少しでもある方なら簡単に2-3時間で構築することができますので是非是非作ってみてください :pray: 

① Githubのレポジトリをフォークします。
② 開発環境を立ち上げます。

```shell
yarn
gatsby develop
```

この時点で試しにlocalhost:8000にアクセスして画面が表示されることを確認してみてください。

③ デフォルトの言語を日本語に切り替えます。

```javascript:src/data/locales.js
module.exports = {
  ja: {
    default: true, // enからjaに移動
    path: 'ja',
    locale: 'Japanese',
  },
  en: {
    path: 'en',
    locale: 'English',
  },
}

```

④ 読み込むデータは基本src/data/{locale}.jsonに入っているので、このファイルに必要な情報を入力していきます。
例として先ほどの名刺の表側の名前と職種を変更してみましょう。
以下のようにルートのキーがコンポーネントの名前になっており、その配下に表示するためのデータくる形になっています。

```json:src/data/index/ja.json
{
  "frontSide": { // コンポーネント名
    "jobTitle": "デザイナー", // データ
    "name": "コアラ子"
  }
  …
}
```

Gatsbyの再ビルドが走り、変更が反映されることが確認できます。
<img width="320" alt="front.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71154/fb0e041d-f274-1646-6d1a-9c9fddaade56.png" />

その他のデータについても同様に全て差し替えて行きます。

また、profile.pngのみGatsybyのImgを使用しているため、直接ディレクトリの画像を差し替えてください。
一通り修正が済んだら、localhost:8000にアクセスして反映を確認してみてください。

⑤ 完成したらサイトをデプロイします。方法はなんでも良いですが、Gatsbyは[Netlify](https://www.gatsbyjs.org/docs/deploying-to-netlify/)と非常に相性がいいのでおすすめです。

⑥ (オプションですが是非やってほしい) 最後に実際の名刺を作成しましょう！
表面については実装したカードフォリオサイトのデザインに合わせて作成し、
裏面については
[こちらのようなQRコードを作成できるサイト](https://www.unitag.io/qrcode)で

```
{your-domain}?fromQR=1
```

というurlでQRコードを作成します。
ex) https://matsumotokazuya.io?fromQR=1

fromQRはQRコードから遷移したことを識別するためのpropsとして使用します。
(:warning: 一度名刺を印字すると変更できないので間違えないように注意してください)

作成したQRコードは画像としてqr-code.pngと差し替えるようにしてください。
これであなたの名刺を受け取った方に上記でみていただいたようなびっくり体験をさせることができ、喜ばれること間違いなしです :boar:

カスタマイズやより詳しい開発の方法は[GithubのREADME.md](https://github.com/kazuooooo/Cardfolio/blob/master/README.md)をご覧ください。

# Cardfolio!を支える技術
ここからはQiitaの記事らしくCardfolio!を作るにあたって使用した技術について書いてきます。

## 名刺の回転

### 裏表の表示

なんとなく想像がつくかと思いますが、表裏の表示実装は

* 表側と裏側のコンポーネントをそれぞれ作成してcontainer(card)配下にposition: absoluteで重ねて置く
* 裏側を180度回転させる
* Containerを回転させて、表裏を切り替える

という方法で実現しています。
図にすると以下のようになります。

![グループ 8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71154/44c0dd8f-eabe-73e9-e733-c6fe838c01b6.png)

1つハマりどころとしてそのまま実装すると裏返ったときに↓のように裏側ではなく表側を反転させたものが表示されてしまいます。
![Cursor_と_awesome-poitras-dnk0s_-_CodeSandbox.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71154/f808e096-7d93-b5ac-a10e-2b336f2661d1.png)

これはtransform3dでDOMを裏返しにした場合、デフォルトではそのオブジェクトの裏側が表示されてしまうためです。
これを解決するためにはそのデフォルトの挙動を変更する[backface-visibility](https://www.w3schools.com/cssref/css3_pr_backface-visibility.asp)というマニアックなプロパティをhiddenに設定にする必要があります。

```css
.front {
  ...
  backface-visibility: hidden;
}
```

こうすることで裏返った場合は何も表示されなくなり、結果として裏側が最上部として画面に表示されることになります。
[サンプルコード](https://codesandbox.io/s/awesome-poitras-dnk0s)も作成したので参考にどうぞ。

### ユーザーの操作に合わせて回転させる
ここまでで表示して自動で回転させるところまでは実装することができました。
今度はユーザーのドラッグ操作に合わせてカードを回転させます。
まずドラッグの検知には[react-use-gesture](https://github.com/react-spring/react-use-gesture)というライブラリのuseDragというhooksを使って簡単に取得することができます。

```javascript
import { useSpring } from 'react-spring'
...
const bind = useDrag(({ down, movement: [moveX], last }) => {
  ...
})
```

ドラッグを検知したら現在のx座標をy軸の角度に変換してカードを回転させます。

```javascript
   // x座標を角度に変換
   const degree = lastDegree + moveXToDegree(moveX)
...
　 // ドラッグ中
  if (down) {
    // ユーザーの操作に合わせてカードを回転させる。
    set({ transform: `rotateY(${degree}deg)` })
    return degree
  }
...
```

さらに指を離したときに表 or 裏にいい感じに戻ってほしいので、現在の回転角から戻るべき方向を判定して、回転アニメーションをさせながら表 or 裏に戻します。

```javascript
    // ユーザーが指を離したら
    if (last) {
      // 裏か表か判定して戻す
      const horizontalDegree = calcHorizontalDegreeToReturn(degree)
      setDegree(horizontalDegree)
    }
```

戻す判定ロジックは以下の図のように

* 第一象限、第三象限にある場合は角度を減らす方向
* 第二象限、第四象限にある場合は角度を増やす方向

に回転させます。

![judge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71154/1f77ac34-b670-3539-77f9-8f04fe72e854.png)

実装の詳細について知りたい方は[ソースコード](https://github.com/kazuooooo/Cardfolio/blob/master/src/helpers/useCardRotation.ts)をご覧ください。


## Gatsby関連
ここまでGatsbyのアドベントカレンダーにも関わらず、Gatsbyの話をほとんどしてこなかったので最後にGatsby関連の技術話を書いておきます :bow: 
Gatsby自体がどんなものかは[こちらの記事](https://qiita.com/hppRC/items/00739eaf9ae7fc95c1ca)初め他のアドベントカレンダーの記事によくまとまっているので、自分は実装していて悩んだややニッチな話を書きます。

### fragmentの活用
Cardfolio!は裏側に表示される自己紹介やスキルセットといったモーダルををそれぞれ1つのコンポーネントとして実装しています。
これらのコンポーネントはpropsと取得するGraphQLクエリのデータ構造が一致しているので、それらを1つのコンポーネント内にまとめたいなと考えました。
そこで初めはGraphQLのクエリを変数としてexportして一番上の親コンポーネントから流し込む以下のような実装を試しました。

```javascript:selfIntroduction.tsx

// propsの定義
interface Social {
  name: string
  id: string
  url: string
}

interface Props {
  data: {
    description: string
    menuItemTitle: string
    socialURLs: Social[]
  }
}

const SelfIntroduction = (props: Props) => {
   ...
}
...
// propsを取得するためのgraphqlクエリ
export const dataQuery = graphql`
  fragment SelfIntroductionData on IndexJson {
    selfIntroduction {
      menuItemTitle
      description
      socialURLs {
        name
        url
      }
    }
  }
`
...
```

```javascript:index.tsx
import { selfIntroductionQuery } from './selfIntroductionQuery'

export default ({ data, location }) => {
}
export const query = graphql`
  query Index($locale: String) {
    file(name: { eq: $locale }, relativeDirectory: { eq: "index" }) {
      childIndexJson {
        ${selfIntroductionQuery}
        ...
      }
    }
  }
`
```

これを実行しようとするとビルドの時点でgraphqlタグに変数を埋め込むことができず以下のようなエラーでこけます。
Gatsbyはビルド時に静的にコード解析をしてgraphql``を探し、その中身を実行してコンポーネントに渡すということをしているため外から変数を渡すことができないようです。

```

String interpolation is not allowed in graphql tag:

  104 |     file(name: { eq: $locale }, relativeDirectory: { eq: "index" }) {
  105 |       childIndexJson {
> 106 |         ${selfIntroductionQuery}
      |        ^^^^^^^^^^^^^^^^^^^^^^^^
```

そこで[fragment](https://graphql.org/learn/queries/#fragments)を使います。
fragmentを使うことで名前の通りクエリを断片として切り出し再利用可能にすることができます。

```javascript:selfIntroduction.tsx

// framgmentとしてコンポーネントのpropsを定義
const SelfIntroduction = (props: Props) => {
   ...
}
// Framgmentとして必要なデータを取得するクエリを定義
export const dataQuery = graphql`
  fragment SelfIntroductionData on IndexJson {
    selfIntroduction {
      menuItemTitle
      description
      socialURLs {
        name
        url
      }
    }
  }
`
```

利用する側は...{Framgment}の構文でフラグメントを利用できます。
1つのめちゃくちゃ大きなクエリにならずに、fragmentが整然と並んでいてとても読みやすいですね :+1:

```javascript:index.tsx
export const query = graphql`
  query Index($locale: String) {
    file(name: { eq: $locale }, relativeDirectory: { eq: "index" }) {
      childIndexJson {
        ...SiteMetaData
        ...FrontSideData
        ...SelfIntroductionData
        ...WorksData
        ...ContactData
        ...CareerData
        ...SkillSetData
      }
    }
  }
`
```

これでビルド時もエラーにならず、元々やりたかったコンポーネント内にクエリ定義を閉じ込めることができました :clap: 

### i18n
多言語化するにあたり、今回やりたいこととしてはシンプルに

/ : 日本語ページ
/en : 英語ページ

のように切り替えたいだけだったので、pluginを使わずに[こちらの記事](https://medium.com/significa/i18n-with-gatsby-528607b4da81)を参考に自前で実装しました。

方法としてはまず以下のようなディレクトリ構成を作り

```
.
├── index
│   ├── en.json
│   └── ja.json
└── locales.js
```

en.jsonとja.jsonには実際の言語データを入れておきます。

```json:ja.json
{
  "frontSide": {
    "jobTitle": "エンジニア",
    "name": "ウォンバット太郎"
  },
...
}
```

```json:en.json
{
  "frontSide": {
    "jobTitle": "Engineer",
    "name": "Wombat John"
  },
...
}
```

locales.jsには言語の情報を定義しておきます。

そしてgatsby-node.jsでページを生成する際にlocales.jsの情報をもとに各言語のpageを生成します。
また、contextを使いlocaleという変数に言語の情報(ja/en)を渡しておきます。
こうすることでgrapqlのクエリ内で参照することができるようになります。

```javascript:gatsby-node.js
const locales = require('./src/data/locales')

exports.onCreatePage = ({ page, actions }) => {
  const { createPage, deletePage } = actions

  return new Promise((resolve) => {
    deletePage(page)

    // 各localeのページを生成
    Object.keys(locales).map((lang) => {
      const localizedPath = locales[lang].default
        ? page.path
        : locales[lang].path + page.path

      return createPage({
        ...page,
        path: localizedPath,
        context: {
          // contextにlocaleの値を渡しておく
          locale: lang,
        },
      })
    })

    resolve()
  })
}
```

最後にindex.tsx内でcontextのlocaleを使い、データをフェッチする箇所をja/en.jsonを切り替えて取得できるように書き換えます。

```javascript

export const query = graphql`
  query Index($locale: String) {
    file(name: { eq: $locale }, relativeDirectory: { eq: "index" }) {
      childIndexJson {
        ...SiteMetaData
        ...FrontSideData
        ...SelfIntroductionData
        ...WorksData
        ...ContactData
        ...CareerData
        ...SkillSetData
      }
    }
  }
`
```

これで

/ : 日本語ページ(default)
/en : 英語ページ

言語を切り替えたページが生成されるようになりました :earth_africa: 

# まとめ

いかがだったでしょうか？
Gatsbyは素晴らしいフレームワークで普通にWebサイトを構築するのも良いですが、今回自分が作成したようなOSSをGatsbyベースで作ってみるのもGatsbyの様々な恩恵を受けつつ開発できるのでオススメです :gift:

この記事を読んだ方のうち1人でもCardfolio!使って自身のカードフォリオサイトを作ってみていただける方が現れてくれれば嬉しいです！！