0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C# WinForms経験者がWebアプリケーションを開発してみた⑥~UI改善~

0
Last updated at Posted at 2026-09-26

前の記事

次の記事

UI改善

globals.css を変更する

src/app/globals.cssを編集する。

/* ========================================
   全体共通
   ======================================== */

/* width / height に padding と border を含める。
   レイアウト計算を分かりやすくするための基本設定。 */
* {
  box-sizing: border-box;
}

/* 画面全体の基本デザイン。
   背景色・文字色・フォントを共通化する。 */
body {
  margin: 0;
  background: #f5f6f8;
  color: #222;
  font-family: Arial, Helvetica, sans-serif;
}

/* 各ページのメイン領域。
   横幅を最大1000pxに制限して中央寄せし、
   白いカードのような見た目にする。 */
main {
  max-width: 1000px;
  margin: 40px auto;
  padding: 32px;
  background: #ffffff;
  border-radius: 8px;
}

/* ページタイトルの余白を調整する。 */
h1 {
  margin-top: 0;
  margin-bottom: 24px;
}

/* 通常リンクの見た目。 */
a {
  color: #2563eb;
  text-decoration: none;
}

/* マウスを重ねたときに下線を表示する。 */
a:hover {
  text-decoration: underline;
}


/* ========================================
   ページヘッダー
   ======================================== */

/* 「タスク管理」のタイトルと
   「新規登録」ボタンを左右に配置する。 */
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}


/* ========================================
   ボタン
   ======================================== */

/* 登録・更新・新規登録・検索などで
   共通利用する基本ボタン。 */
.button {
  display: inline-block;
  padding: 8px 16px;
  border: none;
  border-radius: 6px;
  background: #2563eb;
  color: #ffffff;
  font-size: 14px;
  cursor: pointer;
  text-decoration: none;
}

/* ボタンにマウスを重ねたとき、
   少し透明にして反応を分かりやすくする。 */
.button:hover {
  opacity: 0.9;
  text-decoration: none;
}

/* 登録中・更新中など、
   ボタンを操作できない状態の見た目。 */
.button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* 「クリア」などの補助操作用ボタン。 */
.button-secondary {
  background: #6b7280;
}

/* 「削除」など注意が必要な操作用ボタン。 */
.button-danger {
  background: #dc2626;
}


/* ========================================
   タスク一覧テーブル
   ======================================== */

/* テーブルを囲むコンテナ。
   スマホではページ全体ではなく、
   この領域の中だけ横スクロールできるようにする。 */
.table-container {
  width: 100%;
  max-width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* タスク一覧テーブル。
   PCでは横幅いっぱいに表示し、
   スマホでは列が細くなりすぎないよう最低幅を確保する。 */
.task-table {
  width: 100%;
  min-width: 800px;
  /* セル同士の罫線を1本にまとめる。 */
  border-collapse: collapse;
}

/* 見出し・データセルの共通設定。 */
.task-table th,
.task-table td {
  padding: 12px;
  border: 1px solid #ddd;
  text-align: left;
}

/* 見出しは途中で改行しない。 */
.task-table th {
  background: #f3f4f6;
  white-space: nowrap;
}

.task-table tbody tr:hover {
  background: #f9fafb;
}

/* 編集・削除を横並びのまま表示する。 */
.actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: nowrap;
  white-space: nowrap;
}


/* ========================================
   新規登録・編集フォーム
   ======================================== */

/* フォームが横に広がりすぎないよう、
   最大幅を500pxに制限する。 */
.task-form {
  max-width: 500px;
}

/* 各入力項目の間に余白を付ける。 */
.form-group {
  margin-bottom: 20px;
}

/* 項目名を入力欄の上に表示し、
   太字にして分かりやすくする。 */
.form-group label {
  display: block;
  margin-bottom: 6px;
  font-weight: bold;
}

/* テキストボックスとプルダウンの
   幅・余白・枠線などを統一する。 */
.form-group input,
.form-group select {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid #bbb;
  border-radius: 6px;
  font-size: 16px;
}

/* 入力欄を選択したとき、
   枠線を青くしてフォーカス中であることを示す。 */
.form-group input:focus,
.form-group select:focus {
  border-color: #2563eb;
  outline: none;
}

/* 入力チェックエラーを赤文字で表示する。 */
.form-error {
  margin-top: 6px;
  color: #dc2626;
  font-size: 14px;
}

/* 登録・更新ボタンなどの操作領域。
   複数ボタンがある場合に横並びにする。 */
.form-actions {
  display: flex;
  gap: 12px;
  align-items: center;
  margin-top: 24px;
}


/* ========================================
   検索・絞り込みフォーム
   ======================================== */

/* タスク名・担当者・ステータス・検索ボタンを
   PCでは横並びに配置する。 */
.search-form {
  display: flex;
  gap: 16px;
  align-items: end;
  margin-bottom: 20px;
  padding: 16px;
  background: #f3f4f6;
  border-radius: 6px;
}

/* 検索条件の各項目が、
   使用可能な横幅を均等に利用するようにする。 */
.search-form > div {
  flex: 1;
}

/* 検索条件の項目名を入力欄の上に表示する。 */
.search-form label {
  display: block;
  margin-bottom: 6px;
  font-weight: bold;
}

/* 検索用テキストボックス・プルダウンの見た目を統一する。
   min-width: 0 により、狭い画面でも要素が縮められるようにする。 */
.search-form input,
.search-form select {
  width: 100%;
  min-width: 0;
  padding: 8px 10px;
  border: 1px solid #bbb;
  border-radius: 6px;
  font-size: 14px;
}

/* 「検索」「クリア」ボタンを横並びにする。
   flex: 0 0 auto により、検索条件入力欄のようには伸縮させない。 */
.search-actions {
  display: flex;
  gap: 8px;
  flex: 0 0 auto;
}


/* ========================================
   スマートフォン向けレスポンシブ対応
   ======================================== */

/* 画面幅が600px以下の場合に適用する。
   PCでは横並び、スマホでは縦並びに切り替える。 */
@media (max-width: 600px) {

  /* スマホでは画面端との余白を小さくして、
     表示領域を広く使う。
     width を指定することで横方向にはみ出しにくくする。 */
  main {
    width: calc(100% - 32px);
    margin: 16px;
    padding: 16px;
  }

  /* 検索条件を横並びから縦並びへ変更する。
     横にはみ出すのを防ぐ。 */
  .search-form {
    flex-direction: column;
    align-items: stretch;
  }

  /* 各検索条件をスマホ画面の横幅いっぱいにする。 */
  .search-form > div {
    width: 100%;
    min-width: 0;
  }

  /* 検索・クリアボタンの領域も横幅いっぱいにする。 */
  .search-actions {
    width: 100%;
  }

  /* 検索・クリアボタンを均等な幅にする。 */
  .search-actions .button {
    flex: 1;
    text-align: center;
  }

  /* スマホではタスク一覧の中だけ
     左右にスクロールできるようにする。 */
  .table-container {
    width: 100%;
    max-width: 100%;
    overflow-x: auto;
  }
}

一覧画面を変更する

src/app/page.tsxを、CSSクラスを使う形にする。

import Link from "next/link";

import { getTasks } from "@/lib/task-api";
import DeleteButton from "@/components/DeleteButton";

const statusLabels: Record<number, string> = {
  0: "未着手",
  1: "進行中",
  2: "完了",
};

function formatDateTime(value: string): string {
  const date = new Date(value);

  return new Intl.DateTimeFormat("ja-JP", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
  }).format(date);
}

export default async function Home() {
  const tasks = await getTasks();

  return (
    <main>
      <div className="page-header">
        <h1>タスク管理</h1>

        <Link
          href="/tasks/new"
          className="button"
        >
          + 新規登録
        </Link>
      </div>

      <div className="table-container">
        <table className="task-table">
          <thead>
            <tr>
              <th>ID</th>
              <th>タスク名</th>
              <th>担当者</th>
              <th>ステータス</th>
              <th>予定日</th>
              <th>更新日時</th>
              <th>操作</th>
            </tr>
          </thead>

          <tbody>
            {filteredTasks.map((task) => (
              <tr key={task.id}>
                <td>{task.id}</td>
                <td>{task.taskName}</td>
                <td>{task.operatorName}</td>
                <td>
                  {statusLabels[task.status] ?? "不明"}
                </td>
                <td>
                  {task.plannedDate?.slice(0, 10).replaceAll("-", "/") ?? "-"}
                </td>
                <td>
                  {formatDateTime(task.updatedAt)}
                </td>

                <td>
                  <div className="actions">
                    <Link
                      href={`/tasks/${task.id}/edit`}
                    >
                      編集
                    </Link>

                    <DeleteButton
                      id={task.id}
                      taskName={task.taskName}
                    />
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </main>
  );
}
className="task-table"

を増やしている。

共通フォームをCSS対応する

src/components/TaskFormFields.tsxの各項目を、

<div className="form-group">

に変更する。
完成形

import type { Operator } from "@/types/operator";
import type { Task } from "@/types/task";
import type { TaskActionState } from "@/app/actions/task-actions";

type Props = {
  operators: Operator[];
  task?: Task;
  state: TaskActionState;
};

export default function TaskFormFields({
  operators,
  task,
  state,
}: Props) {
  return (
    <>
      <div className="form-group">
        <label htmlFor="taskName">
          タスク名
        </label>

        <input
          id="taskName"
          name="taskName"
          type="text"
          maxLength={200}
          required
          defaultValue={task?.taskName ?? ""}
          aria-invalid={
            !!state.errors?.taskName
          }
        />

        {state.errors?.taskName?.map(
          (error) => (
            <p
              key={error}
              className="form-error"
            >
              {error}
            </p>
          )
        )}
      </div>

      <div className="form-group">
        <label htmlFor="operatorId">
          担当者
        </label>

        <select
          id="operatorId"
          name="operatorId"
          required
          defaultValue={
            task?.operatorId ?? ""
          }
        >
          {!task && (
            <option value="" disabled>
              選択してください
            </option>
          )}

          {operators.map((operator) => (
            <option
              key={operator.id}
              value={operator.id}
            >
              {operator.name}
            </option>
          ))}
        </select>

        {state.errors?.operatorId?.map(
          (error) => (
            <p
              key={error}
              className="form-error"
            >
              {error}
            </p>
          )
        )}
      </div>

      <div className="form-group">
        <label htmlFor="status">
          ステータス
        </label>

        <select
          id="status"
          name="status"
          defaultValue={task?.status ?? 0}
        >
          <option value="0">
            未着手
          </option>

          <option value="1">
            進行中
          </option>

          <option value="2">
            完了
          </option>
        </select>
      </div>

      <div className="form-group">
        <label htmlFor="plannedDate">
          予定日
        </label>

        <input
          id="plannedDate"
          name="plannedDate"
          type="date"
          defaultValue={
            task?.plannedDate?.slice(0, 10) ?? ""
          }
        />
      </div>

      {state.message && (
        <p className="form-error">
          {state.message}
        </p>
      )}
    </>
  );
}

新規登録フォームを整える

src/components/CreateTaskForm.tsxの<form>に、

className="task-form"

をつける。
ボタン部分も

return (
  <form
    action={formAction}
    className="task-form"
  >
    <TaskFormFields
      operators={operators}
      state={state}
    />

    <div className="form-actions">
      <button
        type="submit"
        className="button"
        disabled={isPending}
      >
        {isPending
          ? "登録中..."
          : "登録"}
      </button>
    </div>
  </form>
);

とする。

編集フォームも同じ

src/components/EditTaskForm.tsxも同様に、

<form
  action={formAction}
  className="task-form"
>

にする。
ボタンも、

<div className="form-actions">
  <button
    type="submit"
    className="button"
    disabled={isPending}
  >
    {isPending
      ? "更新中..."
      : "更新"}
  </button>
</div>

にする。

削除ボタンも整える

src/components/DeleteButton.tsxのボタンを、

<button
  type="submit"
  className="button button-danger"
>
  削除
</button>

にする。

動作確認

タスク管理を表示して、UIが変わっていることを確認。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?