2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

新卒エンジニアがポートフォリオ作ってみた

Last updated at Posted at 2025-03-19

目次

  • ご挨拶
  • コンポーネント構成
  • 各コンポーネント
  • まとめ

ご挨拶

みなさん、初めまして。石川 聖(ヒジリ)と申します。

この度、Qiitaアカウントを開設しました。

一本目の記事として、ポートフォリオサイトを作りがてら、自己紹介をしていくという内容で書いていきます。

(コードはGithubにありますので、よろしければご覧になってください。

また、vercelでデプロイしましたのでご覧下さい。

)

では、早速、コードの解説をやっていきます!

コンポーネント構成

/home/page.tsx

import AboutMeComponents from "./AboutMe";
import ContactComponent from "./Contact";
import HeaderComponent from "./Header";
import LinksComponent from "./Link";
import SkillsComponent from "./Skills";
import FooterComponent from "./Footer";
import "./style.css";

export interface navInfoInterface {
    name: string;
    href: string;
}

const navInfos: navInfoInterface[] = [
    {
        name: "About Me",
        href: "#about"
    },
    {
        name: "Skills",
        href: "#skills"
    },
    {
        name: "Links",
        href: "#links"
    },
    /*{
        name: "Contact",
        href: "#contact"
    },*/
];

const page = () => {
    return (
        <div>
            <HeaderComponent navInfos={navInfos} />

            <section id="about">
                <AboutMeComponents />
            </section>

            <section id="skills">
                <SkillsComponent />
            </section>

            <section id="links">
                <LinksComponent />
            </section>

            {/* <section id="contact">
                <ContactComponent />
            </section> */}

            <FooterComponent />
        </div>
    );
};

export default page;
  • ヘッダー
  • 自己紹介
  • スキル(言語とフレームワーク)
  • 各種SNSリンク
  • フッター

という構成になっています。各コンポーネントについて解説していきます。

各コンポーネント

ヘッダー

Header.tsx

"use client";

import { Menu, X } from "lucide-react"; // ハンバーガーメニュー用
import Image from "next/image";
import { useState } from "react";
import logo from "./icons/github_icon.png"; // 適宜変更
import { navInfoInterface } from "./page";

const HeaderComponent = ({ navInfos }: { navInfos: navInfoInterface[] }) => {
    const [menuOpen, setMenuOpen] = useState(false);

    const toggleMenu = () => {
        setMenuOpen(!menuOpen);
    };

    return (
        <header className="header">
            {/* 左端のロゴ */}
            <div className="logo">
                <Image src={logo} alt="Logo" width={50} height={50} />
            </div>

            {/* メニュー */}
            <nav className={`nav ${menuOpen ? "open" : ""}`}>
                {
                    navInfos && (
                        navInfos.map((navinfo, index) => {
                            return (
                                <a href={navinfo.href} onClick={() => setMenuOpen(false)} key={index}>
                                    {navinfo.name}
                                </a>
                            );
                        })
                    )
                }
            </nav>

            {/* ハンバーガーメニュー(スマホ用) */}
            <button className="menu-btn" onClick={toggleMenu}>
                {menuOpen ? <X size={24} /> : <Menu size={24} />}
            </button>
        </header>
    );
};

export default HeaderComponent;

該当のcss

/* ヘッダー */
.header {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 60px;
    background: rgba(255, 255, 255, 0.9); /* 半透明 */
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 20px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    z-index: 1000;
}

/* ロゴ */
.logo {
    display: flex;
    align-items: center;
}

/* ナビゲーション(PC用) */
.nav {
    display: flex;
    gap: 20px;
    padding-right: 50px;
}

.nav a {
    text-decoration: none;
    color: #333;
    font-weight: bold;
    transition: color 0.3s;
}

.nav a:hover {
    color: #0070f3;
}

/* ハンバーガーメニュー(スマホ用) */
.menu-btn {
    display: none;
    background: none;
    border: none;
    cursor: pointer;
    padding-right: 35px;
}

/* スマホサイズ対応 */
@media (max-width: 800px) {
    .nav {
        display: none;
        flex-direction: column;
        position: absolute;
        top: 60px;
        right: 0;
        background: rgba(255, 255, 255, 0.9);
        width: 200px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        padding: 10px;
    }

    .nav.open {
        display: flex;
    }

    .menu-btn {
        display: block;
    }
}

モバイル対応として、横800px以下でナビゲーションをハンバーガーメニュー(よく見る三本線のメニュー)で表示する様にします。

useState()(react hooks)でmenuOpen(ハンバーガーメニューの開閉フラグ変数)の状態管理をします。

hooksはサーバーサイドレンダリングが使えないため、”use client”でクライアントサイドでレンダリングする様に設定します。

画面はこんな感じです
スクリーンショット 2025-03-19 13.35.33.png

横幅を狭くすると、
スクリーンショット 2025-03-19 13.36.51.png
このようになります。

アイコンは適当にGithubと同じものにしておきますw

自己紹介

AboutMe.tsx

const AboutMeComponents = () => {
    return (
        <div>
            <div className="responsive-splitter">
                <h1>About me</h1>
                <div className="texts">
                    <div>初めまして! 石川 聖(ヒジリ)と申します</div>
                    <div>2025/4からSIer勤務の駆け出しエンジニアです</div>
                    <div>大学ではデータサイエンスを専攻し、個人開発でwebアプリを作っていました</div>
                    <div>バックエンドを主軸にフルスタックエンジニアを目指していきます!</div>
                </div>
            </div>
        </div>
    );
};

export default AboutMeComponents;

該当のcss

/* 共通レイアウト */
/* 各セクション */
section {
    padding-top: 50px;
}

/* 最初の自己紹介だけ、ヘッダーの分、間隔を空ける */
#about {
    padding-top: 110px;
}

/* 見出しと文章を分割する */
.responsive-splitter {
    display: flex;
    /* 縦方向の中央揃え */
    align-items: center;
    justify-content: center;
    padding: 10px;
    width: 95%;
}

/* 各見出し */
h1 {
    flex: 1;
    text-align: center;
    font-size: 2rem;
    font-weight: bold;
}

h2 {
    font-size: 1.6rem;
    font-weight: bold;
    color: #333;
}

h3 {
    font-size: 1.3rem;
    color: #444;
}

/* 文章全体 */
.texts {
    flex: 1;
    text-align: left;
}

/* 各文章 */
.texts > * {
    padding: 3px;
}

/* レスポンシブで縦並びに */
@media (max-width: 800px) {
    .responsive-splitter {
        flex-direction: column;
        text-align: center;
    }

    .responsive-splitter > * {
        width: 100%;
    }

    .responsive-splitter > .texts {
        text-align: center;
    }
}

画面は全画面でこんな感じに、
スクリーンショット 2025-03-19 13.44.35.png
見出しと文章は左右に分割配置、文章は左揃いになり、
横幅を縮めると、
スクリーンショット 2025-03-19 13.44.12.png

見出しと文章は縦並び、文章は中央揃いになる様にしました。

自分語り

25年学部卒で、情報工学・データサイエンスを専攻していました。研究は自然言語分野でした。

仕事もAI分野に進むことも考えましたが、有名大学院卒の人達がゴロゴロいるという魔境に足を踏み入れる勇気も能力もないので大人しくプログラマーになることにしました orz

web系もすでにレッドオーシャンと化してますけどね ^^;

バックエンドを主軸に、周辺知識(フロントエンド、インフラ、セキュリティ)などを習得していこうと思っています。

また、ビジネス的に価値を出すために、工数の削減(≒コストカット)につながる技術選定、開発手法なども学んでいく予定です。

スキル

使えるプログラミング言語やフレームワークの一覧をカード形式で表示します。

Skills.tsx

import { frameworkSkills, langSkills } from "./skillInfos";
import Image from "next/image";
import { skillInterface } from './skillInfos';

const SkillCardComponent = ({ skill }: { skill: skillInterface }) => {
    const image = require(`./icons/${skill.image}`);

    return (
        <article className="skill-card">
            <Image src={image} alt={skill.name} className="skill-icon" />
            <h3 className="skill-name">{skill.name}</h3>
            <p className="skill-level">{`Lv.${skill.level}`}</p>
        </article>
    );
};

const SkillsComponent = () => {
    return (
        <div>
            <div className="responsive-splitter">
                <h1>Skills</h1>
                <div className="texts">
                    <div>Lv.1  軽く触った程度</div>
                    <div>Lv.2  主要な機能を一通り使える</div>
                    <div>Lv.3  自走して業務を行える</div>
                    {/*<p>Lv.4  数年の業務経験がある</p>
                    <p>Lv.5  完全理解</p>*/}
                </div>
            </div>
            <h2 className="skill-type">Languages</h2>
            <div className="skills-grid">
                {langSkills.map((skill, index) => (
                    <SkillCardComponent skill={skill} key={index} />
                ))}
            </div>
            <h2 className="skill-type">Frameworks</h2>
            <div className="skills-grid">
                {frameworkSkills.map((skill, index) => (
                    <SkillCardComponent skill={skill} key={index} />
                ))}
            </div>
        </div>
    );
};

export default SkillsComponent;

該当のcss

/* グリッド配置 */
/* スキル種類 */
/* 共通の幅と中央寄せ */
.skill-type,
.skills-grid {
    width: 100%;
}

.skill-type {
    text-align: center;
    font-size: 1.6rem;
    padding-top: 10px;
    padding-bottom: 5px;
    font-weight: bold;
}

.skills-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    justify-content: center;
}

/* Skill カードデザイン */
.skill-card {
    background: #fff;
    border-radius: 12px;
    padding: 10px;
    text-align: center;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
    transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
    max-width: 180px;
    min-width: 120px;
    width: 100%;
}

/* ホバー時のエフェクト */
.skill-card:hover {
    transform: translateY(-5px);
    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
}

/* スキルアイコン */
.skill-icon {
    width: 80px;
    height: 80px;
    object-fit: contain;
    margin: 10px 0;
}

/* スキル名 */
.skill-name {
    font-size: 1.3rem;
    font-weight: bold;
    margin-bottom: 5px;
}

/* スキルレベル */
.skill-level {
    font-size: 1rem;
    color: #555;
}

見出しと文章の配置は自己紹介コンポーネントと同様です。

スキルに関するカード表示(以下スキルカード)は、
スクリーンショット 2025-03-19 14.13.08.png
このように横並び、中央揃いにしました。

スキルカード数、画面幅に応じて複数列に表示されます。
スクリーンショット 2025-03-19 14.21.25.png
また、ホバー時に浮き上がるようなエフェクトをつけています(左上, TypeScriptのスキルカード)

この時に、ポップアップで詳細な説明を出す機能を追加しようと思っています。

追加していきたい情報として、ライブラリ、インフラ・開発環境、興味関心などを追加していこうと思います。

自分語り

学部時代はデータサイエンスを専攻していたので、言語はpythonを主に使っていました。

インターンでwebアプリの開発をすることになったためpythonのwebフレームワークのDjangoを触ることになり、web開発の面白さを知りました。

Djangoはフルスタックフレームワークですが、フロントエンドの機能に癖があり(テンプレートエンジン)、かつ貧弱なのでモダンなフロントエンド技術であるReactやNext.jsの勉強もすることにしました。

今では、認証・データベース連携・バッチ処理(バックグラウンドタスク)といった実務でよく使う機能は一通り実装できる様になったので、より良い設計やコーディングを学んでいます^^

各種SNSリンク

Link.tsx

import Image from "next/image";

interface LinkInfoInterface {
  name: string;
  link: string;
  image: string;
}

const linkInfos: LinkInfoInterface[] = [
  {
    name: "Qiita",
    link: "https://qiita.com/ishikawahijiri",
    image: "qiita.png",
  },
  {
    name: "Github",
    link: "https://github.com/IshikawaHijiri",
    image: "github.png",
  },
  /*
  {
    name: "X",
    link: "",
    image: "x.jpg",
  },*/
  /*
  {
    name: "Youtube",
    link: "",
    image: "youtube.png",
  },*/
];

const LinkComponent = ({ linkInfo }: { linkInfo: LinkInfoInterface }) => {
  const basepath = "./icons/";
  const imagePath = `${basepath}${linkInfo.image}`;
  //console.log(imagePath);
  const image = require(imagePath);

  return (
    <div>
      <div className="link-icon-pair">
        <Image src={image} alt={linkInfo.name} className="link-icon" />
        <a href={linkInfo.link}>{linkInfo.name}</a>
      </div>
    </div>
  );
};

const LinksComponent = () => {
  return (
    <div>
      <div className="responsive-splitter">
        <h1>Links</h1>
        <div className="texts">
          {linkInfos.map((linkInfo, index) => {
            return <LinkComponent linkInfo={linkInfo} key={index} />;
          })}
        </div>
      </div>
    </div>
  );
};

export default LinksComponent;

該当のcss

/* link icon pair */
.link-icon-pair {
  align-items: center;
  gap: 10px;
}

/* link icon */
.link-icon {
  width: 15px;
  height: auto;
  padding-right: 10px;
}

画面はこんな感じです
スクリーンショット 2025-03-19 14.45.19.png

X(旧twitter)やyoutubeも運用したいと思っていますが、一旦はQiitaとGithubのリンクだけを載せておきます。

フッター

これで最後です。

Footer.tsx

"use client";
import Link from "next/link";

const Footer = () => {
  const currentYear = new Date().getFullYear();

  return (
    <div className="footer-container">
      <footer className="footer">
        <p>Hope you enjoyed exploring my portfolio!</p>
        <button onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}>
          go to top
        </button>
        <p>© {currentYear} Hijiri Ishikawa. All Rights Reserved.</p>
      </footer>
    </div>
  );
};

export default Footer;

該当のcss

/* フッター */
.footer-container {
  padding-top: 50px;
}

.footer {
  text-align: center;
  padding: 20px;
  background-color: #f8f8f8;
  border-top: 1px solid #ddd;
  font-size: 14px;
}

.footer button {
  background: none;
  border: none;
  color: #0070f3;
  cursor: pointer;
  font-size: 14px;
  margin-top: 5px;
}

.footer button:hover {
  text-decoration: underline;
}

画面はこんな感じです
スクリーンショット 2025-03-19 14.53.39.png

まとめ

ここまでご覧になっていただき、誠にありがとうございます m(_ _)m

今後は、機能追加やvpsへのデプロイを行なっていこうと思います。

このアカウントについてですが、これからは週1のペースで投稿していく予定です。

有益な情報を発信していけるよう努力しますので、よろしければ各種SNSをフォローしてお待ちいただけると幸いです。

次の投稿は「新卒エンジニアが読みたい本」を予定しています。

以上、石川からの初投稿でした。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?