Skip to content

Commit

Permalink
Merge pull request #136 from MurakawaTakuya/feat/result-limit-offset
Browse files Browse the repository at this point in the history
resultのoffsetとlimitを実装
  • Loading branch information
MurakawaTakuya authored Jan 8, 2025
2 parents b263186 + c6f0c89 commit 1dcfb4a
Show file tree
Hide file tree
Showing 19 changed files with 264 additions and 88 deletions.
11 changes: 3 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,12 @@ For More information, please refer to the Top Page.
[TODO REAL](https://todo-real-c28fa.web.app/)

## Getting Started
First run this command to install required packages:

```bash
npm install
```

Then, run the development server:
Run the development server:

```bash
npm run dev
```
This includes `npm i` and `next dev`, so you don't have to care about refreshing packages.

## Learn More
To learn more about Next.js, take a look at the following resources:
Expand Down Expand Up @@ -46,7 +41,7 @@ Then, you can start the emulator by running the following command if you have ac
cd .\functions\
npm run emu
```
This command includes `firebase emulators:start` and `npx tsc --watch` which watches the files and restarts the server when the files are changed.
This command includes `npm i`, `firebase emulators:start` and `npx tsc --watch` which watches the files and restarts the server when the files are changed.

if you don't want ts-node to watch the files, just use
```
Expand Down
2 changes: 1 addition & 1 deletion functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log",
"emu": "concurrently -k -n \"TypeScript,Emulator\" -c \"cyan.bold,green.bold\" \"npx tsc --watch\" \"firebase emulators:start\""
"emu": "npm i && concurrently -k -n \"TypeScript,Emulator\" -c \"cyan.bold,green.bold\" \"npx tsc --watch\" \"firebase emulators:start\""
},
"engines": {
"node": "18"
Expand Down
7 changes: 1 addition & 6 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,7 @@ const verifyAppCheckToken = async (
// Postmanを使うためにCloud FunctionsのApp Checkは開発環境では使用しない
if (process.env.NODE_ENV === "production") {
app.use((req, res, next) => {
// /sendNotificationと/receiveTestは別の認証を使用するのでApp Checkを使用しない
if (req.path !== "/sendNotification" && req.path !== "/receiveTest") {
verifyAppCheckToken(req, res, next);
} else {
next();
}
verifyAppCheckToken(req, res, next);
});
}

Expand Down
3 changes: 2 additions & 1 deletion functions/src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ export const createTasksOnGoalCreate = onDocumentCreated(
const postData = {
message: {
token: fcmToken, // 通知を受信する端末のトークン
notification: {
data: {
title: `${marginTime}分以内に目標を完了し写真をアップロードしましょう!`,
body: goalData.text,
icon: "https://todo-real-c28fa.web.app/appIcon.svg",
},
},
};
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "npm i && next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
Expand Down
16 changes: 15 additions & 1 deletion public/messaging-sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,19 @@ const messaging = firebase.messaging();

// バックグラウンド時の通知を処理(通知自体は何もしなくても受信される)
messaging.onBackgroundMessage((payload) => {
console.log("Received background message:", payload);
try {
console.log("Received background message:", payload);

const { title, body, icon } = payload.data;
const notificationOptions = {
body,
icon,
};

self.registration.showNotification(title, notificationOptions);
} catch (error) {
console.error("Error while receiving background message:", error);
}
});

// フォアグラウンド通知は`src\utils\CloudMessaging\getNotification.ts`で処理
46 changes: 26 additions & 20 deletions src/Components/Account/LoggedInView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import NotificationButton from "@/Components/NotificationButton/NotificationButt
import { handleSignOut } from "@/utils/Auth/signOut";
import { getSuccessRate } from "@/utils/successRate";
import { useUser } from "@/utils/UserContext";
import Typography from "@mui/joy/Typography";
import Button from "@mui/material/Button";
import { styled } from "@mui/material/styles";
import Typography from "@mui/material/Typography";
import { useEffect, useState } from "react";

export const RoundedButton = styled(Button)(({ theme }) => ({
Expand Down Expand Up @@ -43,47 +43,53 @@ export default function LoggedInView() {
return (
<>
{user.loginType === "Guest" ? (
<Typography sx={{ textAlign: "center" }}>
<Typography level="body-lg" sx={{ textAlign: "center" }}>
ゲストとしてログイン中
</Typography>
) : (
<>
<Typography sx={{ textAlign: "center" }}>
<Typography level="body-lg" sx={{ textAlign: "center" }}>
ようこそ、{user.name}さん!
</Typography>
<Typography sx={{ textAlign: "center" }}>
連続達成日数: {userStats.streak}日目
</Typography>
<Typography sx={{ textAlign: "center" }}>
目標達成率: {userStats.successRate}%
</Typography>
<Typography sx={{ textAlign: "center" }}>
達成回数: {userStats.completed}
</Typography>
<div style={{ display: "flex", flexDirection: "column", gap: "3px" }}>
<Typography level="title-md" sx={{ textAlign: "center" }}>
連続達成日数: {userStats.streak}日目
</Typography>
<Typography level="title-md" sx={{ textAlign: "center" }}>
目標達成率: {userStats.successRate}%
</Typography>
<Typography level="title-md" sx={{ textAlign: "center" }}>
達成回数: {userStats.completed}
</Typography>
</div>
</>
)}

{!user.isMailVerified && (
<Typography color="error">
<Typography color="danger">
メールに届いた認証リンクを確認してください。
<br />
認証が完了するまで閲覧以外の機能は制限されます。
</Typography>
)}

{user.loginType === "Guest" && (
<Typography color="error">
<Typography color="danger">
ゲストユーザーは閲覧以外の機能は制限されます。
全ての機能を利用するにはログインが必要です。
</Typography>
)}

{/* ゲストかメール未認証の場合は名前を変更できないようにする */}
{/* ゲストかメール未認証の場合は名前の変更や通知の使用をできないようにする */}
{user.loginType !== "Guest" && user.isMailVerified && (
<div style={{ display: "flex", justifyContent: "space-evenly" }}>
<NameUpdate />
<NotificationButton />
</div>
<>
<div style={{ display: "flex", justifyContent: "space-evenly" }}>
<NameUpdate />
<NotificationButton />
</div>
<Typography color="neutral" sx={{ textAlign: "center" }}>
通知を有効にすると、目標が未達成の場合に期限の5分前に通知を送信します。
</Typography>
</>
)}

<RoundedButton variant="contained" onClick={handleSignOut}>
Expand Down
145 changes: 134 additions & 11 deletions src/Components/DashBoard/DashBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import {
fetchResult,
handleFetchResultError,
} from "@/utils/API/Result/fetchResult";
import CircularProgress from "@mui/joy/CircularProgress";
import Typography from "@mui/joy/Typography";
import LinearProgress from "@mui/material/LinearProgress";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import CenterIn from "../Animation/CenterIn";
import Progress from "../Progress/Progress";
import styles from "./DashBoard.module.scss";

Expand All @@ -26,7 +28,7 @@ export default function DashBoard({
failed?: boolean;
pending?: boolean;
orderBy?: "asc" | "desc";
} = {}) {
}) {
const [successResults, setSuccessResults] = useState<GoalWithIdAndUserData[]>(
[]
);
Expand All @@ -38,15 +40,101 @@ export default function DashBoard({
);
const [noResult, setNoResult] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [reachedBottom, setReachedBottom] = useState<boolean>(false);
const [isLoadingMore, setIsLoadingMore] = useState<boolean>(false);
const bottomRef = useRef<HTMLDivElement>(null);
const isAlreadyFetching = useRef(false);
const offset = useRef(0);
const noMore = useRef(false);

const limit = 10; // limitずつ表示

useEffect(() => {
setTimeout(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !isLoading && !noMore.current) {
setReachedBottom(true);
fetchData();
}
},
{ threshold: 1 }
);

if (bottomRef.current) {
observer.observe(bottomRef.current);
}

return () => {
if (bottomRef.current) {
observer.disconnect();
}
};
}, 1000);
}, [isLoading, noMore.current, bottomRef.current, bottomRef]);

useEffect(() => {
offset.current =
successResults.length + failedResults.length + pendingResults.length;
}, [successResults, failedResults, pendingResults]);

const fetchData = () => {
setIsLoading(true);
fetchResult({ userId, success, failed, pending })
if (noMore.current) {
return;
}
if (isAlreadyFetching.current) {
return;
} else {
isAlreadyFetching.current = true;
}
if (reachedBottom && !isLoadingMore) {
setIsLoadingMore(true);
}
fetchResult({
userId,
success,
failed,
pending,
offset: offset.current,
limit,
})
.then((data) => {
setSuccessResults(data.successResults);
setFailedResults(data.failedResults);
setPendingResults(data.pendingResults);
// 既に追加されている場合は追加しない
setSuccessResults((prev) => {
const newResults = data.successResults.filter(
(result: GoalWithIdAndUserData) =>
!prev.some((item) => item.goalId === result.goalId)
);
return [...prev, ...newResults];
});
setFailedResults((prev) => {
const newResults = data.failedResults.filter(
(result: GoalWithIdAndUserData) =>
!prev.some((item) => item.goalId === result.goalId)
);
return [...prev, ...newResults];
});
setPendingResults((prev) => {
const newResults = data.pendingResults.filter(
(result: GoalWithIdAndUserData) =>
!prev.some((item) => item.goalId === result.goalId)
);
return [...prev, ...newResults];
});

if (
data.successResults.length +
data.failedResults.length +
data.pendingResults.length <
limit
) {
noMore.current = true;
}

setIsLoading(false);
setReachedBottom(false);
isAlreadyFetching.current = false;
setIsLoadingMore(false);
})
.catch((error) => {
console.error("Error fetching results:", error);
Expand All @@ -61,7 +149,9 @@ export default function DashBoard({

useEffect(() => {
rerenderDashBoard = fetchData;
fetchData();
if (userId) {
fetchData();
}
}, [userId, success, failed, pending]);

useEffect(() => {
Expand All @@ -83,9 +173,14 @@ export default function DashBoard({
}}
/>
) : noResult ? (
<Typography level="h4" sx={{ textAlign: "center", marginTop: "20px" }}>
+ボタンから目標を作成しましょう!
</Typography>
<CenterIn delay={1}>
<Typography
level="h4"
sx={{ textAlign: "center", marginTop: "20px" }}
>
+ボタンから目標を作成しましょう!
</Typography>
</CenterIn>
) : (
<div className={styles.postsContainer}>
<Progress
Expand All @@ -94,6 +189,34 @@ export default function DashBoard({
pendingResults={pending ? pendingResults : []}
orderBy={orderBy}
/>
<div className="bottom" ref={bottomRef}></div>

{!noMore.current &&
(reachedBottom ? (
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "20px",
}}
>
<CircularProgress
color="primary"
variant="soft"
size="lg"
value={30}
/>
</div>
) : (
<Typography
level="body-md"
color="primary"
textAlign="center"
sx={{ fontWeight: 700 }}
>
スクロールしてもっと表示
</Typography>
))}
</div>
)}
</>
Expand Down
4 changes: 2 additions & 2 deletions src/Components/GoalModal/CopyGoalAfterPostButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export default function CopyGoalAfterPostButton({
onClick={() => {
setOpen(true);
showSnackBar({
message: "1日後の同じ時間で同じ目標を作成できます",
type: "success",
message: "明日の同じ時間で同じ目標を作成できます",
type: "normal",
});
}}
>
Expand Down
4 changes: 2 additions & 2 deletions src/Components/GoalModal/CopyGoalButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export default function CopyModalButton({
onClick={() => {
setOpen(true);
showSnackBar({
message: "1日後の同じ時間で同じ目標を作成できます",
type: "success",
message: "明日の同じ時間で同じ目標を作成できます",
type: "normal",
});
}}
sx={{ cursor: "pointer", fontSize: "23px" }}
Expand Down
Loading

0 comments on commit 1dcfb4a

Please sign in to comment.