フロントエンド開発

天気情報を取得して表示アプリ☀️

こんにちは👋
前回作成した記事で紹介した”「天気情報を取得して表示」アプリを作ってみる”
についてざっくりとした作成方法について話していきたいと思います👏
長くなるのでご了承ください🙇‍♂️

ReactOpenWeather APIを使って、簡単な天気予報アプリを一緒に作っていきましょう!

🛠️ 必要なもの

まずは、天気アプリを作るために必要なものをチェック!✅

  1. Node.js 📦 – JavaScriptの実行環境(ReactアプリはNode.jsを使って開発します)
  2. テキストエディタ 📝 – 自分が使ってるAIエディター『Cursor』がおすすめです。
  3. OpenWeather API キー 🔑 – 無料で取得できる天気情報のAPIキー
  4. インターネット接続 🌐 – パッケージのインストールやAPIの利用に必要です

準備ができたら、早速開発環境をセットアップしていきましょう!💪

💻 開発環境のセットアップ

1. Node.jsのインストール 🚀

React開発には、まずNode.jsが必要です。これはJavaScriptをパソコン上で動かすための環境です。

  1. Node.jsの公式サイト にアクセス
  2. LTS(Long Term Support)バージョンをダウンロード
  3. ダウンロードしたインストーラーを実行して指示に従う

インストールが完了したら、ターミナル(コマンドプロンプト)で以下のコマンドを実行して、正しくインストールされたか確認しましょう:

node -v
npm -v

バージョン番号が表示されれば成功です!✨ これでNode.jsの準備は完了!

2. エディタのインストール

この部分は多くの方が既にインストールされておりますので割愛させて頂きます。

3. OpenWeather APIキーの取得🔑

  1. OpenWeatherMap にアクセスしてアカウント登録
  2. 登録後、APIキーを取得(無料プランで十分です)
  3. APIキーはあとで使うので、安全な場所にメモしておきましょう

🚀 Reactプロジェクトの作成

環境が整ったら、Reactアプリのプロジェクトを作成しましょう!最近のフロントエンド開発では、Vite(ヴィート) というツールが人気です。Viteは従来のcreate-react-appよりも高速で、開発体験が格段に向上しています!😉

Viteとは? ⚡

Viteは「超高速な次世代フロントエンドツール」です。従来のwebpackベースのツールと比べて、開発環境の起動が爆速で、ホットリロード(コードを変更した時の反映)も一瞬です。特に大規模なプロジェクトになるほど、その差は顕著になります!

もっと詳しく知りたい!って方は詳しく解説されてる記事を拝見されたほうがいいです!

おすすめ記事🔗

Qiita

Vite公式


Viteでプロジェクトを作成しよう 📂

ターミナル(コマンドプロンプト)を開き、以下のコマンドを実行します:

npm create vite@latest WeatherMap-animation --template react-ts

コマンドを実行すると、いくつか質問されます:

  1. 「WeatherMap-animation」という名前のプロジェクトを作成📝
  2. フレームワークを選択(Reactを選択)⚛️
  3. 言語を選択(JavaScriptTypeScript、初心者ならJavaScriptがおすすめです)💻
    自分はTypeScript + SWCを選択しました。🟦⚡

https://gyazo.com/2224a07dcb71e14826605ea21776ba85

プロジェクトの設定と依存関係のインストール⚙️📦

コマンド実行後は、以下のような指示が表示されるので順番に実行していきましょう:

cd WeatherMap-animation
npm install

これらのコマンドは:

  • プロジェクトのフォルダに移動📂
  • 必要なパッケージをインストール📦

npm installは実行後数分かかるので待ちましょう☕
npm installを終えたら

APIリクエスト用ライブラリのインストール⚙️📦

npm install axios

Axiosは、OpenWeatherMap APIへのHTTPリクエストを簡潔に記述できるライブラリです。REST APIとの連携が超カンタンになりますよ!😊

アニメーションライブラリの追加✨📦🎞️

また、アプリに素敵なアニメーション効果を追加したい場合は、Framer Motionもインストールするといいでしょう:

npm install framer-motion

Framer Motionは、Reactアプリにスムーズで美しいアニメーションを簡単に追加できる強力なライブラリです。ユーザー体験が一気に向上しますよ!✨

Axiosのインストールが完了したら、開発サーバーを起動できます🚀

npm run dev

これでViteの開発サーバーが起動し、ブラウザでプロジェクトを確認できるようになります🌐👀
OpenWeatherMap APIを使った天気アニメーションの実装を始める準備が整いました🌤️🎬✨

プロジェクトの基本的な構造を作成する:

まず、APIキーを安全に管理するために、プロジェクトのルートディレクトリに.envファイルを作成します🔒🗂️
.envファイルは、APIキーやシークレットなどの「環境変数」を安全に管理するためのファイルです。📝🔑

VITE_OPENWEATHER_API_KEY=あなたのAPIキーをここに入力

 Viteでの環境変数の使い方

  • プロジェクト内のJavaScriptやTypeScriptファイルからは、次のようにして値を参照します💻
const apiKey = import.meta.env.VITE_OPENWEATHER_API_KEY;

Viteでは環境変数にはVITE_プレフィックスが必要です。また、.gitignoreファイルに.envが含まれていることを確認してください(APIキーを誤ってGitにコミットしないため)。

このままだと.gitignoreファイルに.envが記載されてないので追加しましょう。⚠️
.gitignoreファイルの中身は恐らく以下のようになってると思います。📄

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

以下の手順で.gitignoreファイルを更新してください🛠️:

  1. プロジェクトのルートディレクトリにある.gitignoreファイルを開きます📂
  2. ファイルの末尾に以下の行を追加します✍️
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

これにより、APIキーやパスワードなどの漏洩リスクが大幅に減少します🔑🚫

📱 天気アプリのコンポーネント作成

さて、ここからが本格的な開発の始まりです!👨‍💻 まずは、プロジェクトの構造を整理しましょう。

Reactプロジェクトでは、コンポーネントを分けて実装することをおすすめします✨

アニメーションのコンポーネントを独立させておくことで、他の場所でも再利用しやすくなります♻️。また、データ取得のロジックをUIから切り離すことで、将来的に別のAPIへ切り替える場合も柔軟に対応可能です🔄。

ただし、これはあくまで一般的な推奨です⚠️。プロジェクトの規模や目的、チームの方針によっては、必ずしも細かく分割する必要はありません。たとえば、小規模で今後の拡張予定がない場合は、1つのファイルにまとめてシンプルに管理するのも選択肢のひとつです📁。

コンポーネントを分けることで、コードが見やすくなり👀、使い回しやすく♻️、管理もしやすくなります🛠️。その結果、開発効率や品質の向上につながります🚀。

今回はシンプルに1つのファイルにまとめます🗂️
実際の自分で作ったのはコンポーネントを分けて実装してますがシンプルの方が分かりやすいと思いますのでそちらで解説していきます💡

コンポーネントファイルを作成します

  • src/componentsディレクトリがない場合は作成します 📁
  • その中にWeatherAnimation.tsxファイルを作成します🌦️

メインコンポーネント(WeatherAnimation)1️⃣

import { useState, useEffect } from 'react';
import axios from 'axios';
import { motion } from 'framer-motion';

// 型定義を追加
interface WeatherData {
  name: string;
  main: {
    temp: number;
    humidity: number;
    feels_like?: number;
  };
  weather: Array<{
    id: number;
    main: string;
    description: string;
    icon: string;
  }>;
  wind: {
    speed: number;
    deg?: number;
  };
}

// 時間帯の型定義
type TimeOfDay = 'dawn' | 'day' | 'dusk' | 'night';


const WeatherAnimation = () => {
  const [weatherData, setWeatherData] = useState<WeatherData | null>(null);
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<string | null>(null);
  const [city, setCity] = useState<string>('Tokyo');
  const [searchInput, setSearchInput] = useState<string>('');
  const [timeOfDay, setTimeOfDay] = useState<TimeOfDay>('day');

↑ここでは、ReactuseState フックを使って、アプリの状態を管理しています。特に注目すべきは timeOfDay 状態で、これにより時間帯に応じた視覚効果を実現しています。🕰️

時間帯の設定(useEffect)2️⃣

  // 時間帯を設定
  useEffect(() => {
    const updateTimeOfDay = () => {
      const hour = new Date().getHours();
      if (hour >= 5 && hour < 8) {
        setTimeOfDay('dawn'); // 夜明け
      } else if (hour >= 8 && hour < 16) {
        setTimeOfDay('day'); // 昼間
      } else if (hour >= 16 && hour < 19) {
        setTimeOfDay('dusk'); // 夕暮れ
      } else {
        setTimeOfDay('night'); // 夜
      }
    };
    
    updateTimeOfDay();
    
    // 1分ごとに時間帯を更新
    const interval = setInterval(updateTimeOfDay, 60000);
    return () => clearInterval(interval);
  }, []);

↑ここでは useEffect フックを使って、現在の時刻から適切な時間帯(dawn、day、dusk、night)を設定しています。1分ごとに更新することで、リアルタイムの変化を実現しています。⏱️

天気データの取得(fetchWeatherData)3️⃣

  useEffect(() => {
    fetchWeatherData();
  }, [city]);

  const fetchWeatherData = async (): Promise<void> => {
    setLoading(true);
    try {
      const apiKey = import.meta.env.VITE_OPENWEATHER_API_KEY;
      const response = await axios.get(
        `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric&lang=ja`
      );
      
      setWeatherData(response.data);
      console.log('取得した天気データ:', response.data);
      setError(null);
    } catch (err) {
      setError('天気データの取得に失敗しました。都市名を確認してください。');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };

↑OpenWeatherMap API を使用して天気データを取得しています。☀️都市名をパラメータとして送信し、結果を weatherData 状態に保存します🌐

背景スタイルの設定(getBackgroundStyle)4️⃣


  const handleSearch = (e: React.FormEvent<HTMLFormElement>): void => {
    e.preventDefault();
    if (searchInput.trim()) {
      setCity(searchInput);
      setSearchInput('');
    }
  };

  // 背景色を時間帯に応じて設定
  const getBackgroundStyle = (): React.CSSProperties => {
    if (!weatherData) return {};
    
    const weatherCondition = weatherData.weather[0].main.toLowerCase();
    
    let bgStyle: React.CSSProperties = {};
    
    // 天気状態による背景の基本設定
    if (weatherCondition === 'clear') {
      if (timeOfDay === 'day') {
        bgStyle = {
          background: 'linear-gradient(to bottom, #4a90e2 0%, #87ceeb 100%)'
        };
      } else if (timeOfDay === 'dawn') {
        bgStyle = {
          background: 'linear-gradient(to bottom, #ff9e7a 0%, #ffcc7a 100%)'
        };
      } else if (timeOfDay === 'dusk') {
        bgStyle = {
          background: 'linear-gradient(to bottom, #ff7e50 0%, #4a90e2 100%)'
        };
      } else { // night
        bgStyle = {
          background: 'linear-gradient(to bottom, #0c1445 0%, #274380 100%)'
        };
      }
    } else if (weatherCondition === 'clouds') {
      if (timeOfDay === 'day') {
        bgStyle = {
          background: 'linear-gradient(to bottom, #9fc5e8 0%, #d0e0e3 100%)'
        };
      } else if (timeOfDay === 'dawn' || timeOfDay === 'dusk') {
        bgStyle = {
          background: 'linear-gradient(to bottom, #a57e7e 0%, #bda0a0 100%)'
        };
      } else { // night
        bgStyle = {
          background: 'linear-gradient(to bottom, #212b49 0%, #394867 100%)'
        };
      }
    } else if (weatherCondition === 'rain' || weatherCondition === 'drizzle') {
      if (timeOfDay === 'day') {
        bgStyle = {
          background: 'linear-gradient(to bottom, #6e7b8c 0%, #a5b1c2 100%)'
        };
      } else { // night, dawn, dusk
        bgStyle = {
          background: 'linear-gradient(to bottom, #141e30 0%, #243b55 100%)'
        };
      }
    } else if (weatherCondition === 'snow') {
      bgStyle = {
        background: 'linear-gradient(to bottom, #c9d6df 0%, #f7f9f9 100%)'
      };
    } else {
      // デフォルトの背景
      if (timeOfDay === 'day') {
        bgStyle = {
          background: 'linear-gradient(to bottom, #87ceeb 0%, #e0f6ff 100%)'
        };
      } else if (timeOfDay === 'dawn') {
        bgStyle = {
          background: 'linear-gradient(to bottom, #ff9a8b 0%, #ffb2af 100%)'
        };
      } else if (timeOfDay === 'dusk') {
        bgStyle = {
          background: 'linear-gradient(to bottom, #f46b45 0%, #eea849 100%)'
        };
      } else { // night
        bgStyle = {
          background: 'linear-gradient(to bottom, #1c1c3d 0%, #2c3e50 100%)'
        };
      }
    }
    
    return bgStyle;
  };

↑ここが魔法の一つ!天気の状態と時間帯に基づいて、背景の色をダイナミックに変更します。晴れた日の昼間は明るい青空、夜は星空のような濃い青、雨の日は灰色がかったトーンになります。🎨

💫 アニメーションコンポーネント

  // 天気状態と時間に基づいてアニメーションを選択
  const renderWeatherAnimation = () => {
    if (!weatherData) return null;
    
    const weatherMain = weatherData.weather[0].main;
    console.log('現在の天気:', weatherMain);
    
    switch(weatherMain.toLowerCase()) {
      case 'rain':
      case 'drizzle':
      case 'thunderstorm':
        return <RainAnimation timeOfDay={timeOfDay} />;
      case 'clear':
        return <SunnyAnimation timeOfDay={timeOfDay} />;
      case 'clouds':
        return <CloudyAnimation timeOfDay={timeOfDay} />;
      case 'snow':
        return <SnowAnimation timeOfDay={timeOfDay} />;
      case 'mist':
      case 'fog':
      case 'haze':
        return <FogAnimation timeOfDay={timeOfDay} />;
      default:
        return <DefaultAnimation weatherMain={weatherMain} timeOfDay={timeOfDay} />;
    }
  };

🔄 天気状態によるコンポーネント切り替え

この関数が天気の状態に応じて適切なアニメーションコンポーネントを返します。雨、晴れ、曇り、雪、霧など、様々な天気に対応できるようになっています。👌


  return (
    <div className="weather-container">
      <form onSubmit={handleSearch} className="search-form">
        <input
          type="text"
          value={searchInput}
          onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchInput(e.target.value)}
          placeholder="都市名を入力(例: Nagoya, Osaka, Sapporo)"
          className="search-input"
        />
        <button type="submit" className="search-button">検索</button>
      </form>

      {loading && <div className="loading">Loading...</div>}
      {error && <div className="error">{error}</div>}
      
      {weatherData && !loading && (
        <>
          <div className="weather-card">
            <div className="location">
              <i className="location-icon">📍</i>
              <span>{weatherData.name}</span>
            </div>
            <div className="temp-display">
              <span className="temperature">{Math.round(weatherData.main.temp)}°C</span>
            </div>
            <div className="weather-info">
              <span>{weatherData.weather[0].description}</span>
            </div>
            <div className="additional-info">
              <div className="info-item">
                <span className="info-label">湿度</span>
                <span className="info-value">{weatherData.main.humidity}%</span>
              </div>
              <div className="info-item">
                <span className="info-label">風速</span>
                <span className="info-value">{Math.round(weatherData.wind.speed * 10) / 10} m/s</span>
              </div>
            </div>
          </div>
          
          <div 
            className="animation-container"
            style={getBackgroundStyle()}
          >
            {renderWeatherAnimation()}
          </div>
        </>
      )}
    </div>
  );
};

// 雨のアニメーションコンポーネント(時間帯対応)
const RainAnimation = ({ timeOfDay }: { timeOfDay: TimeOfDay }) => {
  const isDark = timeOfDay === 'night' || timeOfDay === 'dusk';
  
  return (
    <div className="rain-container">
      {[...Array(40)].map((_, i) => (
        <motion.div
          key={i}
          className={`raindrop ${isDark ? 'raindrop-dark' : ''}`}
          initial={{ y: -10, x: Math.random() * 500 }}
          animate={{ y: 500 }}
          transition={{
            duration: 0.7 + Math.random() * 0.6,
            repeat: Infinity,
            delay: Math.random() * 0.5,
            ease: "linear"
          }}
          style={{
            height: 15 + Math.random() * 15,
            left: `${Math.random() * 100}%`,
            opacity: 0.6 + Math.random() * 0.4
          }}
        />
      ))}
    </div>
  );
};

🌧️ 雨のアニメーション

Framer Motion の motion.div を使って雨粒を表現しています。各雨粒はランダムな位置からスタートし、ランダムな速度で落下します。夜間は暗い色の雨粒になるよう調整されています。💧

// 晴れのアニメーションコンポーネント(時間帯対応)
const SunnyAnimation = ({ timeOfDay }: { timeOfDay: TimeOfDay }) => {
  if (timeOfDay === 'night') {
    // 夜間は月と星を表示
    return (
      <>
        <div className="night-stars">
          {[...Array(30)].map((_, i) => (
            <motion.div
              key={i}
              className="star"
              animate={{
                opacity: [0.3, 1, 0.3],
                scale: [1, 1.2, 1]
              }}
              transition={{
                duration: 2 + Math.random() * 3,
                repeat: Infinity,
                delay: Math.random() * 2,
                ease: "easeInOut"
              }}
              style={{
                left: `${Math.random() * 100}%`,
                top: `${Math.random() * 80}%`,
              }}
            />
          ))}
        </div>
        <motion.div
          className="moon"
          animate={{
            scale: [1, 1.05, 1],
            opacity: [0.9, 1, 0.9]
          }}
          transition={{
            duration: 10,
            repeat: Infinity,
            ease: "easeInOut"
          }}
        />
      </>
    );
  }
  
  // 日中、夜明け、夕暮れは太陽を表示
  const sunColor = timeOfDay === 'dawn' || timeOfDay === 'dusk' ? '#ff7e50' : '#ffd700';
  const glowColor = timeOfDay === 'dawn' || timeOfDay === 'dusk' ? '#ff5500' : '#ff9500';
  
  return (
    <>
      <motion.div
        className="sun"
        animate={{
          scale: [1, 1.1, 1],
          rotate: 360,
        }}
        transition={{
          duration: 10,
          repeat: Infinity,
          ease: "linear"
        }}
        style={{
          backgroundColor: sunColor,
          boxShadow: `0 0 30px ${glowColor}`
        }}
      />
      <div className="sun-rays" style={{ background: `radial-gradient(circle, rgba(255, 215, 0, 0.4) 0%, rgba(255, 215, 0, 0) 70%)` }} />
    </>
  );
};

☀️ 晴れのアニメーション

昼間は太陽、夜は月と星を表示するスマートな設計です。時間帯によって太陽の色も変わり、夕暮れ時には赤みがかった太陽になります。✨

// 曇りのアニメーションコンポーネント(時間帯対応)
const CloudyAnimation = ({ timeOfDay }: { timeOfDay: TimeOfDay }) => {
  const isDark = timeOfDay === 'night' || timeOfDay === 'dusk';
  const cloudColor = isDark ? '#3a4a63' : '#f0f0f0';
  const cloudShadow = isDark ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.1)';
  
  return (
    <div className="cloudy-container">
      {[...Array(5)].map((_, i) => (
        <motion.div
          key={i}
          className="cloud"
          initial={{ x: -100, opacity: 0 }}
          animate={{ 
            x: [null, 600], 
            opacity: [null, 1, 1, 0] 
          }}
          transition={{
            duration: 20,
            repeat: Infinity,
            delay: i * 3,
            ease: "linear"
          }}
          style={{
            top: `${20 + i * 15}%`,
            backgroundColor: cloudColor,
            boxShadow: `0 0 10px ${cloudShadow}`
          }}
        />
      ))}
      
      {/* 夜間の場合は月を追加 */}
      {timeOfDay === 'night' && (
        <motion.div
          className="moon-behind-clouds"
          animate={{
            opacity: [0.5, 0.7, 0.5]
          }}
          transition={{
            duration: 4,
            repeat: Infinity,
            ease: "easeInOut"
          }}
        />
      )}
    </div>
  );
};

// 雪のアニメーションコンポーネント(時間帯対応)
const SnowAnimation = ({ timeOfDay }: { timeOfDay: TimeOfDay }) => {
  const isDark = timeOfDay === 'night' || timeOfDay === 'dusk';
  
  return (
    <div className="snow-container">
      {[...Array(40)].map((_, i) => (
        <motion.div
          key={i}
          className={`snowflake ${isDark ? 'snowflake-dark' : ''}`}
          initial={{ 
            y: -10, 
            x: Math.random() * 500,
            opacity: Math.random() * 0.3 + 0.7
          }}
          animate={{ 
            y: 500,
            x: [null, (Math.random() - 0.5) * 200 + Math.random() * 500]
          }}
          transition={{
            duration: 5 + Math.random() * 5,
            repeat: Infinity,
            delay: Math.random() * 3,
            ease: "linear"
          }}
          style={{
            width: 3 + Math.random() * 5,
            height: 3 + Math.random() * 5,
          }}
        />
      ))}
    </div>
  );
};

// 霧のアニメーションコンポーネント(時間帯対応)
const FogAnimation = ({ timeOfDay }: { timeOfDay: TimeOfDay }) => {
  const isDark = timeOfDay === 'night' || timeOfDay === 'dusk';
  const fogColor = isDark ? 'rgba(100, 100, 120, 0.7)' : 'rgba(200, 200, 200, 0.7)';
  
  return (
    <div className="fog-container">
      {[...Array(5)].map((_, i) => (
        <motion.div
          key={i}
          className="fog-layer"
          initial={{ opacity: 0.3 }}
          animate={{ 
            opacity: [0.3, 0.7, 0.3],
            x: [0, 20, 0, -20, 0]
          }}
          transition={{
            duration: 10,
            repeat: Infinity,
            delay: i * 2,
            ease: "easeInOut"
          }}
          style={{
            top: `${10 + i * 15}%`,
            backgroundColor: fogColor,
          }}
        />
      ))}
    </div>
  );
};

🎨 カスタマイズのポイント

新しい天気アニメーションの追加 🌪️

例えば、雷や竜巻のようなアニメーションを追加したい場合は、新しいアニメーションコンポーネントを作成し、renderWeatherAnimation 関数に条件を追加するだけです。



// デフォルトアニメーション(時間帯対応)
const DefaultAnimation = ({ weatherMain, timeOfDay }: { weatherMain: string, timeOfDay: TimeOfDay }) => {
  const isDark = timeOfDay === 'night' || timeOfDay === 'dusk';
  
  return (
    <div className="default-animation">
      <motion.div
        className={`weather-icon ${isDark ? 'weather-icon-dark' : ''}`}
        animate={{
          scale: [1, 1.1, 1],
          opacity: [0.7, 1, 0.7]
        }}
        transition={{
          duration: 2,
          repeat: Infinity,
          ease: "easeInOut"
        }}
      >
        {weatherMain}
      </motion.div>
    </div>
  );
};

export default WeatherAnimation;

Framer Motion の効果的な使用 🎬

motion.div コンポーネントと animate プロパティを使ったスムーズなアニメーションが、アプリに命を吹き込んでいます。特にタイミングやイージングを調整することで、自然な動きを実現しています。

App.cssまたはindex.cssに以下のコードを追加するスタイルシートです。

/* WeatherAnimation コンポーネント用のスタイル */

/* 天気カード */
.weather-container {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

.weather-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 15px;
  margin-bottom: 20px;
  background: rgba(255, 255, 255, 0.8);
  border-radius: 15px;
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
  backdrop-filter: blur(10px);
}

.location {
  display: flex;
  align-items: center;
  margin-bottom: 10px;
  font-size: 20px;
  color: #333;
}

.location-icon {
  margin-right: 8px;
  font-size: 18px;
}

.temp-display {
  margin: 10px 0;
}

.temperature {
  font-size: 48px;
  font-weight: 700;
  color: #222;
}

.weather-info {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 10px 0;
  font-size: 18px;
  color: #555;
  text-transform: capitalize;
}

.weather-description {
  font-size: 16px;
  color: #666;
  margin-top: 4px;
}

/* 追加情報のスタイル */
.additional-info {
  display: flex;
  justify-content: center;
  gap: 20px;
  margin-top: 15px;
  width: 100%;
}

