diff --git a/src/q1-type-definition/UserCard.tsx b/src/q1-type-definition/UserCard.tsx index 0209cc2..f19dad0 100644 --- a/src/q1-type-definition/UserCard.tsx +++ b/src/q1-type-definition/UserCard.tsx @@ -34,9 +34,59 @@ import React from "react"; // ↓↓↓ ここに UserCardProps 型の定義と UserCard コンポーネントを実装してください ↓↓↓ -export const UserCard = () => { - // TODO: 実装してください - return null; +// 権限 (Union Types) +type Role = "admin" | "editor" | "viewer"; + +// 権限: {ロール日本語名} +const roleLabel: Record = { + admin: "管理者", + editor: "編集者", + viewer: "閲覧者", +}; + + +type UserCardProps = { + name: string; + age: number; + email?: string; + role:Role; + skills:string[]; + onContact?: (email:string) => void; }; + +export const UserCard = ({ + name, + age, + email, + role, + skills, + onContact +}: UserCardProps) => { + // TODO: 実装してください + return ( +
+

{name}

+

年齢: {age}

+

権限: {roleLabel[role]}

+ {email && ( +

Email: {email}

+ )} + + + {email && onContact && ( + + ) + } +
+ ); +}; // ↑↑↑ ここまで ↑↑↑ diff --git a/src/q2-hooks-basics/UserList.tsx b/src/q2-hooks-basics/UserList.tsx index 4e0d8b2..fa65eb0 100644 --- a/src/q2-hooks-basics/UserList.tsx +++ b/src/q2-hooks-basics/UserList.tsx @@ -37,9 +37,49 @@ export const fetchUsers = async (): Promise => { // ↓↓↓ ここに UserList コンポーネントを実装してください ↓↓↓ +// 3つの状態 +type State = +| { status: "loading" } +| { status: "error" } +| { status: "success"; data: User[]}; + + export const UserList: React.FC = () => { // TODO: 実装してください - return null; + const [state, setState] = useState({status: "loading"}); + + useEffect(() => { + const load = async () => { + try { + const users = await fetchUsers(); + setState({status: "success", data: users}); + } catch { + setState({status: "error"}); + } + }; + + load(); + }, []); + + if(state.status === "loading") { + return

読み込み中...

+ } + + if(state.status === "error") { + return

エラーが発生しました

+ } + + if(state.data.length === 0 ){ + return

ユーザーがいません

+ } + + return ( +
    + {state.data.map((user) => ( +
  • {user.name} - {user.department}
  • + ))} +
+ ); }; // ↑↑↑ ここまで ↑↑↑ diff --git a/src/q3-custom-hook/useForm.ts b/src/q3-custom-hook/useForm.ts index 980926b..ebb1bcc 100644 --- a/src/q3-custom-hook/useForm.ts +++ b/src/q3-custom-hook/useForm.ts @@ -56,7 +56,106 @@ export function useForm>( rules: ValidationRules = {} ): UseFormReturn { // TODO: 実装してください - throw new Error("Not implemented"); + const [values, setValues] = useState ({...initialValues}); + const [errors, setErrors] = useState>> ({}); + const [touched, setTouchedState] = useState>> ({}); + + + const validateField = useCallback( + (field: K, currentValues: T): string | null => { + const fieldRules = rules[field]; + if(!fieldRules || fieldRules.length === 0) return null; + + for(const rule of fieldRules){ + const result = rule(currentValues[field]); + if(result != null) return result; + } + return null; + }, + [rules] + ); + + + const applyFieldError = useCallback( + ( + prev: Partial>, + field: keyof T, + error: string | null + ): Partial> => { + const next = { ...prev }; + if(error != null ){ + next[field] = error; + } else { + delete next[field]; + } + return next; + }, + [] + ); + + + const setValue = useCallback( + (field: K, value: T[K]) => { + setValues((prev) => ({ ...prev, [field]: value})); + + if(touched[field]) { + const error = validateField(field, {[field]: value} as T); + setErrors((prev) => applyFieldError(prev, field, error)); + } + }, + [touched, validateField, applyFieldError] + ); + + + const setTouched = useCallback( + (field: K) => { + setTouchedState((prev) => ({...prev, [field]: true })); + + const error = validateField(field, values); + setErrors((prev) => applyFieldError(prev,field, error)); + }, + [values, validateField, applyFieldError] + ); + + + const validate = useCallback((): boolean => { + const allFileds = Object.keys(rules) as (keyof T)[]; + let nextErrors: Partial> = {}; + + for(const field of allFileds){ + const error = validateField(field, values); + if(error != null) { + nextErrors[field] = error; + } + } + + setErrors(nextErrors); + return Object.keys(nextErrors).length ===0; + }, + [rules, values, validateField] + ); + + + const reset = useCallback(() =>{ + setValues({...initialValues}); + setErrors({}); + setTouchedState({}); + }, + [initialValues] + ); + + const isValid = Object.keys(errors).length === 0; + + return { + values, + errors, + touched, + setValue, + setTouched, + validate, + reset, + isValid, + }; } // ↑↑↑ ここまで ↑↑↑ diff --git a/src/q4-refactor/TaskManager.tsx b/src/q4-refactor/TaskManager.tsx index 509609f..44eed19 100644 --- a/src/q4-refactor/TaskManager.tsx +++ b/src/q4-refactor/TaskManager.tsx @@ -16,11 +16,55 @@ * - default export すること */ -import React from "react"; +import React, { useState } from "react"; +import { Task, Priority, Filter } from "./types"; +import TaskForm from "./components/TaskForm"; +import TaskFilter from "./components/TaskFilter"; +import TaskList from "./components/TaskList"; + + const TaskManager: React.FC = () => { - // TODO: 実装してください - return null; + const [tasks, setTasks] = useState([]); + const [filter, setFilter] = useState("all"); + const [nextId, setNextId] = useState(1); + + const addTask = (title: string, priority: Priority) => { + setTasks([...tasks, { id: nextId, title, completed: false, priority }]); + setNextId(nextId + 1); + }; + + const toggleTask = (id: number) => { + setTasks(tasks.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))); + }; + + const deleteTask = (id: number) => { + setTasks(tasks.filter((t) => t.id !== id)); + }; + + const filteredTasks = tasks.filter((t) => { + if (filter === "active") return !t.completed; + if (filter === "completed") return t.completed; + return true; + }); + + const stats = { + total: tasks.length, + active: tasks.filter((t) => !t.completed).length, + completed: tasks.filter((t) => t.completed).length, + }; + + return ( +
+

