前の記事
次の記事
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が変わっていることを確認。