From 5b444217cd638e298254dbdebd107ec68af62c85 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Tue, 7 Oct 2025 02:41:25 +0800 Subject: [PATCH 1/3] feat: material page --- .../materials/_components/points/index.tsx | 66 +++++++++++++ .../materials/_components/schemas/card.tsx | 99 +++++++++++++++++++ .../materials/_components/schemas/index.tsx | 37 +++++++ .../materials/_components/section-header.tsx | 8 ++ app/(app)/materials/page.tsx | 19 +++- components/page-header.tsx | 16 +-- gql/gql.ts | 18 ++++ gql/graphql.ts | 20 ++++ 8 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 app/(app)/materials/_components/points/index.tsx create mode 100644 app/(app)/materials/_components/schemas/card.tsx create mode 100644 app/(app)/materials/_components/schemas/index.tsx create mode 100644 app/(app)/materials/_components/section-header.tsx diff --git a/app/(app)/materials/_components/points/index.tsx b/app/(app)/materials/_components/points/index.tsx new file mode 100644 index 0000000..2af4c47 --- /dev/null +++ b/app/(app)/materials/_components/points/index.tsx @@ -0,0 +1,66 @@ +import SectionHeader from "../section-header"; + +export default function PointCalculateRules() { + return ( +
+ + +
+
+
+

登入點數規則

+ +
    +
  • + 每天登入可以獲得 20 點。 +
  • +
  • + 美洲登入可以獲得 50 點。 +
  • +
+
+ +
+

作答點數規則

+ +
    +
  • + 首次嘗試題目可以獲得 30 點。 +
  • +
  • + 每日嘗試題目可以獲得 30 點。 +
  • +
  • + 正確答案可以獲得 60 點。 +
  • +
  • + 第一個通過題目的人可以獲得 80 點。 +
  • +
+
+ +
+

其他規則

+

這部分是手動發放的。

+ +
    +
  • + 回報問題和提供系統意見,根據重要性可以獲得 25 點50 點 不等的點數。 +
  • +
  • + 到 GitHub{" "} + 以 PR 形式貢獻程式碼並獲得接受,可以獲得 100 點 到超過 250 點 的點數。 +
  • +
+
+
+
+
+ ); +} diff --git a/app/(app)/materials/_components/schemas/card.tsx b/app/(app)/materials/_components/schemas/card.tsx new file mode 100644 index 0000000..59443f0 --- /dev/null +++ b/app/(app)/materials/_components/schemas/card.tsx @@ -0,0 +1,99 @@ +import { Button } from "@/components/ui/button"; +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { type FragmentType, graphql, readFragment } from "@/gql"; +import { useLazyQuery } from "@apollo/client/react"; +import { Database, Download } from "lucide-react"; +import { Remark } from "react-remark"; +import { toast } from "sonner"; + +const MATERIAL_SCHEMA_CARD = graphql(` + fragment MaterialsSchemaCard on Database { + id + slug + description + } +`); + +const MATERIAL_SCHEMA_CONTENT_QUERY = graphql(` + query MaterialsSchemaContent($id: ID!) { + database(id: $id) { + id + schema + } + } +`); + +export function MaterialsSchemaCard({ + fragment, +}: { + fragment: FragmentType; +}) { + const { id, slug, description } = readFragment( + MATERIAL_SCHEMA_CARD, + fragment, + ); + const [getSchemaContent] = useLazyQuery(MATERIAL_SCHEMA_CONTENT_QUERY); + + const handleDownload = async () => { + const loading = toast.loading("正在下載 SQL 檔案……"); + + try { + const { data } = await getSchemaContent({ variables: { id } }); + if (!data) return; + + const blob = new Blob([data.database.schema], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `schema-${slug}.sql`; + a.click(); + URL.revokeObjectURL(url); + + toast.success("成功下載 SQL 檔案", { description: `schema-${slug}.sql` }); + } catch (error) { + toast.error("無法下載 SQL 檔案", { + description: error instanceof Error ? error.message : undefined, + }); + } finally { + toast.dismiss(loading); + } + }; + + return ( + + +
+
+ +
+
+
+ {slug} + + {description + ? ( + + {description} + + ) + : null} +
+
+ + + +
+ ); +} diff --git a/app/(app)/materials/_components/schemas/index.tsx b/app/(app)/materials/_components/schemas/index.tsx new file mode 100644 index 0000000..c8b95b0 --- /dev/null +++ b/app/(app)/materials/_components/schemas/index.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { graphql } from "@/gql"; +import { useSuspenseQuery } from "@apollo/client/react"; +import SectionHeader from "../section-header"; +import { MaterialsSchemaCard } from "./card"; + +const MATERIALS_SCHEMA = graphql(` + query MaterialsSchema { + databases { + id + ...MaterialsSchemaCard + } + } +`); + +export default function MaterialsSchema() { + const { data } = useSuspenseQuery(MATERIALS_SCHEMA); + + return ( +
+ +
+ {data.databases.map((database) => )} +
+
+ ); +} diff --git a/app/(app)/materials/_components/section-header.tsx b/app/(app)/materials/_components/section-header.tsx new file mode 100644 index 0000000..abda26f --- /dev/null +++ b/app/(app)/materials/_components/section-header.tsx @@ -0,0 +1,8 @@ +export default function SectionHeader({ title, description }: { title: string; description: string }) { + return ( +
+

{title}

+

{description}

+
+ ); +} diff --git a/app/(app)/materials/page.tsx b/app/(app)/materials/page.tsx index 8868b93..0c86e74 100644 --- a/app/(app)/materials/page.tsx +++ b/app/(app)/materials/page.tsx @@ -1,9 +1,26 @@ +import PageHeader from "@/components/page-header"; +import { Skeleton } from "@/components/ui/skeleton"; import type { Metadata } from "next"; +import { Suspense } from "react"; +import PointCalculateRules from "./_components/points"; +import MaterialsSchema from "./_components/schemas"; export const metadata: Metadata = { title: "補充資料", }; export default function MaterialsPage() { - return
MaterialsPage
; + return ( +
+ + }> + + + + +
+ ); } diff --git a/components/page-header.tsx b/components/page-header.tsx index 5821069..8b87fd3 100644 --- a/components/page-header.tsx +++ b/components/page-header.tsx @@ -2,24 +2,24 @@ import { Skeleton } from "./ui/skeleton"; export default function PageHeader({ title, description }: { title: string; description: string }) { return ( -
-

{title}

-

{description}

+
+

{title}

+

{description}

); } export function PageHeaderSkeleton({ title, description }: { title?: string; description?: string }) { return ( -
- {title ?

{title}

: ( +
+ {title ?

{title}

: ( )} - {description ?

{description}

: ( + {description ?

{description}

: ( )}
diff --git a/gql/gql.ts b/gql/gql.ts index 3fc50c3..5f8eadd 100644 --- a/gql/gql.ts +++ b/gql/gql.ts @@ -26,6 +26,9 @@ type Documents = { "\n fragment DatabaseStructure on Database {\n id\n structure {\n tables {\n columns\n name\n }\n }\n }\n": typeof types.DatabaseStructureFragmentDoc, "\n query ChallengeStatistics {\n me {\n id\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n }\n }\n }\n": typeof types.ChallengeStatisticsDocument, "\n query ListQuestions($where: QuestionWhereInput, $after: Cursor) {\n questions(where: $where, first: 10, after: $after) {\n edges {\n node {\n id\n ...QuestionCard\n ...QuestionSolvedStatus\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": typeof types.ListQuestionsDocument, + "\n fragment MaterialsSchemaCard on Database {\n id\n slug\n description\n }\n": typeof types.MaterialsSchemaCardFragmentDoc, + "\n query MaterialsSchemaContent($id: ID!) {\n database(id: $id) {\n id\n schema\n }\n }\n": typeof types.MaterialsSchemaContentDocument, + "\n query MaterialsSchema {\n databases {\n id\n ...MaterialsSchemaCard\n }\n }\n": typeof types.MaterialsSchemaDocument, "\n query CompletedQuestions {\n me {\n id\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": typeof types.CompletedQuestionsDocument, "\n query MySolvedQuestionsCount {\n me {\n id\n name\n submissionStatistics {\n solvedQuestions\n }\n }\n }\n": typeof types.MySolvedQuestionsCountDocument, "\n query MyPoints {\n me {\n id\n name\n totalPoints\n }\n }\n": typeof types.MyPointsDocument, @@ -53,6 +56,9 @@ const documents: Documents = { "\n fragment DatabaseStructure on Database {\n id\n structure {\n tables {\n columns\n name\n }\n }\n }\n": types.DatabaseStructureFragmentDoc, "\n query ChallengeStatistics {\n me {\n id\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n }\n }\n }\n": types.ChallengeStatisticsDocument, "\n query ListQuestions($where: QuestionWhereInput, $after: Cursor) {\n questions(where: $where, first: 10, after: $after) {\n edges {\n node {\n id\n ...QuestionCard\n ...QuestionSolvedStatus\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n": types.ListQuestionsDocument, + "\n fragment MaterialsSchemaCard on Database {\n id\n slug\n description\n }\n": types.MaterialsSchemaCardFragmentDoc, + "\n query MaterialsSchemaContent($id: ID!) {\n database(id: $id) {\n id\n schema\n }\n }\n": types.MaterialsSchemaContentDocument, + "\n query MaterialsSchema {\n databases {\n id\n ...MaterialsSchemaCard\n }\n }\n": types.MaterialsSchemaDocument, "\n query CompletedQuestions {\n me {\n id\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": types.CompletedQuestionsDocument, "\n query MySolvedQuestionsCount {\n me {\n id\n name\n submissionStatistics {\n solvedQuestions\n }\n }\n }\n": types.MySolvedQuestionsCountDocument, "\n query MyPoints {\n me {\n id\n name\n totalPoints\n }\n }\n": types.MyPointsDocument, @@ -130,6 +136,18 @@ export function graphql(source: "\n query ChallengeStatistics {\n me {\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 ListQuestions($where: QuestionWhereInput, $after: Cursor) {\n questions(where: $where, first: 10, after: $after) {\n edges {\n node {\n id\n ...QuestionCard\n ...QuestionSolvedStatus\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ListQuestions($where: QuestionWhereInput, $after: Cursor) {\n questions(where: $where, first: 10, after: $after) {\n edges {\n node {\n id\n ...QuestionCard\n ...QuestionSolvedStatus\n }\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "\n fragment MaterialsSchemaCard on Database {\n id\n slug\n description\n }\n"): (typeof documents)["\n fragment MaterialsSchemaCard on Database {\n id\n slug\n description\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 MaterialsSchemaContent($id: ID!) {\n database(id: $id) {\n id\n schema\n }\n }\n"): (typeof documents)["\n query MaterialsSchemaContent($id: ID!) {\n database(id: $id) {\n id\n schema\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 MaterialsSchema {\n databases {\n id\n ...MaterialsSchemaCard\n }\n }\n"): (typeof documents)["\n query MaterialsSchema {\n databases {\n id\n ...MaterialsSchemaCard\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 caa7a4e..bd5dacc 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -1554,6 +1554,23 @@ export type ListQuestionsQuery = { __typename?: 'Query', questions: { __typename & { ' $fragmentRefs'?: { 'QuestionCardFragment': QuestionCardFragment;'QuestionSolvedStatusFragment': QuestionSolvedStatusFragment } } ) | null } | null> | null, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null } } }; +export type MaterialsSchemaCardFragment = { __typename?: 'Database', id: string, slug: string, description?: string | null } & { ' $fragmentName'?: 'MaterialsSchemaCardFragment' }; + +export type MaterialsSchemaContentQueryVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type MaterialsSchemaContentQuery = { __typename?: 'Query', database: { __typename?: 'Database', id: string, schema: string } }; + +export type MaterialsSchemaQueryVariables = Exact<{ [key: string]: never; }>; + + +export type MaterialsSchemaQuery = { __typename?: 'Query', databases: Array<( + { __typename?: 'Database', id: string } + & { ' $fragmentRefs'?: { 'MaterialsSchemaCardFragment': MaterialsSchemaCardFragment } } + )> }; + export type CompletedQuestionsQueryVariables = Exact<{ [key: string]: never; }>; @@ -1623,6 +1640,7 @@ 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 DatabaseStructureFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"DatabaseStructure"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Database"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"structure"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tables"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"columns"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; +export const MaterialsSchemaCardFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MaterialsSchemaCard"},"typeCondition":{"kind":"NamedType","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":"description"}}]}}]} as unknown as DocumentNode; export const PointFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PointFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Point"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"points"}}]}}]} as unknown as DocumentNode; export const QuestionInfoFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"QuestionInfoFragment"},"typeCondition":{"kind":"NamedType","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":"description"}},{"kind":"Field","name":{"kind":"Name","value":"difficulty"}},{"kind":"Field","name":{"kind":"Name","value":"category"}}]}}]} as unknown as DocumentNode; export const QuestionSolvedStatusFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"QuestionSolvedStatus"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Question"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"solved"}},{"kind":"Field","name":{"kind":"Name","value":"attempted"}}]}}]} as unknown as DocumentNode; @@ -1638,6 +1656,8 @@ export const SqlEditorContextDocument = {"kind":"Document","definitions":[{"kind export const SubmissionHistoryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SubmissionHistory"},"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":"userSubmissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"submittedCode"}},{"kind":"Field","name":{"kind":"Name","value":"submittedAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const ChallengeStatisticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ChallengeStatistics"},"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":"submissionStatistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalQuestions"}},{"kind":"Field","name":{"kind":"Name","value":"solvedQuestions"}},{"kind":"Field","name":{"kind":"Name","value":"attemptedQuestions"}}]}}]}}]}}]} as unknown as DocumentNode; export const ListQuestionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListQuestions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"QuestionWhereInput"}}},{"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":"questions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}},{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"10"}},{"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":"FragmentSpread","name":{"kind":"Name","value":"QuestionCard"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"QuestionSolvedStatus"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"QuestionSolvedStatus"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Question"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"solved"}},{"kind":"Field","name":{"kind":"Name","value":"attempted"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"QuestionCard"},"typeCondition":{"kind":"NamedType","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":"description"}},{"kind":"Field","name":{"kind":"Name","value":"difficulty"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"QuestionSolvedStatus"}}]}}]} as unknown as DocumentNode; +export const MaterialsSchemaContentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MaterialsSchemaContent"},"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":"schema"}}]}}]}}]} as unknown as DocumentNode; +export const MaterialsSchemaDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MaterialsSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"databases"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"MaterialsSchemaCard"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"MaterialsSchemaCard"},"typeCondition":{"kind":"NamedType","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":"description"}}]}}]} as unknown as DocumentNode; export const CompletedQuestionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CompletedQuestions"},"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":"submissionStatistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalQuestions"}},{"kind":"Field","name":{"kind":"Name","value":"solvedQuestions"}}]}}]}}]}}]} as unknown as DocumentNode; export const MySolvedQuestionsCountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MySolvedQuestionsCount"},"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":"submissionStatistics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"solvedQuestions"}}]}}]}}]}}]} as unknown as DocumentNode; export const MyPointsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyPoints"},"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":"totalPoints"}}]}}]}}]} as unknown as DocumentNode; From 536e71b1b42c06e5af6992c6408089664729907f Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Tue, 7 Oct 2025 03:03:06 +0800 Subject: [PATCH 2/3] feat: capture global exception --- app/global-error.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/global-error.tsx b/app/global-error.tsx index 7eb9992..d694afe 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -8,6 +8,7 @@ import { ERROR_NOT_FOUND, ERROR_NOT_IMPLEMENTED, ERROR_UNAUTHORIZED, ERROR_USER_ import { CombinedGraphQLErrors, CombinedProtocolErrors } from "@apollo/client"; import { AlertCircle, Code, Home, Lock, RefreshCw, Search, Shield, WifiOff } from "lucide-react"; import Link from "next/link"; +import posthog from "posthog-js"; import { useEffect } from "react"; interface GlobalErrorProps { @@ -111,8 +112,10 @@ export default function GlobalError({ error, reset }: GlobalErrorProps) { const errorInfo = getErrorInfo(error); useEffect(() => { - // Log error to monitoring service - console.error("Global error:", error); + posthog.captureException(error, { + url: window.location.href, + digest: error.digest, + }); }, [error]); return ( From 6feb86707937fd721ceedba52208d27262fd2770 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Tue, 7 Oct 2025 03:03:33 +0800 Subject: [PATCH 3/3] feat: comments WIP page --- app/(app)/comments/page.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/(app)/comments/page.tsx b/app/(app)/comments/page.tsx index 7f2cb14..bcf9d94 100644 --- a/app/(app)/comments/page.tsx +++ b/app/(app)/comments/page.tsx @@ -1,3 +1,7 @@ +import PageHeader from "@/components/page-header"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { AlertCircle } from "lucide-react"; import type { Metadata } from "next"; export const metadata: Metadata = { @@ -5,5 +9,19 @@ export const metadata: Metadata = { }; export default function CommentsPage() { - return
CommentsPage
; + return ( +
+ + + + + 功能正在開發 + + 目前尚未上線分享學習經驗的功能。在服務上線之前,歡迎提供您對這個功能的意見! + + + + +
+ ); }