タスク管理

+
+ 全体: {stats.total} / 未完了: {stats.active} / 完了: {stats.completed} +
+ + + +
+ ); }; export default TaskManager; diff --git a/src/q4-refactor/components/TaskFilter.tsx b/src/q4-refactor/components/TaskFilter.tsx index 09866ca..eb14abb 100644 --- a/src/q4-refactor/components/TaskFilter.tsx +++ b/src/q4-refactor/components/TaskFilter.tsx @@ -13,3 +13,43 @@ * - ラベル: "すべて" / "未完了" / "完了" * - 現在選択中のフィルターは fontWeight: "bold" */ + +import { Filter } from "../types"; + +// フィルターボタン +// 要件: +// - 3つのボタン: data-testid="filter-all" / "filter-active" / "filter-completed" +// - ラベル: "すべて" / "未完了" / "完了" +const FILTER_OPTIONS: { value: Filter; label: string; testId: string }[] = [ + { value: "all", label: "すべて", testId: "filter-all" }, + { value: "active", label: "未完了", testId: "filter-active" }, + { value: "completed", label: "完了", testId: "filter-completed" }, +]; + +// +interface TaskFilterProps { + current: Filter; + onChange: (filter: Filter) => void; +} + +const TaskFilter: React.FC = ({ current, onChange }) => { + return ( +
+ {FILTER_OPTIONS.map(({ value, label, testId }) => ( + + ))} +
+ ); +}; + +export {TaskFilter}; +export default TaskFilter; \ No newline at end of file diff --git a/src/q4-refactor/components/TaskForm.tsx b/src/q4-refactor/components/TaskForm.tsx index 875fa16..2121302 100644 --- a/src/q4-refactor/components/TaskForm.tsx +++ b/src/q4-refactor/components/TaskForm.tsx @@ -17,3 +17,38 @@ * - * - 追加後、input は空文字、select は "medium" にリセット */ +import { useState } from "react"; +import { Priority } from "../types"; + +interface TaskFormProps { + onAdd: (title: string, priority: Priority) => void; +} + +const TaskForm: React.FC = ({ onAdd }) => { + const [title, setTitle] = useState(""); + const [priority, setPriority] = useState("medium"); + + const handleAdd = () => { + if (!title.trim()) return; + onAdd(title.trim(), priority); + + //要件: - 追加後、input は空文字、select は "medium" にリセット + setTitle(""); + setPriority("medium"); + }; + + return ( +
+ setTitle(e.target.value)} placeholder="タスク入力" /> + + +
+ ); +}; + +export {TaskForm}; +export default TaskForm; diff --git a/src/q4-refactor/components/TaskItem.tsx b/src/q4-refactor/components/TaskItem.tsx index a856a69..ceff0a4 100644 --- a/src/q4-refactor/components/TaskItem.tsx +++ b/src/q4-refactor/components/TaskItem.tsx @@ -15,3 +15,25 @@ * - 優先度: data-testid={`priority-${task.id}`} → [{task.priority}] * - 削除ボタン: data-testid={`delete-${task.id}`} */ + +import { Task } from "../types"; + +interface TaskItemProps { + task: Task; + onToggle: (id: number) => void; + onDelete: (id: number) => void; +} + +const TaskItem: React.FC = ({ task, onToggle, onDelete }) => { + return ( +
  • + onToggle(task.id)} /> + {task.title} + [{task.priority}] + +
  • + ); +}; + +export {TaskItem} +export default TaskItem; diff --git a/src/q4-refactor/components/TaskList.tsx b/src/q4-refactor/components/TaskList.tsx index 667e20b..20be899 100644 --- a/src/q4-refactor/components/TaskList.tsx +++ b/src/q4-refactor/components/TaskList.tsx @@ -12,3 +12,24 @@ * -
      で囲む * - 各タスクを TaskItem で表示 */ +import { Task } from "../types"; +import TaskItem from "./TaskItem"; + +interface TaskListProps { + tasks: Task[]; + onToggle: (id: number) => void; + onDelete: (id: number) => void; +} + +const TaskList: React.FC = ({ tasks, onToggle, onDelete }) => { + return ( +
        + {tasks.map((task) => ( + + ))} +
      + ); +}; + +export {TaskList} +export default TaskList; diff --git a/src/q4-refactor/types.ts b/src/q4-refactor/types.ts index 5b586f2..ac792f7 100644 --- a/src/q4-refactor/types.ts +++ b/src/q4-refactor/types.ts @@ -6,3 +6,13 @@ * - Filter: "all" | "active" | "completed" * - Task: { id, title, completed, priority } */ + +export type Priority = "high" | "medium" | "low"; +export type Filter = "all" | "active" | "completed"; + +export interface Task { + id: number; + title: string; + completed: boolean; + priority: Priority; +} \ No newline at end of file