Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions src/q1-type-definition/UserCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Role, string> = {
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 (
<div>
<h2 data-testid="name">{name}</h2>
<p data-testid="age">年齢: {age}</p>
<p data-testid="role">権限: {roleLabel[role]}</p>
{email && (
<p data-testid="email">Email: {email}</p>
)}

<ul data-testid="skills">
{skills.map((skill, index) => (
<li key={index}>{skill}</li>
))}
</ul>
{email && onContact && (
<button data-testid="contact-btn"
onClick = {() => onContact(email)}
>
コンタクト
</button>
)
}
</div>
);
};
// ↑↑↑ ここまで ↑↑↑
42 changes: 41 additions & 1 deletion src/q2-hooks-basics/UserList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,49 @@ export const fetchUsers = async (): Promise<User[]> => {

// ↓↓↓ ここに UserList コンポーネントを実装してください ↓↓↓

// 3つの状態
type State =
| { status: "loading" }
| { status: "error" }
| { status: "success"; data: User[]};


export const UserList: React.FC = () => {
// TODO: 実装してください
return null;
const [state, setState] = useState<State>({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 <p data-testid="loading">読み込み中...</p>
}

if(state.status === "error") {
return <p data-testid="error">エラーが発生しました</p>
}

if(state.data.length === 0 ){
return <p data-testid="empty">ユーザーがいません</p>
}

return (
<ul data-testid="user-list">
{state.data.map((user) => (
<li key={user.id} data-testid={`user-${user.id}`}>{user.name} - {user.department}</li>
))}
</ul>
);
};

// ↑↑↑ ここまで ↑↑↑
101 changes: 100 additions & 1 deletion src/q3-custom-hook/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,106 @@ export function useForm<T extends Record<string, any>>(
rules: ValidationRules<T> = {}
): UseFormReturn<T> {
// TODO: 実装してください
throw new Error("Not implemented");
const [values, setValues] = useState<T> ({...initialValues});
const [errors, setErrors] = useState<Partial<Record<keyof T,string>>> ({});
const [touched, setTouchedState] = useState<Partial<Record<keyof T, boolean>>> ({});


const validateField = useCallback(
<K extends keyof T>(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<Record<keyof T, string>>,
field: keyof T,
error: string | null
): Partial<Record<keyof T, string>> => {
const next = { ...prev };
if(error != null ){
next[field] = error;
} else {
delete next[field];
}
return next;
},
[]
);


const setValue = useCallback(
<K extends keyof T>(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(
<K extends keyof T>(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<Record<keyof T, string>> = {};

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,
};
}

// ↑↑↑ ここまで ↑↑↑
50 changes: 47 additions & 3 deletions src/q4-refactor/TaskManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Task[]>([]);
const [filter, setFilter] = useState<Filter>("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 (
<div>
<h1>タスク管理</h1>
<div data-testid="stats">
全体: {stats.total} / 未完了: {stats.active} / 完了: {stats.completed}
</div>
<TaskForm onAdd={addTask} />
<TaskFilter current={filter} onChange={setFilter} />
<TaskList tasks={filteredTasks} onToggle={toggleTask} onDelete={deleteTask} />
</div>
);
};

export default TaskManager;
40 changes: 40 additions & 0 deletions src/q4-refactor/components/TaskFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TaskFilterProps> = ({ current, onChange }) => {
return (
<div data-testid="task-filter">
{FILTER_OPTIONS.map(({ value, label, testId }) => (
<button
key={value}
data-testid={testId}
onClick={() => onChange(value)}
style={{
fontWeight: current === value ? "bold" : "normal",
}}
>
{label}
</button>
))}
</div>
);
};

export {TaskFilter};
export default TaskFilter;
35 changes: 35 additions & 0 deletions src/q4-refactor/components/TaskForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,38 @@
* - <button data-testid="add-btn">追加</button>
* - 追加後、input は空文字、select は "medium" にリセット
*/
import { useState } from "react";
import { Priority } from "../types";

interface TaskFormProps {
onAdd: (title: string, priority: Priority) => void;
}

const TaskForm: React.FC<TaskFormProps> = ({ onAdd }) => {
const [title, setTitle] = useState("");
const [priority, setPriority] = useState<Priority>("medium");

const handleAdd = () => {
if (!title.trim()) return;
onAdd(title.trim(), priority);

//要件: - 追加後、input は空文字、select は "medium" にリセット
setTitle("");
setPriority("medium");
};

return (
<div data-testid="task-form">
<input data-testid="title-input" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="タスク入力" />
<select data-testid="priority-select" value={priority} onChange={(e) => setPriority(e.target.value as Priority)}>
<option value="high">高</option>
<option value="medium">中</option>
<option value="low">低</option>
</select>
<button data-testid="add-btn" onClick={handleAdd}>追加</button>
</div>
);
};

export {TaskForm};
export default TaskForm;
22 changes: 22 additions & 0 deletions src/q4-refactor/components/TaskItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TaskItemProps> = ({ task, onToggle, onDelete }) => {
return (
<li data-testid={`task-${task.id}`}>
<input type="checkbox" data-testid={`toggle-${task.id}`} checked={task.completed} onChange={() => onToggle(task.id)} />
<span data-testid={`title-${task.id}`} style={{ textDecoration: task.completed ? "line-through" : "none" }}>{task.title}</span>
<span data-testid={`priority-${task.id}`}>[{task.priority}]</span>
<button data-testid={`delete-${task.id}`} onClick={() => onDelete(task.id)}>削除</button>
</li>
);
};

export {TaskItem}
export default TaskItem;
Loading