Skip to content

Commit 919ea89

Browse files
authored
Merge pull request #16 from database-playground/pan93412/dbp-37-materials-page
DBP-37: Materials Page
2 parents 68e1b5a + 6feb867 commit 919ea89

File tree

10 files changed

+298
-12
lines changed

10 files changed

+298
-12
lines changed

app/(app)/comments/page.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
1+
import PageHeader from "@/components/page-header";
2+
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
3+
import { Button } from "@/components/ui/button";
4+
import { AlertCircle } from "lucide-react";
15
import type { Metadata } from "next";
26

37
export const metadata: Metadata = {
48
title: "經驗分享",
59
};
610

711
export default function CommentsPage() {
8-
return <div>CommentsPage</div>;
12+
return (
13+
<section className="space-y-6">
14+
<PageHeader title="經驗分享" description="分享您的學習心得和經驗。" />
15+
16+
<Alert>
17+
<AlertCircle />
18+
<AlertTitle>功能正在開發</AlertTitle>
19+
<AlertDescription>
20+
目前尚未上線分享學習經驗的功能。在服務上線之前,歡迎提供您對這個功能的意見!
21+
</AlertDescription>
22+
</Alert>
23+
24+
<Button variant="outline" id="comment-feedback-button">提供意見</Button>
25+
</section>
26+
);
927
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import SectionHeader from "../section-header";
2+
3+
export default function PointCalculateRules() {
4+
return (
5+
<section className="space-y-8">
6+
<SectionHeader title="計分標準" description="計分規則說明。" />
7+
8+
<div className="space-y-6">
9+
<div
10+
className={`
11+
grid w-full grid-cols-1
12+
md:grid-cols-2
13+
lg:grid-cols-3
14+
`}
15+
>
16+
<div className="prose text-foreground">
17+
<h3>登入點數規則</h3>
18+
19+
<ul>
20+
<li>
21+
每天登入可以獲得 <strong>20 點</strong>
22+
</li>
23+
<li>
24+
美洲登入可以獲得 <strong>50 點</strong>
25+
</li>
26+
</ul>
27+
</div>
28+
29+
<div className="prose text-foreground">
30+
<h3>作答點數規則</h3>
31+
32+
<ul>
33+
<li>
34+
首次嘗試題目可以獲得 <strong>30 點</strong>
35+
</li>
36+
<li>
37+
每日嘗試題目可以獲得 <strong>30 點</strong>
38+
</li>
39+
<li>
40+
正確答案可以獲得 <strong>60 點</strong>
41+
</li>
42+
<li>
43+
第一個通過題目的人可以獲得 <strong>80 點</strong>
44+
</li>
45+
</ul>
46+
</div>
47+
48+
<div className="prose text-foreground">
49+
<h3>其他規則</h3>
50+
<p>這部分是手動發放的。</p>
51+
52+
<ul>
53+
<li>
54+
回報問題和提供系統意見,根據重要性可以獲得 <strong>25 點</strong><strong>50 點</strong> 不等的點數。
55+
</li>
56+
<li>
57+
<a href="https://github.com/database-playground" target="_blank">GitHub</a>{" "}
58+
以 PR 形式貢獻程式碼並獲得接受,可以獲得 <strong>100 點</strong> 到超過 <strong>250 點</strong> 的點數。
59+
</li>
60+
</ul>
61+
</div>
62+
</div>
63+
</div>
64+
</section>
65+
);
66+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { Button } from "@/components/ui/button";
2+
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
3+
import { type FragmentType, graphql, readFragment } from "@/gql";
4+
import { useLazyQuery } from "@apollo/client/react";
5+
import { Database, Download } from "lucide-react";
6+
import { Remark } from "react-remark";
7+
import { toast } from "sonner";
8+
9+
const MATERIAL_SCHEMA_CARD = graphql(`
10+
fragment MaterialsSchemaCard on Database {
11+
id
12+
slug
13+
description
14+
}
15+
`);
16+
17+
const MATERIAL_SCHEMA_CONTENT_QUERY = graphql(`
18+
query MaterialsSchemaContent($id: ID!) {
19+
database(id: $id) {
20+
id
21+
schema
22+
}
23+
}
24+
`);
25+
26+
export function MaterialsSchemaCard({
27+
fragment,
28+
}: {
29+
fragment: FragmentType<typeof MATERIAL_SCHEMA_CARD>;
30+
}) {
31+
const { id, slug, description } = readFragment(
32+
MATERIAL_SCHEMA_CARD,
33+
fragment,
34+
);
35+
const [getSchemaContent] = useLazyQuery(MATERIAL_SCHEMA_CONTENT_QUERY);
36+
37+
const handleDownload = async () => {
38+
const loading = toast.loading("正在下載 SQL 檔案……");
39+
40+
try {
41+
const { data } = await getSchemaContent({ variables: { id } });
42+
if (!data) return;
43+
44+
const blob = new Blob([data.database.schema], { type: "text/plain" });
45+
const url = URL.createObjectURL(blob);
46+
const a = document.createElement("a");
47+
a.href = url;
48+
a.download = `schema-${slug}.sql`;
49+
a.click();
50+
URL.revokeObjectURL(url);
51+
52+
toast.success("成功下載 SQL 檔案", { description: `schema-${slug}.sql` });
53+
} catch (error) {
54+
toast.error("無法下載 SQL 檔案", {
55+
description: error instanceof Error ? error.message : undefined,
56+
});
57+
} finally {
58+
toast.dismiss(loading);
59+
}
60+
};
61+
62+
return (
63+
<Card
64+
className={`
65+
flex flex-col
66+
hover:border-primary/50 hover:shadow-lg
67+
`}
68+
>
69+
<CardHeader className="space-y-3">
70+
<div className="flex items-center justify-between">
71+
<div className="w-fit rounded-lg bg-primary/10 p-2 text-primary">
72+
<Database className="h-5 w-5" />
73+
</div>
74+
</div>
75+
<div className="space-y-1.5">
76+
<CardTitle className="text-xl">{slug}</CardTitle>
77+
78+
{description
79+
? (
80+
<CardDescription className="prose text-sm text-foreground">
81+
<Remark>{description}</Remark>
82+
</CardDescription>
83+
)
84+
: null}
85+
</div>
86+
</CardHeader>
87+
<CardFooter className="pt-0">
88+
<Button
89+
onClick={handleDownload}
90+
className="group/btn w-full"
91+
variant="default"
92+
>
93+
<Download className="mr-2 h-4 w-4" />
94+
下載 SQL 檔案
95+
</Button>
96+
</CardFooter>
97+
</Card>
98+
);
99+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client";
2+
3+
import { graphql } from "@/gql";
4+
import { useSuspenseQuery } from "@apollo/client/react";
5+
import SectionHeader from "../section-header";
6+
import { MaterialsSchemaCard } from "./card";
7+
8+
const MATERIALS_SCHEMA = graphql(`
9+
query MaterialsSchema {
10+
databases {
11+
id
12+
...MaterialsSchemaCard
13+
}
14+
}
15+
`);
16+
17+
export default function MaterialsSchema() {
18+
const { data } = useSuspenseQuery(MATERIALS_SCHEMA);
19+
20+
return (
21+
<section className="space-y-6">
22+
<SectionHeader
23+
title="題庫資料庫列表"
24+
description="這裡可以下載題庫使用到的資料庫,您可以匯入到 phpMyAdmin 等工具,使用您習慣的工具進行練習。"
25+
/>
26+
<div
27+
className={`
28+
grid grid-cols-1 gap-6
29+
md:grid-cols-2
30+
lg:grid-cols-3
31+
`}
32+
>
33+
{data.databases.map((database) => <MaterialsSchemaCard key={database.id} fragment={database} />)}
34+
</div>
35+
</section>
36+
);
37+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function SectionHeader({ title, description }: { title: string; description: string }) {
2+
return (
3+
<div className="space-y-2">
4+
<h2 className="text-2xl font-semibold tracking-tight">{title}</h2>
5+
<p className="text-muted-foreground">{description}</p>
6+
</div>
7+
);
8+
}

app/(app)/materials/page.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
1+
import PageHeader from "@/components/page-header";
2+
import { Skeleton } from "@/components/ui/skeleton";
13
import type { Metadata } from "next";
4+
import { Suspense } from "react";
5+
import PointCalculateRules from "./_components/points";
6+
import MaterialsSchema from "./_components/schemas";
27

38
export const metadata: Metadata = {
49
title: "補充資料",
510
};
611

712
export default function MaterialsPage() {
8-
return <div>MaterialsPage</div>;
13+
return (
14+
<div className="space-y-12">
15+
<PageHeader
16+
title="補充資料"
17+
description="詳列題庫使用的資料庫結構、計分標準等項目。"
18+
/>
19+
<Suspense fallback={<Skeleton className="h-96 w-full rounded-lg" />}>
20+
<MaterialsSchema />
21+
</Suspense>
22+
23+
<PointCalculateRules />
24+
</div>
25+
);
926
}

app/global-error.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ERROR_NOT_FOUND, ERROR_NOT_IMPLEMENTED, ERROR_UNAUTHORIZED, ERROR_USER_
88
import { CombinedGraphQLErrors, CombinedProtocolErrors } from "@apollo/client";
99
import { AlertCircle, Code, Home, Lock, RefreshCw, Search, Shield, WifiOff } from "lucide-react";
1010
import Link from "next/link";
11+
import posthog from "posthog-js";
1112
import { useEffect } from "react";
1213

1314
interface GlobalErrorProps {
@@ -111,8 +112,10 @@ export default function GlobalError({ error, reset }: GlobalErrorProps) {
111112
const errorInfo = getErrorInfo(error);
112113

113114
useEffect(() => {
114-
// Log error to monitoring service
115-
console.error("Global error:", error);
115+
posthog.captureException(error, {
116+
url: window.location.href,
117+
digest: error.digest,
118+
});
116119
}, [error]);
117120

118121
return (

components/page-header.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,24 @@ import { Skeleton } from "./ui/skeleton";
22

33
export default function PageHeader({ title, description }: { title: string; description: string }) {
44
return (
5-
<div>
6-
<h1 className="text-2xl font-bold">{title}</h1>
7-
<p className="text-muted-foreground">{description}</p>
5+
<div className="space-y-2">
6+
<h1 className="text-3xl font-bold tracking-tight">{title}</h1>
7+
<p className="text-base text-muted-foreground">{description}</p>
88
</div>
99
);
1010
}
1111

1212
export function PageHeaderSkeleton({ title, description }: { title?: string; description?: string }) {
1313
return (
14-
<div>
15-
{title ? <h1 className="text-2xl font-bold">{title}</h1> : (
14+
<div className="space-y-2">
15+
{title ? <h1 className="text-3xl font-bold tracking-tight">{title}</h1> : (
1616
<Skeleton
17-
className={`h-8 w-64`}
17+
className={`h-9 w-64`}
1818
/>
1919
)}
20-
{description ? <p className="text-muted-foreground">{description}</p> : (
20+
{description ? <p className="text-base text-muted-foreground">{description}</p> : (
2121
<Skeleton
22-
className={`h-4 w-1/3`}
22+
className={`h-5 w-1/2`}
2323
/>
2424
)}
2525
</div>

gql/gql.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ type Documents = {
2626
"\n fragment DatabaseStructure on Database {\n id\n structure {\n tables {\n columns\n name\n }\n }\n }\n": typeof types.DatabaseStructureFragmentDoc,
2727
"\n query ChallengeStatistics {\n me {\n id\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n }\n }\n }\n": typeof types.ChallengeStatisticsDocument,
2828
"\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,
29+
"\n fragment MaterialsSchemaCard on Database {\n id\n slug\n description\n }\n": typeof types.MaterialsSchemaCardFragmentDoc,
30+
"\n query MaterialsSchemaContent($id: ID!) {\n database(id: $id) {\n id\n schema\n }\n }\n": typeof types.MaterialsSchemaContentDocument,
31+
"\n query MaterialsSchema {\n databases {\n id\n ...MaterialsSchemaCard\n }\n }\n": typeof types.MaterialsSchemaDocument,
2932
"\n query CompletedQuestions {\n me {\n id\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": typeof types.CompletedQuestionsDocument,
3033
"\n query MySolvedQuestionsCount {\n me {\n id\n name\n submissionStatistics {\n solvedQuestions\n }\n }\n }\n": typeof types.MySolvedQuestionsCountDocument,
3134
"\n query MyPoints {\n me {\n id\n name\n totalPoints\n }\n }\n": typeof types.MyPointsDocument,
@@ -53,6 +56,9 @@ const documents: Documents = {
5356
"\n fragment DatabaseStructure on Database {\n id\n structure {\n tables {\n columns\n name\n }\n }\n }\n": types.DatabaseStructureFragmentDoc,
5457
"\n query ChallengeStatistics {\n me {\n id\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n }\n }\n }\n": types.ChallengeStatisticsDocument,
5558
"\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,
59+
"\n fragment MaterialsSchemaCard on Database {\n id\n slug\n description\n }\n": types.MaterialsSchemaCardFragmentDoc,
60+
"\n query MaterialsSchemaContent($id: ID!) {\n database(id: $id) {\n id\n schema\n }\n }\n": types.MaterialsSchemaContentDocument,
61+
"\n query MaterialsSchema {\n databases {\n id\n ...MaterialsSchemaCard\n }\n }\n": types.MaterialsSchemaDocument,
5662
"\n query CompletedQuestions {\n me {\n id\n submissionStatistics {\n totalQuestions\n solvedQuestions\n }\n }\n }\n": types.CompletedQuestionsDocument,
5763
"\n query MySolvedQuestionsCount {\n me {\n id\n name\n submissionStatistics {\n solvedQuestions\n }\n }\n }\n": types.MySolvedQuestionsCountDocument,
5864
"\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
130136
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
131137
*/
132138
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"];
139+
/**
140+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
141+
*/
142+
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"];
143+
/**
144+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
145+
*/
146+
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"];
147+
/**
148+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
149+
*/
150+
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"];
133151
/**
134152
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
135153
*/

0 commit comments

Comments
 (0)