.info-item {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.info-label {
  font-size: 14px;
  color: #666;
}

.info-value {
  font-size: 16px;
  font-weight: 600;
  color: #333;
}

/* アニメーションコンテナ */
.animation-container {
  position: relative;
  height: 300px;
  margin-top: 20px;
  overflow: hidden;
  border-radius: 15px;
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
  transition: background 0.5s ease;
}

/* 時間帯に応じた要素スタイル */
/* 太陽 */
.sun {
  position: absolute;
  top: 50px;
  left: 50%;
  transform: translateX(-50%);
  width: 80px;
  height: 80px;
  background-color: #ffd700;
  border-radius: 50%;
  box-shadow: 0 0 30px #ff9500;
  z-index: 10;
}

.sun-rays {
  position: absolute;
  top: 40px;
  left: 50%;
  transform: translateX(-50%);
  width: 120px;
  height: 120px;
  z-index: 9;
}

/* 月 */
.moon {
  position: absolute;
  top: 50px;
  right: 100px;
  width: 60px;
  height: 60px;
  background-color: #f0f0f0;
  border-radius: 50%;
  box-shadow: 0 0 20px rgba(255, 255, 255, 0.6);
  z-index: 10;
}

.moon-behind-clouds {
  position: absolute;
  top: 40px;
  right: 100px;
  width: 70px;
  height: 70px;
  background-color: #f5f5f5;
  border-radius: 50%;
  box-shadow: 0 0 30px rgba(255, 255, 255, 0.3);
  z-index: 5;
}

/* 星 */
.night-stars {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

.star {
  position: absolute;
  width: 2px;
  height: 2px;
  background-color: white;
  border-radius: 50%;
}

/* 雨 */
.rain-container {
  position: relative;
  width: 100%;
  height: 100%;
}

.raindrop {
  position: absolute;
  width: 2px;
  background-color: #0066cc;
  border-radius: 0 0 5px 5px;
  opacity: 0.9;
  box-shadow: 0 0 2px #0066cc;
}

.raindrop-dark {
  background-color: #a0c8ff;
  box-shadow: 0 0 2px #a0c8ff;
}

/* 雪 */
.snow-container {
  position: relative;
  width: 100%;
  height: 100%;
}

.snowflake {
  position: absolute;
  background-color: white;
  border-radius: 50%;
  box-shadow: 0 0 5px rgba(255, 255, 255, 0.8);
}

.snowflake-dark {
  box-shadow: 0 0 8px rgba(255, 255, 255, 1);
}

/* 曇り */
.cloudy-container {
  position: relative;
  width: 100%;
  height: 100%;
}

.cloud {
  position: absolute;
  width: 100px;
  height: 50px;
  background-color: #f0f0f0;
  border-radius: 25px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.cloud:before {
  content: '';
  position: absolute;
  top: -15px;
  left: 15px;
  width: 40px;
  height: 40px;
  background-color: inherit;
  border-radius: 50%;
}

.cloud:after {
  content: '';
  position: absolute;
  top: -30px;
  left: 45px;
  width: 60px;
  height: 60px;
  background-color: inherit;
  border-radius: 50%;
}

/* 霧 */
.fog-container {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.fog-layer {
  position: absolute;
  width: 150%;
  height: 40px;
  border-radius: 50px;
  filter: blur(15px);
  left: -25%;
}

/* デフォルトアニメーション */
.default-animation {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
}

.weather-icon {
  font-size: 32px;
  color: #444;
  background-color: #f0f0f0;
  padding: 20px;
  border-radius: 50%;
  box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
}

.weather-icon-dark {
  background-color: #3a4a63;
  color: #f0f0f0;
  box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
}

/* 検索フォーム */
.search-form {
  display: flex;
  margin-bottom: 20px;
}

.search-input {
  flex: 1;
  padding: 12px;
  font-size: 16px;
  border: 1px solid #ddd;
  border-radius: 25px 0 0 25px;
  outline: none;
  transition: border-color 0.3s;
}

.search-input:focus {
  border-color: #0077cc;
}

.search-button {
  padding: 12px 20px;
  background-color: #0077cc;
  color: white;
  border: none;
  border-radius: 0 25px 25px 0;
  cursor: pointer;
  font-weight: bold;
  transition: background-color 0.3s;
}

.search-button:hover {
  background-color: #005fa3;
}

/* ローディングと通知 */
.loading, .error {
  margin: 20px 0;
  padding: 15px;
  border-radius: 10px;
  text-align: center;
}

.loading {
  background-color: #f0f0f0;
  color: #555;
}

.error {
  background-color: #ffe0e0;
  color: #d00;
}

/* レスポンシブデザイン */
@media (max-width: 600px) {
  .weather-container {
    padding: 10px;
  }
  
  .temperature {
    font-size: 36px;
  }
  
  .animation-container {
    height: 250px;
  }
}

最後に、App.tsxの更新も必要です

import WeatherAnimation from './components/WeatherAnimation';
import './App.css';

function App() {
  return (
    <div className="App">
      <h1>天気アニメーション</h1>
      <WeatherAnimation />
    </div>
  );
}

export default App;

これで簡単なReactを使ったWEBアプリの完成です!
長い文章の中見てくださりありがとうございます!
最後に

npm run build

を実行したらreactがビルドされdistフォルダーが出来上がります!

📝 まとめ

この天気アプリは、React、TypeScript、Framer Motion の強力な組み合わせを活かした素晴らしい例です。特に:

  • ⏰ 時間帯に応じた視覚効果
  • 🌦️ 天気に合わせたダイナミックなアニメーション
  • 🎭 Framer Motion による滑らかな動き
  • 📱 レスポンシブなユーザーインターフェース

のポイントが秀逸です。このコードをベースに、さらなる機能を追加したり、デザインをカスタマイズしたりして、あなただけの天気アプリを作ってみてください🚀

ぜひ試してみて、感想をコメント欄に書いてくださいね!🌟

hisa

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA