Skip to content

Commit 9ea22b4

Browse files
authored
Merge pull request #4 from database-playground/pan93412/dbp-33-statistics-page
feat: statistics page
2 parents 2f8c129 + bb54f74 commit 9ea22b4

File tree

10 files changed

+286
-1
lines changed

10 files changed

+286
-1
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use client";
2+
3+
import { graphql } from "@/gql";
4+
import { useSuspenseQuery } from "@apollo/client/react";
5+
6+
const COMPLETED_QUESTIONS = graphql(`
7+
query CompletedQuestions {
8+
me {
9+
submissionStatistics {
10+
totalQuestions
11+
solvedQuestions
12+
}
13+
}
14+
}
15+
`);
16+
17+
export default function CompletedQuestionsPercentage() {
18+
const { data } = useSuspenseQuery(COMPLETED_QUESTIONS);
19+
const totalQuestions = data.me.submissionStatistics.totalQuestions;
20+
const solvedQuestions = data.me.submissionStatistics.solvedQuestions;
21+
const completedPercentage = (solvedQuestions / totalQuestions) * 100;
22+
23+
return <>{solvedQuestions}/{totalQuestions} ({completedPercentage.toFixed(2)}%)</>;
24+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Suspense } from "react";
2+
import CompletedQuestionsPercentage from "./completed-questions";
3+
4+
export default function Board() {
5+
return (
6+
<section
7+
className={`
8+
relative mb-6 flex h-42 w-full items-end justify-between rounded
9+
bg-primary/20 px-6 py-4
10+
`}
11+
>
12+
<div className="flex flex-col gap-2">
13+
<p className="tracking-widest text-muted-foreground uppercase">
14+
Welcome to Database Playground
15+
</p>
16+
<h1 className="text-xl font-bold tracking-wide">
17+
歡迎使用「資料庫練功坊」!🎉
18+
<br />
19+
試試看到「挑戰題目」中,開始挑戰 SQL 題目!
20+
</h1>
21+
</div>
22+
23+
<Suspense>
24+
<div className="flex flex-col items-end leading-none">
25+
<p className="text-sm text-muted-foreground">完成題數</p>
26+
<p className="text-xl font-bold">
27+
<CompletedQuestionsPercentage />
28+
</p>
29+
</div>
30+
</Suspense>
31+
32+
<div className="absolute top-4 right-6 text-6xl opacity-25 select-none">
33+
🥳
34+
</div>
35+
</section>
36+
);
37+
}

app/(app)/statistics/page.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,56 @@
1+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
2+
import { Pickaxe } from "lucide-react";
13
import type { Metadata } from "next";
4+
import { Suspense } from "react";
5+
import Board from "./board";
6+
import Points from "./statistics/points";
7+
import ResolvedQuestions from "./statistics/resolved-questions";
28

39
export const metadata: Metadata = {
410
title: "統計資料",
511
};
612

