PROGRAMMING

ReactとTailwind CSSでポートフォリオサイトを作成した話

はじめに

「エンジニアならポートフォリオサイトは作らないとな…でも、どうせなら楽しく作りたい!」 そんな気持ちで始めたポートフォリオサイト制作。

React初心者の僕が、どのように実装していったのか、その過程で学んだことや苦労した点を赤裸々に共有していきます!最後まで見ていただけると嬉しいです😌

こちらが作成したポートフォリオサイトです!

1. プロジェクトの概要

なぜポートフォリオサイトを作ろうと思ったのか

個人開発をしているときに、フロントの開発がすごく楽しかったのでフロントエンドの技術に興味を持ち始めました。

そこでReactを使用しておしゃれなポートフォリオサイトを作ったら勉強にもなるし、これからのキャリアにも役立つなと思ったのが制作のきっかけです😅

使用技術の選定

「モダンな技術スタックを使いたい!」という気持ちと「まずは確実に完成させたい」という思いのバランスを考えて、以下の技術を選択しました。

・React:UI構築の主軸として
・Tailwind CSS:個人開発で使用して慣れていたため
Framer Motion:スムーズなアニメーションの実装に
・React Scroll:スクロールアニメーションの実装に

開発期間と進め方

全体の開発期間は約2週間でした。

1週目

・デザインとベースの実装
・Figmaでデザインカンプ作成
・基本的なコンポーネント構造の実装
・レスポンシブデザインの対応

2週目

・アニメーションとブラッシュアップ
Framer Motionでのアニメーション実装
コンポーネントのリファクタリング

2. 主要な実装ポイント

ページ最初に表示されるアニメーション

サイトの第一印象を大切にしたくて、こだわったのがアニメーションです。
Framer Motionを使って、以下のようなコードで実装しました。

useEffect(() => {
    async function sequence() {
      // 1. 名前のフェードインとスライドアップ
      await nameControls.start({
        opacity: 1,
        y: 0,
        transition: { duration: 0.6 },
      });

      // 2. 薄いラインのスケールイン
      await thinLineControls.start({
        scaleX: 1,
        transition: { duration: 0.4 },
      });

      // 3. スケールインの間に遅延を追加(0.3秒)
      await new Promise((resolve) => setTimeout(resolve, 300));

      // 4. 濃いロードラインのスケールイン(薄いラインのスケールイン後に開始)
      await thickLineControls.start({
        scaleX: 1,
        transition: { duration: 0.6 },
      });

      // 5. 名前のフェードアウトとスライドダウン
      await nameControls.start({
        opacity: 0,
        y: 5, // 5px 下に移動
        transition: { duration: 0.7 },
      });

      // 6. 薄いラインと濃いラインの transformOrigin を右に変更
      await Promise.all([
        thinLineControls.set({ transformOrigin: 'right' }),
        thickLineControls.set({ transformOrigin: 'right' }),
      ]);

      // 7. 薄いラインと濃いラインのスケールアウト(同時に消える)
      await Promise.all([
        thinLineControls.start({
          scaleX: 0,
          transition: { duration: 0.5, ease: 'easeIn' },
        }),
        thickLineControls.start({
          scaleX: 0,
          transition: { duration: 0.5, ease: 'easeIn' },
        }),
      ]);

      // 8. アニメーション終了後にメインコンテンツを表示
      onFinish();
    }

    sequence();
  }, [nameControls, thinLineControls, thickLineControls, onFinish]);

  return (
    <div className="fixed inset-0 flex flex-col items-center justify-center bg-background z-50">
      {/* 名前のフェードイン・アウト */}
      <motion.h1
        initial={{ opacity: 0, y: 5 }}
        animate={nameControls}
        className="tracking-[0.4em] font-bold mb-2"
      >
        DAICHI OKAMOTO
      </motion.h1>

      {/* ラインのコンテナ */}
      <div className="w-72 h-1 relative">
        {/* 薄いライン */}
        <motion.div
          initial={{ scaleX: 0 }}
          animate={thinLineControls}
          className="absolute top-0 left-0 w-full h-1 bg-black bg-opacity-10 origin-left z-10"
        ></motion.div>

        {/* 濃いライン */}
        <motion.div
          initial={{ scaleX: 0 }}
          animate={thickLineControls}
          className="absolute top-0 left-0 w-full h-1 bg-black origin-left z-20"
        ></motion.div>
      </div>
    </div>
  );
}

 

ポートフォリオ作品のモーダル表示実装

ポートフォリオ作品の詳細表示には、モーダルウィンドウを採用しました。実装のポイントを説明していきます!

1. データの構造と管理

まず、プロジェクトデータをsrc/data/projects.jsとして別ファイルで管理することにしました

// src/data/projects.js
export const projects = [
  {
    id: 1,
    title: 'Care Shift',
    description: 'シフト自動作成アプリ',
    image: '/portfolio2.png',
    images: ['/pf-cs-top.jpg', '/pf-cs-staff1.png', '/pf-cs-shift.png'],
    details: {
      text: '前職の介護施設のシフト自動作成アプリを作成しました...',
      languages: 'Ruby on Rails, Python, Tailwind CSS...',
      githubLink: 'https://github.com/...',
      websiteLink: 'https://...',
      siteTitle: 'Care Shift',
      githubTitle: 'Github',
    }
  },
  // 他のプロジェクトデータ...
];

 

このようにデータを分離することで、以下の利点があります。

・コンポーネントとデータの関心を分離
・データの更新や管理が容易に
・コードの可読性向上

