Skip to content

Commit

Permalink
Merge pull request #165 from MurakawaTakuya/feat/107-edit-goal-and-post
Browse files Browse the repository at this point in the history
目標と投稿の編集機能を実装
  • Loading branch information
MurakawaTakuya authored Jan 17, 2025
2 parents e0070a1 + 45f2dd5 commit 70f98c3
Show file tree
Hide file tree
Showing 20 changed files with 532 additions and 78 deletions.
2 changes: 1 addition & 1 deletion functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import resultRouter from "./routers/resultRouter";
import userRouer from "./routers/userRouter";

const app = express();
app.set("trust proxy", true);
app.set("trust proxy", true); // trueにしないと全ユーザーでlimitが共通になる
app.use(helmet());
app.use(cors());
app.use(express.json());
Expand Down
4 changes: 3 additions & 1 deletion functions/src/routers/resultRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ const getResults = async (
onlyFinished = false
) => {
let baseQuery = db.collection("goal").limit(limit).offset(offset);
let failedBaseQuery = db.collection("goal").limit(100).offset(0);

if (userId) {
const userDoc = await getUserFromId(userId);
if (!userDoc.exists) {
return res.status(404).json({ message: "User not found" });
}
baseQuery = baseQuery.where("userId", "==", userId);
failedBaseQuery = failedBaseQuery.where("userId", "==", userId);
}

if (onlyPending && onlyFinished) {
Expand Down Expand Up @@ -73,7 +75,7 @@ const getResults = async (
};
}

const failedSnapshot = await baseQuery
const failedSnapshot = await failedBaseQuery
.where("post", "==", null)
.where("deadline", "<=", now)
.where("deadline", ">", earliestSubmittedAt)
Expand Down
6 changes: 3 additions & 3 deletions src/Components/GoalModal/CreateGoalModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ export default function CreateGoalModal({
defaultText?: string;
defaultDeadline?: string;
}) {
const [text, setText] = useState("");
const [deadline, setDeadline] = useState("");

const { user } = useUser();
const addResult = useAddGoal();

const [text, setText] = useState("");
const [deadline, setDeadline] = useState("");

const resetDeadline = () => {
// 次の日の23時に設定
const nextDay = new Date();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,34 @@ export default function DeleteGoalModal({ goalId }: { goalId: string }) {
const deleleGoal = useDeleteGoal();
const [open, setOpen] = useState(false);

const handleDeleteGoal = async () => {
const response = await fetch(`${functionsEndpoint}/goal/${goalId}`, {
method: "DELETE",
headers: {
"X-Firebase-AppCheck": appCheckToken,
"Content-Type": "application/json",
},
});
const handleDelete = async () => {
try {
const response = await fetch(`${functionsEndpoint}/goal/${goalId}`, {
method: "DELETE",
headers: {
"X-Firebase-AppCheck": appCheckToken,
"Content-Type": "application/json",
},
});

if (!response.ok) {
if (!response.ok) {
showSnackBar({
message: "目標の削除に失敗しました",
type: "warning",
});
} else {
setOpen(false);
showSnackBar({
message: "目標を削除しました",
type: "success",
});
deleleGoal(goalId);
}
} catch {
showSnackBar({
message: "目標の削除に失敗しました",
type: "warning",
});
} else {
setOpen(false);
showSnackBar({
message: "目標を削除しました",
type: "success",
});
deleleGoal(goalId);
}
};

Expand Down Expand Up @@ -66,11 +73,7 @@ export default function DeleteGoalModal({ goalId }: { goalId: string }) {
>
キャンセル
</JoyButton>
<Button
variant="contained"
color="primary"
onClick={handleDeleteGoal}
>
<Button variant="contained" color="error" onClick={handleDelete}>
削除
</Button>
</Stack>
Expand Down
184 changes: 184 additions & 0 deletions src/Components/GoalModal/EditGoalModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"use client";
import { handleUpdateGoalError, updateGoal } from "@/utils/API/Goal/updateGoal";
import { useResults } from "@/utils/ResultContext";
import { useUser } from "@/utils/UserContext";
import EditIcon from "@mui/icons-material/Edit";
import {
Button,
DialogContent,
DialogTitle,
Input,
Modal,
ModalDialog,
Typography,
} from "@mui/joy";
import Stack from "@mui/material/Stack";
import { useState } from "react";
import { showSnackBar } from "../SnackBar/SnackBar";

export default function EditGoalModal({
goalId,
resultType,
}: {
goalId: string;
resultType: "success" | "failed" | "pending";
}) {
const { user } = useUser();
const {
pendingResults,
setPendingResults,
successResults,
setSuccessResults,
} = useResults();

let defaultText = "";
let defaultDeadline = "";
if (resultType === "pending") {
defaultText =
pendingResults.find((result) => result.goalId === goalId)?.text || "";
defaultDeadline =
pendingResults.find((result) => result.goalId === goalId)?.deadline || "";
} else if (resultType === "success") {
defaultText =
successResults.find((result) => result.goalId === goalId)?.text || "";
defaultDeadline =
successResults.find((result) => result.goalId === goalId)?.deadline || "";
}

const defaultDate = new Date(defaultDeadline);
const localDate = new Date(
defaultDate.getTime() - defaultDate.getTimezoneOffset() * 60000
);

const [open, setOpen] = useState(false);
const [text, setText] = useState(defaultText);
const [deadline, setDeadline] = useState(
localDate.toISOString().slice(0, 16)
);

if (!defaultText || !defaultDeadline) {
return;
}

const handleEdit = async (event: React.FormEvent) => {
event.preventDefault();

if (deadline < new Date().toISOString().slice(0, 16)) {
showSnackBar({
message: "過去の時間を設定することはできません",
type: "warning",
});
return;
}

const putData = {
text,
deadline: new Date(deadline),
};

try {
await updateGoal(goalId, putData);

showSnackBar({
message: "目標を編集しました",
type: "success",
});

// resultContextを更新
if (resultType === "pending") {
setPendingResults((prev) =>
prev.map((result) =>
result.goalId === goalId ? { ...result, text, deadline } : result
)
);
} else {
setSuccessResults((prev) =>
prev.map((result) =>
result.goalId === goalId ? { ...result, text, deadline } : result
)
);
}

setOpen(false);
} catch (error: unknown) {
const message = handleUpdateGoalError(error);
showSnackBar({
message,
type: "warning",
});
}
};

// 失敗の時は表示しない
if (resultType === "failed") {
return;
}

return (
<>
<EditIcon
onClick={() => setOpen(true)}
sx={{ cursor: "pointer", fontSize: "23px" }}
/>

<Modal
open={open}
onClose={() => setOpen(false)}
keepMounted
disablePortal={false}
>
<ModalDialog
aria-labelledby="create-goal-title"
aria-describedby="create-goal-description"
sx={{ width: "90%", maxWidth: 400 }}
>
<DialogTitle>目標を編集</DialogTitle>
<DialogContent>
編集したいテキストと期限を入れてください
</DialogContent>
<form onSubmit={handleEdit}>
<Stack spacing={2}>
<Input
placeholder="目標内容"
value={text}
onChange={(e) => setText(e.target.value)}
required
/>
<Input
type="datetime-local"
value={deadline}
onChange={(e) => setDeadline(e.target.value)}
required
/>
<Typography color="danger">
期限が1時間以内の目標は削除できなくなります
</Typography>
<Stack direction="row" spacing={1} justifyContent="flex-end">
<Button
variant="plain"
color="neutral"
onClick={() => setOpen(false)}
>
キャンセル
</Button>
<Button
type="submit"
variant="solid"
color="success"
disabled={
!user ||
user?.loginType === "Guest" ||
!user?.isMailVerified
}
endDecorator={<EditIcon />}
>
編集
</Button>
</Stack>
</Stack>
</form>
</ModalDialog>
</Modal>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,15 @@ import { styled } from "@mui/material/styles";
import React, { ChangeEvent, useState } from "react";

export default function CreatePostModal({ goalId }: { goalId: string }) {
const { user } = useUser();
const addPost = useAddPost();

const [open, setOpen] = useState(false);
const [text, setText] = useState<string>("");
const [image, setImage] = useState<File | null>(null);
const [progress, setProgress] = useState<number>(100);
const [fileName, setFileName] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const { user } = useUser();
const addPost = useAddPost();

const handleTextChange = (event: ChangeEvent<HTMLInputElement>) => {
setText(event.target.value);
};

const handleImageChange = async (event: ChangeEvent<HTMLInputElement>) => {
const selectedFile = event.target.files?.[0];
Expand Down Expand Up @@ -189,7 +186,7 @@ export default function CreatePostModal({ goalId }: { goalId: string }) {
<Input
type="text"
value={text}
onChange={handleTextChange}
onChange={(e) => setText(e.target.value)}
placeholder="投稿コメントを入力して下さい"
/>
<MuiButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,34 @@ export default function DeletePostModal({
const deletePost = useDeletePost();
const [open, setOpen] = useState(false);

const handleDeletePost = async () => {
const response = await fetch(`${functionsEndpoint}/post/${goalId}`, {
method: "DELETE",
headers: {
"X-Firebase-AppCheck": appCheckToken,
"Content-Type": "application/json",
},
});
const handleDelete = async () => {
try {
const response = await fetch(`${functionsEndpoint}/post/${goalId}`, {
method: "DELETE",
headers: {
"X-Firebase-AppCheck": appCheckToken,
"Content-Type": "application/json",
},
});

if (!response.ok) {
if (!response.ok) {
showSnackBar({
message: "目標の削除に失敗しました",
type: "warning",
});
} else {
setOpen(false);
showSnackBar({
message: "目標を削除しました",
type: "success",
});
deletePost(goalId);
}
} catch {
showSnackBar({
message: "目標の削除に失敗しました",
type: "warning",
});
} else {
setOpen(false);
showSnackBar({
message: "目標を削除しました",
type: "success",
});
deletePost(goalId);
}
};

Expand Down Expand Up @@ -77,11 +84,7 @@ export default function DeletePostModal({
>
キャンセル
</JoyButton>
<Button
variant="contained"
color="primary"
onClick={handleDeletePost}
>
<Button variant="contained" color="error" onClick={handleDelete}>
削除
</Button>
</Stack>
Expand Down
Loading

0 comments on commit 70f98c3

Please sign in to comment.