713
export default function StatisticsPage() {
8-
return <div>StatisticsPage</div>;
14+
return (
15+
<div>
16+
<Board />
17+
18+
<div
19+
className={`
20+
grid grid-cols-1 gap-8
21+
md:grid-cols-2
22+
lg:grid-cols-3
23+
`}
24+
>
25+
<section>
26+
<h2 className="mb-2 text-lg font-bold">成就報告</h2>
27+
28+
<Suspense>
29+
<div className="space-y-4">
30+
<ResolvedQuestions />
31+
</div>
32+
</Suspense>
33+
</section>
34+
35+
<section>
36+
<h2 className="mb-2 text-lg font-bold">攻克歷史</h2>
37+
38+
<Alert>
39+
<Pickaxe />
40+
<AlertTitle>正在實作</AlertTitle>
41+
<AlertDescription>
42+
功能正在實作,這裡先佔位!
43+
</AlertDescription>
44+
</Alert>
45+
</section>
46+
47+
<section>
48+
<h2 className="mb-2 text-lg font-bold">點數</h2>
49+
<Suspense>
50+
<Points />
51+
</Suspense>
52+
</section>
53+
</div>
54+
</div>
55+
);
956
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use client";
2+
3+
import { type FragmentType, graphql, readFragment } from "@/gql";
4+
import { useSuspenseQuery } from "@apollo/client/react";
5+
6+
const POINTS = graphql(`
7+
query Points {
8+
me {
9+
totalPoints
10+
11+
points(first: 5) {
12+
edges {
13+
node {
14+
id
15+
...PointFragment
16+
}
17+
}
18+
}
19+
}
20+
}
21+
`);
22+
23+
const POINT_FRAGMENT = graphql(`
24+
fragment PointFragment on Point {
25+
description
26+
points
27+
}
28+
`);
29+
30+
export default function Points() {
31+
const { data } = useSuspenseQuery(POINTS);
32+
const totalPoints = data.me.totalPoints;
33+
34+
return (
35+
<section>
36+
<div className="mb-1 text-2xl font-bold text-primary">
37+
{totalPoints}
38+
</div>
39+
<ul className="flex list-inside list-disc flex-col gap-1">
40+
{data.me.points.edges?.map((edge) => (
41+
edge?.node && <PointHistoryLine key={edge.node.id} fragment={edge.node} />
42+
))}
43+
</ul>
44+
</section>
45+
);
46+
}
47+
48+
function PointHistoryLine({ fragment }: { fragment: FragmentType<typeof POINT_FRAGMENT> }) {
49+
const point = readFragment(POINT_FRAGMENT, fragment);
50+
const symbol = point.points > 0 ? "+" : "-";
51+
52+
return <li>{point.description} ({symbol}{Math.abs(point.points)})</li>;
53+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"use client";
2+
3+
import { Progress } from "@/components/ui/progress";
4+
import { graphql } from "@/gql";
5+
import { useSuspenseQuery } from "@apollo/client/react";
6+
7+
const RESOLVED_QUESTIONS = graphql(`
8+
query CompletedQuestions {
9+
me {
10+
submissionStatistics {
11+
totalQuestions
12+
solvedQuestions
13+
}
14+
}
15+
}
16+
`);
17+
18+
export default function ResolvedQuestions() {
19+
const { data } = useSuspenseQuery(RESOLVED_QUESTIONS);
20+
const totalQuestions = data.me.submissionStatistics.totalQuestions;
21+
const solvedQuestions = data.me.submissionStatistics.solvedQuestions;
22+
const resolvedQuestions = solvedQuestions / totalQuestions;
23+
24+
return (
25+
<div className="flex flex-col gap-1">
26+
你攻克了 {resolvedQuestions.toFixed(0)}% 的題目!
27+
<Progress className="max-w-[50%]" value={resolvedQuestions * 100} />
28+
</div>
29+
);
30+
}

components/ui/progress.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client";
2+
3+
import * as ProgressPrimitive from "@radix-ui/react-progress";
4+
import * as React from "react";
5+
6+
import { cn } from "@/lib/utils";
7+
8+
function Progress({
9+
className,
10+
value,
11+
...props
12+
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
13+
return (
14+
<ProgressPrimitive.Root
15+
data-slot="progress"
16+
className={cn(
17+
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
18+
className,
19+
)}
20+
{...props}
21+
>
22+
<ProgressPrimitive.Indicator
23+
data-slot="progress-indicator"
24+
className="h-full w-full flex-1 bg-primary transition-all"
25+
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
26+
/>
27+
</ProgressPrimitive.Root>
28+
);
29+
}
30+
31+
export { Progress };

gql/gql.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ type Documents = {
1818
"\n fragment QuestionCard on Question {\n id\n title\n description\n difficulty\n category\n\n ...QuestionSolvedStatus\n }\n": typeof types.QuestionCardFragmentDoc,
1919
"\n fragment QuestionSolvedStatus on Question {\n solved\n attempted\n }\n": typeof types.QuestionSolvedStatusFragmentDoc,
2020
"\n query ListQuestions($where: QuestionWhereInput, $after: Cursor) {\n questions(where: $where, first: 10, after: $after) {\n edges {\n node {\n id\n ...QuestionCard\n ...QuestionSolvedStatus\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.ListQuestionsDocument,
21+
"\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": typeof types.CompletedQuestionsDocument,
22+
"\n query Points {\n me {\n totalPoints\n\n points(first: 5) {\n edges {\n node {\n id\n ...PointFragment\n }\n }\n }\n }\n }\n": typeof types.PointsDocument,
23+
"\n fragment PointFragment on Point {\n description\n points\n }\n": typeof types.PointFragmentFragmentDoc,
2124
"\n query BasicUserInfo {\n me {\n id\n name\n email\n avatar\n\n group {\n name\n }\n }\n }\n": typeof types.BasicUserInfoDocument,
2225
};
2326
const documents: Documents = {
2427
"\n query ChallengeStatisticsQuery {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n }\n }\n }\n": types.ChallengeStatisticsQueryDocument,
2528
"\n fragment QuestionCard on Question {\n id\n title\n description\n difficulty\n category\n\n ...QuestionSolvedStatus\n }\n": types.QuestionCardFragmentDoc,
2629
"\n fragment QuestionSolvedStatus on Question {\n solved\n attempted\n }\n": types.QuestionSolvedStatusFragmentDoc,
2730
"\n query ListQuestions($where: QuestionWhereInput, $after: Cursor) {\n questions(where: $where, first: 10, after: $after) {\n edges {\n node {\n id\n ...QuestionCard\n ...QuestionSolvedStatus\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.ListQuestionsDocument,
31+
"\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": types.CompletedQuestionsDocument,
32+
"\n query Points {\n me {\n totalPoints\n\n points(first: 5) {\n edges {\n node {\n id\n ...PointFragment\n }\n }\n }\n }\n }\n": types.PointsDocument,
33+
"\n fragment PointFragment on Point {\n description\n points\n }\n": types.PointFragmentFragmentDoc,
2834
"\n query BasicUserInfo {\n me {\n id\n name\n email\n avatar\n\n group {\n name\n }\n }\n }\n": types.BasicUserInfoDocument,
2935
};
3036

@@ -58,6 +64,18 @@ export function graphql(source: "\n fragment QuestionSolvedStatus on Question
5864
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
5965
*/
6066
export function graphql(source: "\n query ListQuestions($where: QuestionWhereInput, $after: Cursor) {\n questions(where: $where, first: 10, after: $after) {\n edges {\n node {\n id\n ...QuestionCard\n ...QuestionSolvedStatus\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ListQuestions($where: QuestionWhereInput, $after: Cursor) {\n questions(where: $where, first: 10, after: $after) {\n edges {\n node {\n id\n ...QuestionCard\n ...QuestionSolvedStatus\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n"];
67+
/**
68+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
69+
*/
70+
export function graphql(source: "\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n"): (typeof documents)["\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n"];
71+
/**
72+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
73+
*/
74+
export function graphql(source: "\n query Points {\n me {\n totalPoints\n\n points(first: 5) {\n edges {\n node {\n id\n ...PointFragment\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query Points {\n me {\n totalPoints\n\n points(first: 5) {\n edges {\n node {\n id\n ...PointFragment\n }\n }\n }\n }\n }\n"];
75+
/**
76+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
77+
*/
78+
export function graphql(source: "\n fragment PointFragment on Point {\n description\n points\n }\n"): (typeof documents)["\n fragment PointFragment on Point {\n description\n points\n }\n"];
6179
/**
6280
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
6381
*/

0 commit comments

Comments
 (0)