2. モーダルコンポーネントの実装

const ProjectModal = ({ project, onClose }) => {
  if (!project) return null;

  return (
    <motion.div
      className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-75 flex items-center justify-center z-50"
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
      onClick={onClose}
    >
      <motion.div
        className="bg-neutral-200 p-8 w-5/6 h-5/6 overflow-y-auto flex flex-col lg:flex-row relative"
        initial={{ y: 0, opacity: 0 }}
        animate={{ y: 0, opacity: 1 }}
        exit={{ y: 0, opacity: 0 }}
        onClick={(e) => e.stopPropagation()}
      >
        <button 
          className="absolute top-2 right-4 lg:text-xl xl:text-2xl text-black" 
          onClick={onClose}
        >
          <FontAwesomeIcon icon={faXmark} />
        </button>

        <div className="w-full lg:w-1/3 lg:p-2 xl:p-4 text-black">
          <div className='flex justify-center items-center mb-2'>
            <h2 className="text-sm md:text-base lg:text-xl font-bold">
              {project.title}
            </h2>
          </div>
          <div className='text-left text-xs lg:text-sm 2xl:text-base'>
            <p className='tracking-wider leading-relaxed lg:leading-relaxed xl:leading-loose'>
              {project.details.text}
            </p>
          </div>
          <div className='my-8'>
            <div className='mb-2'>
              <h2 className='flex justify-center items-center font-bold text-black text-sm md:text-base lg:text-lg'>
                使用言語など
              </h2>
            </div>
            <div className='text-left text-xs lg:text-sm 2xl:text-base'>
              <p className='tracking-wider leading-relaxed lg:leading-relaxed xl:leading-loose'>
                {project.details.languages}
              </p>
            </div>
          </div>
          {project.details.githubLink && (
            <div>
              <a 
                href={project.details.githubLink} 
                className='flex items-center space-x-1 text-header text-xs lg:text-sm font-medium underline w-1/5 hover:opacity-70' 
                target="_blank" 
                rel="noopener noreferrer"
              >
                {project.details.githubIcon}
                <p>{project.details.githubTitle}</p>
              </a>
            </div>
          )}
          {project.details.websiteLink && (
            <div className='mt-2'>
              <a 
                href={project.details.websiteLink} 
                className='text-header font-medium underline text-xs lg:text-sm hover:opacity-70' 
                target="_blank" 
                rel="noopener noreferrer"
              >
                {project.details.siteTitle}
              </a>
            </div>
          )}
        </div>

        <div className="w-full lg:w-2/3 p-4 lg:mt-32 xl:mt-16">
          <Slider {...sliderSettings}>
            {project.images && project.images.map((image, index) => (
              <div key={index}>
                <img 
                  src={image} 
                  alt={`${project.title} Slide ${index + 1}`} 
                  className="w-full h-auto"
                />
              </div>
            ))}
          </Slider>
        </div>
      </motion.div>
    </motion.div>
  );
};

モーダルの実装で特に工夫した点は以下の3つです。

アニメーションの実装

initial={{ opacity: 0 }}  // 初期状態は透明
animate={{ opacity: 1 }}  // 表示時にフェードイン
exit={{ opacity: 0 }}    // 閉じる時にフェードアウト

Framer Motionを使用することで、スムーズな表示/非表示のアニメーションを実現できました。

クリックイベントの制御

onClick={onClose}  // 背景クリック時
onClick={(e) => e.stopPropagation()}  // モーダルコンテンツクリック時

モーダルの外側(背景)をクリックした時も閉じるように実装。モーダルコンテンツ内のクリックはstopPropagation()で伝播を止めています。

レスポンシブ対応
Tailwind CSSのユーティリティクラスを使用して、画面サイズに応じたレイアウトの切り替えを実装しました。

コンポーネント設計での学び

最初は全てMainContent.jsxに書いていましたが、コードの見通しが悪くなってきたので、 以下のような構造にリファクタリングしました。

src/
  components/
    common/      # 共通コンポーネント
      FadeInSection.jsx
    layout/      # レイアウト用コンポーネント
      Header.jsx
      Hero.jsx
      Footer.jsx
    sections/    # 各セクションのコンポーネント
      About/
        About.jsx
      Contact/
        Contact.jsx
        ContactForm.jsx
      Portfolio/
        Portfolio.jsx
        ProjectCard.jsx
        ProjectModal.jsx
  MainContent.jsx

 

3. 苦労した点と解決方法

モーダル実装での試行錯誤

ポートフォリオの作品詳細をモーダルで表示する際、以下のような課題に直面しました

1. モーダルが表示されない
→ Framer MotionのAnimatePresenceの使い方を理解していなかった
2. スライダーの画像が表示されない
→ slick-carouselのCSSのインポートを忘れていた

愚直にドキュメントを読んで、一つずつ解決していきました😅

4. 今後の展望

フロントエンドの開発が楽しいので、これからもフロントの技術はキャッチアップしていきたいと思いました!

次回のプロジェクトは、最初からちゃんとコンポーネント設計を考えてから実装していきたいと思います😊

詳細なコードはGitHubにて公開していますので、興味のある方はぜひご覧ください!
作成したポートフォリオサイトは下記のURLからアクセスできます。
https://www.daichi-okamoto.com/

それではまた!

ABOUT ME
daichi
介護士からWEBエンジニアに転身しました。 主にRubyを学習中です。WEBエンジニアとしての学びや日々の成長を発信していきます。
プロフィール

COMMENT

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

CAPTCHA