diff --git a/backend_training/app/src/config.php b/backend_training/app/src/config.php index b36c397..6cd9fdf 100644 --- a/backend_training/app/src/config.php +++ b/backend_training/app/src/config.php @@ -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', diff --git a/backend_training/app/src/index.php b/backend_training/app/src/index.php index b5e3d4d..9c75d9d 100644 --- a/backend_training/app/src/index.php +++ b/backend_training/app/src/index.php @@ -31,6 +31,9 @@ 'DELETE' => [ // TODO: 他のエンドポイントを追加 '#^/todos\?id=(\d+)$#' => 'handleDeleteTodo', + ], + 'OPTIONS' => [ + '#^.*$#' => 'handleOptions' ] ]; @@ -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; +} diff --git a/frontend_training/package-lock.json b/frontend_training/package-lock.json index 86f0bc6..4452e5e 100644 --- a/frontend_training/package-lock.json +++ b/frontend_training/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", + "swr": "^2.3.2", "uuid": "^11.1.0" }, "devDependencies": { @@ -1644,6 +1645,15 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/esbuild": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", @@ -2667,6 +2677,19 @@ "node": ">=8" } }, + "node_modules/swr": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.2.tgz", + "integrity": "sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2753,6 +2776,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/frontend_training/package.json b/frontend_training/package.json index 4f96718..2d1fa52 100644 --- a/frontend_training/package.json +++ b/frontend_training/package.json @@ -12,6 +12,7 @@ "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", + "swr": "^2.3.2", "uuid": "^11.1.0" }, "devDependencies": { diff --git a/frontend_training/src/App.tsx b/frontend_training/src/App.tsx index afcd568..c18c553 100644 --- a/frontend_training/src/App.tsx +++ b/frontend_training/src/App.tsx @@ -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([]); const [todo, setTodo] = useState(""); - 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 ( @@ -68,23 +77,23 @@ function App() { ); diff --git a/frontend_training/src/EditTodo.tsx b/frontend_training/src/EditTodo.tsx index 7c2416e..e42f0cd 100644 --- a/frontend_training/src/EditTodo.tsx +++ b/frontend_training/src/EditTodo.tsx @@ -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 = ({ todo, - updateTodo, + updateTodoDetails, cancelEdit, }: EditTodoProps) => { - const [editText, setEditText] = useState(todo.text); + const [editText, setEditText] = useState(todo.title); + const [editName, setEditName] = useState(todo.name); return (
@@ -20,7 +21,17 @@ export const EditTodo: React.FC = ({ onChange={(e) => setEditText(e.target.value)} placeholder="TODOを編集" /> - + +
); diff --git a/frontend_training/src/hooks/useCreateTodo.ts b/frontend_training/src/hooks/useCreateTodo.ts new file mode 100644 index 0000000..b9a0b37 --- /dev/null +++ b/frontend_training/src/hooks/useCreateTodo.ts @@ -0,0 +1,26 @@ +import { mutate } from "swr"; +import { Todo, API_URL } from "../models/Todo"; + +export function useCreateTodo() { + async function createTodo(newTodo: Omit) { + 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 }; +} diff --git a/frontend_training/src/hooks/useDeleteTodo.ts b/frontend_training/src/hooks/useDeleteTodo.ts new file mode 100644 index 0000000..12c5edb --- /dev/null +++ b/frontend_training/src/hooks/useDeleteTodo.ts @@ -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 }; +} diff --git a/frontend_training/src/hooks/useList.ts b/frontend_training/src/hooks/useList.ts new file mode 100644 index 0000000..37bc124 --- /dev/null +++ b/frontend_training/src/hooks/useList.ts @@ -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(API_URL, fetcher); + + const processedData = data?.data.map((todo) => ({ + ...todo, + isEdit: false, + })); + + return processedData; +} diff --git a/frontend_training/src/hooks/useUpdateTodo.ts b/frontend_training/src/hooks/useUpdateTodo.ts new file mode 100644 index 0000000..bc0e69a --- /dev/null +++ b/frontend_training/src/hooks/useUpdateTodo.ts @@ -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 }; +} diff --git a/frontend_training/src/models/Todo.ts b/frontend_training/src/models/Todo.ts new file mode 100644 index 0000000..6d3a631 --- /dev/null +++ b/frontend_training/src/models/Todo.ts @@ -0,0 +1,10 @@ +export type name = `pending` | `completed` | `active`; + +export const API_URL = "http://localhost/todos"; + +export type Todo = { + id: string; + title: string; + name: name; + isEdit: boolean; +}; \ No newline at end of file diff --git a/frontend_training/src/utils/convertNameToCompleted.ts b/frontend_training/src/utils/convertNameToCompleted.ts new file mode 100644 index 0000000..49b729d --- /dev/null +++ b/frontend_training/src/utils/convertNameToCompleted.ts @@ -0,0 +1,27 @@ +import { name } from "../models/Todo"; + +export function convertNameToCompleted(name: name): string { + switch (name) { + case "pending": + return "1"; + case "completed": + return "2"; + case "active": + return "3"; + default: + throw new Error(`Invalid name: ${name}`); + } +} + +export function convertNameToJa(name: name): string { + switch (name) { + case "pending": + return "未完了"; + case "completed": + return "完了"; + case "active": + return "進行中"; + default: + throw new Error(`Invalid name: ${name}`); + } +}