diff --git a/typing-app/.env b/typing-app/.env index cc53660..966485d 100644 --- a/typing-app/.env +++ b/typing-app/.env @@ -1 +1 @@ -NEXT_PUBLIC_API_URL=http://localhost:8080 \ No newline at end of file +NEXT_PUBLIC_API_URL=http://localhost:8080 diff --git a/typing-app/src/components/atoms/RefreshButton.tsx b/typing-app/src/components/atoms/RefreshButton.tsx index 6f6ad12..3fd399a 100644 --- a/typing-app/src/components/atoms/RefreshButton.tsx +++ b/typing-app/src/components/atoms/RefreshButton.tsx @@ -8,9 +8,9 @@ const RefreshIcon = () => ( viewBox="0 0 24 24" fill="none" stroke="currentColor" - stroke-width="2" - stroke-linecap="round" - stroke-linejoin="round" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" > diff --git a/typing-app/src/components/molecules/RankingTableBody.tsx b/typing-app/src/components/molecules/RankingTableBody.tsx index d8959c7..ff013dd 100644 --- a/typing-app/src/components/molecules/RankingTableBody.tsx +++ b/typing-app/src/components/molecules/RankingTableBody.tsx @@ -1,16 +1,16 @@ import { Tbody } from "@chakra-ui/react"; import RankingTableRow from "./RankingTableRow"; -import { ScoreRanking } from "../organism/RankingTabs"; +import { components } from "@/libs/api/v1"; export type RankingTableBodyProps = { - scoreRankings: ScoreRanking[]; + scoreRankings: components["schemas"]["ScoreRanking"][]; }; const RankingTableBody: React.FC = ({ scoreRankings }) => { return ( {scoreRankings.map((scoreRanking) => ( - + ))} ); diff --git a/typing-app/src/components/molecules/RankingTableRow.tsx b/typing-app/src/components/molecules/RankingTableRow.tsx index b45da86..da1cf2a 100644 --- a/typing-app/src/components/molecules/RankingTableRow.tsx +++ b/typing-app/src/components/molecules/RankingTableRow.tsx @@ -1,23 +1,35 @@ import { Td, Tr } from "@chakra-ui/react"; -import { ScoreRanking } from "../organism/RankingTabs"; +import { components } from "@/libs/api/v1"; -const RankingTableRow: React.FC = (scoreRanking) => { +const RankingTableRow: React.FC = (scoreRanking) => { + const accuracy = scoreRanking.score?.accuracy ?? 0; + const formattedAccuracy = new Intl.NumberFormat("en-US", { style: "percent", maximumFractionDigits: 2 }).format( + accuracy + ); + const formattedCreatedAt = scoreRanking.score?.created_at + ? new Date(scoreRanking.score.created_at).toISOString().split("T")[0] + : ""; return ( - + {String(scoreRanking.rank)} - {scoreRanking.user.studentNumber} + {scoreRanking.score?.user?.student_number} - {String(scoreRanking.keystrokes)} + {String(scoreRanking.score?.keystrokes)} - {String(scoreRanking.accuracy)} + {formattedAccuracy} - {scoreRanking.createdAt.toLocaleDateString("ja-JP")} + {formattedCreatedAt} ); diff --git a/typing-app/src/components/organism/RankingTabs.tsx b/typing-app/src/components/organism/RankingTabs.tsx index b5aaea8..c02e63e 100644 --- a/typing-app/src/components/organism/RankingTabs.tsx +++ b/typing-app/src/components/organism/RankingTabs.tsx @@ -1,51 +1,56 @@ "use client"; + import { Tabs, TabList, TabPanels, Tab, TabPanel, Flex, Center, Box, Grid } from "@chakra-ui/react"; import RankingTable from "../organism/RankingTable"; import { Pagination } from "../molecules/Pagination"; -//import { CustomButton } from "../atoms/CustomButton"; import RefreshButton from "../atoms/RefreshButton"; import { useEffect, useState } from "react"; -// import { client } from "@/libs/api"; +import { client } from "@/libs/api"; +import { components } from "@/libs/api/v1"; const RankingTabs = () => { - const [scoreRankings, setScoreRankings] = useState(demoAccuracyRankings); - const [rankingStartFrom, setRankingStartFrom] = useState(0); + const [scoreRankings, setScoreRankings] = useState([]); + const [rankingStartFrom, setRankingStartFrom] = useState(1); const [sortBy, setSortBy] = useState<"accuracy" | "keystrokes">("accuracy"); - const LIMIT = 10; + const [error, setError] = useState(undefined); + const [totalRankingCount, setTotalRankingCount] = useState(0); - const MAXIMUM = 100; // TODO: MAXIMUMをAPIから取得する + const LIMIT = 10; + const MAXIMUM = totalRankingCount; + const fetchData = async () => { + const { data } = await client.GET("/scores/ranking", { + params: { + query: { + sort_by: sortBy, + start: rankingStartFrom, + limit: LIMIT, + }, + }, + }); + if (data) { + setScoreRankings(data.rankings); + setTotalRankingCount(data.total_count); + } else { + setError("データの取得中にエラーが発生しました。"); + } + }; useEffect(() => { - // ページが読み込まれたときにデータを取得 fetchData(); - }); + }, [sortBy, rankingStartFrom]); const handleTabChange = (index: number) => { - if (index === 0) { - setSortBy("accuracy"); - } else if (index === 1) { - setSortBy("keystrokes"); - } - - fetchData; + const sortOption = index === 0 ? "accuracy" : "keystrokes"; + setSortBy(sortOption); + setRankingStartFrom(1); }; - // 演算子を引数にとる、ボタンを押したときのハンドラ関数 const handlePaginationClick = (direction: "next" | "prev") => { const newStartFrom = - direction === "prev" ? Math.max(rankingStartFrom - LIMIT, 0) : Math.min(rankingStartFrom + LIMIT, MAXIMUM); + direction === "prev" + ? Math.max(rankingStartFrom - LIMIT, 1) + : Math.min(rankingStartFrom + LIMIT, MAXIMUM - LIMIT); setRankingStartFrom(newStartFrom); - - fetchData; - }; - - const fetchData = async () => { - // TODO: APIを使ってデータをフェッチ - if (sortBy == "accuracy") { - setScoreRankings(demoAccuracyRankings); - } else if (sortBy == "keystrokes") { - setScoreRankings(demoKeyStrokeRankings); - } }; return ( @@ -57,10 +62,21 @@ const RankingTabs = () => { 正打率 入力文字数 - fetchData()} isDisabled={false} /> + { + setRankingStartFrom(1); + fetchData(); + }} + isDisabled={false} + /> - + {error && ( +
+ Error: {error} +
+ )} + ( @@ -69,151 +85,17 @@ const RankingTabs = () => { + )
handlePaginationClick("prev")} onNext={() => handlePaginationClick("next")} - isPrevDisabled={rankingStartFrom <= 0} + isPrevDisabled={rankingStartFrom <= 1} isNextDisabled={rankingStartFrom + LIMIT >= MAXIMUM} />
); }; -export default RankingTabs; -export interface User { - id: string; - studentNumber: string; - handleName: string; -} - -export interface ScoreRanking { - rank: Number; - user: User; - keystrokes: Number; - accuracy: Number; - createdAt: Date; -} - -const demoUsers: User[] = [ - { - id: "1", - studentNumber: "70310000", - handleName: "X", - }, - { - id: "2", - studentNumber: "70310000", - handleName: "Y", - }, - { - id: "3", - studentNumber: "70310000", - handleName: "Z", - }, - { - id: "4", - studentNumber: "70310000", - handleName: "A", - }, - { - id: "5", - studentNumber: "70310000", - handleName: "B", - }, -]; - -const demoKeyStrokeRankings: ScoreRanking[] = [ - { - rank: 1, - user: { - id: "1", - studentNumber: "70310000", - handleName: "X", - }, - keystrokes: 100, - accuracy: 100, - createdAt: new Date(), - }, - { - rank: 2, - user: { - id: "2", - studentNumber: "70310000", - handleName: "Y", - }, - keystrokes: 90, - accuracy: 90, - createdAt: new Date(), - }, - { - rank: 3, - user: { - id: "3", - studentNumber: "70310000", - handleName: "Z", - }, - keystrokes: 80, - accuracy: 80, - createdAt: new Date(), - }, -]; - -const demoAccuracyRankings: ScoreRanking[] = [ - { - rank: 1, - user: { - id: "1", - studentNumber: "70310000", - handleName: "X", - }, - keystrokes: 100, - accuracy: 100, - createdAt: new Date(), - }, - { - rank: 2, - user: { - id: "2", - studentNumber: "70310000", - handleName: "Y", - }, - keystrokes: 90, - accuracy: 90, - createdAt: new Date(), - }, - { - rank: 3, - user: { - id: "3", - studentNumber: "70310000", - handleName: "Z", - }, - keystrokes: 80, - accuracy: 80, - createdAt: new Date(), - }, - { - rank: 4, - user: { - id: "4", - studentNumber: "70310000", - handleName: "A", - }, - keystrokes: 70, - accuracy: 70, - createdAt: new Date(), - }, - { - rank: 5, - user: { - id: "5", - studentNumber: "70310000", - handleName: "B", - }, - keystrokes: 60, - accuracy: 60, - createdAt: new Date(), - }, -]; +export default RankingTabs; diff --git a/typing-app/src/config/index.ts b/typing-app/src/config/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/typing-app/src/constants/index.ts b/typing-app/src/constants/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/typing-app/src/utils/textManager/index.ts b/typing-app/src/utils/textManager/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/typing-app/src/utils/toast.ts b/typing-app/src/utils/toast.ts new file mode 100644 index 0000000..2d8fa36 --- /dev/null +++ b/typing-app/src/utils/toast.ts @@ -0,0 +1,48 @@ +import { createStandaloneToast } from "@chakra-ui/react"; + +const { toast: toastChakra } = createStandaloneToast(); + +const toast = ({ + status, + title = "", + description = "", +}: { + status: "success" | "error" | "warning" | "info"; + title?: string; + description?: string; +}) => { + toastChakra({ + title, + description, + status, + duration: 3000, + isClosable: true, + position: "top", + }); +}; + +export const showSuccessToast = (title: string, description?: string) => { + toast({ + title, + description, + status: "success", + }); +}; + +export const showWarningToast = (title: string, description?: string) => { + toast({ + title, + description, + status: "warning", + }); +}; + +export const showErrorToast = (title: string, description?: string) => { + toast({ + title, + description, + status: "error", + }); +}; + +export default toast; diff --git a/typing-server/api/cmd/main.go b/typing-server/api/cmd/main.go index 5826503..eba4b0f 100644 --- a/typing-server/api/cmd/main.go +++ b/typing-server/api/cmd/main.go @@ -76,18 +76,21 @@ func main() { } else { logger.Info("シードデータは挿入されませんでした") } - // WaitGroupの宣言 var wg sync.WaitGroup + // エラーを通知するためのチャネル errChan := make(chan error, 1) + // シグナルハンドリングの準備 sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + // HTTPサーバーの非同期起動 wg.Add(1) go func() { defer wg.Done() // 関数終了時にWaitGroupをデクリメント + // サーバーの設定 // ルーティングの設定 r := router.SetupRouter() @@ -97,29 +100,32 @@ func main() { Addr: ":8080", Handler: r, } + // 非同期でサーバーを開始 go func() { logger.Info("server is running at Addr :8080") - if err := server.ListenAndServe(); err != http.ErrServerClosed { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error("failed to listen and serve: %v", err) errChan <- err // エラーをチャネルに送信 } }() - // シグナルを待機 - <-sigChan - logger.Info("shutting down the server...") - ctx := context.TODO() // Use context.TODO() as a temporary placeholder - if err := server.Shutdown(ctx); err != nil { - logger.Error("error during server shutdown: %v", err) - errChan <- err // エラーをチャネルに送信 + + // エラーまたはシグナルを待機 + select { + case err := <-errChan: + logger.Error("server stopped due to an error: %v", err) + case sig := <-sigChan: + logger.Info("received signal: %v", sig) + // グレースフルシャットダウン + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Shutdown(ctx); err != nil { + logger.Error("error during server shutdown: %v", err) + errChan <- err // エラーをチャネルに送信 + } } }() - select { - case <-errChan: // エラーが発生した場合 - logger.Error("server stopped due to an error") - case sig := <-sigChan: // シグナルを受信した場合 - logger.Info("received signal: %s", sig) - } + wg.Wait() // HTTPサーバーの終了を待機 close(errChan) logger.Info("server exited")