Skip to content

Commit d2acf19

Browse files
authored
Merge pull request #30 from database-playground/pan93412/dbp-123-filtering-and-load-more-ux
DBP-123: 支援 filtering 和 UX 比較好的 load more
2 parents 70f9922 + 1736783 commit d2acf19

File tree

8 files changed

+735
-664
lines changed

8 files changed

+735
-664
lines changed

app/(app)/challenges/_components/filter/tag.tsx

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,35 @@ import { difficultyTranslation } from "@/components/question/difficulty-badge";
22
import { solvedStatusTranslation } from "@/components/question/solved-status-badge";
33
import { Checkbox } from "@/components/ui/checkbox";
44
import { Label } from "@/components/ui/label";
5+
import { graphql } from "@/gql";
56
import { QuestionDifficulty } from "@/gql/graphql";
67
import type { SolvedStatus } from "@/lib/solved-status";
8+
import { useSuspenseQuery } from "@apollo/client/react";
79
import { FilterIcon } from "lucide-react";
810

911
export interface TagState {
1012
solvedStatus: SolvedStatus[];
1113
difficulty: QuestionDifficulty[];
14+
categories: string[];
1215
}
1316

1417
export interface TagFilterSectionProps {
1518
value: TagState;
1619
onChange: (tags: TagState) => void;
1720
}
1821

22+
const TAG_FILTER_SECTION_QUERY = graphql(`
23+
query TagFilterSection {
24+
questionCategories
25+
}
26+
`);
27+
1928
export default function TagFilterSection({
2029
value,
2130
onChange,
2231
}: TagFilterSectionProps) {
32+
const { data } = useSuspenseQuery(TAG_FILTER_SECTION_QUERY);
33+
2334
const getSolvedStatus = (solvedStatus: SolvedStatus) => {
2435
return value.solvedStatus.includes(solvedStatus);
2536
};
@@ -28,6 +39,10 @@ export default function TagFilterSection({
2839
return value.difficulty.includes(difficulty);
2940
};
3041

42+
const getCategory = (category: string) => {
43+
return value.categories.includes(category);
44+
};
45+
3146
const handleDifficultyChange = (difficulty: QuestionDifficulty) => {
3247
return (checked: boolean) => {
3348
onChange({
@@ -50,6 +65,15 @@ export default function TagFilterSection({
5065
};
5166
};
5267

68+
const handleCategoryChange = (category: string) => {
69+
return (checked: boolean) => {
70+
onChange({
71+
...value,
72+
categories: checked ? [...value.categories, category] : value.categories.filter((c) => c !== category),
73+
});
74+
};
75+
};
76+
5377
return (
5478
<div className="space-y-3">
5579
<label className="flex items-center gap-2 font-bold">
@@ -81,7 +105,7 @@ export default function TagFilterSection({
81105
</div>
82106
</div>
83107

84-
<div className="space-y-2 text-sm text-muted-foreground">
108+
<div className="mb-4 space-y-2 text-sm text-muted-foreground">
85109
難度
86110
<div className="mt-2 space-y-2">
87111
<TagCheckbox
@@ -110,6 +134,22 @@ export default function TagFilterSection({
110134
/>
111135
</div>
112136
</div>
137+
138+
<div className="space-y-2 text-sm text-muted-foreground">
139+
分類
140+
<div className="mt-2 space-y-2">
141+
{data.questionCategories.map((category) => (
142+
<div key={category} className="flex items-center gap-2">
143+
<Checkbox
144+
id={category}
145+
checked={getCategory(category)}
146+
onCheckedChange={handleCategoryChange(category)}
147+
/>
148+
<Label htmlFor={category}>{category}</Label>
149+
</div>
150+
))}
151+
</div>
152+
</div>
113153
</div>
114154
);
115155
}

app/(app)/challenges/_components/questions-list.tsx

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { useDebouncedValue } from "foxact/use-debounced-value";
4-
import { Suspense, useState } from "react";
4+
import { Suspense, useState, useTransition } from "react";
55
import type { TagState } from "./filter/tag";
66

77
import QuestionCard from "@/components/question/question-card";
@@ -41,6 +41,7 @@ export default function QuestionsList() {
4141
QuestionDifficulty.Hard,
4242
QuestionDifficulty.Unspecified,
4343
],
44+
categories: [],
4445
});
4546

4647
const deferredSearch = useDebouncedValue(search, 200);
@@ -54,12 +55,10 @@ export default function QuestionsList() {
5455
{
5556
descriptionContainsFold: deferredSearch,
5657
},
57-
{
58-
categoryContainsFold: deferredSearch,
59-
},
6058
]
6159
: undefined,
62-
difficultyIn: tags.difficulty.length > 0 ? tags.difficulty : undefined,
60+
difficultyIn: tags.difficulty || undefined,
61+
categoryIn: tags.categories || undefined,
6362
};
6463

6564
return (
@@ -94,6 +93,7 @@ export function ChallengeQuestionsList({
9493
where: QuestionWhereInput;
9594
solvedStatusContains: SolvedStatus[];
9695
}) {
96+
const [isPending, startTransition] = useTransition();
9797
const { data, fetchMore } = useSuspenseQuery(LIST_QUESTIONS, {
9898
variables: { where },
9999
});
@@ -117,23 +117,27 @@ export function ChallengeQuestionsList({
117117
{data?.questions.pageInfo.hasNextPage && (
118118
<div className="flex w-full justify-center">
119119
<Button
120-
onClick={() =>
121-
fetchMore({
122-
variables: {
123-
after: data?.questions.pageInfo.endCursor,
124-
},
125-
updateQuery(previousQueryResult, options) {
126-
return {
127-
questions: {
128-
edges: [
129-
...(previousQueryResult.questions.edges || []),
130-
...(options.fetchMoreResult.questions.edges || []),
131-
],
132-
pageInfo: options.fetchMoreResult.questions.pageInfo,
133-
},
134-
};
135-
},
136-
})}
120+
disabled={isPending}
121+
onClick={() => {
122+
startTransition(() => {
123+
fetchMore({
124+
variables: {
125+
after: data?.questions.pageInfo.endCursor,
126+
},
127+
updateQuery(previousQueryResult, options) {
128+
return {
129+
questions: {
130+
edges: [
131+
...(previousQueryResult.questions.edges || []),
132+
...(options.fetchMoreResult.questions.edges || []),
133+
],
134+
pageInfo: options.fetchMoreResult.questions.pageInfo,
135+
},
136+
};
137+
},
138+
});
139+
});
140+
}}
137141
>
138142
載入更多
139143
</Button>

components/question/question-card.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ export default function QuestionCard({
3434
<div className="flex-1 space-y-3 bg-white p-4">
3535
<div>
3636
<h2 className="font-bold tracking-wider">{question.title}</h2>
37-
<p className="tracking-wide">
37+
<div className="tracking-wide">
3838
<Remark>{descriptionFirstLine}</Remark>
39-
</p>
39+
</div>
4040
</div>
4141
<div className="flex flex-wrap gap-1">
4242
<SolvedStatusBadge solvedStatus={solvedStatus} />

gql/gql.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type Documents = {
2424
"\n query SqlEditorContext($id: ID!) {\n question(id: $id) {\n id\n database {\n id\n ...DatabaseStructure\n }\n lastSubmission {\n id\n submittedCode\n }\n }\n }\n": typeof types.SqlEditorContextDocument,
2525
"\n query SubmissionHistory($id: ID!) {\n question(id: $id) {\n id\n userSubmissions {\n id\n status\n submittedCode\n submittedAt\n }\n }\n }\n": typeof types.SubmissionHistoryDocument,
2626
"\n fragment DatabaseStructure on Database {\n id\n structure {\n tables {\n columns\n name\n }\n }\n }\n": typeof types.DatabaseStructureFragmentDoc,
27+
"\n query TagFilterSection {\n questionCategories\n }\n": typeof types.TagFilterSectionDocument,
2728
"\n query ChallengeStatistics {\n me {\n id\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n }\n }\n }\n": typeof types.ChallengeStatisticsDocument,
2829
"\n query ListQuestions($where: QuestionWhereInput, $after: Cursor) {\n questions(where: $where, first: 10, after: $after) {\n edges {\n node {\n id\n ...QuestionCard\n ...QuestionSolvedStatus\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.ListQuestionsDocument,
2930
"\n fragment MaterialsSchemaCard on Database {\n id\n slug\n description\n }\n": typeof types.MaterialsSchemaCardFragmentDoc,
@@ -54,6 +55,7 @@ const documents: Documents = {
5455
"\n query SqlEditorContext($id: ID!) {\n question(id: $id) {\n id\n database {\n id\n ...DatabaseStructure\n }\n lastSubmission {\n id\n submittedCode\n }\n }\n }\n": types.SqlEditorContextDocument,
5556
"\n query SubmissionHistory($id: ID!) {\n question(id: $id) {\n id\n userSubmissions {\n id\n status\n submittedCode\n submittedAt\n }\n }\n }\n": types.SubmissionHistoryDocument,
5657
"\n fragment DatabaseStructure on Database {\n id\n structure {\n tables {\n columns\n name\n }\n }\n }\n": types.DatabaseStructureFragmentDoc,
58+
"\n query TagFilterSection {\n questionCategories\n }\n": types.TagFilterSectionDocument,
5759
"\n query ChallengeStatistics {\n me {\n id\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n }\n }\n }\n": types.ChallengeStatisticsDocument,
5860
"\n query ListQuestions($where: QuestionWhereInput, $after: Cursor) {\n questions(where: $where, first: 10, after: $after) {\n edges {\n node {\n id\n ...QuestionCard\n ...QuestionSolvedStatus\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.ListQuestionsDocument,
5961
"\n fragment MaterialsSchemaCard on Database {\n id\n slug\n description\n }\n": types.MaterialsSchemaCardFragmentDoc,
@@ -128,6 +130,10 @@ export function graphql(source: "\n query SubmissionHistory($id: ID!) {\n qu
128130
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
129131
*/
130132
export function graphql(source: "\n fragment DatabaseStructure on Database {\n id\n structure {\n tables {\n columns\n name\n }\n }\n }\n"): (typeof documents)["\n fragment DatabaseStructure on Database {\n id\n structure {\n tables {\n columns\n name\n }\n }\n }\n"];
133+
/**
134+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
135+
*/
136+
export function graphql(source: "\n query TagFilterSection {\n questionCategories\n }\n"): (typeof documents)["\n query TagFilterSection {\n questionCategories\n }\n"];
131137
/**
132138
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
133139
*/

0 commit comments

Comments
 (0)