From d6be4c82381850b7b6a81240fed6512a300338da Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 19 Oct 2025 02:41:41 +0800 Subject: [PATCH 01/11] feat: add skeleton to data table --- app/(admin)/(activity-management)/events/page.tsx | 6 +++++- app/(admin)/(activity-management)/points/page.tsx | 6 +++++- app/(admin)/(activity-management)/submissions/page.tsx | 6 +++++- app/(admin)/(question-management)/database/page.tsx | 6 +++++- app/(admin)/(question-management)/questions/page.tsx | 6 +++++- app/(admin)/(user-management)/groups/page.tsx | 6 +++++- app/(admin)/(user-management)/scopesets/page.tsx | 6 +++++- app/(admin)/(user-management)/users/page.tsx | 6 +++++- 8 files changed, 40 insertions(+), 8 deletions(-) diff --git a/app/(admin)/(activity-management)/events/page.tsx b/app/(admin)/(activity-management)/events/page.tsx index ed2df20..7f42b79 100644 --- a/app/(admin)/(activity-management)/events/page.tsx +++ b/app/(admin)/(activity-management)/events/page.tsx @@ -1,6 +1,8 @@ import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; import { EventsDataTable } from "./_components/data-table"; +import { Suspense } from "react"; +import { DataTableSkeleton } from "@/components/data-table/skeleton"; export const metadata: Metadata = { title: "事件管理", @@ -23,7 +25,9 @@ export default function Page() {
- + }> + +
diff --git a/app/(admin)/(activity-management)/points/page.tsx b/app/(admin)/(activity-management)/points/page.tsx index febd8e9..393ab2b 100644 --- a/app/(admin)/(activity-management)/points/page.tsx +++ b/app/(admin)/(activity-management)/points/page.tsx @@ -1,6 +1,8 @@ import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; import { PointsDataTable } from "./_components/data-table"; +import { Suspense } from "react"; +import { DataTableSkeleton } from "@/components/data-table/skeleton"; export const metadata: Metadata = { title: "積分管理", @@ -23,7 +25,9 @@ export default function Page() {
- + }> + +
diff --git a/app/(admin)/(activity-management)/submissions/page.tsx b/app/(admin)/(activity-management)/submissions/page.tsx index dba01a3..1e1309b 100644 --- a/app/(admin)/(activity-management)/submissions/page.tsx +++ b/app/(admin)/(activity-management)/submissions/page.tsx @@ -1,6 +1,8 @@ import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; import { SubmissionsDataTable } from "./_components/data-table"; +import { Suspense } from "react"; +import { DataTableSkeleton } from "@/components/data-table/skeleton"; export const metadata: Metadata = { title: "提交記錄", @@ -23,7 +25,9 @@ export default function Page() {
- + }> + +
diff --git a/app/(admin)/(question-management)/database/page.tsx b/app/(admin)/(question-management)/database/page.tsx index d64686b..3159f53 100644 --- a/app/(admin)/(question-management)/database/page.tsx +++ b/app/(admin)/(question-management)/database/page.tsx @@ -2,6 +2,8 @@ import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; import { CreateDatabaseTrigger } from "./_components/create"; import { DatabaseDataTable } from "./_components/data-table"; +import { Suspense } from "react"; +import { DataTableSkeleton } from "@/components/data-table/skeleton"; export const metadata: Metadata = { title: "資料庫", @@ -25,7 +27,9 @@ export default function Page() {
- + }> + +
diff --git a/app/(admin)/(question-management)/questions/page.tsx b/app/(admin)/(question-management)/questions/page.tsx index f7e64cf..de95081 100644 --- a/app/(admin)/(question-management)/questions/page.tsx +++ b/app/(admin)/(question-management)/questions/page.tsx @@ -2,6 +2,8 @@ import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; import { CreateQuestionTrigger } from "./_components/create"; import { QuestionsDataTable } from "./_components/data-table"; +import { Suspense } from "react"; +import { DataTableSkeleton } from "@/components/data-table/skeleton"; export const metadata: Metadata = { title: "題庫", @@ -25,7 +27,9 @@ export default function Page() {
- + }> + +
diff --git a/app/(admin)/(user-management)/groups/page.tsx b/app/(admin)/(user-management)/groups/page.tsx index 076fc4c..1b14b80 100644 --- a/app/(admin)/(user-management)/groups/page.tsx +++ b/app/(admin)/(user-management)/groups/page.tsx @@ -2,6 +2,8 @@ import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; import { CreateGroupTrigger } from "./_components/create"; import { GroupDataTable } from "./_components/data-table"; +import { Suspense } from "react"; +import { DataTableSkeleton } from "@/components/data-table/skeleton"; export const metadata: Metadata = { title: "群組", @@ -25,7 +27,9 @@ export default function GroupsPage() {
- + }> + +
diff --git a/app/(admin)/(user-management)/scopesets/page.tsx b/app/(admin)/(user-management)/scopesets/page.tsx index 7372da9..4a49b63 100644 --- a/app/(admin)/(user-management)/scopesets/page.tsx +++ b/app/(admin)/(user-management)/scopesets/page.tsx @@ -2,6 +2,8 @@ import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; import { CreateScopeSetTrigger } from "./_components/create"; import { ScopeSetDataTable } from "./_components/data-table"; +import { Suspense } from "react"; +import { DataTableSkeleton } from "@/components/data-table/skeleton"; export const metadata: Metadata = { title: "權限集", @@ -25,7 +27,9 @@ export default function ScopesetPage() {
- + }> + +
diff --git a/app/(admin)/(user-management)/users/page.tsx b/app/(admin)/(user-management)/users/page.tsx index e779622..09b54b6 100644 --- a/app/(admin)/(user-management)/users/page.tsx +++ b/app/(admin)/(user-management)/users/page.tsx @@ -1,6 +1,8 @@ import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; import { UsersDataTable } from "./_components/data-table"; +import { Suspense } from "react"; +import { DataTableSkeleton } from "@/components/data-table/skeleton"; export const metadata: Metadata = { title: "使用者", @@ -23,7 +25,9 @@ export default function Page() {
- + }> + +
From 552196997ebba6e0971302292d0ef34c97120f28 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 19 Oct 2025 02:43:14 +0800 Subject: [PATCH 02/11] feat: make page size larger --- .../(activity-management)/events/_components/data-table.tsx | 2 +- .../(activity-management)/points/_components/data-table.tsx | 2 +- .../submissions/_components/data-table.tsx | 2 +- .../(question-management)/questions/_components/data-table.tsx | 2 +- app/(admin)/(user-management)/users/_components/data-table.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/(admin)/(activity-management)/events/_components/data-table.tsx b/app/(admin)/(activity-management)/events/_components/data-table.tsx index 3ab21a7..852bf38 100644 --- a/app/(admin)/(activity-management)/events/_components/data-table.tsx +++ b/app/(admin)/(activity-management)/events/_components/data-table.tsx @@ -8,7 +8,7 @@ import { columns, type Event } from "./data-table-columns"; import { EVENTS_TABLE_QUERY } from "./query"; export function EventsDataTable() { - const PAGE_SIZE = 10; + const PAGE_SIZE = 20; const [cursors, setCursors] = useState<(string | null)[]>([null]); const [currentIndex, setCurrentIndex] = useState(0); diff --git a/app/(admin)/(activity-management)/points/_components/data-table.tsx b/app/(admin)/(activity-management)/points/_components/data-table.tsx index 36ff67a..b7074b6 100644 --- a/app/(admin)/(activity-management)/points/_components/data-table.tsx +++ b/app/(admin)/(activity-management)/points/_components/data-table.tsx @@ -8,7 +8,7 @@ import { columns, type Point } from "./data-table-columns"; import { POINTS_TABLE_QUERY } from "./query"; export function PointsDataTable() { - const PAGE_SIZE = 10; + const PAGE_SIZE = 20; const [cursors, setCursors] = useState<(string | null)[]>([null]); const [currentIndex, setCurrentIndex] = useState(0); diff --git a/app/(admin)/(activity-management)/submissions/_components/data-table.tsx b/app/(admin)/(activity-management)/submissions/_components/data-table.tsx index 2a76235..8786a9c 100644 --- a/app/(admin)/(activity-management)/submissions/_components/data-table.tsx +++ b/app/(admin)/(activity-management)/submissions/_components/data-table.tsx @@ -8,7 +8,7 @@ import { columns, type Submission } from "./data-table-columns"; import { SUBMISSIONS_TABLE_QUERY } from "./query"; export function SubmissionsDataTable() { - const PAGE_SIZE = 10; + const PAGE_SIZE = 20; const [cursors, setCursors] = useState<(string | null)[]>([null]); const [currentIndex, setCurrentIndex] = useState(0); diff --git a/app/(admin)/(question-management)/questions/_components/data-table.tsx b/app/(admin)/(question-management)/questions/_components/data-table.tsx index 8f551f3..429eb52 100644 --- a/app/(admin)/(question-management)/questions/_components/data-table.tsx +++ b/app/(admin)/(question-management)/questions/_components/data-table.tsx @@ -8,7 +8,7 @@ import { columns, type Question } from "./data-table-columns"; import { QUESTIONS_TABLE_QUERY } from "./query"; export function QuestionsDataTable() { - const PAGE_SIZE = 5; + const PAGE_SIZE = 20; const [cursors, setCursors] = useState<(string | null)[]>([null]); const [currentIndex, setCurrentIndex] = useState(0); diff --git a/app/(admin)/(user-management)/users/_components/data-table.tsx b/app/(admin)/(user-management)/users/_components/data-table.tsx index 0127159..9f2f831 100644 --- a/app/(admin)/(user-management)/users/_components/data-table.tsx +++ b/app/(admin)/(user-management)/users/_components/data-table.tsx @@ -8,7 +8,7 @@ import { columns, type User } from "./data-table-columns"; import { USERS_TABLE_QUERY } from "./query"; export function UsersDataTable() { - const PAGE_SIZE = 5; + const PAGE_SIZE = 20; const [cursors, setCursors] = useState<(string | null)[]>([null]); const [currentIndex, setCurrentIndex] = useState(0); From 6e45933df304575fff64a7ff1c9b494ed1ef13f7 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 19 Oct 2025 03:16:04 +0800 Subject: [PATCH 03/11] feat: ranking in backend --- app/(admin)/_components/rank.tsx | 321 +++++++++++++++++++++++++++++++ app/(admin)/page.tsx | 8 +- gql/gql.ts | 30 +++ gql/graphql.ts | 29 ++- 4 files changed, 385 insertions(+), 3 deletions(-) create mode 100644 app/(admin)/_components/rank.tsx diff --git a/app/(admin)/_components/rank.tsx b/app/(admin)/_components/rank.tsx new file mode 100644 index 0000000..78b5b19 --- /dev/null +++ b/app/(admin)/_components/rank.tsx @@ -0,0 +1,321 @@ +"use client"; + +import { graphql, useFragment, type FragmentType } from "@/gql"; +import { RankingBy, RankingOrder, RankingPeriod } from "@/gql/graphql"; +import { useSuspenseQuery } from "@apollo/client/react"; +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import DataTablePagination from "@/components/data-table/pagination"; +import { Badge } from "@/components/ui/badge"; + +const OVERVIEW_RANKING = graphql(` + query OverviewRanking($filter: RankingFilter!, $first: Int!, $after: Cursor) { + ranking(filter: $filter, first: $first, after: $after) { + edges { + node { + id + name + } + ...ScoreCell + } + pageInfo { + endCursor + hasNextPage + } + } + } +`); + +const SCORE_CELL_FRAGMENT = graphql(` + fragment ScoreCell on RankingEdge { + ...UserCompletedQuestions + ...UserTotalPoints + ...RankingFragment + } +`); + +const USER_COMPLETED_QUESTIONS_FRAGMENT = graphql(` + fragment UserCompletedQuestions on RankingEdge { + node { + submissionStatistics { + solvedQuestions + } + } + } +`); + +const USER_TOTAL_POINTS_FRAGMENT = graphql(` + fragment UserTotalPoints on RankingEdge { + node { + totalPoints + } + } +`); + +const RANKING_FRAGMENT = graphql(` + fragment RankingFragment on RankingEdge { + score + } +`); + +const RANKING_BY_LABELS: Record = { + [RankingBy.Points]: "積分", + [RankingBy.CompletedQuestions]: "完成題數", +}; + +const RANKING_ORDER_LABELS: Record = { + [RankingOrder.Asc]: "遞增", + [RankingOrder.Desc]: "遞減", +}; + +const RANKING_PERIOD_LABELS: Record = { + [RankingPeriod.Daily]: "今日", + [RankingPeriod.Weekly]: "本週", +}; + +export function OverviewRanking() { + const PAGE_SIZE = 20; + + const [rankingBy, setRankingBy] = useState(RankingBy.Points); + const [rankingOrder, setRankingOrder] = useState( + RankingOrder.Desc + ); + const [rankingPeriod, setRankingPeriod] = useState( + RankingPeriod.Daily + ); + const [cursors, setCursors] = useState([]); + + const { data } = useSuspenseQuery(OVERVIEW_RANKING, { + variables: { + filter: { by: rankingBy, order: rankingOrder, period: rankingPeriod }, + first: PAGE_SIZE, + after: cursors?.[cursors.length - 1], + }, + }); + + const handlePageChange = (direction: "forward" | "backward") => { + if (direction === "forward" && data.ranking.pageInfo.endCursor) { + setCursors((prev) => [...prev, data.ranking.pageInfo.endCursor!]); + } else if (direction === "backward" && cursors.length > 0) { + setCursors((prev) => prev.slice(0, -1)); + } + }; + + // 當篩選條件改變時,重置 cursors + const handleFilterChange = () => { + setCursors([]); + }; + + return ( + + + 使用者排行榜 + + +
+ {/* 篩選器 */} +
+
+ 排序依據: + +
+ +
+ 排序方向: + +
+ +
+ 時間範圍: + +
+
+ + {/* 表格 */} + + + + 排名 + 使用者 + + {rankingBy === RankingBy.Points ? "積分" : "提交次數"} + + + + + {data.ranking.edges.length === 0 ? ( + + + 無資料 + + + ) : ( + data.ranking.edges.map((edge, index) => { + const rank = index + 1 + cursors.length * PAGE_SIZE; + return ( + + + {rank <= 3 ? ( + + #{rank} + + ) : ( + #{rank} + )} + + + {edge.node.name} + + + + + + ); + }) + )} + +
+ + {/* 分頁 */} + 0} + onPageChange={handlePageChange} + /> +
+
+
+ ); +} + +function ScoreCell({ userFragment, rankingBy }: { userFragment: FragmentType, rankingBy: RankingBy }) { + const user = useFragment(SCORE_CELL_FRAGMENT, userFragment); + + const components = { + [RankingBy.Points]: , + [RankingBy.CompletedQuestions]: , + } + + return ( + + {components[rankingBy] ?? 發現未定義的排序依據 {rankingBy}}{' | '} + + + ); +} + +function CompletedQuestion({ userFragment }: { userFragment: FragmentType }) { + const user = useFragment(USER_COMPLETED_QUESTIONS_FRAGMENT, userFragment); + + return ( + + {user.node.submissionStatistics.solvedQuestions} + + ); +} + +function TotalPoints({ userFragment }: { userFragment: FragmentType }) { + const user = useFragment(USER_TOTAL_POINTS_FRAGMENT, userFragment); + + return ( + + {user.node.totalPoints} + + ); +} + +function ScoreDiff({ userFragment }: { userFragment: FragmentType }) { + const user = useFragment(RANKING_FRAGMENT, userFragment); + const absScore = Math.abs(user.score); + + if (user.score > 0) { + return +{absScore}; + } else if (user.score < 0) { + return -{absScore}; + } + + return 沒有變化; +} diff --git a/app/(admin)/page.tsx b/app/(admin)/page.tsx index 0279834..90b8412 100644 --- a/app/(admin)/page.tsx +++ b/app/(admin)/page.tsx @@ -1,6 +1,8 @@ -import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; +import { Suspense } from "react"; +import { OverviewRanking } from "./_components/rank"; +import { Skeleton } from "@/components/ui/skeleton"; export const metadata: Metadata = { title: "概覽", @@ -16,7 +18,9 @@ export default function Home() { md:p-8 `} > - + }> + + ); diff --git a/gql/gql.ts b/gql/gql.ts index 57a6d37..fb58a60 100644 --- a/gql/gql.ts +++ b/gql/gql.ts @@ -62,6 +62,11 @@ type Documents = { "\n query UserById($id: ID!) {\n user(id: $id) {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n": typeof types.UserByIdDocument, "\n query GroupList {\n groups {\n id\n name\n }\n }\n": typeof types.GroupListDocument, "\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.UsersTableDocument, + "\n query OverviewRanking($filter: RankingFilter!, $first: Int!, $after: Cursor) {\n ranking(filter: $filter, first: $first, after: $after) {\n edges {\n node {\n id\n name\n }\n ...ScoreCell\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n": typeof types.OverviewRankingDocument, + "\n fragment ScoreCell on RankingEdge {\n ...UserCompletedQuestions\n ...UserTotalPoints\n ...RankingFragment\n }\n": typeof types.ScoreCellFragmentDoc, + "\n fragment UserCompletedQuestions on RankingEdge {\n node {\n submissionStatistics {\n solvedQuestions\n }\n }\n }\n": typeof types.UserCompletedQuestionsFragmentDoc, + "\n fragment UserTotalPoints on RankingEdge {\n node {\n totalPoints\n }\n }\n": typeof types.UserTotalPointsFragmentDoc, + "\n fragment RankingFragment on RankingEdge {\n score\n }\n": typeof types.RankingFragmentFragmentDoc, "\n mutation MeUpdateUserInfo($input: UpdateUserInput!) {\n updateMe(input: $input) {\n id\n }\n }\n": typeof types.MeUpdateUserInfoDocument, "\n query MeUserInfo {\n me {\n id\n name\n avatar\n }\n }\n": typeof types.MeUserInfoDocument, "\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, @@ -115,6 +120,11 @@ const documents: Documents = { "\n query UserById($id: ID!) {\n user(id: $id) {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n": types.UserByIdDocument, "\n query GroupList {\n groups {\n id\n name\n }\n }\n": types.GroupListDocument, "\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.UsersTableDocument, + "\n query OverviewRanking($filter: RankingFilter!, $first: Int!, $after: Cursor) {\n ranking(filter: $filter, first: $first, after: $after) {\n edges {\n node {\n id\n name\n }\n ...ScoreCell\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n": types.OverviewRankingDocument, + "\n fragment ScoreCell on RankingEdge {\n ...UserCompletedQuestions\n ...UserTotalPoints\n ...RankingFragment\n }\n": types.ScoreCellFragmentDoc, + "\n fragment UserCompletedQuestions on RankingEdge {\n node {\n submissionStatistics {\n solvedQuestions\n }\n }\n }\n": types.UserCompletedQuestionsFragmentDoc, + "\n fragment UserTotalPoints on RankingEdge {\n node {\n totalPoints\n }\n }\n": types.UserTotalPointsFragmentDoc, + "\n fragment RankingFragment on RankingEdge {\n score\n }\n": types.RankingFragmentFragmentDoc, "\n mutation MeUpdateUserInfo($input: UpdateUserInput!) {\n updateMe(input: $input) {\n id\n }\n }\n": types.MeUpdateUserInfoDocument, "\n query MeUserInfo {\n me {\n id\n name\n avatar\n }\n }\n": types.MeUserInfoDocument, "\n query BasicUserInfo {\n me {\n id\n name\n email\n avatar\n\n group {\n name\n }\n }\n }\n": types.BasicUserInfoDocument, @@ -326,6 +336,26 @@ export function graphql(source: "\n query GroupList {\n groups {\n id\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 UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\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 OverviewRanking($filter: RankingFilter!, $first: Int!, $after: Cursor) {\n ranking(filter: $filter, first: $first, after: $after) {\n edges {\n node {\n id\n name\n }\n ...ScoreCell\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n"): (typeof documents)["\n query OverviewRanking($filter: RankingFilter!, $first: Int!, $after: Cursor) {\n ranking(filter: $filter, first: $first, after: $after) {\n edges {\n node {\n id\n name\n }\n ...ScoreCell\n }\n pageInfo {\n endCursor\n hasNextPage\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 ScoreCell on RankingEdge {\n ...UserCompletedQuestions\n ...UserTotalPoints\n ...RankingFragment\n }\n"): (typeof documents)["\n fragment ScoreCell on RankingEdge {\n ...UserCompletedQuestions\n ...UserTotalPoints\n ...RankingFragment\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 UserCompletedQuestions on RankingEdge {\n node {\n submissionStatistics {\n solvedQuestions\n }\n }\n }\n"): (typeof documents)["\n fragment UserCompletedQuestions on RankingEdge {\n node {\n submissionStatistics {\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 fragment UserTotalPoints on RankingEdge {\n node {\n totalPoints\n }\n }\n"): (typeof documents)["\n fragment UserTotalPoints on RankingEdge {\n node {\n totalPoints\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 RankingFragment on RankingEdge {\n score\n }\n"): (typeof documents)["\n fragment RankingFragment on RankingEdge {\n score\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/gql/graphql.ts b/gql/graphql.ts index e388281..0469f73 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -1852,6 +1852,29 @@ export type UsersTableQueryVariables = Exact<{ export type UsersTableQuery = { __typename?: 'Query', users: { __typename?: 'UserConnection', totalCount: number, edges?: Array<{ __typename?: 'UserEdge', node?: { __typename?: 'User', id: string, name: string, email: string, avatar?: string | null, createdAt: string, updatedAt: string, group: { __typename?: 'Group', id: string, name: string } } | null } | null> | null, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, endCursor?: any | null, startCursor?: any | null } } }; +export type OverviewRankingQueryVariables = Exact<{ + filter: RankingFilter; + first: Scalars['Int']['input']; + after?: InputMaybe; +}>; + + +export type OverviewRankingQuery = { __typename?: 'Query', ranking: { __typename?: 'RankingConnection', edges: Array<( + { __typename?: 'RankingEdge', node: { __typename?: 'User', id: string, name: string } } + & { ' $fragmentRefs'?: { 'ScoreCellFragment': ScoreCellFragment } } + )>, pageInfo: { __typename?: 'PageInfo', endCursor?: any | null, hasNextPage: boolean } } }; + +export type ScoreCellFragment = ( + { __typename?: 'RankingEdge' } + & { ' $fragmentRefs'?: { 'UserCompletedQuestionsFragment': UserCompletedQuestionsFragment;'UserTotalPointsFragment': UserTotalPointsFragment;'RankingFragmentFragment': RankingFragmentFragment } } +) & { ' $fragmentName'?: 'ScoreCellFragment' }; + +export type UserCompletedQuestionsFragment = { __typename?: 'RankingEdge', node: { __typename?: 'User', submissionStatistics: { __typename?: 'SubmissionStatistics', solvedQuestions: number } } } & { ' $fragmentName'?: 'UserCompletedQuestionsFragment' }; + +export type UserTotalPointsFragment = { __typename?: 'RankingEdge', node: { __typename?: 'User', totalPoints: number } } & { ' $fragmentName'?: 'UserTotalPointsFragment' }; + +export type RankingFragmentFragment = { __typename?: 'RankingEdge', score: number } & { ' $fragmentName'?: 'RankingFragmentFragment' }; + export type MeUpdateUserInfoMutationVariables = Exact<{ input: UpdateUserInput; }>; @@ -1869,7 +1892,10 @@ export type BasicUserInfoQueryVariables = Exact<{ [key: string]: never; }>; export type BasicUserInfoQuery = { __typename?: 'Query', me: { __typename?: 'User', id: string, name: string, email: string, avatar?: string | null, group: { __typename?: 'Group', name: string } } }; - +export const UserCompletedQuestionsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserCompletedQuestions"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submissionStatistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"solvedQuestions"}}]}}]}}]}}]} as unknown as DocumentNode; +export const UserTotalPointsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserTotalPoints"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPoints"}}]}}]}}]} as unknown as DocumentNode; +export const RankingFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RankingFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"score"}}]}}]} as unknown as DocumentNode; +export const ScoreCellFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ScoreCell"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserCompletedQuestions"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserTotalPoints"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RankingFragment"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserCompletedQuestions"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submissionStatistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"solvedQuestions"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserTotalPoints"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPoints"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RankingFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"score"}}]}}]} as unknown as DocumentNode; export const EventByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}},{"kind":"Field","name":{"kind":"Name","value":"triggeredAt"}}]}}]}}]} as unknown as DocumentNode; export const EventsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"field"},"value":{"kind":"EnumValue","value":"TRIGGERED_AT"}},{"kind":"ObjectField","name":{"kind":"Name","value":"direction"},"value":{"kind":"EnumValue","value":"DESC"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"triggeredAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; export const PointByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PointById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pointGrant"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"points"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"grantedAt"}}]}}]}}]} as unknown as DocumentNode; @@ -1918,6 +1944,7 @@ export const ImpersonateUserDocument = {"kind":"Document","definitions":[{"kind" export const UserByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; export const GroupListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GroupList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groups"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const UsersTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UsersTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; +export const OverviewRankingDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"OverviewRanking"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RankingFilter"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ranking"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ScoreCell"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserCompletedQuestions"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submissionStatistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"solvedQuestions"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserTotalPoints"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPoints"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RankingFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"score"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ScoreCell"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserCompletedQuestions"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserTotalPoints"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RankingFragment"}}]}}]} as unknown as DocumentNode; export const MeUpdateUserInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MeUpdateUserInfo"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateUserInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMe"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const MeUserInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MeUserInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}}]} as unknown as DocumentNode; export const BasicUserInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BasicUserInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file From 66d54013357af7d292afd98f95ef76e85f37bba6 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 19 Oct 2025 03:56:14 +0800 Subject: [PATCH 04/11] feat: overview page --- app/(admin)/_components/header.tsx | 18 ++++ app/(admin)/_components/login-count.tsx | 85 +++++++++++++++++++ app/(admin)/_components/rank.tsx | 6 +- app/(admin)/_components/submit-count.tsx | 101 +++++++++++++++++++++++ app/(admin)/page.tsx | 13 ++- components/ui/spinner.tsx | 16 ++++ components/ui/tabs.tsx | 66 +++++++++++++++ gql/gql.ts | 12 +++ gql/graphql.ts | 16 ++++ package.json | 1 + pnpm-lock.yaml | 32 +++++++ 11 files changed, 362 insertions(+), 4 deletions(-) create mode 100644 app/(admin)/_components/header.tsx create mode 100644 app/(admin)/_components/login-count.tsx create mode 100644 app/(admin)/_components/submit-count.tsx create mode 100644 components/ui/spinner.tsx create mode 100644 components/ui/tabs.tsx diff --git a/app/(admin)/_components/header.tsx b/app/(admin)/_components/header.tsx new file mode 100644 index 0000000..a7c341d --- /dev/null +++ b/app/(admin)/_components/header.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { useUser } from "@/providers/use-user"; + +export function Header() { + const { user } = useUser(); + + return ( +
+
+

哈囉,{user.name}!

+

+ 這裡可以快速總覽所有統計資料,也可以點選左邊的側邊來進行資料管理。 +

+
+
+ ); +} diff --git a/app/(admin)/_components/login-count.tsx b/app/(admin)/_components/login-count.tsx new file mode 100644 index 0000000..02b7c19 --- /dev/null +++ b/app/(admin)/_components/login-count.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { graphql } from "@/gql"; +import { SubmissionStatus, type SubmissionWhereInput } from "@/gql/graphql"; +import { useLazyQuery } from "@apollo/client/react"; +import { useEffect, useState } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; + +const LOGIN_TOTAL_COUNT_QUERY = graphql(` + query LoginTotalCount($where: EventWhereInput!) { + events(where: $where) { + totalCount + } + } +`); + +type TimeRange = "daily" | "weekly" | "all"; + +const TIME_RANGE_LABELS: Record = { + daily: "今日", + weekly: "本週", + all: "全部", +}; + +export default function LoginTotalCount() { + const [timeRange, setTimeRange] = useState("daily"); + + const [getLoginTotalCount, { data, loading }] = useLazyQuery(LOGIN_TOTAL_COUNT_QUERY); + + useEffect(() => { + const now = new Date(); + + const timeRangeWhere: Record = { + daily: { + submittedAtGTE: new Date(now.setDate(now.getDate() - 1)).toISOString(), + }, + weekly: { + submittedAtGTE: new Date(now.setDate(now.getDate() - 7)).toISOString(), + }, + all: {}, + }; + + const where = { + ...timeRangeWhere[timeRange], + type: "login", + } + + getLoginTotalCount({ variables: { where } }); + }, [timeRange, getLoginTotalCount]); + + return ( + + + 登入總數 + + 所有使用者在這段期間的總登入次數。 + + + + setTimeRange(value as TimeRange)}> + + {Object.entries(TIME_RANGE_LABELS).map(([value, label]) => ( + + {label} + + ))} + + +
+ {!loading && (data?.events.totalCount?.toLocaleString('zh-TW') ?? 0)} + {loading && } +
+

+ {TIME_RANGE_LABELS[timeRange]}的登入次數 +

+
+
+
+
+ ); +} diff --git a/app/(admin)/_components/rank.tsx b/app/(admin)/_components/rank.tsx index 78b5b19..b06296c 100644 --- a/app/(admin)/_components/rank.tsx +++ b/app/(admin)/_components/rank.tsx @@ -23,7 +23,7 @@ import { import DataTablePagination from "@/components/data-table/pagination"; import { Badge } from "@/components/ui/badge"; -const OVERVIEW_RANKING = graphql(` +const OVERVIEW_RANKING_QUERY = graphql(` query OverviewRanking($filter: RankingFilter!, $first: Int!, $after: Cursor) { ranking(filter: $filter, first: $first, after: $after) { edges { @@ -88,7 +88,7 @@ const RANKING_PERIOD_LABELS: Record = { [RankingPeriod.Weekly]: "本週", }; -export function OverviewRanking() { +export default function OverviewRanking() { const PAGE_SIZE = 20; const [rankingBy, setRankingBy] = useState(RankingBy.Points); @@ -100,7 +100,7 @@ export function OverviewRanking() { ); const [cursors, setCursors] = useState([]); - const { data } = useSuspenseQuery(OVERVIEW_RANKING, { + const { data } = useSuspenseQuery(OVERVIEW_RANKING_QUERY, { variables: { filter: { by: rankingBy, order: rankingOrder, period: rankingPeriod }, first: PAGE_SIZE, diff --git a/app/(admin)/_components/submit-count.tsx b/app/(admin)/_components/submit-count.tsx new file mode 100644 index 0000000..18c8941 --- /dev/null +++ b/app/(admin)/_components/submit-count.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { graphql } from "@/gql"; +import { SubmissionStatus, type SubmissionWhereInput } from "@/gql/graphql"; +import { useLazyQuery } from "@apollo/client/react"; +import { useEffect, useState } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; + +const SUBMISSIONS_TOTAL_COUNT_QUERY = graphql(` + query SubmissionsTotalCount($where: SubmissionWhereInput!) { + submissions(where: $where) { + totalCount + } + } +`); + +type TimeRange = "daily" | "weekly" | "all"; + +const TIME_RANGE_LABELS: Record = { + daily: "今日", + weekly: "本週", + all: "全部", +}; + +export default function SubmissionsTotalCount() { + const [showSuccessOnly, setShowSuccessOnly] = useState(false); + const [timeRange, setTimeRange] = useState("daily"); + + const [getSubmissionsTotalCount, { data, loading }] = useLazyQuery(SUBMISSIONS_TOTAL_COUNT_QUERY); + + useEffect(() => { + const now = new Date(); + + const timeRangeWhere: Record = { + daily: { + submittedAtGTE: new Date(now.setDate(now.getDate() - 1)).toISOString(), + }, + weekly: { + submittedAtGTE: new Date(now.setDate(now.getDate() - 7)).toISOString(), + }, + all: {}, + }; + + const where: SubmissionWhereInput = showSuccessOnly + ? { + ...timeRangeWhere[timeRange], + status: SubmissionStatus.Success, + } + : timeRangeWhere[timeRange]; + + getSubmissionsTotalCount({ variables: { where } }); + }, [showSuccessOnly, timeRange, getSubmissionsTotalCount]); + + return ( + + + 提交總數 + + 所有使用者在這段期間的總提交數量。 + + + + setTimeRange(value as TimeRange)}> + + {Object.entries(TIME_RANGE_LABELS).map(([value, label]) => ( + + {label} + + ))} + + +
+ {!loading && (data?.submissions.totalCount?.toLocaleString('zh-TW') ?? 0)} + {loading && } +
+

+ {TIME_RANGE_LABELS[timeRange]}的提交數量 +

+
+
+
+ setShowSuccessOnly(checked === true)} + /> + +
+
+
+ ); +} diff --git a/app/(admin)/page.tsx b/app/(admin)/page.tsx index 90b8412..29c0821 100644 --- a/app/(admin)/page.tsx +++ b/app/(admin)/page.tsx @@ -1,8 +1,11 @@ import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; import { Suspense } from "react"; -import { OverviewRanking } from "./_components/rank"; +import OverviewRanking from "./_components/rank"; import { Skeleton } from "@/components/ui/skeleton"; +import SubmissionsTotalCount from "./_components/submit-count"; +import LoginTotalCount from "./_components/login-count"; +import { Header } from "./_components/header"; export const metadata: Metadata = { title: "概覽", @@ -18,6 +21,14 @@ export default function Home() { md:p-8 `} > +
+
+ + +
}> diff --git a/components/ui/spinner.tsx b/components/ui/spinner.tsx new file mode 100644 index 0000000..a70e713 --- /dev/null +++ b/components/ui/spinner.tsx @@ -0,0 +1,16 @@ +import { Loader2Icon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Spinner({ className, ...props }: React.ComponentProps<"svg">) { + return ( + + ) +} + +export { Spinner } diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx new file mode 100644 index 0000000..497ba5e --- /dev/null +++ b/components/ui/tabs.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/gql/gql.ts b/gql/gql.ts index fb58a60..5a14ea2 100644 --- a/gql/gql.ts +++ b/gql/gql.ts @@ -62,11 +62,13 @@ type Documents = { "\n query UserById($id: ID!) {\n user(id: $id) {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n": typeof types.UserByIdDocument, "\n query GroupList {\n groups {\n id\n name\n }\n }\n": typeof types.GroupListDocument, "\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.UsersTableDocument, + "\n query LoginTotalCount($where: EventWhereInput!) {\n events(where: $where) {\n totalCount\n }\n }\n": typeof types.LoginTotalCountDocument, "\n query OverviewRanking($filter: RankingFilter!, $first: Int!, $after: Cursor) {\n ranking(filter: $filter, first: $first, after: $after) {\n edges {\n node {\n id\n name\n }\n ...ScoreCell\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n": typeof types.OverviewRankingDocument, "\n fragment ScoreCell on RankingEdge {\n ...UserCompletedQuestions\n ...UserTotalPoints\n ...RankingFragment\n }\n": typeof types.ScoreCellFragmentDoc, "\n fragment UserCompletedQuestions on RankingEdge {\n node {\n submissionStatistics {\n solvedQuestions\n }\n }\n }\n": typeof types.UserCompletedQuestionsFragmentDoc, "\n fragment UserTotalPoints on RankingEdge {\n node {\n totalPoints\n }\n }\n": typeof types.UserTotalPointsFragmentDoc, "\n fragment RankingFragment on RankingEdge {\n score\n }\n": typeof types.RankingFragmentFragmentDoc, + "\n query SubmissionsTotalCount($where: SubmissionWhereInput!) {\n submissions(where: $where) {\n totalCount\n }\n }\n": typeof types.SubmissionsTotalCountDocument, "\n mutation MeUpdateUserInfo($input: UpdateUserInput!) {\n updateMe(input: $input) {\n id\n }\n }\n": typeof types.MeUpdateUserInfoDocument, "\n query MeUserInfo {\n me {\n id\n name\n avatar\n }\n }\n": typeof types.MeUserInfoDocument, "\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, @@ -120,11 +122,13 @@ const documents: Documents = { "\n query UserById($id: ID!) {\n user(id: $id) {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n": types.UserByIdDocument, "\n query GroupList {\n groups {\n id\n name\n }\n }\n": types.GroupListDocument, "\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.UsersTableDocument, + "\n query LoginTotalCount($where: EventWhereInput!) {\n events(where: $where) {\n totalCount\n }\n }\n": types.LoginTotalCountDocument, "\n query OverviewRanking($filter: RankingFilter!, $first: Int!, $after: Cursor) {\n ranking(filter: $filter, first: $first, after: $after) {\n edges {\n node {\n id\n name\n }\n ...ScoreCell\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n": types.OverviewRankingDocument, "\n fragment ScoreCell on RankingEdge {\n ...UserCompletedQuestions\n ...UserTotalPoints\n ...RankingFragment\n }\n": types.ScoreCellFragmentDoc, "\n fragment UserCompletedQuestions on RankingEdge {\n node {\n submissionStatistics {\n solvedQuestions\n }\n }\n }\n": types.UserCompletedQuestionsFragmentDoc, "\n fragment UserTotalPoints on RankingEdge {\n node {\n totalPoints\n }\n }\n": types.UserTotalPointsFragmentDoc, "\n fragment RankingFragment on RankingEdge {\n score\n }\n": types.RankingFragmentFragmentDoc, + "\n query SubmissionsTotalCount($where: SubmissionWhereInput!) {\n submissions(where: $where) {\n totalCount\n }\n }\n": types.SubmissionsTotalCountDocument, "\n mutation MeUpdateUserInfo($input: UpdateUserInput!) {\n updateMe(input: $input) {\n id\n }\n }\n": types.MeUpdateUserInfoDocument, "\n query MeUserInfo {\n me {\n id\n name\n avatar\n }\n }\n": types.MeUserInfoDocument, "\n query BasicUserInfo {\n me {\n id\n name\n email\n avatar\n\n group {\n name\n }\n }\n }\n": types.BasicUserInfoDocument, @@ -336,6 +340,10 @@ export function graphql(source: "\n query GroupList {\n groups {\n id\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 UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\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 LoginTotalCount($where: EventWhereInput!) {\n events(where: $where) {\n totalCount\n }\n }\n"): (typeof documents)["\n query LoginTotalCount($where: EventWhereInput!) {\n events(where: $where) {\n totalCount\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -356,6 +364,10 @@ export function graphql(source: "\n fragment UserTotalPoints on RankingEdge {\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 RankingFragment on RankingEdge {\n score\n }\n"): (typeof documents)["\n fragment RankingFragment on RankingEdge {\n score\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 SubmissionsTotalCount($where: SubmissionWhereInput!) {\n submissions(where: $where) {\n totalCount\n }\n }\n"): (typeof documents)["\n query SubmissionsTotalCount($where: SubmissionWhereInput!) {\n submissions(where: $where) {\n totalCount\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/gql/graphql.ts b/gql/graphql.ts index 0469f73..1e2a8c6 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -1852,6 +1852,13 @@ export type UsersTableQueryVariables = Exact<{ export type UsersTableQuery = { __typename?: 'Query', users: { __typename?: 'UserConnection', totalCount: number, edges?: Array<{ __typename?: 'UserEdge', node?: { __typename?: 'User', id: string, name: string, email: string, avatar?: string | null, createdAt: string, updatedAt: string, group: { __typename?: 'Group', id: string, name: string } } | null } | null> | null, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, endCursor?: any | null, startCursor?: any | null } } }; +export type LoginTotalCountQueryVariables = Exact<{ + where: EventWhereInput; +}>; + + +export type LoginTotalCountQuery = { __typename?: 'Query', events: { __typename?: 'EventConnection', totalCount: number } }; + export type OverviewRankingQueryVariables = Exact<{ filter: RankingFilter; first: Scalars['Int']['input']; @@ -1875,6 +1882,13 @@ export type UserTotalPointsFragment = { __typename?: 'RankingEdge', node: { __ty export type RankingFragmentFragment = { __typename?: 'RankingEdge', score: number } & { ' $fragmentName'?: 'RankingFragmentFragment' }; +export type SubmissionsTotalCountQueryVariables = Exact<{ + where: SubmissionWhereInput; +}>; + + +export type SubmissionsTotalCountQuery = { __typename?: 'Query', submissions: { __typename?: 'SubmissionConnection', totalCount: number } }; + export type MeUpdateUserInfoMutationVariables = Exact<{ input: UpdateUserInput; }>; @@ -1944,7 +1958,9 @@ export const ImpersonateUserDocument = {"kind":"Document","definitions":[{"kind" export const UserByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; export const GroupListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GroupList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groups"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const UsersTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UsersTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; +export const LoginTotalCountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LoginTotalCount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EventWhereInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]} as unknown as DocumentNode; export const OverviewRankingDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"OverviewRanking"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RankingFilter"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ranking"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ScoreCell"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserCompletedQuestions"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submissionStatistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"solvedQuestions"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserTotalPoints"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPoints"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RankingFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"score"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ScoreCell"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserCompletedQuestions"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserTotalPoints"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RankingFragment"}}]}}]} as unknown as DocumentNode; +export const SubmissionsTotalCountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SubmissionsTotalCount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubmissionWhereInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submissions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]} as unknown as DocumentNode; export const MeUpdateUserInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MeUpdateUserInfo"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateUserInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMe"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const MeUserInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MeUserInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}}]} as unknown as DocumentNode; export const BasicUserInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BasicUserInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/package.json b/package.json index e9885e6..d65beea 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@swc-contrib/plugin-graphql-codegen-client-preset": "^0.8.0", "@tailwindcss/typography": "^0.5.19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8535589..8a55add 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,6 +58,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.2.2)(react@19.3.0-canary-1873ad79-20251015) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.3.0-canary-1873ad79-20251015(react@19.3.0-canary-1873ad79-20251015))(react@19.3.0-canary-1873ad79-20251015) '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.3.0-canary-1873ad79-20251015(react@19.3.0-canary-1873ad79-20251015))(react@19.3.0-canary-1873ad79-20251015) @@ -1477,6 +1480,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tooltip@1.2.8': resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} peerDependencies: @@ -5764,6 +5780,22 @@ snapshots: optionalDependencies: '@types/react': 19.2.2 + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.3.0-canary-1873ad79-20251015(react@19.3.0-canary-1873ad79-20251015))(react@19.3.0-canary-1873ad79-20251015)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.3.0-canary-1873ad79-20251015) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.3.0-canary-1873ad79-20251015) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.3.0-canary-1873ad79-20251015) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.3.0-canary-1873ad79-20251015(react@19.3.0-canary-1873ad79-20251015))(react@19.3.0-canary-1873ad79-20251015) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.3.0-canary-1873ad79-20251015(react@19.3.0-canary-1873ad79-20251015))(react@19.3.0-canary-1873ad79-20251015) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.3.0-canary-1873ad79-20251015(react@19.3.0-canary-1873ad79-20251015))(react@19.3.0-canary-1873ad79-20251015) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.3.0-canary-1873ad79-20251015) + react: 19.3.0-canary-1873ad79-20251015 + react-dom: 19.3.0-canary-1873ad79-20251015(react@19.3.0-canary-1873ad79-20251015) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.3.0-canary-1873ad79-20251015(react@19.3.0-canary-1873ad79-20251015))(react@19.3.0-canary-1873ad79-20251015)': dependencies: '@radix-ui/primitive': 1.1.3 From d9635f89194278437d3f29b0db77ea4867d5240d Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 19 Oct 2025 03:56:42 +0800 Subject: [PATCH 05/11] style: reformat codebase --- .../(activity-management)/events/page.tsx | 4 +- .../(activity-management)/points/page.tsx | 4 +- .../submissions/page.tsx | 4 +- .../(question-management)/database/page.tsx | 4 +- .../(question-management)/questions/page.tsx | 4 +- app/(admin)/(user-management)/groups/page.tsx | 4 +- .../(user-management)/scopesets/page.tsx | 4 +- app/(admin)/(user-management)/users/page.tsx | 4 +- app/(admin)/_components/login-count.tsx | 16 +-- app/(admin)/_components/rank.tsx | 126 ++++++++---------- app/(admin)/_components/submit-count.tsx | 20 +-- app/(admin)/page.tsx | 16 ++- components/ui/spinner.tsx | 8 +- components/ui/tabs.tsx | 44 ++++-- 14 files changed, 135 insertions(+), 127 deletions(-) diff --git a/app/(admin)/(activity-management)/events/page.tsx b/app/(admin)/(activity-management)/events/page.tsx index 7f42b79..b5e4b77 100644 --- a/app/(admin)/(activity-management)/events/page.tsx +++ b/app/(admin)/(activity-management)/events/page.tsx @@ -1,8 +1,8 @@ +import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; -import { EventsDataTable } from "./_components/data-table"; import { Suspense } from "react"; -import { DataTableSkeleton } from "@/components/data-table/skeleton"; +import { EventsDataTable } from "./_components/data-table"; export const metadata: Metadata = { title: "事件管理", diff --git a/app/(admin)/(activity-management)/points/page.tsx b/app/(admin)/(activity-management)/points/page.tsx index 393ab2b..f9d95be 100644 --- a/app/(admin)/(activity-management)/points/page.tsx +++ b/app/(admin)/(activity-management)/points/page.tsx @@ -1,8 +1,8 @@ +import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; -import { PointsDataTable } from "./_components/data-table"; import { Suspense } from "react"; -import { DataTableSkeleton } from "@/components/data-table/skeleton"; +import { PointsDataTable } from "./_components/data-table"; export const metadata: Metadata = { title: "積分管理", diff --git a/app/(admin)/(activity-management)/submissions/page.tsx b/app/(admin)/(activity-management)/submissions/page.tsx index 1e1309b..04d57b7 100644 --- a/app/(admin)/(activity-management)/submissions/page.tsx +++ b/app/(admin)/(activity-management)/submissions/page.tsx @@ -1,8 +1,8 @@ +import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; -import { SubmissionsDataTable } from "./_components/data-table"; import { Suspense } from "react"; -import { DataTableSkeleton } from "@/components/data-table/skeleton"; +import { SubmissionsDataTable } from "./_components/data-table"; export const metadata: Metadata = { title: "提交記錄", diff --git a/app/(admin)/(question-management)/database/page.tsx b/app/(admin)/(question-management)/database/page.tsx index 3159f53..69b52f7 100644 --- a/app/(admin)/(question-management)/database/page.tsx +++ b/app/(admin)/(question-management)/database/page.tsx @@ -1,9 +1,9 @@ +import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; +import { Suspense } from "react"; import { CreateDatabaseTrigger } from "./_components/create"; import { DatabaseDataTable } from "./_components/data-table"; -import { Suspense } from "react"; -import { DataTableSkeleton } from "@/components/data-table/skeleton"; export const metadata: Metadata = { title: "資料庫", diff --git a/app/(admin)/(question-management)/questions/page.tsx b/app/(admin)/(question-management)/questions/page.tsx index de95081..d3c56f5 100644 --- a/app/(admin)/(question-management)/questions/page.tsx +++ b/app/(admin)/(question-management)/questions/page.tsx @@ -1,9 +1,9 @@ +import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; +import { Suspense } from "react"; import { CreateQuestionTrigger } from "./_components/create"; import { QuestionsDataTable } from "./_components/data-table"; -import { Suspense } from "react"; -import { DataTableSkeleton } from "@/components/data-table/skeleton"; export const metadata: Metadata = { title: "題庫", diff --git a/app/(admin)/(user-management)/groups/page.tsx b/app/(admin)/(user-management)/groups/page.tsx index 1b14b80..01366dd 100644 --- a/app/(admin)/(user-management)/groups/page.tsx +++ b/app/(admin)/(user-management)/groups/page.tsx @@ -1,9 +1,9 @@ +import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; +import { Suspense } from "react"; import { CreateGroupTrigger } from "./_components/create"; import { GroupDataTable } from "./_components/data-table"; -import { Suspense } from "react"; -import { DataTableSkeleton } from "@/components/data-table/skeleton"; export const metadata: Metadata = { title: "群組", diff --git a/app/(admin)/(user-management)/scopesets/page.tsx b/app/(admin)/(user-management)/scopesets/page.tsx index 4a49b63..9ed2565 100644 --- a/app/(admin)/(user-management)/scopesets/page.tsx +++ b/app/(admin)/(user-management)/scopesets/page.tsx @@ -1,9 +1,9 @@ +import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; +import { Suspense } from "react"; import { CreateScopeSetTrigger } from "./_components/create"; import { ScopeSetDataTable } from "./_components/data-table"; -import { Suspense } from "react"; -import { DataTableSkeleton } from "@/components/data-table/skeleton"; export const metadata: Metadata = { title: "權限集", diff --git a/app/(admin)/(user-management)/users/page.tsx b/app/(admin)/(user-management)/users/page.tsx index 09b54b6..4b3b848 100644 --- a/app/(admin)/(user-management)/users/page.tsx +++ b/app/(admin)/(user-management)/users/page.tsx @@ -1,8 +1,8 @@ +import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; -import { UsersDataTable } from "./_components/data-table"; import { Suspense } from "react"; -import { DataTableSkeleton } from "@/components/data-table/skeleton"; +import { UsersDataTable } from "./_components/data-table"; export const metadata: Metadata = { title: "使用者", diff --git a/app/(admin)/_components/login-count.tsx b/app/(admin)/_components/login-count.tsx index 02b7c19..07c0624 100644 --- a/app/(admin)/_components/login-count.tsx +++ b/app/(admin)/_components/login-count.tsx @@ -1,14 +1,12 @@ "use client"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { graphql } from "@/gql"; -import { SubmissionStatus, type SubmissionWhereInput } from "@/gql/graphql"; +import { type SubmissionWhereInput } from "@/gql/graphql"; import { useLazyQuery } from "@apollo/client/react"; import { useEffect, useState } from "react"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Label } from "@/components/ui/label"; -import { Skeleton } from "@/components/ui/skeleton"; const LOGIN_TOTAL_COUNT_QUERY = graphql(` query LoginTotalCount($where: EventWhereInput!) { @@ -43,11 +41,11 @@ export default function LoginTotalCount() { }, all: {}, }; - + const where = { ...timeRangeWhere[timeRange], type: "login", - } + }; getLoginTotalCount({ variables: { where } }); }, [timeRange, getLoginTotalCount]); @@ -71,7 +69,7 @@ export default function LoginTotalCount() {
- {!loading && (data?.events.totalCount?.toLocaleString('zh-TW') ?? 0)} + {!loading && (data?.events.totalCount?.toLocaleString("zh-TW") ?? 0)} {loading && }

diff --git a/app/(admin)/_components/rank.tsx b/app/(admin)/_components/rank.tsx index b06296c..1a870f9 100644 --- a/app/(admin)/_components/rank.tsx +++ b/app/(admin)/_components/rank.tsx @@ -1,27 +1,14 @@ "use client"; -import { graphql, useFragment, type FragmentType } from "@/gql"; +import DataTablePagination from "@/components/data-table/pagination"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { type FragmentType, graphql, useFragment } from "@/gql"; import { RankingBy, RankingOrder, RankingPeriod } from "@/gql/graphql"; import { useSuspenseQuery } from "@apollo/client/react"; import { useState } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import DataTablePagination from "@/components/data-table/pagination"; -import { Badge } from "@/components/ui/badge"; const OVERVIEW_RANKING_QUERY = graphql(` query OverviewRanking($filter: RankingFilter!, $first: Int!, $after: Cursor) { @@ -93,10 +80,10 @@ export default function OverviewRanking() { const [rankingBy, setRankingBy] = useState(RankingBy.Points); const [rankingOrder, setRankingOrder] = useState( - RankingOrder.Desc + RankingOrder.Desc, ); const [rankingPeriod, setRankingPeriod] = useState( - RankingPeriod.Daily + RankingPeriod.Daily, ); const [cursors, setCursors] = useState([]); @@ -170,7 +157,7 @@ export default function OverviewRanking() { {label} - ) + ), )} @@ -194,7 +181,7 @@ export default function OverviewRanking() { {label} - ) + ), )} @@ -213,48 +200,48 @@ export default function OverviewRanking() { - {data.ranking.edges.length === 0 ? ( - - - 無資料 - - - ) : ( - data.ranking.edges.map((edge, index) => { - const rank = index + 1 + cursors.length * PAGE_SIZE; - return ( - - - {rank <= 3 ? ( - - #{rank} - - ) : ( - #{rank} - )} - - - {edge.node.name} - - - - - - ); - }) - )} + {data.ranking.edges.length === 0 + ? ( + + + 無資料 + + + ) + : ( + data.ranking.edges.map((edge, index) => { + const rank = index + 1 + cursors.length * PAGE_SIZE; + return ( + + + {rank <= 3 + ? ( + + #{rank} + + ) + : #{rank}} + + + {edge.node.name} + + + + + + ); + }) + )} @@ -271,17 +258,20 @@ export default function OverviewRanking() { ); } -function ScoreCell({ userFragment, rankingBy }: { userFragment: FragmentType, rankingBy: RankingBy }) { +function ScoreCell( + { userFragment, rankingBy }: { userFragment: FragmentType; rankingBy: RankingBy }, +) { const user = useFragment(SCORE_CELL_FRAGMENT, userFragment); const components = { [RankingBy.Points]: , [RankingBy.CompletedQuestions]: , - } + }; return ( - {components[rankingBy] ?? 發現未定義的排序依據 {rankingBy}}{' | '} + {components[rankingBy] ?? 發現未定義的排序依據 {rankingBy}} + {" | "} ); diff --git a/app/(admin)/_components/submit-count.tsx b/app/(admin)/_components/submit-count.tsx index 18c8941..5b28ff6 100644 --- a/app/(admin)/_components/submit-count.tsx +++ b/app/(admin)/_components/submit-count.tsx @@ -1,14 +1,14 @@ "use client"; -import { graphql } from "@/gql"; -import { SubmissionStatus, type SubmissionWhereInput } from "@/gql/graphql"; -import { useLazyQuery } from "@apollo/client/react"; -import { useEffect, useState } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { graphql } from "@/gql"; +import { SubmissionStatus, type SubmissionWhereInput } from "@/gql/graphql"; +import { useLazyQuery } from "@apollo/client/react"; +import { useEffect, useState } from "react"; const SUBMISSIONS_TOTAL_COUNT_QUERY = graphql(` query SubmissionsTotalCount($where: SubmissionWhereInput!) { @@ -44,12 +44,12 @@ export default function SubmissionsTotalCount() { }, all: {}, }; - + const where: SubmissionWhereInput = showSuccessOnly ? { - ...timeRangeWhere[timeRange], - status: SubmissionStatus.Success, - } + ...timeRangeWhere[timeRange], + status: SubmissionStatus.Success, + } : timeRangeWhere[timeRange]; getSubmissionsTotalCount({ variables: { where } }); @@ -74,7 +74,7 @@ export default function SubmissionsTotalCount() {

- {!loading && (data?.submissions.totalCount?.toLocaleString('zh-TW') ?? 0)} + {!loading && (data?.submissions.totalCount?.toLocaleString("zh-TW") ?? 0)} {loading && }

diff --git a/app/(admin)/page.tsx b/app/(admin)/page.tsx index 29c0821..c1101ed 100644 --- a/app/(admin)/page.tsx +++ b/app/(admin)/page.tsx @@ -1,11 +1,11 @@ import { SiteHeader } from "@/components/site-header"; +import { Skeleton } from "@/components/ui/skeleton"; import type { Metadata } from "next"; import { Suspense } from "react"; +import { Header } from "./_components/header"; +import LoginTotalCount from "./_components/login-count"; import OverviewRanking from "./_components/rank"; -import { Skeleton } from "@/components/ui/skeleton"; import SubmissionsTotalCount from "./_components/submit-count"; -import LoginTotalCount from "./_components/login-count"; -import { Header } from "./_components/header"; export const metadata: Metadata = { title: "概覽", @@ -22,10 +22,12 @@ export default function Home() { `} >

-
+
diff --git a/components/ui/spinner.tsx b/components/ui/spinner.tsx index a70e713..055ac76 100644 --- a/components/ui/spinner.tsx +++ b/components/ui/spinner.tsx @@ -1,6 +1,6 @@ -import { Loader2Icon } from "lucide-react" +import { Loader2Icon } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Spinner({ className, ...props }: React.ComponentProps<"svg">) { return ( @@ -10,7 +10,7 @@ function Spinner({ className, ...props }: React.ComponentProps<"svg">) { className={cn("size-4 animate-spin", className)} {...props} /> - ) + ); } -export { Spinner } +export { Spinner }; diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx index 497ba5e..02a9354 100644 --- a/components/ui/tabs.tsx +++ b/components/ui/tabs.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Tabs({ className, @@ -15,7 +15,7 @@ function Tabs({ className={cn("flex flex-col gap-2", className)} {...props} /> - ) + ); } function TabsList({ @@ -26,12 +26,15 @@ function TabsList({ - ) + ); } function TabsTrigger({ @@ -42,12 +45,27 @@ function TabsTrigger({ - ) + ); } function TabsContent({ @@ -60,7 +78,7 @@ function TabsContent({ className={cn("flex-1 outline-none", className)} {...props} /> - ) + ); } -export { Tabs, TabsList, TabsTrigger, TabsContent } +export { Tabs, TabsContent, TabsList, TabsTrigger }; From 4916e9bc74af7a793ce2bac90a1bb3a70e875495 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 19 Oct 2025 04:12:11 +0800 Subject: [PATCH 06/11] feat(questions): allow filtering table --- .../questions/_components/data-table.tsx | 31 +++++++---- .../_components/filterable-data-table.tsx | 51 +++++++++++++++++++ .../questions/_components/query.ts | 19 ++++++- .../(question-management)/questions/page.tsx | 12 ++--- gql/gql.ts | 6 +-- gql/graphql.ts | 4 +- package.json | 1 + pnpm-lock.yaml | 23 +++++++++ 8 files changed, 124 insertions(+), 23 deletions(-) create mode 100644 app/(admin)/(question-management)/questions/_components/filterable-data-table.tsx diff --git a/app/(admin)/(question-management)/questions/_components/data-table.tsx b/app/(admin)/(question-management)/questions/_components/data-table.tsx index 429eb52..f6e8c9d 100644 --- a/app/(admin)/(question-management)/questions/_components/data-table.tsx +++ b/app/(admin)/(question-management)/questions/_components/data-table.tsx @@ -2,18 +2,27 @@ import { CursorDataTable } from "@/components/data-table/cursor"; import type { Direction } from "@/components/data-table/pagination"; +import { QuestionDifficulty } from "@/gql/graphql"; import { useSuspenseQuery } from "@apollo/client/react"; +import type { VariablesOf } from "@graphql-typed-document-node/core"; import { useState } from "react"; import { columns, type Question } from "./data-table-columns"; import { QUESTIONS_TABLE_QUERY } from "./query"; -export function QuestionsDataTable() { +export type DifficultyFilter = "all" | QuestionDifficulty; + +export function QuestionsDataTable({ query, difficulty }: { query?: string; difficulty: DifficultyFilter }) { const PAGE_SIZE = 20; const [cursors, setCursors] = useState<(string | null)[]>([null]); const [currentIndex, setCurrentIndex] = useState(0); const after = cursors[currentIndex]; - const variables = { first: PAGE_SIZE, after }; + const variables = { + first: PAGE_SIZE, + after, + query, + difficulty: difficulty === "all" ? undefined : difficulty, + } satisfies VariablesOf; const { data } = useSuspenseQuery(QUESTIONS_TABLE_QUERY, { variables, @@ -53,13 +62,15 @@ export function QuestionsDataTable() { }; return ( - 0} - onPageChange={handlePageChange} - /> + <> + 0} + onPageChange={handlePageChange} + /> + ); } diff --git a/app/(admin)/(question-management)/questions/_components/filterable-data-table.tsx b/app/(admin)/(question-management)/questions/_components/filterable-data-table.tsx new file mode 100644 index 0000000..38fd0f8 --- /dev/null +++ b/app/(admin)/(question-management)/questions/_components/filterable-data-table.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { Input } from "@/components/ui/input"; +import { SelectItem } from "@/components/ui/select"; + +import { DataTableSkeleton } from "@/components/data-table/skeleton"; +import { Select, SelectContent, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { QuestionDifficulty } from "@/gql/graphql"; +import { useDebouncedValue } from "foxact/use-debounced-value"; +import { Suspense, useState } from "react"; +import { type DifficultyFilter, QuestionsDataTable } from "./data-table"; + +export default function FilterableDataTable() { + const [query, setQuery] = useState(""); + const [difficulty, setDifficulty] = useState("all"); + + const debouncedQuery = useDebouncedValue(query, 200); + + return ( +
+
+ setQuery(e.target.value)} + /> + +
+ + }> + + +
+ ); +} diff --git a/app/(admin)/(question-management)/questions/_components/query.ts b/app/(admin)/(question-management)/questions/_components/query.ts index 34ffa51..a64bb98 100644 --- a/app/(admin)/(question-management)/questions/_components/query.ts +++ b/app/(admin)/(question-management)/questions/_components/query.ts @@ -32,9 +32,24 @@ export const QUESTIONS_TABLE_QUERY = graphql(` $first: Int $after: Cursor $last: Int - $before: Cursor + $before: Cursor, + $query: String, + $difficulty: QuestionDifficulty ) { - questions(first: $first, after: $after, last: $last, before: $before) { + questions( + first: $first, + after: $after, + last: $last, + before: $before, + where: { + or: [ + { titleContains: $query }, + { categoryContains: $query }, + { descriptionContains: $query }, + ], + difficulty: $difficulty, + }, + ) { edges { node { id diff --git a/app/(admin)/(question-management)/questions/page.tsx b/app/(admin)/(question-management)/questions/page.tsx index d3c56f5..17d2bc1 100644 --- a/app/(admin)/(question-management)/questions/page.tsx +++ b/app/(admin)/(question-management)/questions/page.tsx @@ -1,9 +1,7 @@ -import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; -import { Suspense } from "react"; import { CreateQuestionTrigger } from "./_components/create"; -import { QuestionsDataTable } from "./_components/data-table"; +import FilterableDataTable from "./_components/filterable-data-table"; export const metadata: Metadata = { title: "題庫", @@ -22,14 +20,14 @@ export default function Page() {

題庫管理

-

管理 SQL 練習題目,設定難度與分類。

+

+ 管理 SQL 練習題目,設定難度與分類。 +

- }> - - +
diff --git a/gql/gql.ts b/gql/gql.ts index 5a14ea2..c3892c7 100644 --- a/gql/gql.ts +++ b/gql/gql.ts @@ -33,7 +33,7 @@ type Documents = { "\n mutation DeleteQuestion($id: ID!) {\n deleteQuestion(id: $id)\n }\n": typeof types.DeleteQuestionDocument, "\n query QuestionById($id: ID!) {\n question(id: $id) {\n id\n title\n description\n category\n difficulty\n referenceAnswer\n database {\n id\n slug\n }\n }\n }\n": typeof types.QuestionByIdDocument, "\n query DatabaseList {\n databases {\n id\n slug\n description\n }\n }\n": typeof types.DatabaseListDocument, - "\n query QuestionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n questions(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n title\n description\n category\n difficulty\n referenceAnswer\n database {\n id\n slug\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.QuestionsTableDocument, + "\n query QuestionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor,\n $query: String,\n $difficulty: QuestionDifficulty\n ) {\n questions(\n first: $first,\n after: $after,\n last: $last,\n before: $before,\n where: {\n or: [\n { titleContains: $query },\n { categoryContains: $query },\n { descriptionContains: $query },\n ],\n difficulty: $difficulty,\n },\n ) {\n edges {\n node {\n id\n title\n description\n category\n difficulty\n referenceAnswer\n database {\n id\n slug\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.QuestionsTableDocument, "\n query GroupAuditInfo($id: ID!) {\n group(id: $id) {\n id\n createdAt\n updatedAt\n }\n }\n": typeof types.GroupAuditInfoDocument, "\n query GroupHeader($id: ID!) {\n group(id: $id) {\n id\n name\n description\n }\n }\n": typeof types.GroupHeaderDocument, "\n query GroupMembers($id: ID!) {\n users(where: { hasGroupWith: { id: $id } }) {\n totalCount\n }\n }\n": typeof types.GroupMembersDocument, @@ -93,7 +93,7 @@ const documents: Documents = { "\n mutation DeleteQuestion($id: ID!) {\n deleteQuestion(id: $id)\n }\n": types.DeleteQuestionDocument, "\n query QuestionById($id: ID!) {\n question(id: $id) {\n id\n title\n description\n category\n difficulty\n referenceAnswer\n database {\n id\n slug\n }\n }\n }\n": types.QuestionByIdDocument, "\n query DatabaseList {\n databases {\n id\n slug\n description\n }\n }\n": types.DatabaseListDocument, - "\n query QuestionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n questions(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n title\n description\n category\n difficulty\n referenceAnswer\n database {\n id\n slug\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.QuestionsTableDocument, + "\n query QuestionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor,\n $query: String,\n $difficulty: QuestionDifficulty\n ) {\n questions(\n first: $first,\n after: $after,\n last: $last,\n before: $before,\n where: {\n or: [\n { titleContains: $query },\n { categoryContains: $query },\n { descriptionContains: $query },\n ],\n difficulty: $difficulty,\n },\n ) {\n edges {\n node {\n id\n title\n description\n category\n difficulty\n referenceAnswer\n database {\n id\n slug\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.QuestionsTableDocument, "\n query GroupAuditInfo($id: ID!) {\n group(id: $id) {\n id\n createdAt\n updatedAt\n }\n }\n": types.GroupAuditInfoDocument, "\n query GroupHeader($id: ID!) {\n group(id: $id) {\n id\n name\n description\n }\n }\n": types.GroupHeaderDocument, "\n query GroupMembers($id: ID!) {\n users(where: { hasGroupWith: { id: $id } }) {\n totalCount\n }\n }\n": types.GroupMembersDocument, @@ -227,7 +227,7 @@ export function graphql(source: "\n query DatabaseList {\n databases {\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 QuestionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n questions(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n title\n description\n category\n difficulty\n referenceAnswer\n database {\n id\n slug\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query QuestionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n questions(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n title\n description\n category\n difficulty\n referenceAnswer\n database {\n id\n slug\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"]; +export function graphql(source: "\n query QuestionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor,\n $query: String,\n $difficulty: QuestionDifficulty\n ) {\n questions(\n first: $first,\n after: $after,\n last: $last,\n before: $before,\n where: {\n or: [\n { titleContains: $query },\n { categoryContains: $query },\n { descriptionContains: $query },\n ],\n difficulty: $difficulty,\n },\n ) {\n edges {\n node {\n id\n title\n description\n category\n difficulty\n referenceAnswer\n database {\n id\n slug\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query QuestionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor,\n $query: String,\n $difficulty: QuestionDifficulty\n ) {\n questions(\n first: $first,\n after: $after,\n last: $last,\n before: $before,\n where: {\n or: [\n { titleContains: $query },\n { categoryContains: $query },\n { descriptionContains: $query },\n ],\n difficulty: $difficulty,\n },\n ) {\n edges {\n node {\n id\n title\n description\n category\n difficulty\n referenceAnswer\n database {\n id\n slug\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/gql/graphql.ts b/gql/graphql.ts index 1e2a8c6..8952212 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -1655,6 +1655,8 @@ export type QuestionsTableQueryVariables = Exact<{ after?: InputMaybe; last?: InputMaybe; before?: InputMaybe; + query?: InputMaybe; + difficulty?: InputMaybe; }>; @@ -1929,7 +1931,7 @@ export const UpdateQuestionDocument = {"kind":"Document","definitions":[{"kind": export const DeleteQuestionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteQuestion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteQuestion"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode; export const QuestionByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"QuestionById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"question"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"difficulty"}},{"kind":"Field","name":{"kind":"Name","value":"referenceAnswer"}},{"kind":"Field","name":{"kind":"Name","value":"database"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode; export const DatabaseListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DatabaseList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"databases"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]} as unknown as DocumentNode; -export const QuestionsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"QuestionsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"questions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"difficulty"}},{"kind":"Field","name":{"kind":"Name","value":"referenceAnswer"}},{"kind":"Field","name":{"kind":"Name","value":"database"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; +export const QuestionsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"QuestionsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"query"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"difficulty"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"QuestionDifficulty"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"questions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"or"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"titleContains"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"categoryContains"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}}]},{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"descriptionContains"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}}]}]}},{"kind":"ObjectField","name":{"kind":"Name","value":"difficulty"},"value":{"kind":"Variable","name":{"kind":"Name","value":"difficulty"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"difficulty"}},{"kind":"Field","name":{"kind":"Name","value":"referenceAnswer"}},{"kind":"Field","name":{"kind":"Name","value":"database"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; export const GroupAuditInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GroupAuditInfo"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"group"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; export const GroupHeaderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GroupHeader"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"group"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]} as unknown as DocumentNode; export const GroupMembersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GroupMembers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"hasGroupWith"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]} as unknown as DocumentNode; diff --git a/package.json b/package.json index d65beea..da31a99 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "babel-plugin-react-compiler": "19.1.0-rc.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "foxact": "^0.2.49", "graphql": "^16.11.0", "lucide-react": "^0.545.0", "next": "16.0.0-canary.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a55add..6de889f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + foxact: + specifier: ^0.2.49 + version: 0.2.49(react@19.3.0-canary-1873ad79-20251015) graphql: specifier: ^16.11.0 version: 16.11.0 @@ -2652,6 +2655,14 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} + foxact@0.2.49: + resolution: {integrity: sha512-9Pbu4IbkaNqtS/H4887/QWegclMpBn54mzbPp3t1mg0iJuB83jpQGBY2fshal50NmchlAFIT/GSWBFsa0YI31Q==} + peerDependencies: + react: '*' + peerDependenciesMeta: + react: + optional: true + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -3785,6 +3796,9 @@ packages: sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -7104,6 +7118,13 @@ snapshots: dependencies: fetch-blob: 3.2.0 + foxact@0.2.49(react@19.3.0-canary-1873ad79-20251015): + dependencies: + client-only: 0.0.1 + server-only: 0.0.1 + optionalDependencies: + react: 19.3.0-canary-1873ad79-20251015 + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -8379,6 +8400,8 @@ snapshots: tslib: 2.8.1 upper-case-first: 2.0.2 + server-only@0.0.1: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 From 168acc09ce5578ad28462f66928e0f6021d52679 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 19 Oct 2025 04:30:46 +0800 Subject: [PATCH 07/11] feat: implement query of data tables Fixed DBP-112 --- .../events/_components/data-table.tsx | 10 +++- .../_components/filterable-data-table.tsx | 29 ++++++++++++ .../events/_components/query.ts | 3 +- .../(activity-management)/events/page.tsx | 8 +--- .../points/_components/data-table.tsx | 10 +++- .../_components/filterable-data-table.tsx | 29 ++++++++++++ .../points/_components/query.ts | 3 +- .../(activity-management)/points/page.tsx | 8 +--- .../database/_components/data-table.tsx | 35 ++++++++++---- .../_components/filterable-data-table.tsx | 29 ++++++++++++ .../(question-management)/database/page.tsx | 8 +--- .../_components/filterable-data-table.tsx | 2 +- .../groups/_components/data-table.tsx | 39 +++++++++++----- .../_components/filterable-data-table.tsx | 29 ++++++++++++ app/(admin)/(user-management)/groups/page.tsx | 8 +--- .../scopesets/_components/data-table.tsx | 46 +++++++++++++++---- .../_components/filterable-data-table.tsx | 29 ++++++++++++ .../(user-management)/scopesets/page.tsx | 8 +--- .../users/_components/data-table.tsx | 17 ++++++- .../_components/filterable-data-table.tsx | 29 ++++++++++++ .../users/_components/query.ts | 3 +- app/(admin)/(user-management)/users/page.tsx | 8 +--- gql/gql.ts | 18 ++++---- gql/graphql.ts | 9 ++-- 24 files changed, 331 insertions(+), 86 deletions(-) create mode 100644 app/(admin)/(activity-management)/events/_components/filterable-data-table.tsx create mode 100644 app/(admin)/(activity-management)/points/_components/filterable-data-table.tsx create mode 100644 app/(admin)/(question-management)/database/_components/filterable-data-table.tsx create mode 100644 app/(admin)/(user-management)/groups/_components/filterable-data-table.tsx create mode 100644 app/(admin)/(user-management)/scopesets/_components/filterable-data-table.tsx create mode 100644 app/(admin)/(user-management)/users/_components/filterable-data-table.tsx diff --git a/app/(admin)/(activity-management)/events/_components/data-table.tsx b/app/(admin)/(activity-management)/events/_components/data-table.tsx index 852bf38..60f666b 100644 --- a/app/(admin)/(activity-management)/events/_components/data-table.tsx +++ b/app/(admin)/(activity-management)/events/_components/data-table.tsx @@ -3,17 +3,23 @@ import { CursorDataTable } from "@/components/data-table/cursor"; import type { Direction } from "@/components/data-table/pagination"; import { useSuspenseQuery } from "@apollo/client/react"; +import type { VariablesOf } from "@graphql-typed-document-node/core"; import { useState } from "react"; import { columns, type Event } from "./data-table-columns"; import { EVENTS_TABLE_QUERY } from "./query"; -export function EventsDataTable() { +export function EventsDataTable({ query }: { query?: string }) { const PAGE_SIZE = 20; const [cursors, setCursors] = useState<(string | null)[]>([null]); const [currentIndex, setCurrentIndex] = useState(0); const after = cursors[currentIndex]; - const variables = { first: PAGE_SIZE, after }; + + const variables = { + first: PAGE_SIZE, + after, + where: query ? { typeContains: query } : undefined, + } satisfies VariablesOf; const { data } = useSuspenseQuery(EVENTS_TABLE_QUERY, { variables, diff --git a/app/(admin)/(activity-management)/events/_components/filterable-data-table.tsx b/app/(admin)/(activity-management)/events/_components/filterable-data-table.tsx new file mode 100644 index 0000000..35aeb40 --- /dev/null +++ b/app/(admin)/(activity-management)/events/_components/filterable-data-table.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Input } from "@/components/ui/input"; + +import { DataTableSkeleton } from "@/components/data-table/skeleton"; +import { useDebouncedValue } from "foxact/use-debounced-value"; +import { Suspense, useState } from "react"; +import { EventsDataTable } from "./data-table"; + +export default function FilterableDataTable() { + const [query, setQuery] = useState(""); + const debouncedQuery = useDebouncedValue(query, 200); + + return ( +
+
+ setQuery(e.target.value)} + /> +
+ + }> + + +
+ ); +} diff --git a/app/(admin)/(activity-management)/events/_components/query.ts b/app/(admin)/(activity-management)/events/_components/query.ts index 8c0db5f..1ee3e97 100644 --- a/app/(admin)/(activity-management)/events/_components/query.ts +++ b/app/(admin)/(activity-management)/events/_components/query.ts @@ -6,8 +6,9 @@ export const EVENTS_TABLE_QUERY = graphql(` $after: Cursor $last: Int $before: Cursor + $where: EventWhereInput ) { - events(first: $first, after: $after, last: $last, before: $before, orderBy: { field: TRIGGERED_AT, direction: DESC }) { + events(first: $first, after: $after, last: $last, before: $before, where: $where, orderBy: { field: TRIGGERED_AT, direction: DESC }) { edges { node { id diff --git a/app/(admin)/(activity-management)/events/page.tsx b/app/(admin)/(activity-management)/events/page.tsx index b5e4b77..28b8c4d 100644 --- a/app/(admin)/(activity-management)/events/page.tsx +++ b/app/(admin)/(activity-management)/events/page.tsx @@ -1,8 +1,6 @@ -import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; -import { Suspense } from "react"; -import { EventsDataTable } from "./_components/data-table"; +import FilterableDataTable from "./_components/filterable-data-table"; export const metadata: Metadata = { title: "事件管理", @@ -25,9 +23,7 @@ export default function Page() {
- }> - - +
diff --git a/app/(admin)/(activity-management)/points/_components/data-table.tsx b/app/(admin)/(activity-management)/points/_components/data-table.tsx index b7074b6..a034825 100644 --- a/app/(admin)/(activity-management)/points/_components/data-table.tsx +++ b/app/(admin)/(activity-management)/points/_components/data-table.tsx @@ -3,17 +3,23 @@ import { CursorDataTable } from "@/components/data-table/cursor"; import type { Direction } from "@/components/data-table/pagination"; import { useSuspenseQuery } from "@apollo/client/react"; +import type { VariablesOf } from "@graphql-typed-document-node/core"; import { useState } from "react"; import { columns, type Point } from "./data-table-columns"; import { POINTS_TABLE_QUERY } from "./query"; -export function PointsDataTable() { +export function PointsDataTable({ query }: { query?: string }) { const PAGE_SIZE = 20; const [cursors, setCursors] = useState<(string | null)[]>([null]); const [currentIndex, setCurrentIndex] = useState(0); const after = cursors[currentIndex]; - const variables = { first: PAGE_SIZE, after }; + + const variables = { + first: PAGE_SIZE, + after, + where: query ? { descriptionContains: query } : undefined, + } satisfies VariablesOf; const { data } = useSuspenseQuery(POINTS_TABLE_QUERY, { variables, diff --git a/app/(admin)/(activity-management)/points/_components/filterable-data-table.tsx b/app/(admin)/(activity-management)/points/_components/filterable-data-table.tsx new file mode 100644 index 0000000..a69767a --- /dev/null +++ b/app/(admin)/(activity-management)/points/_components/filterable-data-table.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Input } from "@/components/ui/input"; + +import { DataTableSkeleton } from "@/components/data-table/skeleton"; +import { useDebouncedValue } from "foxact/use-debounced-value"; +import { Suspense, useState } from "react"; +import { PointsDataTable } from "./data-table"; + +export default function FilterableDataTable() { + const [query, setQuery] = useState(""); + const debouncedQuery = useDebouncedValue(query, 200); + + return ( +
+
+ setQuery(e.target.value)} + /> +
+ + }> + + +
+ ); +} diff --git a/app/(admin)/(activity-management)/points/_components/query.ts b/app/(admin)/(activity-management)/points/_components/query.ts index 948b2e9..f56dee5 100644 --- a/app/(admin)/(activity-management)/points/_components/query.ts +++ b/app/(admin)/(activity-management)/points/_components/query.ts @@ -6,8 +6,9 @@ export const POINTS_TABLE_QUERY = graphql(` $after: Cursor $last: Int $before: Cursor + $where: PointWhereInput ) { - points(first: $first, after: $after, last: $last, before: $before, orderBy: { field: GRANTED_AT, direction: DESC }) { + points(first: $first, after: $after, last: $last, before: $before, where: $where, orderBy: { field: GRANTED_AT, direction: DESC }) { edges { node { id diff --git a/app/(admin)/(activity-management)/points/page.tsx b/app/(admin)/(activity-management)/points/page.tsx index f9d95be..a1fb244 100644 --- a/app/(admin)/(activity-management)/points/page.tsx +++ b/app/(admin)/(activity-management)/points/page.tsx @@ -1,8 +1,6 @@ -import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; -import { Suspense } from "react"; -import { PointsDataTable } from "./_components/data-table"; +import FilterableDataTable from "./_components/filterable-data-table"; export const metadata: Metadata = { title: "積分管理", @@ -25,9 +23,7 @@ export default function Page() {
- }> - - +
diff --git a/app/(admin)/(question-management)/database/_components/data-table.tsx b/app/(admin)/(question-management)/database/_components/data-table.tsx index 4d50bda..235cf2d 100644 --- a/app/(admin)/(question-management)/database/_components/data-table.tsx +++ b/app/(admin)/(question-management)/database/_components/data-table.tsx @@ -5,16 +5,35 @@ import { useSuspenseQuery } from "@apollo/client/react"; import { columns, type Database } from "./data-table-columns"; import { DATABASES_TABLE_QUERY } from "./query"; -export function DatabaseDataTable() { +export function DatabaseDataTable({ query }: { query?: string }) { const { data } = useSuspenseQuery(DATABASES_TABLE_QUERY); - const databaseList = data?.databases?.map((database) => ({ - id: database.id, - slug: database.slug, - description: database.description, - schema: database.schema, - relationFigure: database.relationFigure, - } satisfies Database)) ?? []; + const databaseList = data?.databases + ?.map( + (database) => ({ + id: database.id, + slug: database.slug, + description: database.description, + schema: database.schema, + relationFigure: database.relationFigure, + } satisfies Database), + ) + .filter((database) => { + if (query && database.slug.includes(query)) { + return true; + } + + if (query && database.description?.includes(query)) { + return true; + } + + // If no query is provided, return all databases + if (!query) { + return true; + } + + return false; + }) ?? []; return ( +
+ setQuery(e.target.value)} + /> +
+ + }> + + + + ); +} diff --git a/app/(admin)/(question-management)/database/page.tsx b/app/(admin)/(question-management)/database/page.tsx index 69b52f7..611e328 100644 --- a/app/(admin)/(question-management)/database/page.tsx +++ b/app/(admin)/(question-management)/database/page.tsx @@ -1,9 +1,7 @@ -import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; -import { Suspense } from "react"; import { CreateDatabaseTrigger } from "./_components/create"; -import { DatabaseDataTable } from "./_components/data-table"; +import FilterableDataTable from "./_components/filterable-data-table"; export const metadata: Metadata = { title: "資料庫", @@ -27,9 +25,7 @@ export default function Page() {
- }> - - +
diff --git a/app/(admin)/(question-management)/questions/_components/filterable-data-table.tsx b/app/(admin)/(question-management)/questions/_components/filterable-data-table.tsx index 38fd0f8..a62d38e 100644 --- a/app/(admin)/(question-management)/questions/_components/filterable-data-table.tsx +++ b/app/(admin)/(question-management)/questions/_components/filterable-data-table.tsx @@ -20,7 +20,7 @@ export default function FilterableDataTable() {
setQuery(e.target.value)} /> diff --git a/app/(admin)/(user-management)/groups/_components/data-table.tsx b/app/(admin)/(user-management)/groups/_components/data-table.tsx index 80b0d89..a089cc1 100644 --- a/app/(admin)/(user-management)/groups/_components/data-table.tsx +++ b/app/(admin)/(user-management)/groups/_components/data-table.tsx @@ -5,19 +5,36 @@ import { useSuspenseQuery } from "@apollo/client/react"; import { columns, type Group } from "./data-table-columns"; import { GROUPS_TABLE_QUERY } from "./query"; -export function GroupDataTable() { +export function GroupDataTable({ query }: { query?: string }) { const { data } = useSuspenseQuery(GROUPS_TABLE_QUERY); - const groupList = data?.groups.map( - (group) => ({ - id: group.id, - name: group.name, - description: group.description ?? "", - scopeSets: group.scopeSets ?? [], - createdAt: group.createdAt, - updatedAt: group.updatedAt, - } satisfies Group), - ) ?? []; + const groupList = data?.groups + .map( + (group) => ({ + id: group.id, + name: group.name, + description: group.description ?? "", + scopeSets: group.scopeSets ?? [], + createdAt: group.createdAt, + updatedAt: group.updatedAt, + } satisfies Group), + ) + .filter((group) => { + if (query && group.name.includes(query)) { + return true; + } + + if (query && group.description?.includes(query)) { + return true; + } + + // If no query is provided, return all groups + if (!query) { + return true; + } + + return false; + }) ?? []; return ; } diff --git a/app/(admin)/(user-management)/groups/_components/filterable-data-table.tsx b/app/(admin)/(user-management)/groups/_components/filterable-data-table.tsx new file mode 100644 index 0000000..2943830 --- /dev/null +++ b/app/(admin)/(user-management)/groups/_components/filterable-data-table.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Input } from "@/components/ui/input"; + +import { DataTableSkeleton } from "@/components/data-table/skeleton"; +import { useDebouncedValue } from "foxact/use-debounced-value"; +import { Suspense, useState } from "react"; +import { GroupDataTable } from "./data-table"; + +export default function FilterableDataTable() { + const [query, setQuery] = useState(""); + const debouncedQuery = useDebouncedValue(query, 200); + + return ( +
+
+ setQuery(e.target.value)} + /> +
+ + }> + + +
+ ); +} diff --git a/app/(admin)/(user-management)/groups/page.tsx b/app/(admin)/(user-management)/groups/page.tsx index 01366dd..8c6b4f2 100644 --- a/app/(admin)/(user-management)/groups/page.tsx +++ b/app/(admin)/(user-management)/groups/page.tsx @@ -1,9 +1,7 @@ -import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; -import { Suspense } from "react"; import { CreateGroupTrigger } from "./_components/create"; -import { GroupDataTable } from "./_components/data-table"; +import FilterableDataTable from "./_components/filterable-data-table"; export const metadata: Metadata = { title: "群組", @@ -27,9 +25,7 @@ export default function GroupsPage() {
- }> - - +
diff --git a/app/(admin)/(user-management)/scopesets/_components/data-table.tsx b/app/(admin)/(user-management)/scopesets/_components/data-table.tsx index ca64e4c..12f7ef8 100644 --- a/app/(admin)/(user-management)/scopesets/_components/data-table.tsx +++ b/app/(admin)/(user-management)/scopesets/_components/data-table.tsx @@ -5,17 +5,45 @@ import { useSuspenseQuery } from "@apollo/client/react"; import { columns, type ScopeSet } from "./data-table-columns"; import { SCOPE_SET_TABLE_QUERY } from "./query"; -export function ScopeSetDataTable() { +export function ScopeSetDataTable({ + query, +}: { + query?: string; +}) { const { data } = useSuspenseQuery(SCOPE_SET_TABLE_QUERY); - const scopeSetList = data?.scopeSets.map( - (scopeSet) => ({ - id: scopeSet.id, - slug: scopeSet.slug, - description: scopeSet.description ?? "", - scopes: scopeSet.scopes ?? [], - } satisfies ScopeSet), - ) ?? []; + const scopeSetList = data?.scopeSets + .map( + (scopeSet) => ({ + id: scopeSet.id, + slug: scopeSet.slug, + description: scopeSet.description ?? "", + scopes: scopeSet.scopes ?? [], + } satisfies ScopeSet), + ) + .filter((scopeSet) => { + if (query && scopeSet.slug.includes(query)) { + return true; + } + + if ( + query + && scopeSet.scopes.some((scope) => scope.includes(query)) + ) { + return true; + } + + if (query && scopeSet.description?.includes(query)) { + return true; + } + + // If no query is provided, return all scope sets + if (!query) { + return true; + } + + return false; + }) ?? []; return ; } diff --git a/app/(admin)/(user-management)/scopesets/_components/filterable-data-table.tsx b/app/(admin)/(user-management)/scopesets/_components/filterable-data-table.tsx new file mode 100644 index 0000000..9a4757b --- /dev/null +++ b/app/(admin)/(user-management)/scopesets/_components/filterable-data-table.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Input } from "@/components/ui/input"; + +import { DataTableSkeleton } from "@/components/data-table/skeleton"; +import { useDebouncedValue } from "foxact/use-debounced-value"; +import { Suspense, useState } from "react"; +import { ScopeSetDataTable } from "./data-table"; + +export default function FilterableDataTable() { + const [query, setQuery] = useState(""); + const debouncedQuery = useDebouncedValue(query, 200); + + return ( +
+
+ setQuery(e.target.value)} + /> +
+ + }> + + +
+ ); +} diff --git a/app/(admin)/(user-management)/scopesets/page.tsx b/app/(admin)/(user-management)/scopesets/page.tsx index 9ed2565..c466875 100644 --- a/app/(admin)/(user-management)/scopesets/page.tsx +++ b/app/(admin)/(user-management)/scopesets/page.tsx @@ -1,9 +1,7 @@ -import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; -import { Suspense } from "react"; import { CreateScopeSetTrigger } from "./_components/create"; -import { ScopeSetDataTable } from "./_components/data-table"; +import FilterableDataTable from "./_components/filterable-data-table"; export const metadata: Metadata = { title: "權限集", @@ -27,9 +25,7 @@ export default function ScopesetPage() {
- }> - - +
diff --git a/app/(admin)/(user-management)/users/_components/data-table.tsx b/app/(admin)/(user-management)/users/_components/data-table.tsx index 9f2f831..4dc5233 100644 --- a/app/(admin)/(user-management)/users/_components/data-table.tsx +++ b/app/(admin)/(user-management)/users/_components/data-table.tsx @@ -3,17 +3,30 @@ import { CursorDataTable } from "@/components/data-table/cursor"; import type { Direction } from "@/components/data-table/pagination"; import { useSuspenseQuery } from "@apollo/client/react"; +import type { VariablesOf } from "@graphql-typed-document-node/core"; import { useState } from "react"; import { columns, type User } from "./data-table-columns"; import { USERS_TABLE_QUERY } from "./query"; -export function UsersDataTable() { +export function UsersDataTable({ query }: { query?: string }) { const PAGE_SIZE = 20; const [cursors, setCursors] = useState<(string | null)[]>([null]); const [currentIndex, setCurrentIndex] = useState(0); const after = cursors[currentIndex]; - const variables = { first: PAGE_SIZE, after }; + + const variables = { + first: PAGE_SIZE, + after, + where: query + ? { + or: [ + { nameContains: query }, + { emailContains: query }, + ], + } + : undefined, + } satisfies VariablesOf; const { data } = useSuspenseQuery(USERS_TABLE_QUERY, { variables, diff --git a/app/(admin)/(user-management)/users/_components/filterable-data-table.tsx b/app/(admin)/(user-management)/users/_components/filterable-data-table.tsx new file mode 100644 index 0000000..c5eb739 --- /dev/null +++ b/app/(admin)/(user-management)/users/_components/filterable-data-table.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Input } from "@/components/ui/input"; + +import { DataTableSkeleton } from "@/components/data-table/skeleton"; +import { useDebouncedValue } from "foxact/use-debounced-value"; +import { Suspense, useState } from "react"; +import { UsersDataTable } from "./data-table"; + +export default function FilterableDataTable() { + const [query, setQuery] = useState(""); + const debouncedQuery = useDebouncedValue(query, 200); + + return ( +
+
+ setQuery(e.target.value)} + /> +
+ + }> + + +
+ ); +} diff --git a/app/(admin)/(user-management)/users/_components/query.ts b/app/(admin)/(user-management)/users/_components/query.ts index 706d452..deed088 100644 --- a/app/(admin)/(user-management)/users/_components/query.ts +++ b/app/(admin)/(user-management)/users/_components/query.ts @@ -32,8 +32,9 @@ export const USERS_TABLE_QUERY = graphql(` $after: Cursor $last: Int $before: Cursor + $where: UserWhereInput ) { - users(first: $first, after: $after, last: $last, before: $before) { + users(first: $first, after: $after, last: $last, before: $before, where: $where) { edges { node { id diff --git a/app/(admin)/(user-management)/users/page.tsx b/app/(admin)/(user-management)/users/page.tsx index 4b3b848..30d5c54 100644 --- a/app/(admin)/(user-management)/users/page.tsx +++ b/app/(admin)/(user-management)/users/page.tsx @@ -1,8 +1,6 @@ -import { DataTableSkeleton } from "@/components/data-table/skeleton"; import { SiteHeader } from "@/components/site-header"; import type { Metadata } from "next"; -import { Suspense } from "react"; -import { UsersDataTable } from "./_components/data-table"; +import FilterableDataTable from "./_components/filterable-data-table"; export const metadata: Metadata = { title: "使用者", @@ -25,9 +23,7 @@ export default function Page() {
- }> - - +
diff --git a/gql/gql.ts b/gql/gql.ts index c3892c7..ae1fab5 100644 --- a/gql/gql.ts +++ b/gql/gql.ts @@ -15,9 +15,9 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document- */ type Documents = { "\n query EventById($id: ID!) {\n event(id: $id) {\n id\n user {\n id\n name\n }\n type\n payload\n triggeredAt\n }\n }\n": typeof types.EventByIdDocument, - "\n query EventsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n events(first: $first, after: $after, last: $last, before: $before, orderBy: { field: TRIGGERED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n type\n triggeredAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.EventsTableDocument, + "\n query EventsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: EventWhereInput\n ) {\n events(first: $first, after: $after, last: $last, before: $before, where: $where, orderBy: { field: TRIGGERED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n type\n triggeredAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.EventsTableDocument, "\n query PointById($id: ID!) {\n pointGrant(id: $id) {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n": typeof types.PointByIdDocument, - "\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n points(first: $first, after: $after, last: $last, before: $before, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.PointsTableDocument, + "\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: PointWhereInput\n ) {\n points(first: $first, after: $after, last: $last, before: $before, where: $where, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.PointsTableDocument, "\n query SubmissionById($id: ID!) {\n submission(id: $id) {\n id\n user {\n id\n name\n }\n queryResult {\n columns\n rows\n matchAnswer\n }\n question {\n id\n }\n error\n submittedCode\n status\n submittedAt\n }\n }\n": typeof types.SubmissionByIdDocument, "\n query SubmissionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n submissions(first: $first, after: $after, last: $last, before: $before, orderBy: { field: SUBMITTED_AT, direction: DESC }) {\n edges {\n node {\n id\n submittedCode\n status\n user {\n id\n name\n }\n question {\n id\n title\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.SubmissionsTableDocument, "\n query DatabaseDetail($id: ID!) {\n database(id: $id) {\n id\n slug\n description\n schema\n relationFigure\n }\n }\n": typeof types.DatabaseDetailDocument, @@ -61,7 +61,7 @@ type Documents = { "\n mutation ImpersonateUser($userID: ID!) {\n impersonateUser(userID: $userID)\n }\n": typeof types.ImpersonateUserDocument, "\n query UserById($id: ID!) {\n user(id: $id) {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n": typeof types.UserByIdDocument, "\n query GroupList {\n groups {\n id\n name\n }\n }\n": typeof types.GroupListDocument, - "\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.UsersTableDocument, + "\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: UserWhereInput\n ) {\n users(first: $first, after: $after, last: $last, before: $before, where: $where) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": typeof types.UsersTableDocument, "\n query LoginTotalCount($where: EventWhereInput!) {\n events(where: $where) {\n totalCount\n }\n }\n": typeof types.LoginTotalCountDocument, "\n query OverviewRanking($filter: RankingFilter!, $first: Int!, $after: Cursor) {\n ranking(filter: $filter, first: $first, after: $after) {\n edges {\n node {\n id\n name\n }\n ...ScoreCell\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n": typeof types.OverviewRankingDocument, "\n fragment ScoreCell on RankingEdge {\n ...UserCompletedQuestions\n ...UserTotalPoints\n ...RankingFragment\n }\n": typeof types.ScoreCellFragmentDoc, @@ -75,9 +75,9 @@ type Documents = { }; const documents: Documents = { "\n query EventById($id: ID!) {\n event(id: $id) {\n id\n user {\n id\n name\n }\n type\n payload\n triggeredAt\n }\n }\n": types.EventByIdDocument, - "\n query EventsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n events(first: $first, after: $after, last: $last, before: $before, orderBy: { field: TRIGGERED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n type\n triggeredAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.EventsTableDocument, + "\n query EventsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: EventWhereInput\n ) {\n events(first: $first, after: $after, last: $last, before: $before, where: $where, orderBy: { field: TRIGGERED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n type\n triggeredAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.EventsTableDocument, "\n query PointById($id: ID!) {\n pointGrant(id: $id) {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n": types.PointByIdDocument, - "\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n points(first: $first, after: $after, last: $last, before: $before, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.PointsTableDocument, + "\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: PointWhereInput\n ) {\n points(first: $first, after: $after, last: $last, before: $before, where: $where, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.PointsTableDocument, "\n query SubmissionById($id: ID!) {\n submission(id: $id) {\n id\n user {\n id\n name\n }\n queryResult {\n columns\n rows\n matchAnswer\n }\n question {\n id\n }\n error\n submittedCode\n status\n submittedAt\n }\n }\n": types.SubmissionByIdDocument, "\n query SubmissionsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n submissions(first: $first, after: $after, last: $last, before: $before, orderBy: { field: SUBMITTED_AT, direction: DESC }) {\n edges {\n node {\n id\n submittedCode\n status\n user {\n id\n name\n }\n question {\n id\n title\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.SubmissionsTableDocument, "\n query DatabaseDetail($id: ID!) {\n database(id: $id) {\n id\n slug\n description\n schema\n relationFigure\n }\n }\n": types.DatabaseDetailDocument, @@ -121,7 +121,7 @@ const documents: Documents = { "\n mutation ImpersonateUser($userID: ID!) {\n impersonateUser(userID: $userID)\n }\n": types.ImpersonateUserDocument, "\n query UserById($id: ID!) {\n user(id: $id) {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n": types.UserByIdDocument, "\n query GroupList {\n groups {\n id\n name\n }\n }\n": types.GroupListDocument, - "\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.UsersTableDocument, + "\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: UserWhereInput\n ) {\n users(first: $first, after: $after, last: $last, before: $before, where: $where) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n": types.UsersTableDocument, "\n query LoginTotalCount($where: EventWhereInput!) {\n events(where: $where) {\n totalCount\n }\n }\n": types.LoginTotalCountDocument, "\n query OverviewRanking($filter: RankingFilter!, $first: Int!, $after: Cursor) {\n ranking(filter: $filter, first: $first, after: $after) {\n edges {\n node {\n id\n name\n }\n ...ScoreCell\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n": types.OverviewRankingDocument, "\n fragment ScoreCell on RankingEdge {\n ...UserCompletedQuestions\n ...UserTotalPoints\n ...RankingFragment\n }\n": types.ScoreCellFragmentDoc, @@ -155,7 +155,7 @@ export function graphql(source: "\n query EventById($id: ID!) {\n event( /** * 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 EventsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n events(first: $first, after: $after, last: $last, before: $before, orderBy: { field: TRIGGERED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n type\n triggeredAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query EventsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n events(first: $first, after: $after, last: $last, before: $before, orderBy: { field: TRIGGERED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n type\n triggeredAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"]; +export function graphql(source: "\n query EventsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: EventWhereInput\n ) {\n events(first: $first, after: $after, last: $last, before: $before, where: $where, orderBy: { field: TRIGGERED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n type\n triggeredAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query EventsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: EventWhereInput\n ) {\n events(first: $first, after: $after, last: $last, before: $before, where: $where, orderBy: { field: TRIGGERED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n type\n triggeredAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -163,7 +163,7 @@ export function graphql(source: "\n query PointById($id: ID!) {\n pointGrant /** * 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 PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n points(first: $first, after: $after, last: $last, before: $before, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n points(first: $first, after: $after, last: $last, before: $before, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"]; +export function graphql(source: "\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: PointWhereInput\n ) {\n points(first: $first, after: $after, last: $last, before: $before, where: $where, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query PointsTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: PointWhereInput\n ) {\n points(first: $first, after: $after, last: $last, before: $before, where: $where, orderBy: { field: GRANTED_AT, direction: DESC }) {\n edges {\n node {\n id\n user {\n id\n name\n }\n points\n description\n grantedAt\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -339,7 +339,7 @@ export function graphql(source: "\n query GroupList {\n groups {\n id\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 UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n ) {\n users(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"]; +export function graphql(source: "\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: UserWhereInput\n ) {\n users(first: $first, after: $after, last: $last, before: $before, where: $where) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"): (typeof documents)["\n query UsersTable(\n $first: Int\n $after: Cursor\n $last: Int\n $before: Cursor\n $where: UserWhereInput\n ) {\n users(first: $first, after: $after, last: $last, before: $before, where: $where) {\n edges {\n node {\n id\n name\n email\n avatar\n createdAt\n updatedAt\n group {\n id\n name\n }\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n endCursor\n startCursor\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/gql/graphql.ts b/gql/graphql.ts index 8952212..f19de73 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -1522,6 +1522,7 @@ export type EventsTableQueryVariables = Exact<{ after?: InputMaybe; last?: InputMaybe; before?: InputMaybe; + where?: InputMaybe; }>; @@ -1539,6 +1540,7 @@ export type PointsTableQueryVariables = Exact<{ after?: InputMaybe; last?: InputMaybe; before?: InputMaybe; + where?: InputMaybe; }>; @@ -1849,6 +1851,7 @@ export type UsersTableQueryVariables = Exact<{ after?: InputMaybe; last?: InputMaybe; before?: InputMaybe; + where?: InputMaybe; }>; @@ -1913,9 +1916,9 @@ export const UserTotalPointsFragmentDoc = {"kind":"Document","definitions":[{"ki export const RankingFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RankingFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"score"}}]}}]} as unknown as DocumentNode; export const ScoreCellFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ScoreCell"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserCompletedQuestions"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserTotalPoints"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RankingFragment"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserCompletedQuestions"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submissionStatistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"solvedQuestions"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserTotalPoints"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPoints"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RankingFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"score"}}]}}]} as unknown as DocumentNode; export const EventByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}},{"kind":"Field","name":{"kind":"Name","value":"triggeredAt"}}]}}]}}]} as unknown as DocumentNode; -export const EventsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"field"},"value":{"kind":"EnumValue","value":"TRIGGERED_AT"}},{"kind":"ObjectField","name":{"kind":"Name","value":"direction"},"value":{"kind":"EnumValue","value":"DESC"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"triggeredAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; +export const EventsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"EventWhereInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"field"},"value":{"kind":"EnumValue","value":"TRIGGERED_AT"}},{"kind":"ObjectField","name":{"kind":"Name","value":"direction"},"value":{"kind":"EnumValue","value":"DESC"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"triggeredAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; export const PointByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PointById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pointGrant"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"points"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"grantedAt"}}]}}]}}]} as unknown as DocumentNode; -export const PointsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PointsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"points"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"field"},"value":{"kind":"EnumValue","value":"GRANTED_AT"}},{"kind":"ObjectField","name":{"kind":"Name","value":"direction"},"value":{"kind":"EnumValue","value":"DESC"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"points"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"grantedAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; +export const PointsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PointsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PointWhereInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"points"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"field"},"value":{"kind":"EnumValue","value":"GRANTED_AT"}},{"kind":"ObjectField","name":{"kind":"Name","value":"direction"},"value":{"kind":"EnumValue","value":"DESC"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"points"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"grantedAt"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; export const SubmissionByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SubmissionById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submission"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"queryResult"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"columns"}},{"kind":"Field","name":{"kind":"Name","value":"rows"}},{"kind":"Field","name":{"kind":"Name","value":"matchAnswer"}}]}},{"kind":"Field","name":{"kind":"Name","value":"question"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"submittedCode"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"submittedAt"}}]}}]}}]} as unknown as DocumentNode; export const SubmissionsTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SubmissionsTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submissions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"field"},"value":{"kind":"EnumValue","value":"SUBMITTED_AT"}},{"kind":"ObjectField","name":{"kind":"Name","value":"direction"},"value":{"kind":"EnumValue","value":"DESC"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"submittedCode"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"question"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; export const DatabaseDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DatabaseDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"database"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"relationFigure"}}]}}]}}]} as unknown as DocumentNode; @@ -1959,7 +1962,7 @@ export const LogoutUserDevicesDocument = {"kind":"Document","definitions":[{"kin export const ImpersonateUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ImpersonateUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userID"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"impersonateUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userID"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userID"}}}]}]}}]} as unknown as DocumentNode; export const UserByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; export const GroupListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GroupList"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groups"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; -export const UsersTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UsersTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; +export const UsersTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UsersTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"last"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"before"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UserWhereInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}},{"kind":"Argument","name":{"kind":"Name","value":"last"},"value":{"kind":"Variable","name":{"kind":"Name","value":"last"}}},{"kind":"Argument","name":{"kind":"Name","value":"before"},"value":{"kind":"Variable","name":{"kind":"Name","value":"before"}}},{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}}]}}]}}]}}]} as unknown as DocumentNode; export const LoginTotalCountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LoginTotalCount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EventWhereInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]} as unknown as DocumentNode; export const OverviewRankingDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"OverviewRanking"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RankingFilter"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"first"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"after"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ranking"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"Variable","name":{"kind":"Name","value":"first"}}},{"kind":"Argument","name":{"kind":"Name","value":"after"},"value":{"kind":"Variable","name":{"kind":"Name","value":"after"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ScoreCell"}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserCompletedQuestions"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submissionStatistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"solvedQuestions"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserTotalPoints"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalPoints"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RankingFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"score"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ScoreCell"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RankingEdge"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserCompletedQuestions"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserTotalPoints"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RankingFragment"}}]}}]} as unknown as DocumentNode; export const SubmissionsTotalCountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SubmissionsTotalCount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubmissionWhereInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"submissions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]} as unknown as DocumentNode; From 94edbe065d4fcd1f9fb3a922f702dfb944b2020d Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 19 Oct 2025 04:39:06 +0800 Subject: [PATCH 08/11] feat(scopesets): implement scopes count --- .../_components/data-table-columns.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/(admin)/(user-management)/scopesets/_components/data-table-columns.tsx b/app/(admin)/(user-management)/scopesets/_components/data-table-columns.tsx index 2569805..af7ff78 100644 --- a/app/(admin)/(user-management)/scopesets/_components/data-table-columns.tsx +++ b/app/(admin)/(user-management)/scopesets/_components/data-table-columns.tsx @@ -8,6 +8,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { StyledLink } from "@/components/ui/link"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import type { ColumnDef } from "@tanstack/react-table"; import { MoreHorizontal } from "lucide-react"; import Link from "next/link"; @@ -49,7 +50,27 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => { const scopes = row.original.scopes; - return {scopes.join(", ")}; + return ( +
+ + + {scopes.join(", ")} + + + + 總共 {scopes.length} 個權限 + + +
+ ); }, }, { From c22eaa48f569da861df559a013edcd410b96a1a7 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 19 Oct 2025 04:43:38 +0800 Subject: [PATCH 09/11] fix: issues indicated by Copilot --- app/(admin)/_components/login-count.tsx | 8 ++++---- app/(admin)/_components/rank.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/(admin)/_components/login-count.tsx b/app/(admin)/_components/login-count.tsx index 07c0624..8ca727f 100644 --- a/app/(admin)/_components/login-count.tsx +++ b/app/(admin)/_components/login-count.tsx @@ -4,7 +4,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { graphql } from "@/gql"; -import { type SubmissionWhereInput } from "@/gql/graphql"; +import { type EventWhereInput } from "@/gql/graphql"; import { useLazyQuery } from "@apollo/client/react"; import { useEffect, useState } from "react"; @@ -32,12 +32,12 @@ export default function LoginTotalCount() { useEffect(() => { const now = new Date(); - const timeRangeWhere: Record = { + const timeRangeWhere: Record = { daily: { - submittedAtGTE: new Date(now.setDate(now.getDate() - 1)).toISOString(), + triggeredAtGTE: new Date(now.setDate(now.getDate() - 1)).toISOString(), }, weekly: { - submittedAtGTE: new Date(now.setDate(now.getDate() - 7)).toISOString(), + triggeredAtGTE: new Date(now.setDate(now.getDate() - 7)).toISOString(), }, all: {}, }; diff --git a/app/(admin)/_components/rank.tsx b/app/(admin)/_components/rank.tsx index 1a870f9..9a451fb 100644 --- a/app/(admin)/_components/rank.tsx +++ b/app/(admin)/_components/rank.tsx @@ -270,7 +270,7 @@ function ScoreCell( return ( - {components[rankingBy] ?? 發現未定義的排序依據 {rankingBy}} + {components[rankingBy]} {" | "} From 84f4f374d17c0cbae512be538df69f3dc5d147c5 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 19 Oct 2025 04:44:18 +0800 Subject: [PATCH 10/11] fix(home): header typo --- app/(admin)/_components/header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(admin)/_components/header.tsx b/app/(admin)/_components/header.tsx index a7c341d..718e768 100644 --- a/app/(admin)/_components/header.tsx +++ b/app/(admin)/_components/header.tsx @@ -10,7 +10,7 @@ export function Header() {

哈囉,{user.name}!

- 這裡可以快速總覽所有統計資料,也可以點選左邊的側邊來進行資料管理。 + 這裡可以快速總覽所有統計資料,也可以點選左邊的側邊欄來進行資料管理。

From 03f014db266cce776727913ed7c435f10834e195 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Sun, 19 Oct 2025 04:49:22 +0800 Subject: [PATCH 11/11] refactor: use dayjs for reliable daily/weekly calculation --- app/(admin)/_components/login-count.tsx | 7 +++---- app/(admin)/_components/submit-count.tsx | 7 +++---- package.json | 1 + pnpm-lock.yaml | 8 ++++++++ 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/(admin)/_components/login-count.tsx b/app/(admin)/_components/login-count.tsx index 8ca727f..03c49c6 100644 --- a/app/(admin)/_components/login-count.tsx +++ b/app/(admin)/_components/login-count.tsx @@ -6,6 +6,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { graphql } from "@/gql"; import { type EventWhereInput } from "@/gql/graphql"; import { useLazyQuery } from "@apollo/client/react"; +import dayjs from "dayjs"; import { useEffect, useState } from "react"; const LOGIN_TOTAL_COUNT_QUERY = graphql(` @@ -30,14 +31,12 @@ export default function LoginTotalCount() { const [getLoginTotalCount, { data, loading }] = useLazyQuery(LOGIN_TOTAL_COUNT_QUERY); useEffect(() => { - const now = new Date(); - const timeRangeWhere: Record = { daily: { - triggeredAtGTE: new Date(now.setDate(now.getDate() - 1)).toISOString(), + triggeredAtGTE: dayjs().startOf("day").toISOString(), }, weekly: { - triggeredAtGTE: new Date(now.setDate(now.getDate() - 7)).toISOString(), + triggeredAtGTE: dayjs().startOf("week").toISOString(), }, all: {}, }; diff --git a/app/(admin)/_components/submit-count.tsx b/app/(admin)/_components/submit-count.tsx index 5b28ff6..211ef12 100644 --- a/app/(admin)/_components/submit-count.tsx +++ b/app/(admin)/_components/submit-count.tsx @@ -8,6 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { graphql } from "@/gql"; import { SubmissionStatus, type SubmissionWhereInput } from "@/gql/graphql"; import { useLazyQuery } from "@apollo/client/react"; +import dayjs from "dayjs"; import { useEffect, useState } from "react"; const SUBMISSIONS_TOTAL_COUNT_QUERY = graphql(` @@ -33,14 +34,12 @@ export default function SubmissionsTotalCount() { const [getSubmissionsTotalCount, { data, loading }] = useLazyQuery(SUBMISSIONS_TOTAL_COUNT_QUERY); useEffect(() => { - const now = new Date(); - const timeRangeWhere: Record = { daily: { - submittedAtGTE: new Date(now.setDate(now.getDate() - 1)).toISOString(), + submittedAtGTE: dayjs().startOf("day").toISOString(), }, weekly: { - submittedAtGTE: new Date(now.setDate(now.getDate() - 7)).toISOString(), + submittedAtGTE: dayjs().startOf("week").toISOString(), }, all: {}, }; diff --git a/package.json b/package.json index da31a99..3afde5e 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "babel-plugin-react-compiler": "19.1.0-rc.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.18", "foxact": "^0.2.49", "graphql": "^16.11.0", "lucide-react": "^0.545.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6de889f..ce8af46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + dayjs: + specifier: ^1.11.18 + version: 1.11.18 foxact: specifier: ^0.2.49 version: 0.2.49(react@19.3.0-canary-1873ad79-20251015) @@ -2292,6 +2295,9 @@ packages: dataloader@2.2.3: resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} + dayjs@1.11.18: + resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + debounce@2.2.0: resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==} engines: {node: '>=18'} @@ -6606,6 +6612,8 @@ snapshots: dataloader@2.2.3: {} + dayjs@1.11.18: {} + debounce@2.2.0: {} debug@3.2.7: