Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
105 changes: 105 additions & 0 deletions app/(admin)/(user-management)/users/[id]/_components/points.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"use client";

import { CardLayout } from "@/components/card-layout";
import { type FragmentType, graphql, useFragment } from "@/gql";
import { Trophy } from "lucide-react";

const USER_POINTS_CARD_FRAGMENT = graphql(`
fragment UserPointsCard on User {
totalPoints

points(first: 5, orderBy: { field: GRANTED_AT, direction: DESC }) {
edges {
node {
id
...UserPointHistoryLine
}
}
}
}
`);

const USER_POINT_HISTORY_LINE_FRAGMENT = graphql(`
fragment UserPointHistoryLine on Point {
points
description
grantedAt
}
`);

export function PointsCard({
fragment,
}: {
fragment: FragmentType<typeof USER_POINTS_CARD_FRAGMENT>;
}) {
const { totalPoints, points } = useFragment(
USER_POINTS_CARD_FRAGMENT,
fragment,
);

return (
<CardLayout title="總積分" description="這個使用者的總積分與最近積分紀錄。">
<div className="space-y-4">
<div className="flex items-center gap-3">
<Trophy className="h-8 w-8 text-yellow-500" />
<div>
<p className="text-3xl font-bold">{totalPoints}</p>
<p className="text-sm text-muted-foreground">積分</p>
</div>
</div>

{points?.edges && points.edges.length > 0 && (
<div className="border-t pt-4">
<p className="mb-2 text-sm font-medium">最近積分紀錄</p>
<div className="space-y-2">
{points.edges
.map((edge) => {
if (!edge?.node) return null;
return <PointHistoryLine key={edge.node.id} fragment={edge.node} />;
})}
</div>
</div>
)}
</div>
</CardLayout>
);
}

function PointHistoryLine({
fragment,
}: {
fragment: FragmentType<typeof USER_POINT_HISTORY_LINE_FRAGMENT>;
}) {
const { points, description, grantedAt } = useFragment(
USER_POINT_HISTORY_LINE_FRAGMENT,
fragment,
);

return (
<div className={`flex items-start justify-between gap-2 text-sm`}>
<div className="flex-1">
<p className="font-medium">{description || "積分取得"}</p>
<p className="text-xs text-muted-foreground">
{new Date(grantedAt).toLocaleString("zh-TW", {
timeZone: "Asia/Taipei",
})}
</p>
</div>
<Point point={points} />
</div>
);
}

function Point({ point }: { point: number }) {
const pointAbs = Math.abs(point);

if (point > 0) {
return <span className="font-bold text-green-600">+{pointAbs}</span>;
}

if (point < 0) {
return <span className="font-bold text-red-600">-{pointAbs}</span>;
}

return <span className="text-muted-foreground">{pointAbs}</span>;
}
88 changes: 88 additions & 0 deletions app/(admin)/(user-management)/users/[id]/_components/questions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"use client";

import { CardLayout } from "@/components/card-layout";
import { type FragmentType, graphql, useFragment } from "@/gql";
import { DIFFICULTY_TRANSLATION } from "@/lib/translation";
import { BookOpen, CheckCircle2, FileQuestion } from "lucide-react";

const USER_QUESTIONS_CARD_FRAGMENT = graphql(`
fragment UserQuestionsCard on User {
submissionStatistics {
totalQuestions
solvedQuestions
attemptedQuestions

solvedQuestionByDifficulty {
difficulty
solvedQuestions
}
}
}
`);

export function QuestionsCard({ fragment }: { fragment: FragmentType<typeof USER_QUESTIONS_CARD_FRAGMENT> }) {
const { submissionStatistics } = useFragment(USER_QUESTIONS_CARD_FRAGMENT, fragment);

if (!submissionStatistics) {
return (
<CardLayout title="做題統計" description="這個使用者的做題統計資訊。">
<p className="text-sm text-muted-foreground">暫無資料</p>
</CardLayout>
);
}

const { totalQuestions, solvedQuestions, attemptedQuestions, solvedQuestionByDifficulty } = submissionStatistics;

return (
<CardLayout title="做題統計" description="這個使用者的做題統計資訊。">
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col gap-1">
<div
className={`flex items-center gap-2 text-sm text-muted-foreground`}
>
<FileQuestion className="h-4 w-4" />
<span>總題數</span>
</div>
<p className="text-2xl font-bold">{totalQuestions}</p>
</div>
<div className="flex flex-col gap-1">
<div
className={`flex items-center gap-2 text-sm text-muted-foreground`}
>
<BookOpen className="h-4 w-4" />
<span>嘗試題數</span>
</div>
<p className="text-2xl font-bold">{attemptedQuestions}</p>
</div>
<div className="flex flex-col gap-1">
<div
className={`flex items-center gap-2 text-sm text-muted-foreground`}
>
<CheckCircle2 className="h-4 w-4" />
<span>完成題數</span>
</div>
<p className="text-2xl font-bold">{solvedQuestions}</p>
</div>
</div>

{solvedQuestionByDifficulty && solvedQuestionByDifficulty.length > 0 && (
<div className="border-t pt-4">
<p className="mb-2 text-sm font-medium">各難度完成題數</p>
<div className="space-y-2">
{solvedQuestionByDifficulty.map(({ difficulty, solvedQuestions }) => (
<div
key={difficulty}
className={`flex items-center justify-between`}
>
<span className="text-sm">{DIFFICULTY_TRANSLATION[difficulty]}</span>
<span className="font-medium">{solvedQuestions}</span>
</div>
))}
</div>
</div>
)}
</div>
</CardLayout>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import { graphql } from "@/gql";
import { useSuspenseQuery } from "@apollo/client/react";
import { AuditInfoCard } from "./audit-info";
import { GroupsCard } from "./groups";
import { PointsCard } from "./points";
import { QuestionsCard } from "./questions";

const USER_CARDS_QUERY = graphql(`
query UserCards($id: ID!) {
user(id: $id) {
id
...UserGroupsCard
...UserAuditInfoCard
...UserPointsCard
...UserQuestionsCard
}
}
`);
Expand All @@ -31,6 +35,8 @@ export function UserCards({ id }: { id: string }) {
>
<GroupsCard fragment={fragment} />
<AuditInfoCard fragment={fragment} />
<PointsCard fragment={fragment} />
<QuestionsCard fragment={fragment} />
</div>
);
}
30 changes: 21 additions & 9 deletions gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ type Documents = {
"\n fragment PointDetailsCard on Point {\n points\n description\n grantedAt\n }\n": typeof types.PointDetailsCardFragmentDoc,
"\n fragment PointUserCard on Point {\n user {\n id\n name\n }\n }\n": typeof types.PointUserCardFragmentDoc,
"\n mutation CreatePoint($input: CreatePointInput!) {\n createPoint(input: $input) {\n id\n }\n }\n": typeof types.CreatePointDocument,
"\n query CreatePointDialogContent {\n users(first: 100) {\n edges {\n node {\n id\n name\n email\n }\n }\n }\n }\n": typeof types.CreatePointDialogContentDocument,
"\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: PointWhereInput\n ) {\n points(\n first: $first\n after: $after\n last: $last\n before: $before\n where: $where\n orderBy: { field: GRANTED_AT, direction: DESC }\n ) {\n edges {\n node {\n id\n ...PointsTableRow\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.PointsTableDocument,
"\n fragment PointsTableRow on Point {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n": typeof types.PointsTableRowFragmentDoc,
"\n query UpdatePointsFormUserInfo($id: ID!) {\n user(id: $id) {\n id\n name\n email\n }\n }\n": typeof types.UpdatePointsFormUserInfoDocument,
Expand Down Expand Up @@ -80,7 +79,10 @@ type Documents = {
"\n fragment UserAuditInfoCard on User {\n createdAt\n updatedAt\n }\n": typeof types.UserAuditInfoCardFragmentDoc,
"\n fragment UserGroupsCard on User {\n group {\n id\n name\n }\n }\n": typeof types.UserGroupsCardFragmentDoc,
"\n query UserHeader($id: ID!) {\n user(id: $id) {\n id\n name\n email\n avatar\n }\n }\n": typeof types.UserHeaderDocument,
"\n query UserCards($id: ID!) {\n user(id: $id) {\n id\n ...UserGroupsCard\n ...UserAuditInfoCard\n }\n }\n": typeof types.UserCardsDocument,
"\n fragment UserPointsCard on User {\n totalPoints\n\n points(first: 5, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n ...UserPointHistoryLine\n }\n }\n }\n }\n": typeof types.UserPointsCardFragmentDoc,
"\n fragment UserPointHistoryLine on Point {\n points\n description\n grantedAt\n }\n": typeof types.UserPointHistoryLineFragmentDoc,
"\n fragment UserQuestionsCard on User {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n\n solvedQuestionByDifficulty {\n difficulty\n solvedQuestions\n }\n }\n }\n": typeof types.UserQuestionsCardFragmentDoc,
"\n query UserCards($id: ID!) {\n user(id: $id) {\n id\n ...UserGroupsCard\n ...UserAuditInfoCard\n ...UserPointsCard\n ...UserQuestionsCard\n }\n }\n": typeof types.UserCardsDocument,
"\n mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {\n updateUser(id: $id, input: $input) {\n id\n }\n }\n": typeof types.UpdateUserDocument,
"\n mutation DeleteUser($id: ID!) {\n deleteUser(id: $id)\n }\n": typeof types.DeleteUserDocument,
"\n mutation LogoutUserDevices($userID: ID!) {\n logoutUser(userID: $userID)\n }\n": typeof types.LogoutUserDevicesDocument,
Expand All @@ -107,7 +109,6 @@ const documents: Documents = {
"\n fragment PointDetailsCard on Point {\n points\n description\n grantedAt\n }\n": types.PointDetailsCardFragmentDoc,
"\n fragment PointUserCard on Point {\n user {\n id\n name\n }\n }\n": types.PointUserCardFragmentDoc,
"\n mutation CreatePoint($input: CreatePointInput!) {\n createPoint(input: $input) {\n id\n }\n }\n": types.CreatePointDocument,
"\n query CreatePointDialogContent {\n users(first: 100) {\n edges {\n node {\n id\n name\n email\n }\n }\n }\n }\n": types.CreatePointDialogContentDocument,
"\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: PointWhereInput\n ) {\n points(\n first: $first\n after: $after\n last: $last\n before: $before\n where: $where\n orderBy: { field: GRANTED_AT, direction: DESC }\n ) {\n edges {\n node {\n id\n ...PointsTableRow\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.PointsTableDocument,
"\n fragment PointsTableRow on Point {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n": types.PointsTableRowFragmentDoc,
"\n query UpdatePointsFormUserInfo($id: ID!) {\n user(id: $id) {\n id\n name\n email\n }\n }\n": types.UpdatePointsFormUserInfoDocument,
Expand Down Expand Up @@ -166,7 +167,10 @@ const documents: Documents = {
"\n fragment UserAuditInfoCard on User {\n createdAt\n updatedAt\n }\n": types.UserAuditInfoCardFragmentDoc,
"\n fragment UserGroupsCard on User {\n group {\n id\n name\n }\n }\n": types.UserGroupsCardFragmentDoc,
"\n query UserHeader($id: ID!) {\n user(id: $id) {\n id\n name\n email\n avatar\n }\n }\n": types.UserHeaderDocument,
"\n query UserCards($id: ID!) {\n user(id: $id) {\n id\n ...UserGroupsCard\n ...UserAuditInfoCard\n }\n }\n": types.UserCardsDocument,
"\n fragment UserPointsCard on User {\n totalPoints\n\n points(first: 5, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n ...UserPointHistoryLine\n }\n }\n }\n }\n": types.UserPointsCardFragmentDoc,
"\n fragment UserPointHistoryLine on Point {\n points\n description\n grantedAt\n }\n": types.UserPointHistoryLineFragmentDoc,
"\n fragment UserQuestionsCard on User {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n\n solvedQuestionByDifficulty {\n difficulty\n solvedQuestions\n }\n }\n }\n": types.UserQuestionsCardFragmentDoc,
"\n query UserCards($id: ID!) {\n user(id: $id) {\n id\n ...UserGroupsCard\n ...UserAuditInfoCard\n ...UserPointsCard\n ...UserQuestionsCard\n }\n }\n": types.UserCardsDocument,
"\n mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {\n updateUser(id: $id, input: $input) {\n id\n }\n }\n": types.UpdateUserDocument,
"\n mutation DeleteUser($id: ID!) {\n deleteUser(id: $id)\n }\n": types.DeleteUserDocument,
"\n mutation LogoutUserDevices($userID: ID!) {\n logoutUser(userID: $userID)\n }\n": types.LogoutUserDevicesDocument,
Expand Down Expand Up @@ -228,10 +232,6 @@ export function graphql(source: "\n fragment PointUserCard on Point {\n user
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation CreatePoint($input: CreatePointInput!) {\n createPoint(input: $input) {\n id\n }\n }\n"): (typeof documents)["\n mutation CreatePoint($input: CreatePointInput!) {\n createPoint(input: $input) {\n id\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 CreatePointDialogContent {\n users(first: 100) {\n edges {\n node {\n id\n name\n email\n }\n }\n }\n }\n"): (typeof documents)["\n query CreatePointDialogContent {\n users(first: 100) {\n edges {\n node {\n id\n name\n email\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down Expand Up @@ -467,7 +467,19 @@ export function graphql(source: "\n query UserHeader($id: ID!) {\n user(id:
/**
* 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 UserCards($id: ID!) {\n user(id: $id) {\n id\n ...UserGroupsCard\n ...UserAuditInfoCard\n }\n }\n"): (typeof documents)["\n query UserCards($id: ID!) {\n user(id: $id) {\n id\n ...UserGroupsCard\n ...UserAuditInfoCard\n }\n }\n"];
export function graphql(source: "\n fragment UserPointsCard on User {\n totalPoints\n\n points(first: 5, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n ...UserPointHistoryLine\n }\n }\n }\n }\n"): (typeof documents)["\n fragment UserPointsCard on User {\n totalPoints\n\n points(first: 5, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n ...UserPointHistoryLine\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 UserPointHistoryLine on Point {\n points\n description\n grantedAt\n }\n"): (typeof documents)["\n fragment UserPointHistoryLine on Point {\n points\n description\n grantedAt\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 UserQuestionsCard on User {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n\n solvedQuestionByDifficulty {\n difficulty\n solvedQuestions\n }\n }\n }\n"): (typeof documents)["\n fragment UserQuestionsCard on User {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n\n solvedQuestionByDifficulty {\n difficulty\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 UserCards($id: ID!) {\n user(id: $id) {\n id\n ...UserGroupsCard\n ...UserAuditInfoCard\n ...UserPointsCard\n ...UserQuestionsCard\n }\n }\n"): (typeof documents)["\n query UserCards($id: ID!) {\n user(id: $id) {\n id\n ...UserGroupsCard\n ...UserAuditInfoCard\n ...UserPointsCard\n ...UserQuestionsCard\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading