📺

NHKが大好きな人のAstroを用いたWebページ制作

2023/07/08に公開

はじめに

私はNHKが大好きです!

特にドキュメント72時間という、1つの現場に72時間撮影クルーが待機してそこに偶然居合わせた一般の方々へインタビューするドキュメンタリー番組が好きで毎週観ています!

番組の観覧応募は欠かさずしていて、The Covers10周年を記念した斉藤 和義さんのスペシャルライブに当選したときは、坂本 龍一さんが生前の最後に演奏されたCR509スタジオで聴けたので本当にうれしかったです。

そこで、NHKの魅力を伝えられるポートフォリオを制作しようと思い立ち、NHKが提供しているAPIを探したところNHK番組表APIを見つけました。

簡易的ではありますが、NHK番組表APIと静的サイトジェネレータとして人気が出てきたAstroを用いたWebページを制作してみましたので、最後までお読みいただければ幸いです。

ポートフォリオ

NHK番組表APIを2つ呼び出して、2023年7月8日10時54分時点での番組を表示しています。

NHKのポートフォリオ

NHK番組表API

NHK番組表APIとは、全国のNHKの放送番組のタイトルや放送時間などの番組情報を提供しているAPIのことです。

ポートフォリオで使用したAPIは以下の2つです。

  • Program Info API (Ver.2) → 番組IDを指定することで、現在放送している番組情報を取得
  • Now On Air API (Ver.2) → 放送地域、サービス(放送波)を指定することで、現在放送している番組情報を取得

Program Info API (Ver.2)では、

  • 現在放送している番組のロゴ

を取得するために以下のコードを書きました。

src/components/ProgramChange.astro
try {
  const responseProgramInfo = await axios.all(
    Object.values(service).map(
      async (serviceId, index) =>
        await axios.get(
          `https://api.nhk.or.jp/v2/pg/info/${area.東京}/${serviceId}/${presentId[index]}.json?key=${programListApiKey}`
        )
    )
  );

  responseProgramInfo.forEach((response) => {
    programInfoList = {
      ...programInfoList,
      ...response.data.list,
    };
  });

  programLogos = Object.keys(programInfoList).flatMap((key) =>
    // @ts-ignore
    programInfoList[key].map((obj: { program_logo: { url: string } }) =>
      obj.program_logo && obj.program_logo.url ? obj.program_logo.url : ''
    )
  );
} catch (error) {
  //
}

Now On Air API (Ver.2)では、

  • サービスのLサイズのロゴ
  • サービス名(例えば、NHK総合1、NHKEテレ1など)
  • 現在放送している番組のID(Program Info API (Ver.2)のIDと紐づけて、現在放送している番組のロゴを取得するため)
  • 現在放送している番組のタイトル

を取得するために以下のコードを書きました。

src/pages/index.astro
try {
  const responseNowOnAir = await axios.all(
    Object.values(service).map(
      async (serviceId) =>
        await axios.get(
          `https://api.nhk.or.jp/v2/pg/now/${area.東京}/${serviceId}.json?key=${programListApiKey}`
        )
    )
  );
  responseNowOnAir.forEach((response) => {
    nowOnAirList = { ...nowOnAirList, ...response.data.nowonair_list };
  });
} catch (error) {
  nowOnAirList = nowOnAirAllDummy;
}
const [logoLurls, serviceNames, presentId, presentTitle] = Object.values(
  nowOnAirList
).reduce(
  (acc: [string[], string[], string[], string[]], obj: any) => {
    acc[0].push(obj.present.service.logo_l.url);
    acc[1].push(obj.present.service.name);
    acc[2].push(obj.present.id);
    acc[3].push(obj.present.title);
    return acc;
  },
  [[], [], [], []] as [string[], string[], string[], string[]]
);

ポートフォリオのWebページへアクセスしたとき、2つのAPIでNHKワンセグ2を除く7つのサービスのリクエストをしています。

Program Info API (Ver.2)の利用回数は300回/日で、Now On Air API (Ver.2)の利用回数制限は1500回/日と決まっています。

したがって、前者では1日43回以上で後者では1日215回以上アクセスされると、現在放送している番組情報を取得できなくなります。

取得できないときは、src/images配下のダミー画像(現在放送している番組のロゴがnullのときにも使用)とconstants配下のダミーオブジェクトを代わりに取得するようにしています。

コードの説明

以下のすべてのコードは抜粋していることをご了承ください。

設定ファイル

package.json

Astroは2022年4月4日にベータ版がリリースされたばかりで参考資料が少なく、ChatGPTへコードを質問しても2021年9月までの情報を元にするのでReactのコードだと勘違いしてしまいます。

したがって、以下の2つの方針を取ることにしました。

  • 便利な機能はAstroが勧めているインテグレーションをなるべく使うこと
  • 静的解析ツールを多く導入して、コードの誤りを限りなく少なくすること

以下に記載したコードでdependenciesdevDependencies以外は、npm create astro@latestを実行したときに書かれるコードと同じなので、その部分は省略しました。

package.json
{
  "dependencies": {
    "@astrojs/tailwind": "^3.1.3",
    "astro": "^2.5.5",
    "astro-icon": "^0.8.1",
    "axios": "^1.4.0",
    "nanostores": "^0.9.2",
    "sass": "^1.63.3",
    "splitting": "^1.0.6",
    "stylelint-scss": "^5.0.1",
    "tailwindcss": "^3.3.2",
    "webfontloader": "^1.6.28"
  },
  "devDependencies": {
    "@markuplint/astro-parser": "^3.7.0",
    "@typescript-eslint/parser": "^5.59.11",
    "astro-eslint-parser": "^0.14.0",
    "eslint": "^8.42.0",
    "eslint-plugin-astro": "^0.27.1",
    "eslint-plugin-tailwindcss": "^3.12.1",
    "markuplint": "^3.10.0",
    "postcss-html": "^1.5.0",
    "stylelint": "^15.7.0",
    "stylelint-config-html": "^1.1.0",
    "stylelint-config-standard-scss": "^9.0.0"
  }
}

tailwind.config.cjs

Astroの公式はTailwind CSSの使用を勧めているので採用しました。

Tailwind CSSを採用して最も良いなと思った部分は、設定ファイルでデザインシステムを記述できることです。

例えば、設定ファイルでcolorsで指定していないtext-grayとclass名に書いたときにeslint-plugin-tailwindcssClassname 'text-gray' is not a Tailwind CSS class!eslinttailwindcss/no-custom-classnameと警告してくれます。

したがって、制作が共同作業になったときに、設定がバラバラになることを未然に防ぐことができます。

tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
  future: { hoverOnlyWhenSupported: true },
  theme: {
    colors: {
      darkgray: '#333333', // 文章
      sub: '#858585', // 補足文章
      critical: '#ef4b4b', // エラー
      warning: '#ffc107', // 注意
      success: '#28a745', // 処理完了
      disabled: '#CECECE', // 無効な要素
      on: '#CECECE', // 塗りつぶしの上に使う色
      link: '#0085c7', // リンク
      focused: '#FF5720', // フォーカス
      base: '#ffffff', // 背景色(70%)
      main: '#808080', // イメージカラー(25%)
      accent: '#000000', // 大事な情報(5%)
    },
    fontSize: {
      sm: '1.4rem', // 14px
      rg: '1.6rem', // 16px
      lg: '2.2rem', // 22px
      xl: '2.8rem', // 28px
      exl: '4.6rem', // 46px
    },
    fontWeight: { th: 100, rg: 300, bd: 600, eb: 800 },
    extend: {
      fontFamily: {
        custom: ['line-seed-jp'],
      },
      cursor: {
        fancy: 'url(hand.cur), pointer',
      },
    },
  },
  plugins: [],
};

.env

NHK番組表APIを使用するのでAPIキーが必要です。

コードで文字列と書かれた部分は、発行したAPIキーの文字列へ置き換えます。

.env
NHK_PROGRAM_LIST_API_KEY=文字列

.markuplintrc.cjs, .stylelintrc.cjs, .eslintrc.cjs, astro.config.mjs

セマンティックHTMLで書けるように、Markuplintを採用しました。

Markuplintは子コンポーネントのHTMLタグを認識できないので、設定ファイルで認識できるようにします。

.stylelintrc.cjs.eslintrc.cjsはAstroと組み合わせるときに書くべきコードとほぼ同じで、astro.config.mjsも初期値からほぼ変えてないので説明は省略します。

.markuplintrc.cjs
module.exports = {
  rules: { 'character-reference': false },
  parser: {
    '.astro$': '@markuplint/astro-parser',
  },
  extends: ['markuplint:recommended'],
  pretenders: [
    {
      selector: 'Head',
      as: {
        element: 'head',
      },
    },
  ],
  nodeRules: [
    {
      selector: 'head',
      rules: {
        'permitted-contents': false,
        'required-element': false,
      },
    },
  ],
};
.stylelintrc.cjs
module.exports = {
  extends: [
    'stylelint-config-standard-scss',
    'stylelint-config-html/astro',
    'stylelint-config-html',
  ],
  rules: {
    'property-no-vendor-prefix': null,
  },
};
.eslintrc.cjs
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
  },
  extends: [
    'plugin:tailwindcss/recommended',
    'plugin:astro/recommended',
    'eslint:recommended',
  ],
  overrides: [
    {
      files: ['*.astro'],
      parser: 'astro-eslint-parser',
      parserOptions: {
        parser: '@typescript-eslint/parser',
        extraFileExtensions: ['.astro'],
        sourceType: 'module',
        ecmaVersion: 'latest',
      },
      rules: {
      },
    },
    {
      files: ['*.js', '*.mjs'],
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
      },
    },
  ],
  parser: '@typescript-eslint/parser',
};
astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';

// https://astro.build/config
export default defineConfig({
  integrations: [tailwind()],
  server: { port: 3001 },
});

requestParameters.js

NHK番組表API Keyは、それぞれのコンポーネントで必要になるので共通化しました。

今回のポートフォリオでは、地域IDは東京で固定なので使っていないのですが、後々制作するかも知れないので書きました。

constants/requestParameters.js
// NHK番組表API Key
export const programListApiKey = import.meta.env.NHK_PROGRAM_LIST_API_KEY;
// 地域ID
export const area = {
  札幌: '010',
  函館: '011',
  旭川: '012',
  ...
  千葉: '120',
  東京: '130',
  横浜: '140',
  ...
  宮崎: '450',
  鹿児島: '460',
  沖縄: '470',
};
// サービスID
export const service = {
  NHK総合1: 'g1',
  NHKEテレ1: 'e1',
  NHKBS1: 's1',
  NHKBSプレミアム: 's3',
  NHKラジオ第1: 'r1',
  NHKラジオ第2: 'r2',
  NHKFM: 'r3',
};

フォント

フォントはLINE Seedを採用して、フォントの太さは4種類としました。

フォルダの構成は、以下にしています。

  • public/fonts/LINESeedJP_OTF_Th.woff2
  • public/fonts/LINESeedJP_OTF_Rg.woff2
  • public/fonts/LINESeedJP_OTF_Bd.woff2
  • public/fonts/LINESeedJP_OTF_Eb.woff2

Layout.astroでstyleタグ(<style lang="scss" is:global>)の中へ以下のコードを書くことで、font-familyをLINE Seedで指定しています。

src/layouts/Layout.astro
@font-face {
  font-family: line-seed-jp;
  src: url('/fonts/LINESeedJP_OTF_Th.woff2') format('woff2');
  font-weight: 100; // font-thin
  font-style: normal;
}

@font-face {
  font-family: line-seed-jp;
  src: url('/fonts/LINESeedJP_OTF_Rg.woff2') format('woff2');
  font-weight: 300; // font-light
  font-style: normal;
}

@font-face {
  font-family: line-seed-jp;
  src: url('/fonts/LINESeedJP_OTF_Bd.woff2') format('woff2');
  font-weight: 600; // font-semibold
  font-style: normal;
}

@font-face {
  font-family: line-seed-jp;
  src: url('/fonts/LINESeedJP_OTF_Eb.woff2') format('woff2');
  font-weight: 800; // font-extrabold
  font-style: normal;
}

srcフォルダ

Layout.astro

NHKのテーマカラーは灰色(#808080)なので、デフォルトのマウスカーソルの代わりに灰色の円をマウスカーソルにして、ホバーしたときにCSSアニメーションを動作させるようにしました。

以下に灰色の円へマウスカーソルへ変更するコードを掲載します。
(ただし、Markuplintでこのコンテキストでは、要素「body」に要素「style」を含めることはできませんとエラーメッセージが出ているので、後ほどリファクタリングします。)

src/layouts/Layout.astro
<body>
  <div id="cursor"></div>
  <slot />
  <style lang="scss" is:global>
    html {
      font-size: 62.5%; // 1rem = 10px
      cursor: none;
    }

    #cursor {
      pointer-events: none;
      position: fixed;
      top: -0.8rem; // 座標調節(カーソル位置と円の中心を合わせる)
      left: -0.8rem; // 座標調節(カーソル位置と円の中心を合わせる)
      width: 1.6rem; // カーソルの直径
      height: 1.6rem; // カーソルの直径
      background: rgb(128 128 128 / 100%);
      border-radius: 50%;
      z-index: 999;
      transition: width 0.13s, height 0.13s, top 0.13s, left 0.13s;
      mix-blend-mode: difference;
    }

    #cursor.hover-max {
      top: -2rem; // 大きくなった分の座標調節
      left: -2rem; // 大きくなった分の座標調節
      width: 4rem; // カーソルの直径
      height: 4rem; // カーソルの直径
    }

    #cursor.hover {
      top: -1.6rem; // 大きくなった分の座標調節
      left: -1.6rem; // 大きくなった分の座標調節
      width: 3.2rem; // カーソルの直径
      height: 3.2rem; // カーソルの直径
    }
  </style>

  <script>
    // ----------------
    // マウスカーソルを小さいドットに変更
    // ----------------
    // カーソル用のdivを取得
    const cursor = document.getElementById('cursor');
    const html = document.querySelector('html');
    // cursorが存在する場合のみイベントリスナーを設定する
    if (cursor) {
      if (!('ontouchstart' in window)) {
        // buttonタグのみcssでカーソルが非表示とならなかったので、jsで非表示
        let buttons = document.querySelectorAll('button');
        buttons.forEach((button) => {
          button.style.cursor = 'none';
        });
        // ブラウザのストレージからカーソルの位置を取得
        const storedCursorX = localStorage.getItem('cursorX');
        const storedCursorY = localStorage.getItem('cursorY');
        if (storedCursorX && storedCursorY) {
          // ブラウザのストレージに位置情報が保存されている場合、それを反映
          cursor.style.transform = `translate(${parseInt(
            storedCursorX
          )}px, ${parseInt(storedCursorY)}px)`;
        }
        // マウス移動イベントを設定
        document.addEventListener('mousemove', (e) => {
          // カーソルの位置をマウスの位置に設定する
          const cursorX = e.clientX;
          const cursorY = e.clientY;
          cursor.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`;
          // カーソルの位置情報をブラウザのストレージに保存
          localStorage.setItem('cursorX', cursorX.toString());
          localStorage.setItem('cursorY', cursorY.toString());
        });
        // リンク要素を全て取得
        const linkElems = document.querySelectorAll('a, button, [data-hover]');
        // 各リンク要素に対して処理を行う
        linkElems.forEach((linkElem) => {
          // @ts-ignore
          let hoverTimeout;
          // マウスオーバー時にカーソルのスタイルを変更する
          linkElem.addEventListener('mouseover', () => {
            cursor.classList.add('hover-max');
            // @ts-ignore
            if (hoverTimeout) {
              clearTimeout(hoverTimeout);
            }
            hoverTimeout = setTimeout(() => {
              cursor.classList.add('hover');
            }, 130);
          });
          // マウスアウト時にカーソルのスタイルを元に戻す
          linkElem.addEventListener('mouseout', () => {
            cursor.classList.remove('hover-max');
            cursor.classList.remove('hover');
            // @ts-ignore
            if (hoverTimeout) {
              clearTimeout(hoverTimeout);
            }
          });
        });
      } else {
        cursor.style.background = 'transparent';
        if (html) {
          // タッチデバイスでマウスを使う場合、初期値のカーソルを表示
          html.style.cursor = 'auto';
        }
      }
    }
  </script>
</body>

index.astro

Astroはpropsが使えるので、Now On Air API(Ver.2)で取得した情報をProgramChangeコンポーネントへ渡しています。

data-type-shuffleは、Type Shuffle Animationを動作させるためのdata属性です。

data-type-shuffleで最初に文字を"N"、"H"、"K"でランダム表示させるようにしています。

data-max-cell-iterations="5"data-type-shuffleを動作させる時間を表します。

data-text-slideはCSSアニメーションで白色の縦帯を左から右へ流すことで、番組タイトルが変わる様子を表現しています。

data-presenttitle={presentTitle}はあまり見慣れない書き方だと思いますので、フロントマター変数をスクリプトに渡すをご覧いただければと思います。

src/pages/index.astro
<Layout head={head}>
  <main>
    <section>
      <div>
        <h2
          data-type-shuffle
          data-max-cell-iterations="5"
          class="mb-[1rem] text-xl font-eb"
        >
          Now On Air
        </h2>
        <h3
          data-text-slide
          data-presenttitle={presentTitle}
          class="h-[7.2rem] w-full text-rg font-bd"
        >
          {presentTitle[0]}
        </h3>
      </div>
      <ProgramChange
        logoLurls={logoLurls}
        serviceNames={serviceNames}
        presentId={presentId}
        presentTitle={presentTitle}
        className={''}
      />
    </section>
  </main>
</Layout>

ProgramChange.astro

ポートフォリオの中心となるコンポーネントです。

以下にProgramChange.astroのコンポーネントを掲載します。

ProgramChange

SVGのアイコンはAstro Iconを使いました。

Astro IconはSVGスプライトとして扱えるので、短いコードで簡単にスタイルを変更できます。

Astro Iconを円(ph:circle-thin)とTV(eva:tv-outline)で使いました。

以下JSXライクな部分をすべて掲載します。

src/components/ProgramChange.astro
<ul
  data-ul-diameter
  class=`grid aspect-square place-content-center pt-[1rem] ${className}`
>
  <li class="col-start-1 row-start-1">
    <Icon
      data-circle-scale
      data-circle-color
      name="ph:circle-thin"
      class="text-[#FB0D06]"
    />
  </li>
  <li class="col-start-1 row-start-1 mt-[-1rem] scale-150 text-main">
    <Icon name="eva:tv-outline" class="pointer-events-none" />
  </li>
  <li
    class="relative col-start-1 row-start-1 flex scale-110 items-center justify-center"
  >
    <img
      data-programlogos={programLogos}
      data-presenttitle={presentTitle}
      class="max-[539px]:mt-[-0.7rem] max-[539px]:w-[4.8rem]"
      src={`https:${programLogos[0]}`}
      decoding="async"
      loading="lazy"
      width={itemDiameter * 0.67}
      height={itemDiameter * 0.67}
      alt={presentTitle[0]}
      onerror={`this.onerror=null; this.src='/src/images/program-logo-dummy.webp';`}
    />
    <img
      data-serviceLogoM
      class="absolute bottom-[-0.7rem] left-[27%] max-[539px]:bottom-[-0.2rem] max-[539px]:left-[28%] max-[539px]:h-[1.6rem] max-[539px]:w-[3.2rem]"
      src="/src/images/serviceLogoM/gtv-200x100.webp"
      decoding="async"
      loading="lazy"
      width="46"
      height="23"
      alt={serviceNames[0]}
    />
  </li>
  {
    logoLurls.map((logoLurl, index) => (
      <li data-transform data-logoLurl-js class="col-start-1 row-start-1">
        <img
          data-hover
          data-test
          src={`https:${logoLurl}`}
          decoding="async"
          loading="lazy"
          width={`${itemDiameter / 2}`}
          height={`${itemDiameter / 2}`}
          class="w-[7.2rem] rounded-full border-[0.6rem] border-main duration-200 ease-linear hover:scale-110 hover:border-0 min-[540px]:w-[10rem] min-[540px]:border-[0.8rem]"
          alt={serviceNames[index]}
          onerror={`this.onerror=null; this.src='/src/images/serviceLogoL/${logoLurl
            .replace('//www.nhk.or.jp/common/img/media/', '')
            .replace(/\.png$/, '.webp')}';`}
        />
      </li>
    ))
  }
</ul>

当初はCSSをすべてTailwind CSSで書く予定だったのですが、jitモードではクラスに変数を使えないことを途中で知ったので、その部分はScoped CSSで書くことにしました。

特にレスポンシブ対応が難しかったです。

以下にstyleタグの中をすべて掲載します。

src/components/ProgramChange.astro
<style lang="scss">
  $breakpoint: 540px; // ulDiameter + 4rem(左右のページのスペースの2rem)
  $logo-l-urls-length: 7; // logoLurls.length
  $ul-diameter: 50; // ulDiameter
  $item-diameter: 100; // itemDiameter

  @for $index from 1 through ($logo-l-urls-length + 3) {
    // 「3」は、liタグの最初からスタイルを適用しない要素
    [data-transform]:nth-child(#{$index}) {
      // $logo-l-urls-length: 7のとき、4から10まで代入
      transform: rotate(($index * calc(360 / $logo-l-urls-length) - 295deg))
        translate(30vw)
        rotate(-($index * calc(360 / $logo-l-urls-length) - 295deg));

      @media (min-width: $breakpoint) {
        /* stylelint-disable length-zero-no-unit */
        transform: rotate(($index * calc(360 / $logo-l-urls-length) - 295deg))
          translate((calc($ul-diameter / 2) - calc($item-diameter / 20) + 0rem))
          rotate(-($index * calc(360 / $logo-l-urls-length) - 295deg));
        /* stylelint-enable length-zero-no-unit */
      }
    }
  }

  [data-ul-diameter] {
    max-width: 39.8rem;

    @media (min-width: $breakpoint) {
      /* stylelint-disable-next-line length-zero-no-unit */
      max-width: $ul-diameter + 0rem;
    }
  }

  [data-circle-scale] {
    $scale-values: (
      $breakpoint - 1: 6,
      $breakpoint - 21: 5.9,
      $breakpoint - 41: 5.6,
      $breakpoint - 61: 5.45,
      $breakpoint - 81: 5.2,
      $breakpoint - 101: 5,
      $breakpoint - 121: 4.8,
      $breakpoint - 141: 4.6,
      $breakpoint - 161: 4.35,
      $breakpoint - 181: 4.1,
      $breakpoint - 201: 3.85
    );

    transform: scale(5.5);

    @each $breakpoint, $scale in $scale-values {
      @media (max-width: $breakpoint) {
        transform: scale($scale);
      }
    }
  }

  // 初めての画面描写での、番組の最初の要素に適用
  .js-logo-l-url-img-active-initial {
    border: none;
    transform: scale(1.1);
  }

  .js-logo-l-url-active-animation {
    animation-name: center-move-out;
    animation-duration: 0.8s; // ここの秒数を変える時、bodyタグのクリック無効化の時間も同じ時間へ変えること
    animation-timing-function: ease-in-out;
    animation-fill-mode: forwards;
  }

  @keyframes center-move-out {
    50% {
      transform: translate(0);
    }

    100% {
      pointer-events: none;
    }
  }

  .js-logo-l-url-img-active-animation {
    animation-name: active;
    animation-duration: 0.8s; // ここの秒数を変える時、bodyタグのクリック無効化の時間も同じ時間へ変えること
    animation-timing-function: ease-in-out;
    animation-fill-mode: forwards;
  }

  @keyframes active {
    0% {
      border: none;
      transform: scale(1.1);
    }

    100% {
      border: none;
      transform: scale(1.1);
    }
  }

  .pointer-events-none {
    pointer-events: none;
  }
</style>

状態管理ツール

子コンポーネントのProgramChangeで7つの内クリック(タップ)された1つのサービスによって、親のindex.astroに書かれているh3タグの中身の{presentTitle[0]}を変更する必要があります。

AstroはReactのようにonClick内にコールバック関数を書いて、子から親へ値を渡すことができません。

そこで、AstroのShare State Between Islandsで書かれているように、状態管理ツールでNano Storesを採用して対処することにしました。

7つのサービスにそれぞれ0から6までの数字を付与して、その数字を親へNano Storesを用いて渡します。

以下に状態管理の元となるコードを掲載します。

src/store/store.js
import { atom } from 'nanostores';

export const $programChangeIndex = atom(0);

以下に子コンポーネントのProgramChangeでstoreに関係するコードを記載します。(なお、...は省略を意味します。)

src/components/ProgramChange.astro
<script>
  import { $programChangeIndex } from '../store/store'; // storeから$programChangeIndexをインポート
  ...
  if (circleColor) {
  ...
    // 各li要素に対してクリックイベントを設定
    initialNode.forEach((element, index) => {
    ...
      element.addEventListener('click', () => {
        $programChangeIndex.set(index); // クリックしたインデックスをセット
        ...
      });
    });
  }
</script>

以下に親のindex.astroへ、子コンポーネントでクリックしたサービスがstoreへセットされたことを検知して、{presentTitle[0]}を変更するコードを記載します。

src/pages/index.astro
<script>
  import { $programChangeIndex } from '../store/store';
  ...
  $programChangeIndex.subscribe((index) => {
    const textslides = document.querySelectorAll('[data-text-slide]');
    textslides.forEach((textslide) => {
      textslide.classList.add('text-slide');
    });
    if (presenttitle) {
      setTimeout(() => {
        // ProgramChangeコンポーネントで、クリックされた現在の番組名を変更
        presenttitle.textContent = presentTitle[index];
        textslides.forEach((textslide) => {
          textslide.classList.remove('text-slide');
        });
      }, 800);
    }
  });
</script>

おわりに

Now On Air API (Ver.2)には、現在放送されている番組だけでなく1つ前の番組や1つ次の番組の情報も含めれていますので、それらも制作したいです。

ゆくゆくは、このWebページを放送史へと変えて10年ごとの情報を一つの画面で表示して、ヘッドレスCMSと連携することでCMS側で文言を更新できるようなアーキテクチャにしたいです。

これから、イベントでNHKハッカソンがあれば参加したいです。

もし、NHKの方がこの記事をご覧になられたら、コメントいただけますと幸いです。

Arsaga Developers Blog

Discussion