【React】APIを使って名言を表示するアプリを作ってみた

## はじめに
ReactとJavaScriptの勉強を一通りできたので簡単なアプリを制作しようと思いました。

ですのでこの記事では、Reactを使用してシンプルな名言アプリを作成する手順を紹介します。名言アプリはQuotable APIを使用してランダムな名言を取得し、ユーザーに表示するアプリケーションです。

間違いがあればご指摘をお願いします。

## 今回作るものの完全イメージ

![Videotogif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3535224/18491b99-28b5-16f2-9db8-2ecbd08118a6.gif)

新しい名言取得をクリックすると名言をランダムで取得ができ、テーマ切り替えをクリックすると文字の背景が変わります。

## 今回行うこと

- コンポーネントの作成
- useStateフックの使用
- useEffectフックの使用
- コンテキストの使用
- APIデータの取得
- 条件付きレンダリング
- イベントハンドリング
- CSSスタイリング
…など

Reactの基礎を一通り使おうと思います。
## 目次
- [使用環境](#使用環境)
- [ディレクトリ構造](#ディレクトリ構造)
- [プロジェクトのセットアップ](#プロジェクトのセットアップ)
- [コンポーネントの作成](#コンポーネントの作成)
- [テーマの管理](#テーマの管理)
- [ソースコード完全版](#ソースコード完全版)
- [おわりに](#おわりに)
- [参考サイト](#参考サイト)

## 使用環境
OS：Windows10
react：18.2.0
npm ：9.6.7
Node.js:18.17.1
api.quotable.io

## ディレクトリ構造
```
- node_modules
- public
- src
   |- App.css
   |- App.js
   └─ components
        |- Quote.css
        |- Quote.js
        |- ThemeContext.js
-package-lock.json
-package-json 
```
srcフォルダの中は使用するファイルのみ記述しています。

## Node.jsとnpmをインストールする

まずは公式ウェブサイト]:https://nodejs.org/ "にアクセスして推奨バージョンのNode.jsをダウンロードします。


## プロジェクトのセットアップ
```
npx create-react-app quote-app
cd quote-app
npm start
```
npm startでアプリケーションを起動します。
![React.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3535224/50e86622-b4f0-20ac-fa37-3f829a50d0e5.png)
この画面がでたら起動成功です！

## コンポーネントの作成
アプリケーション内で使用するコンポーネントを作成しましょう。名言の表示と取得を担当する Quote.js コンポーネントを作成します。

```jsx:components/Quote.js
import React, { useState, useEffect } from 'react';

// エラーメッセージを表示するコンポーネント
function ErrorComponent({ errorMessage }) {
  return <div className="error">{errorMessage}</div>;
}


function Quote() {
  // 名言の状態管理用のステート
  const [quotes, setQuotes] = useState([]);
  // ローディング状態のステート
  const [isLoading, setIsLoading] = useState(false);
  // エラーメッセージのステート
  const [error, setError] = useState(null);

// 名言を取得する非同期関数
const fetchQuotes = () => {
  // ローディング状態をtrueに設定
  setIsLoading(true);
  // エラーメッセージをクリア
  setError(null);
  // APIから名言を取得
  fetch('https://api.quotable.io/quotes?limit=500')
    .then(response => {
      if (!response.ok) {
        // レスポンスが成功でない場合はエラーをスロー
        throw new Error('Network response was not ok'); 
      }
      return response.json();
    })
    .then(data => {
      // 名言をステートにセット
      setQuotes(data.results);
      // ローディング状態をfalseに設定
      setIsLoading(false);
    })
    .catch(error => {
      // エラーメッセージをセット
      setError('名言の取得中にエラーが発生しました。');
      setIsLoading(false);
    });
};

  // コンポーネントがマウントされた際に名言を取得
  useEffect(() => {
    fetchQuotes();
  }, []);


  // 新しい名言を取得するボタンがクリックされたときの処理
  const handleFetchNewQuote = () => {
    const randomIndex = Math.floor(Math.random() * quotes.length);
    setQuotes(prevQuotes => {
      const newQuotes = [...prevQuotes];
      newQuotes[0] = quotes[randomIndex];
      return newQuotes;
    });
  };


  return (
    <div >
      <h2>名言一覧</h2>
      {error && <ErrorComponent errorMessage={error} />}
      {isLoading ? (
        <p>名言を取得中...</p>
      ) : (

        <div>
          <p>{quotes[0]?.content}</p>

          <button onClick={handleFetchNewQuote} disabled={isLoading}>
            {isLoading ? '取得中...' : '新しい名言を取得'}
          </button>
        </div>
      )}
    </div>
  );
}

export default Quote;

```
解説していきます。

## Quote コンポーネントの作成

メインのアプリケーションコンポーネントである Quote コンポーネントを作成します。

```jsx:
//const [state, stateを更新する関数] = useState(初期値);
  const [quotes, setQuotes] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
```
ここはuseStateを使って、変数を宣言しています。
state: ステート変数（現在の状態を保持する変数）です。初期値はuseStateの引数として渡された値（初期値）で初期化されます。

setState: ステートを更新するための関数です。この関数を呼び出すことにより、ステートを新しい値に更新できます

## fetchQuotes 関数の定義
```jsx
const fetchQuotes = () => {
  // ローディング状態をtrueに設定
  setIsLoading(true);
  // エラーメッセージをクリア
  setError(null);
  // APIから名言を取得
  fetch('https://api.quotable.io/quotes?limit=500')
    .then(response => {
      if (!response.ok) {
        // レスポンスが成功でない場合はエラーをスロー
        throw new Error('Network response was not ok'); 
      }
      return response.json();
    })
    .then(data => {
      // 名言をステートにセット
      setQuotes(data.results);
      // ローディング状態をfalseに設定
      setIsLoading(false);
    })
    .catch(error => {
      // エラーメッセージをセット
      setError('名言の取得中にエラーが発生しました。');
      setIsLoading(false);
    });
};
```
APIから名言を取得します。この関数内でステートを更新し、ローディング状態とエラーメッセージを適切に処理します。

`response` はHTTPリクエストの結果を保持するオブジェクトです。

`if (!response.ok)`  は、HTTP レスポンスの ok プロパティが false の場合を判定する条件式です。HTTP レスポンスの ok プロパティは、HTTP ステータスコードが成功を示すものであるかどうかを示すブール値です。

一般的に、HTTP ステータスコードが 200 から 299 の範囲にある場合、response.ok は true になります。これはリクエストが成功したことを示します。しかし、400 番台や 500 番台のステータスコードはエラーを示すものであり、response.ok は false になります。
      
したがって、!response.ok は「レスポンスが成功ではない場合」を意味します。具体的には、サーバーからのレスポンスが成功を示す HTTP ステータスコードではない場合、この条件が成り立ちます。

`setIsLoading(false)` は、Reactコンポーネント内でローディング状態を管理するためのコードです。このコードは以下の主な目的を持っています。

ローディング状態の初期化: コンポーネント内で非同期操作が始まる前に、setIsLoading(true) が呼び出され、ローディング状態が true に設定されます。これにより、ユーザーに対して「名言を取得中...」などのローディングメッセージを表示することができます。

ローディング状態の終了: 非同期操作が完了した後、成功時または失敗時に setIsLoading(false) が呼び出されます。これにより、ローディング状態が false に戻り、ローディングメッセージが非表示になります。

## useEffectとは？

```jsx
  useEffect(() => {
    fetchQuotes();
  }, []);
```

`useEffect(() => { fetchQuotes(); }, []);` は、Reactコンポーネント内で特定の効果（effect）を実行するためのコードです。このコードの主要な目的は、コンポーネントがマウントされたときに一度だけ `fetchQuotes` 関数を実行することです。

具体的な説明は以下の通りです：

- `useEffect` フック: Reactの`useEffect` フックは、コンポーネント内で副作用を実行するために使用されます。副作用は通常、データの取得、購読の設定、コンポーネントの更新などの操作を指します。

- マウント時の実行: コードの中で、この `useEffect` フックは第2引数に空の配列（`[]`）を持っています。これは、この `useEffect` フックが初回のレンダリング時に一度だけ実行されることを示しています。つまり、コンポーネントがマウント（画面に表示）された直後に `fetchQuotes` 関数が呼び出されます。

- `fetchQuotes` 関数の実行: `fetchQuotes` 関数は非同期関数で、APIから名言データを取得する役割を担っています。この関数はマウント時に呼び出され、名言データの取得が開始されます。

簡単に言えば、この `useEffect` フックはコンポーネントが最初に画面に表示されたときに名言データの取得をトリガーし、一度だけ実行されることを保証します。これにより、コンポーネントの初期化時に必要なデータを読み込むことができ、ユーザーに正確な情報を提供できるようになります。

## handleFetchNewQuote 関数の定義

```jsx
  const handleFetchNewQuote = () => {
    const randomIndex = Math.floor(Math.random() * quotes.length);
    setQuotes(prevQuotes => {
      const newQuotes = [...prevQuotes];
      newQuotes[0] = quotes[randomIndex];
      return newQuotes;
    });
  };
```
Reactアプリケーション内で新しい名言を取得し、表示するために `handleFetchNewQuote` 関数を使用します。

この関数の主な機能は次のとおりです：

`randomIndex` 変数は、`quotes` 配列内でランダムなインデックスを生成します。これにより、ランダムな名言が選択されます。

`setQuotes` 関数を使用して、新しい名言を表示するために `quotes` ステートを更新します。setQuotes の引数として、前回のステート（`prevQuotes`）を使用し、新しい名言が既存のリストの先頭に置かれるように設定します。

### ボタンクリックで新しい名言を表示
この `handleFetchNewQuote` 関数を、ボタンクリックなどのイベントに関連付けて使用することで、ユーザーが新しい名言を表示できるようになります。具体的な実装は、Reactコンポーネント内でボタンを作成し、そのボタンがクリックされたときに `handleFetchNewQuote` を呼び出すことです。

```jsx
<button onClick={handleFetchNewQuote}>新しい名言を表示</button>
```

## UIの定義

```jsx
return (
  <div>
    <h2>名言一覧</h2>
    {error && <ErrorComponent errorMessage={error} />}
    {isLoading ? (
      <p>名言を取得中...</p>
    ) : (
      <div>
        <p>{quotes[0]?.content}</p>
        <button onClick={handleFetchNewQuote} disabled={isLoading}>
          {isLoading ? '取得中...' : '新しい名言を取得'}
        </button>
      </div>
    )}
  </div>
);
```


1. `<h2>名言一覧</h2>`: この要素は、ページのタイトルとして "名言一覧" を表示します。

2. `{error && <ErrorComponent errorMessage={error} />}`: `error` ステートが存在する場合、`<ErrorComponent>` コンポーネントがエラーメッセージを表示します。エラーメッセージがない場合、この部分は表示されません。

3. `{isLoading ? ... : ...}`: `isLoading` ステートの値に応じて表示内容を切り替えます。`isLoading` が `true` の場合、名言データの取得中であることを示す「名言を取得中...」のメッセージが表示されます。`isLoading` が `false` の場合、名言のコンテンツと新しい名言を取得するボタンが表示されます。

4. `<p>{quotes[0]?.content}</p>`: `quotes` ステート内の最初の名言のコンテンツを表示します。`?.` 演算子は、`quotes` 配列が空でない場合にのみコンテンツを表示し、エラーを防ぎます。つまり、`quotes` 配列が空である場合でもエラーが発生しません。

5. `<button onClick={handleFetchNewQuote} disabled={isLoading}>...</button>`: "新しい名言を取得" ボタンがあり、ボタンがクリックされたときに `handleFetchNewQuote` 関数が呼び出されます。`isLoading` が `true` の場合、ボタンはクリックできないように `disabled` 属性が設定されます。ボタンのラベルは、`isLoading` の状態に応じて "取得中..." または "新しい名言を取得" と切り替わります。

## App.jsとApp.cssのファイルの中身を書き換える

```jsx:App.js
/*App.js*/

import React from 'react';
import Quote from './components/Quote';
import './App.css';


function App() {
  return (
      <div className="app">
        <Quote />
      </div>
  );
}

export default App;
```

```css:App.css
/* App.css */

body {
  font-family: 'Poppins', sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f7f7f7;
  color: #333333;
}

.app {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  background: linear-gradient(180deg, #00a8cc, #4d4d4d);
}

.quote {
  background-color: #ffffff;
  box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.1);
  border-radius: 15px;
  padding: 30px;
  text-align: center;
  max-width: 400px;
  margin: 0 auto;
}


button {
  background-color: #00a8cc;
  color: #ffffff;
  border: none;
  border-radius: 25px;
  padding: 10px 30px;
  cursor: pointer;
  font-weight: bold;
  transition: background-color 0.3s ease-in-out;
}

button:hover {
  background-color: #007a9e;
}

.error {
  color: #e74c3c;
  font-size: 18px;
  margin-top: 15px;
}

p {
  font-size: 20px;
  margin: 30px 0;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  font-size: 20px;
  margin: 10px 0;
}

```
そしたらこのような画面になります。
![React2 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3535224/e84d5774-00b4-bd92-d5b0-3270b1e41f3c.png)

これでもいいっちゃいいのですが、微妙に見ずらいのでもう少し改良してみましょう。

## テーマの管理

アプリケーション内でテーマを管理するために、ReactのcreateContext、useContext、そしてuseStateを活用して、シンプルなテーマ切り替え機能を実装します。

```jsx:components/ThemeContext.js
import { createContext, useContext, useState } from 'react';


//新しいコンテキストを作成
const ThemeContext = createContext();

//コンテキストを使用するためのフックを定義
export function useTheme() {
  return useContext(ThemeContext);
}

//テーマの設定や切り替えを管理するプロバイダーコンポーネント
export function ThemeProvider({ children }) {
  //テーマの状態を管理するステート
  const [theme, setTheme] = useState('light');

  //テーマを切り替える関数
  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  const value = {
    theme,
    toggleTheme,
  };

//子コンポーネントをラップしてコンテキストを提供
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

```

Reactアプリケーションでテーマを切り替えるために、コンテキスト（Context）を使用する方法を学びましょう。この記事では、Reactの`createContext`、`useContext`、そして`useState`を活用して、シンプルなテーマ切り替え機能を実装します。

## 1. 新しいコンテキストを作成する

最初に、テーマ情報を格納するための新しいコンテキストを作成します。

```jsx
const ThemeContext = createContext();
```

## 2. テーマの設定や切り替えを管理するプロバイダーコンポーネントを作成

テーマ情報や切り替え関数を管理するためのプロバイダーコンポーネントを作成します。このコンポーネントは、テーマの状態を保持し、テーマを切り替えるための関数を提供します。

```jsx
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  const value = {
    theme,
    toggleTheme,
  };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}
```

## 3. テーマコンテキストを使用するカスタムフックを定義

テーマ情報を使用するためのカスタムフック`useTheme`を定義します。

```jsx
export function useTheme() {
  return useContext(ThemeContext);
}
```

## 4. テーマの切り替えを実装

テーマを切り替えるための関数`toggleTheme`を実装しました。この関数は、現在のテーマが`light`なら`dark`に、`dark`なら`light`に切り替えます。
```jsx
const toggleTheme = () => {
   setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };
```
## 5.テーマ情報を提供し、テーマ切り替えを可能にするコンテキストを定義

```jsx
return (
  <ThemeContext.Provider value={value}>
    {children}
  </ThemeContext.Provider>
);
```


1. `const value = {...}`: `value`オブジェクトは、コンテキストに提供される値を定義します。この場合、`theme`と`toggleTheme`をプロパティとして持っています。これらのプロパティは、テーマの状態とテーマ切り替え関数を含んでいます。

2. `<ThemeContext.Provider value={value}>`: `ThemeContext.Provider`はReactのコンテキストプロバイダーコンポーネントです。このコンポーネントは、`value`プロパティを使用してコンテキストの値を設定します。つまり、`theme`と`toggleTheme`がこのコンテキスト内で利用可能になります。

3. `{children}`: プロバイダーコンポーネントの中にある`{children}`は、`ThemeProvider`でラップされた子コンポーネントを指します。このパターンを使用することで、アプリケーション内のすべての子コンポーネントでテーマコンテキストを利用できるようになります。子コンポーネントはテーマ情報や切り替え関数を`useTheme`フックを介して取得できます。

```css:components/Quote.css
/* Quote.css */

.light {
  background-color: #ffffff;
  color: #333333;
}

.dark {
  background-color: #222222 !important;
  color: #ffffff;
}

.theme-button {
  background-color: transparent;
  color: #007bff;
  border: none;
  font-size: 14px;
  cursor: pointer;
}

.theme-button:hover {
  color: #0056b3;
}


```
Quote.cssを作成して上記のコードの貼り付け

```jsx:App.js
import React from 'react';
import Quote from './components/Quote';
import { ThemeProvider } from './components/ThemeContext';//追加
import './App.css';

function App() {
  return (
    //テーマコンテキストをアプリ全体に適用
    <ThemeProvider>{/*追加*/}
      <div className="app">
        {/*名言を表示し、新しい名言を取得するコンポーネント*/}
        <Quote />
      </div>
    </ThemeProvider>//追加
  );
}

export default App;

```
```jsx:components/Quote.js
import { useTheme } from './ThemeContext';
import './Quote.css'; 
import React, { useState, useEffect } from 'react';

function ErrorComponent({ errorMessage }) {
  return <div className="error">{errorMessage}</div>;
}


function Quote() {
  const [quotes, setQuotes] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  //テーマコンテキストからテーマとテーマを切り替える関数を取得
  const { theme, toggleTheme } = useTheme();//追加

const fetchQuotes = () => {
  setIsLoading(true);
  setError(null);
  fetch('https://api.quotable.io/quotes?limit=500')
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not ok'); 
      }
      return response.json();
    })
    .then(data => {
      setQuotes(data.results);
      setIsLoading(false);
    })
    .catch(error => {
      setError('名言の取得中にエラーが発生しました。');
      setIsLoading(false);
    });
};

  useEffect(() => {
    fetchQuotes();
  }, []);


  const handleFetchNewQuote = () => {
    const randomIndex = Math.floor(Math.random() * quotes.length);
    setQuotes(prevQuotes => {
      const newQuotes = [...prevQuotes];
      newQuotes[0] = quotes[randomIndex];
      return newQuotes;
    });
  };


  return (
    <div className={`quote ${theme}`}>{/*追加*/}
      <h2>名言一覧</h2>
      <button className="theme-button" onClick={toggleTheme}>
        テーマ切り替え
      </button>{/*追加*/}
      {error && <ErrorComponent errorMessage={error} />}
      {isLoading ? (
        <p>名言を取得中...</p>
      ) : (

        <div>
          <p>{quotes[0]?.content}</p>
          <button onClick={handleFetchNewQuote} disabled={isLoading}>
            {isLoading ? '取得中...' : '新しい名言を取得'}
          </button>
        </div>
      )}
    </div>
  );
}

export default Quote;
```
追加部分を書いたら完成です！
下記に使用したソースコードをまとめました。

## ソースコード完全版

```jsx:App.js
/*App.js*/

import React from 'react';
import Quote from './components/Quote';
import { ThemeProvider } from './components/ThemeContext';
import './App.css';

function App() {
  return (
    <ThemeProvider>
      <div className="app">
        <Quote />
      </div>
    </ThemeProvider>
  );
}

export default App;

```

```jsx:src/components/Quote.js
// Quote.js

import React, { useState, useEffect } from 'react';
import { useTheme } from './ThemeContext';
import './Quote.css'; 

function ErrorComponent({ errorMessage }) {
  return <div className="error">{errorMessage}</div>;
}

function Quote() {
  const [quotes, setQuotes] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const { theme, toggleTheme } = useTheme();



const fetchQuotes = () => {
  setIsLoading(true);
  setError(null);
  fetch('https://api.quotable.io/quotes?limit=500')
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not ok'); 
      }
      return response.json();
    })
    .then(data => {
      setQuotes(data.results);
      setIsLoading(false);
    })
    .catch(error => {
      setError('名言の取得中にエラーが発生しました。');
      setIsLoading(false);
    });
};


  useEffect(() => {
    fetchQuotes();
  }, []);



  const handleFetchNewQuote = () => {
    const randomIndex = Math.floor(Math.random() * quotes.length);
    setQuotes(prevQuotes => {
      const newQuotes = [...prevQuotes];
      newQuotes[0] = quotes[randomIndex];
      return newQuotes;
    });
  };


  return (
    <div className={`quote ${theme}`}>
      <h2>名言一覧</h2>
      <button className="theme-button" onClick={toggleTheme}>
        テーマ切り替え
      </button>
      {error && <ErrorComponent errorMessage={error} />}
      {isLoading ? (
        <p>名言を取得中...</p>
      ) : (

        <div>
          <p>{quotes[0]?.content}</p>

          <button onClick={handleFetchNewQuote} disabled={isLoading}>
            {isLoading ? '取得中...' : '新しい名言を取得'}
          </button>
        </div>
      )}
    </div>
  );
}

export default Quote;

```

```jsx:src/components/ThemeContext.js
/*ThemeContext.js*/

import { createContext, useContext, useState } from 'react';


const ThemeContext = createContext();

export function useTheme() {
  return useContext(ThemeContext);
}

export function ThemeProvider({ children }) {

  const [theme, setTheme] = useState('light');


  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  const value = {
    theme,
    toggleTheme,
  };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

```
```css:App.css
/* App.css */

body {
  font-family: 'Poppins', sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f7f7f7;
  color: #333333;
}

.app {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  background: linear-gradient(180deg, #00a8cc, #4d4d4d);
}

.quote {
  background-color: #ffffff;
  box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.1);
  border-radius: 15px;
  padding: 30px;
  text-align: center;
  max-width: 400px;
  margin: 0 auto;
}


button {
  background-color: #00a8cc;
  color: #ffffff;
  border: none;
  border-radius: 25px;
  padding: 10px 30px;
  cursor: pointer;
  font-weight: bold;
  transition: background-color 0.3s ease-in-out;
}

button:hover {
  background-color: #007a9e;
}

.error {
  color: #e74c3c;
  font-size: 18px;
  margin-top: 15px;
}

p {
  font-size: 20px;
  margin: 30px 0;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  font-size: 20px;
  margin: 10px 0;
}

```
```css:components/Quote.css
/* Quote.css */

.light {
  background-color: #ffffff;
  color: #333333;
}

.dark {
  background-color: #222222 !important;
  color: #ffffff;
}

.theme-button {
  background-color: transparent;
  color: #007bff;
  border: none;
  font-size: 14px;
  cursor: pointer;
}

.theme-button:hover {
  color: #0056b3;
}


```
## おわりに

次はReact×TypeScriptで何か作ってみたいと思います。

## 参考サイト
React公式ドキュメント
[https://react.dev/learn
](https://react.dev/learn
 "公式ドキュメント")

[https://kurarich.com/](https://kurarich.com/ "くらりっち")