2025-04-21

Reactでポートフォリオサイトを作成する 🚀(3)

ReactNext.jsポートフォリオ

はじめに

「自分にとって新しい技術をインプットしたい」と「長く残るアウトプットを作りたい」という思いから、Reactを使ってポートフォリオサイトの作成に挑戦してみることにしました。 この記事を含め、複数回に分けて作成過程を記録していきます。ここまでの記事は以下です。
今回は以下の内容についてご紹介します。
  • 【ポートフォリオ画面】ポートフォリオ一覧を表示
  • 【ポートフォリオ画面】スキルタイムラインチャートを作成
  • 【コンタクト画面】SSGformを用いてフォームを作成
  • 【フッター】フッターを作成

【ポートフォリオ画面】ポートフォリオ一覧を表示

ポートフォリオ一覧のコンポーネントを、カードコンポーネントを用いて作成する
WorkCard.tsx
import Image from "next/image";

interface WorkCardProps {
  src: string;
  alt: string;
  title: string;
  description: string;
  tags: string[];
}

const WorkCard: React.FC<WorkCardProps> = ({ src, alt, title, description, tags }) => {
  return (
    <div className="max-w-sm rounded overflow-hidden mb-20">
      <Image src={src} alt={alt} width={500} height={500} className="w-full" />
      <div className="px-6 py-4">
        <div className="font-bold text-xl mb-2">{title}</div>
        <p className="text-gray-700 text-base">{description}</p>
      </div>
      <div className="px-6 pt-4 pb-2">
        {tags.map((tag, index) => (
          <span key={index} className="inline-block bg-gray rounded-full px-3 py-1 text-sm font-semibold mr-2 mb-2">
            {tag}
          </span>
        ))}
      </div>
    </div>
  );
};

export default WorkCard;
ポートフォリオ一覧画面にて、mapを用いてWorkCardコンポーネントを表示する
works/page.tsx
<div className="works px-20">
  <h2 className="text-3xl font-bold">制作実績</h2>
  <div className="work-list grid grid-cols-3 gap-4 justify-items-center items-center my-10">
    {works.map((work, index) => (
      <WorkCard
        key={index}
        src={work.src}
        alt={work.alt}
        title={work.title}
        description={work.description}
        tags={work.tags}
      />
    ))}
  </div>
</div>
screenshot

【ポートフォリオ画面】スキルタイムラインチャートを作成

タイムライン型のスキルマップを作成したい
SkillTimeline.tsx
const SkillTimeline: React.FC = () => {
  const totalYears = max - 2019 + 1; // グラフの長さ(2024年8月現在は6.6)
  const sortedSkills = skills.sort((a, b) => b.total - a.total);
  const years = Array.from({ length: Math.ceil(totalYears) }, (_, i) => 2019 + i);

  return (
    <div className="w-4/5 mx-auto p-10 border-l-4 border-gray relative my-10">
      <div className="flex items-center">
        <div className="bg-teal bg-opacity-80 h-3 rounded w-8"></div>
        <p className="ml-2">経験時期</p>
      </div>
      <div className="relative mb-10 py-4">
        {years.map((year, index) => {
          const left = ((year - 2019) / totalYears) * 100;
          return (
            <span
              key={index}
              className="absolute text-md"
              style={{ left: `${left}%`}}
            >
              {year}
            </span>
          );
        })}
      </div>
      {sortedSkills.map((skill, index) => (
        <div key={index} className="mb-5 pl-5 relative">
          <div className="bg-white p-4 rounded-lg relative">
            <h3 className="font-bold">{skill.name}</h3>
            <div className="flex">
              {skill.total.toFixed(1)}              {skill.periods.map((period, i) => {
                const startOffset = ((period.start - 2019) / totalYears) * 100;
                const width = ((period.end - period.start + 0.1) / totalYears) * 100;
                return (
                  <div
                    key={i}
                    className="absolute bg-teal bg-opacity-80 h-3 rounded"
                    style={{
                      left: `${startOffset}%`,
                      width: `${width}%`
                    }}
                  ></div>
                );
              })}
            </div>
          </div>
        </div>
      ))}
    </div>
  );
};

export default SkillTimeline;
screenshot
※更新が手間なので、今後改良したい

【コンタクト画面】SSGformを用いてフォームを作成

SSGformを用いてフォームを作成する
screenshot

【フッター】フッターを作成

アニメーションのついた横棒(PageFace.tsxで使用)をフッターでも使いたいので、コンポーネント化する
components/AnimatedLine.tsx
import React, { useEffect, useRef } from 'react';

const AnimatedLine: React.FC = () => {
  const lineRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (lineRef.current) {
      lineRef.current.classList.remove('w-0');
      lineRef.current.classList.add('w-full');
    }
  }, []);

  return (
    <div
      ref={lineRef}
      className="h-0.5 opacity-50 bg-font-main transition-all duration-1000 ease-in-out w-0 mt-4"
    ></div>
  );
};

export default AnimatedLine;
フッターコンポーネントを作成する
Footer.tsx
'use client'

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPlane } from '@fortawesome/free-solid-svg-icons'
import { faGithub, faXTwitter } from '@fortawesome/free-brands-svg-icons'
import AnimatedLine from './AnimatedLine'

export default function Footer() {
  return (
    <>
      <AnimatedLine />
      <footer className="p-2 mt-auto">
        <div className="container mx-auto flex h-32 py-6">
          <div className="rightFooter w-3/4">
            <p className="mb-3 text-lg">古堅 基史</p>
            <p>
              沖縄
              <FontAwesomeIcon icon={faPlane} className="mx-3 opacity-70" />
              広島
              <FontAwesomeIcon icon={faPlane} className="mx-3 opacity-70" />
              埼玉
              <FontAwesomeIcon icon={faPlane} className="mx-3 opacity-70" />
              広島
              <FontAwesomeIcon icon={faPlane} className="mx-3 opacity-70" />
              横浜
            </p>
          </div>
          <div className="border-l-2 opacity-50 mx-8"></div>
          <div className="leftFooter w-1/4 flex flex-col justify-between">
            <div className="flex space-x-4">
              <a
                href="https://github.com/motoshifurugen"
                target="_blank"
                rel="noopener noreferrer"
              >
                <FontAwesomeIcon icon={faGithub} className="text-2xl" />
              </a>
              <a
                href="https://x.com/cocoahearts21"
                target="_blank"
                rel="noopener noreferrer"
              >
                <FontAwesomeIcon icon={faXTwitter} className="text-2xl" />
              </a>
            </div>
            <p className="self-end opacity-50">&copy; 2024 Furugen</p>
          </div>
        </div>
      </footer>
    </>
  )
}
フッターを表示する
layout.tsx
<html lang="ja">
  <body className={inter.className}>
    <BackgroundWrapper>
      <Header />
      <main className="pt-40">{children}</main>
    </BackgroundWrapper>
    <Footer />
  </body>
</html>
screenshot

To be continued...

次回はGithub pagesにてデプロイ・レスポンシブ対応を行います。