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
9 changes: 9 additions & 0 deletions backend_training/app/src/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

header('Content-Type: application/json');

$allowed_origins = [
'http://localhost:5173',
'http://127.0.0.1:5173'
];

if (isset($_SERVER['HTTP_ORIGIN']) && in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins, true)) {
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
}

// データベース接続情報
$DB_CONFIG = [
'host' => getenv('DB_HOST') ?: 'db',
Expand Down
16 changes: 16 additions & 0 deletions backend_training/app/src/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
'DELETE' => [
// TODO: 他のエンドポイントを追加
'#^/todos\?id=(\d+)$#' => 'handleDeleteTodo',
],
'OPTIONS' => [
'#^.*$#' => 'handleOptions'
]
];

Expand Down Expand Up @@ -341,3 +344,16 @@ function handleDeleteTodo(PDO $pdo): void
}
exit;
}

/**
* OPTIONS リクエストを処理する
* CORSヘッダーを設定して 200 OK レスポンスを返す
*
* @return void
*/
function handleOptions(): void
{
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
exit;
}
32 changes: 32 additions & 0 deletions frontend_training/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend_training/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"swr": "^2.3.2",
"uuid": "^11.1.0"
},
"devDependencies": {
Expand Down
113 changes: 61 additions & 52 deletions frontend_training/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,68 @@
import { useState } from "react";
import "./App.css";
import { v4 as uuid } from "uuid";
import { EditTodo } from "./EditTodo";

export type Todo = {
id: string;
text: string;
isComplete: boolean;
isEdit: boolean;
};
import { useList } from "./hooks/useList";
import { useCreateTodo } from "./hooks/useCreateTodo";
import { useDeleteTodo } from "./hooks/useDeleteTodo";
import { useUpdateTodo } from "./hooks/useUpdateTodo";
import { convertNameToCompleted } from "./utils/convertNameToCompleted";
import { name } from "./models/Todo";
import { convertNameToJa } from "./utils/convertNameToCompleted";

function App() {
const [listTodo, setListTodo] = useState<Todo[]>([]);
const [todo, setTodo] = useState<string>("");

const addTodo = () => {
const listTodo = useList();
const { createTodo } = useCreateTodo();
const { deleteTodo } = useDeleteTodo();
const { updateTodo } = useUpdateTodo();

// 編集状態を管理する state
const [editingTodos, setEditingTodos] = useState<{ [key: string]: boolean }>(
{}
);

const addTodo = async () => {
if (todo.trim() !== "") {
setListTodo([
...listTodo,
{ id: uuid(), text: todo, isComplete: false, isEdit: false },
]);
setTodo("");
try {
await createTodo({ title: todo });
setTodo("");
} catch (error) {
console.error("Todo の作成に失敗しました", error);
}
}
};

const completeTodo = (id: string) => {
setListTodo(
listTodo.map((item) =>
item.id === id ? { ...item, isComplete: true } : item
)
);
const completeTodo = async (id: string) => {
try {
await deleteTodo(id);
} catch (error) {
console.error("Todo の削除に失敗しました", error);
}
};

const editTodo = (id: string) => {
setListTodo(
listTodo.map((item) =>
item.id === id ? { ...item, isEdit: true } : item
)
);
setEditingTodos((prev) => ({ ...prev, [id]: true }));
};

const updateTodo = (id: string, newText: string) => {
setListTodo(
listTodo.map((item) =>
item.id === id ? { ...item, text: newText, isEdit: false } : item
)
);
const updateTodoDetails = async (
id: string,
newTitle: string,
newName: name
) => {
try {
await updateTodo(id, {
title: newTitle,
completed: convertNameToCompleted(newName),
});
setEditingTodos((prev) => ({ ...prev, [id]: false }));
} catch (error) {
console.error("Todo の更新に失敗しました", error);
}
};

const cancelEdit = (id: string) => {
setListTodo(
listTodo.map((item) =>
item.id === id ? { ...item, isEdit: false } : item
)
);
setEditingTodos((prev) => ({ ...prev, [id]: false }));
};

return (
Expand All @@ -68,23 +77,23 @@ function App() {
<button onClick={addTodo}>追加</button>
</div>
<ul>
{listTodo
.filter((item) => !item.isComplete)
.map((item) => (
<div key={item.id}>
<div className="container">
<li onClick={() => editTodo(item.id)}>{item.text}</li>
<button onClick={() => completeTodo(item.id)}>完了</button>
{listTodo?.map((item) => (
<div key={item.id}>
<div className="container">
<div className="container" onClick={() => editTodo(item.id)}>
<li>{item.title}</li>:<div>{convertNameToJa(item.name)}</div>
</div>
{item.isEdit && (
<EditTodo
todo={item}
updateTodo={updateTodo}
cancelEdit={cancelEdit}
/>
)}
<button onClick={() => completeTodo(item.id)}>削除</button>
</div>
))}
{editingTodos[item.id] && (
<EditTodo
todo={item}
updateTodoDetails={updateTodoDetails}
cancelEdit={cancelEdit}
/>
)}
</div>
))}
</ul>
</>
);
Expand Down
21 changes: 16 additions & 5 deletions frontend_training/src/EditTodo.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { useState } from "react";
import { Todo } from "./App";
import { Todo, name } from "./models/Todo";

type EditTodoProps = {
todo: Todo;
updateTodo: (id: string, newText: string) => void;
updateTodoDetails: (id: string, newText: string, newName: name) => void;
cancelEdit: (id: string) => void;
};
export const EditTodo: React.FC<EditTodoProps> = ({
todo,
updateTodo,
updateTodoDetails,
cancelEdit,
}: EditTodoProps) => {
const [editText, setEditText] = useState(todo.text);
const [editText, setEditText] = useState(todo.title);
const [editName, setEditName] = useState<name>(todo.name);

return (
<div className="container">
Expand All @@ -20,7 +21,17 @@ export const EditTodo: React.FC<EditTodoProps> = ({
onChange={(e) => setEditText(e.target.value)}
placeholder="TODOを編集"
/>
<button onClick={() => updateTodo(todo.id, editText)}>更新</button>
<select
value={editName}
onChange={(e) => setEditName(e.target.value as name)}
>
<option value="pending">未完了</option>
<option value="completed">完了</option>
<option value="active">進行中</option>
</select>
<button onClick={() => updateTodoDetails(todo.id, editText, editName)}>
更新
</button>
<button onClick={() => cancelEdit(todo.id)}>キャンセル</button>
</div>
);
Expand Down
26 changes: 26 additions & 0 deletions frontend_training/src/hooks/useCreateTodo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { mutate } from "swr";
import { Todo, API_URL } from "../models/Todo";

export function useCreateTodo() {
async function createTodo(newTodo: Omit<Todo, "id" | "name" | "isEdit">) {
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newTodo),
});

if (!response.ok) {
throw new Error("Failed to create a new todo");
}

const result = await response.json();

mutate(API_URL);

return result;
}

return { createTodo };
}
18 changes: 18 additions & 0 deletions frontend_training/src/hooks/useDeleteTodo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { mutate } from "swr";
import { API_URL } from "../models/Todo";

export function useDeleteTodo() {
async function deleteTodo(id: string) {
const response = await fetch(`${API_URL}?id=${id}`, {
method: "DELETE",
});

if (!response.ok) {
throw new Error("Failed to delete todo");
}

mutate(API_URL);
}

return { deleteTodo };
}
22 changes: 22 additions & 0 deletions frontend_training/src/hooks/useList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import useSWR from "swr";
import { Todo, API_URL } from "../models/Todo";

type jsonData = {
status: string;
data: Todo[];
};

async function fetcher(key: string) {
return fetch(key).then((res) => res.json());
}

export function useList() {
const { data } = useSWR<jsonData>(API_URL, fetcher);

const processedData = data?.data.map((todo) => ({
...todo,
isEdit: false,
}));

return processedData;
}
26 changes: 26 additions & 0 deletions frontend_training/src/hooks/useUpdateTodo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { mutate } from "swr";
import { API_URL } from "../models/Todo";

export function useUpdateTodo() {
async function updateTodo(
id: string,
updatedData: { title: string; completed: string }
) {
const response = await fetch(`${API_URL}?id=${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updatedData),
});

if (!response.ok) {
throw new Error("Failed to update todo");
}

// SWR のキャッシュを更新し、最新のデータを反映
mutate(API_URL);
}

return { updateTodo };
}
Loading