diff --git a/src/Components/Progress/FailedStep.tsx b/src/Components/Progress/FailedStep.tsx index ae507df..8c4755b 100644 --- a/src/Components/Progress/FailedStep.tsx +++ b/src/Components/Progress/FailedStep.tsx @@ -13,11 +13,7 @@ export const FailedStep = ({ user: User; }) => { return ( - + diff --git a/src/Components/Progress/PendingStep.tsx b/src/Components/Progress/PendingStep.tsx index d79d0e5..89460cb 100644 --- a/src/Components/Progress/PendingStep.tsx +++ b/src/Components/Progress/PendingStep.tsx @@ -18,11 +18,7 @@ export const PendingStep = ({ const [isSubmitted, setIsSubmitted] = useState(false); return ( - + { + const goalId = result.goalId; + + const { user } = useUser(); + + const [isReacted, setIsReacted] = useState(""); + const [isOnceClicked, setIsOnceClicked] = useState(true); + const [reactionList, setReactionList] = useState< + Record + >({}); + + useEffect(() => { + // 絵文字の種類を定義 + let emojiList: Record = {}; + if (resultType === "success") { + emojiList = ReactionType.success; + const updatedReactionList = Object.keys(emojiList).reduce( + (acc: Record, key) => { + acc[key] = { icon: emojiList[key], count: 0 }; + return acc; + }, + {} as Record + ); + setReactionList(updatedReactionList); + } else if (resultType === "failed") { + emojiList = ReactionType.failed; + const updatedReactionList = Object.keys(emojiList).reduce( + (acc: Record, key) => { + acc[key] = { icon: emojiList[key], count: 0 }; + return acc; + }, + {} as Record + ); + setReactionList(updatedReactionList); + } + + setTimeout(() => { + setIsOnceClicked(false); + }, 1000); + }, [resultType]); + + useEffect(() => { + // リアクションのカウントを更新 + if (result.reaction) { + Object.entries(result.reaction).forEach(([userId, reactionType]) => { + if (!validReactionTypes.includes(reactionType)) { + return; + } + + if (reactionList[reactionType] === undefined) { + // successやfailedで別のリアクションが押されている場合 + return; + } + + if (reactionList[reactionType].count === undefined) { + // 初期化失敗 + return; + } + + reactionList[reactionType].count += 1; + if (user?.userId === userId) { + setIsReacted(reactionType); + setIsOnceClicked(true); + } + }); + } + }, [reactionList]); + + const handleReaction = (type: string) => { + if (user?.loginType === "Guest" || !user?.isMailVerified) { + return; + } + + // typeがReactionTypeに含まれない場合 + if (!validReactionTypes.includes(type)) { + showSnackBar({ + message: "リアクションに失敗しました。", + type: "warning", + }); + return; + } + + setIsOnceClicked(true); + + const updatedReactionList = { ...reactionList }; + if (updatedReactionList[type].count === undefined) { + return; + } + + try { + if (isReacted === type) { + // 既に同じものが押されている場合(リアクションを削除) + updateReaction(user.userId, goalId, ""); + updatedReactionList[type].count -= 1; + setIsReacted(""); + } else { + // 押されていない場合(リアクションを追加もしくは変更) + updateReaction(user.userId, goalId, type as ReactionTypeMap); + updatedReactionList[type].count += 1; // 新しいリアクションを追加 + if (isReacted && updatedReactionList[isReacted].count) { + updatedReactionList[isReacted].count -= 1; // 古いリアクションを削除 + } + setIsReacted(type); + } + } catch { + showSnackBar({ + message: "リアクションに失敗しました。", + type: "warning", + }); + } + }; + + return ( + <> + {(resultType === "success" || resultType === "failed") && ( +
+ +
+ {Object.entries(reactionList).map(([key, reaction]) => ( + + + {reaction.count} + + ))} +
+
+
+ )} + + ); +}; diff --git a/src/Components/Progress/StepperBlock.tsx b/src/Components/Progress/StepperBlock.tsx index e8e8534..17e106f 100644 --- a/src/Components/Progress/StepperBlock.tsx +++ b/src/Components/Progress/StepperBlock.tsx @@ -1,4 +1,4 @@ -import { User } from "@/types/types"; +import { GoalWithIdAndUserData } from "@/types/types"; import { getSuccessRate } from "@/utils/successRate"; import { Divider } from "@mui/joy"; import Card from "@mui/joy/Card"; @@ -8,6 +8,7 @@ import Stepper from "@mui/joy/Stepper"; import Typography, { typographyClasses } from "@mui/joy/Typography"; import { ReactNode } from "react"; import CenterIn from "../Animation/CenterIn"; +import { Reaction } from "./Reaction"; const outerBorderColors = { success: "#008c328a", @@ -17,13 +18,15 @@ const outerBorderColors = { export const StepperBlock = ({ children, - userData = null, resultType, + result, }: { children: ReactNode; - userData?: User | null; resultType?: "success" | "failed" | "pending"; + result: GoalWithIdAndUserData; }) => { + const userData = result.userData; + const successRate = userData ? getSuccessRate(userData.completed, userData.failed) : 0; @@ -119,6 +122,8 @@ export const StepperBlock = ({ > {children} + + ); diff --git a/src/Components/Progress/SuccessStep.tsx b/src/Components/Progress/SuccessStep.tsx index 060db48..227cd67 100644 --- a/src/Components/Progress/SuccessStep.tsx +++ b/src/Components/Progress/SuccessStep.tsx @@ -66,11 +66,7 @@ export const SuccessStep = ({ }); return ( - + & { submittedAt: string }; + reaction?: Record; } export interface GoalWithIdAndUserData extends Goal { @@ -36,14 +37,35 @@ export interface PostWithGoalId extends Post { goalId: string; } -export const animationTypes = [ +export const ReactionType = { + success: { + laugh: "😆", + surprised: "😲", + clap: "👏", + }, + failed: { + sad: "😢", + angry: "😠", + muscle: "💪", + }, +}; + +export type ReactionTypeMap = + | "laugh" + | "surprised" + | "clap" + | "sad" + | "angry" + | "muscle"; + +export const AnimationTypes = [ "left", "right", "center", "bottom", "top", ] as const; -export type AnimationType = (typeof animationTypes)[number]; +export type AnimationType = (typeof AnimationTypes)[number]; export interface AnimationConfigs { children: ReactNode; diff --git a/src/utils/API/Reaction/updateReaction.ts b/src/utils/API/Reaction/updateReaction.ts new file mode 100644 index 0000000..92e1b2f --- /dev/null +++ b/src/utils/API/Reaction/updateReaction.ts @@ -0,0 +1,35 @@ +import { appCheckToken, functionsEndpoint } from "@/app/firebase"; +import { ReactionTypeMap } from "@/types/types"; + +/** + * Cloud FunctionsのAPIを呼び出して、リアクションを更新する + * reactionTypeに`""`を入れるとリアクションを削除する + * + * @param {string} userId + * @param {string} goalId + * @param {string} reactionType + * @return {*} + */ +export const updateReaction = async ( + userId: string, + goalId: string, + reactionType: ReactionTypeMap | "" +) => { + const response = await fetch(`${functionsEndpoint}/reaction/${goalId}`, { + method: "PUT", + headers: { + "X-Firebase-AppCheck": appCheckToken, + "Content-Type": "application/json", + }, + body: JSON.stringify({ userId, reactionType }), + }); + + if (!response.ok) { + const status = response.status; + const data = await response.json(); + throw new Error(`Error ${status}: ${data.message}`); + } + + const data = await response.json(); + return data; +};