-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1a83275
commit f13080f
Showing
7 changed files
with
281 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
import { | ||
GoalWithIdAndUserData, | ||
ReactionType, | ||
ReactionTypeMap, | ||
} from "@/types/types"; | ||
import { updateReaction } from "@/utils/API/Reaction/updateReaction"; | ||
import { useUser } from "@/utils/UserContext"; | ||
import { Fade, Tooltip } from "@mui/material"; | ||
import React, { useEffect, useState } from "react"; | ||
import { showSnackBar } from "../SnackBar/SnackBar"; | ||
|
||
export interface reactionValue { | ||
icon: string; | ||
count?: number; | ||
} | ||
|
||
const validReactionTypes = [ | ||
...Object.keys(ReactionType.success), | ||
...Object.keys(ReactionType.failed), | ||
]; | ||
|
||
export const Reaction = ({ | ||
resultType, | ||
result, | ||
}: { | ||
resultType?: "success" | "failed" | "pending"; | ||
result: GoalWithIdAndUserData; | ||
}) => { | ||
const goalId = result.goalId; | ||
|
||
const { user } = useUser(); | ||
|
||
const [isReacted, setIsReacted] = useState(""); | ||
const [isOnceClicked, setIsOnceClicked] = useState(true); | ||
const [reactionList, setReactionList] = useState<{ | ||
[key: string]: reactionValue; | ||
}>({}); | ||
|
||
useEffect(() => { | ||
// 絵文字の種類を定義 | ||
let emojiList: { [key: string]: string } = {}; | ||
if (resultType === "success") { | ||
emojiList = ReactionType.success; | ||
// keyをreactionListのkey、emojiListをそれぞれのiconに入れる | ||
const updatedReactionList = Object.keys(emojiList).reduce( | ||
(acc: { [key: string]: reactionValue }, key) => { | ||
acc[key] = { icon: emojiList[key], count: 0 }; | ||
return acc; | ||
}, | ||
{} as { [key: string]: reactionValue } | ||
); | ||
setReactionList(updatedReactionList); | ||
} else if (resultType === "failed") { | ||
emojiList = ReactionType.failed; | ||
const updatedReactionList = Object.keys(emojiList).reduce( | ||
(acc: { [key: string]: reactionValue }, key) => { | ||
acc[key] = { icon: emojiList[key], count: 0 }; | ||
return acc; | ||
}, | ||
{} as { [key: string]: reactionValue } | ||
); | ||
setReactionList(updatedReactionList); | ||
} | ||
|
||
setTimeout(() => { | ||
setIsOnceClicked(false); | ||
}, 1000); | ||
}, []); | ||
|
||
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); | ||
} | ||
}); | ||
} | ||
}, [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 (error) { | ||
showSnackBar({ | ||
message: "リアクションに失敗しました。", | ||
type: "warning", | ||
}); | ||
} | ||
}; | ||
|
||
return ( | ||
<> | ||
{(resultType === "success" || resultType === "failed") && ( | ||
<div | ||
style={{ | ||
display: "flex", | ||
justifyContent: "flex-end", | ||
margin: "5px 8px 3px", | ||
}} | ||
> | ||
<Tooltip | ||
title={ | ||
user?.loginType === "Guest" | ||
? "ログインしてリアクションしよう!" | ||
: !user?.isMailVerified | ||
? "メール認証してリアクションしよう!" | ||
: resultType === "success" | ||
? "お祝いしよう!" | ||
: "応援しよう!" | ||
} | ||
slots={{ | ||
transition: Fade, | ||
}} | ||
slotProps={{ | ||
transition: { timeout: 400 }, | ||
}} | ||
placement="left" | ||
arrow | ||
open={!isOnceClicked && !isReacted} | ||
sx={{ zIndex: 1 }} | ||
> | ||
<div style={{ display: "flex", alignItems: "center" }}> | ||
{Object.entries(reactionList).map(([key, reaction]) => ( | ||
<React.Fragment key={key}> | ||
<button | ||
key={key} | ||
style={{ | ||
minWidth: "0", | ||
padding: "0", | ||
margin: "0 5px", // アイコンの間隔を調整 | ||
border: "none", | ||
backgroundColor: "transparent", | ||
cursor: "pointer", | ||
}} | ||
onClick={() => handleReaction(key)} | ||
> | ||
<span | ||
style={{ | ||
fontSize: "28px", | ||
lineHeight: "1", | ||
filter: isReacted === key ? "none" : "grayscale(1)", | ||
transition: "0.3s", | ||
}} | ||
> | ||
{reaction.icon} | ||
</span> | ||
</button> | ||
<span>{reaction.count}</span> | ||
</React.Fragment> | ||
))} | ||
</div> | ||
</Tooltip> | ||
</div> | ||
)} | ||
</> | ||
); | ||
}; | ||
|
||
// TODO: 押したときにエフェクト | ||
// TODO: トップページ、h1, h2だけアニメーション無しでもいいかも | ||
// TODO: z-index |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}; |