Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/(app)/statistics/board/completed-questions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

import { graphql } from "@/gql";
import { useSuspenseQuery } from "@apollo/client/react";

const COMPLETED_QUESTIONS = graphql(`
query CompletedQuestions {
me {
submissionStatistics {
totalQuestions
solvedQuestions
}
}
}
`);

export default function CompletedQuestionsPercentage() {
const { data } = useSuspenseQuery(COMPLETED_QUESTIONS);
const totalQuestions = data.me.submissionStatistics.totalQuestions;
const solvedQuestions = data.me.submissionStatistics.solvedQuestions;
const completedPercentage = (solvedQuestions / totalQuestions) * 100;

return <>{solvedQuestions}/{totalQuestions} ({completedPercentage.toFixed(2)}%)</>;
}
37 changes: 37 additions & 0 deletions app/(app)/statistics/board/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Suspense } from "react";
import CompletedQuestionsPercentage from "./completed-questions";

export default function Board() {
return (
<section
className={`
relative mb-6 flex h-42 w-full items-end justify-between rounded
bg-primary/20 px-6 py-4
`}
>
<div className="flex flex-col gap-2">
<p className="tracking-widest text-muted-foreground uppercase">
Welcome to Database Playground
</p>
<h1 className="text-xl font-bold tracking-wide">
歡迎使用「資料庫練功坊」!🎉
<br />
試試看到「挑戰題目」中,開始挑戰 SQL 題目!
</h1>
</div>

<Suspense>
<div className="flex flex-col items-end leading-none">
<p className="text-sm text-muted-foreground">完成題數</p>
<p className="text-xl font-bold">
<CompletedQuestionsPercentage />
</p>
</div>
</Suspense>

<div className="absolute top-4 right-6 text-6xl opacity-25 select-none">
🥳
</div>
</section>
);
}
49 changes: 48 additions & 1 deletion app/(app)/statistics/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,56 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Pickaxe } from "lucide-react";
import type { Metadata } from "next";
import { Suspense } from "react";
import Board from "./board";
import Points from "./statistics/points";
import ResolvedQuestions from "./statistics/resolved-questions";

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

export default function StatisticsPage() {
return <div>StatisticsPage</div>;
return (
<div>
<Board />

<div
className={`
grid grid-cols-1 gap-8
md:grid-cols-2
lg:grid-cols-3
`}
>
<section>
<h2 className="mb-2 text-lg font-bold">成就報告</h2>

<Suspense>
<div className="space-y-4">
<ResolvedQuestions />
</div>
</Suspense>
</section>

<section>
<h2 className="mb-2 text-lg font-bold">攻克歷史</h2>

<Alert>
<Pickaxe />
<AlertTitle>正在實作</AlertTitle>
<AlertDescription>
功能正在實作,這裡先佔位!
</AlertDescription>
</Alert>
</section>

<section>
<h2 className="mb-2 text-lg font-bold">點數</h2>
<Suspense>
<Points />
</Suspense>
</section>
</div>
</div>
);
}
53 changes: 53 additions & 0 deletions app/(app)/statistics/statistics/points.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"use client";

import { type FragmentType, graphql, readFragment } from "@/gql";
import { useSuspenseQuery } from "@apollo/client/react";

const POINTS = graphql(`
query Points {
me {
totalPoints

points(first: 5) {
edges {
node {
id
...PointFragment
}
}
}
}
}
`);

const POINT_FRAGMENT = graphql(`
fragment PointFragment on Point {
description
points
}
`);

export default function Points() {
const { data } = useSuspenseQuery(POINTS);
const totalPoints = data.me.totalPoints;

return (
<section>
<div className="mb-1 text-2xl font-bold text-primary">
{totalPoints} 點
</div>
<ul className="flex list-inside list-disc flex-col gap-1">
{data.me.points.edges?.map((edge) => (
edge?.node && <PointHistoryLine key={edge.node.id} fragment={edge.node} />
))}
</ul>
</section>
);
}

function PointHistoryLine({ fragment }: { fragment: FragmentType<typeof POINT_FRAGMENT> }) {
const point = readFragment(POINT_FRAGMENT, fragment);
const symbol = point.points > 0 ? "+" : "-";

return <li>{point.description} ({symbol}{Math.abs(point.points)})</li>;
}
30 changes: 30 additions & 0 deletions app/(app)/statistics/statistics/resolved-questions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

import { Progress } from "@/components/ui/progress";
import { graphql } from "@/gql";
import { useSuspenseQuery } from "@apollo/client/react";

const RESOLVED_QUESTIONS = graphql(`
query CompletedQuestions {
me {
submissionStatistics {
totalQuestions
solvedQuestions
}
}
}
`);

export default function ResolvedQuestions() {
const { data } = useSuspenseQuery(RESOLVED_QUESTIONS);
const totalQuestions = data.me.submissionStatistics.totalQuestions;
const solvedQuestions = data.me.submissionStatistics.solvedQuestions;
const resolvedQuestions = solvedQuestions / totalQuestions;

return (
<div className="flex flex-col gap-1">
你攻克了 {resolvedQuestions.toFixed(0)}% 的題目!
<Progress className="max-w-[50%]" value={resolvedQuestions * 100} />
</div>
);
}
31 changes: 31 additions & 0 deletions components/ui/progress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import * as ProgressPrimitive from "@radix-ui/react-progress";
import * as React from "react";

import { cn } from "@/lib/utils";

function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
}

export { Progress };
18 changes: 18 additions & 0 deletions gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ type Documents = {
"\n fragment QuestionCard on Question {\n id\n title\n description\n difficulty\n category\n\n ...QuestionSolvedStatus\n }\n": typeof types.QuestionCardFragmentDoc,
"\n fragment QuestionSolvedStatus on Question {\n solved\n attempted\n }\n": typeof types.QuestionSolvedStatusFragmentDoc,
"\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,
"\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": typeof types.CompletedQuestionsDocument,
"\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,
"\n fragment PointFragment on Point {\n description\n points\n }\n": typeof types.PointFragmentFragmentDoc,
"\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,
};
const documents: Documents = {
"\n query ChallengeStatisticsQuery {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n }\n }\n }\n": types.ChallengeStatisticsQueryDocument,
"\n fragment QuestionCard on Question {\n id\n title\n description\n difficulty\n category\n\n ...QuestionSolvedStatus\n }\n": types.QuestionCardFragmentDoc,
"\n fragment QuestionSolvedStatus on Question {\n solved\n attempted\n }\n": types.QuestionSolvedStatusFragmentDoc,
"\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,
"\n query CompletedQuestions {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": types.CompletedQuestionsDocument,
"\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,
"\n fragment PointFragment on Point {\n description\n points\n }\n": types.PointFragmentFragmentDoc,
"\n query BasicUserInfo {\n me {\n id\n name\n email\n avatar\n\n group {\n name\n }\n }\n }\n": types.BasicUserInfoDocument,
};

Expand Down Expand Up @@ -58,6 +64,18 @@ export function graphql(source: "\n fragment QuestionSolvedStatus on Question
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
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"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
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"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
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"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
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"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading