Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion app/(app)/comments/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
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 = {
title: "經驗分享",
};

export default function CommentsPage() {
return <div>CommentsPage</div>;
return (
<section className="space-y-6">
<PageHeader title="經驗分享" description="分享您的學習心得和經驗。" />

<Alert>
<AlertCircle />
<AlertTitle>功能正在開發</AlertTitle>
<AlertDescription>
目前尚未上線分享學習經驗的功能。在服務上線之前,歡迎提供您對這個功能的意見!
</AlertDescription>
</Alert>

<Button variant="outline" id="comment-feedback-button">提供意見</Button>
</section>
);
}
66 changes: 66 additions & 0 deletions app/(app)/materials/_components/points/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import SectionHeader from "../section-header";

export default function PointCalculateRules() {
return (
<section className="space-y-8">
<SectionHeader title="計分標準" description="計分規則說明。" />

<div className="space-y-6">
<div
className={`
grid w-full grid-cols-1
md:grid-cols-2
lg:grid-cols-3
`}
>
<div className="prose text-foreground">
<h3>登入點數規則</h3>

<ul>
<li>
每天登入可以獲得 <strong>20 點</strong>。
</li>
<li>
美洲登入可以獲得 <strong>50 點</strong>。
</li>
</ul>
</div>

<div className="prose text-foreground">
<h3>作答點數規則</h3>

<ul>
<li>
首次嘗試題目可以獲得 <strong>30 點</strong>。
</li>
<li>
每日嘗試題目可以獲得 <strong>30 點</strong>。
</li>
<li>
正確答案可以獲得 <strong>60 點</strong>。
</li>
<li>
第一個通過題目的人可以獲得 <strong>80 點</strong>。
</li>
</ul>
</div>

<div className="prose text-foreground">
<h3>其他規則</h3>
<p>這部分是手動發放的。</p>

<ul>
<li>
回報問題和提供系統意見,根據重要性可以獲得 <strong>25 點</strong> 到 <strong>50 點</strong> 不等的點數。
</li>
<li>
到 <a href="https://github.com/database-playground" target="_blank">GitHub</a>{" "}
以 PR 形式貢獻程式碼並獲得接受,可以獲得 <strong>100 點</strong> 到超過 <strong>250 點</strong> 的點數。
</li>
</ul>
</div>
</div>
</div>
</section>
);
}
99 changes: 99 additions & 0 deletions app/(app)/materials/_components/schemas/card.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof MATERIAL_SCHEMA_CARD>;
}) {
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 (
<Card
className={`
flex flex-col
hover:border-primary/50 hover:shadow-lg
`}
>
<CardHeader className="space-y-3">
<div className="flex items-center justify-between">
<div className="w-fit rounded-lg bg-primary/10 p-2 text-primary">
<Database className="h-5 w-5" />
</div>
</div>
<div className="space-y-1.5">
<CardTitle className="text-xl">{slug}</CardTitle>

{description
? (
<CardDescription className="prose text-sm text-foreground">
<Remark>{description}</Remark>
</CardDescription>
)
: null}
</div>
</CardHeader>
<CardFooter className="pt-0">
<Button
onClick={handleDownload}
className="group/btn w-full"
variant="default"
>
<Download className="mr-2 h-4 w-4" />
下載 SQL 檔案
</Button>
</CardFooter>
</Card>
);
}
37 changes: 37 additions & 0 deletions app/(app)/materials/_components/schemas/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section className="space-y-6">
<SectionHeader
title="題庫資料庫列表"
description="這裡可以下載題庫使用到的資料庫,您可以匯入到 phpMyAdmin 等工具,使用您習慣的工具進行練習。"
/>
<div
className={`
grid grid-cols-1 gap-6
md:grid-cols-2
lg:grid-cols-3
`}
>
{data.databases.map((database) => <MaterialsSchemaCard key={database.id} fragment={database} />)}
</div>
</section>
);
}
8 changes: 8 additions & 0 deletions app/(app)/materials/_components/section-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function SectionHeader({ title, description }: { title: string; description: string }) {
return (
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">{title}</h2>
<p className="text-muted-foreground">{description}</p>
</div>
);
}
19 changes: 18 additions & 1 deletion app/(app)/materials/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>MaterialsPage</div>;
return (
<div className="space-y-12">
<PageHeader
title="補充資料"
description="詳列題庫使用的資料庫結構、計分標準等項目。"
/>
<Suspense fallback={<Skeleton className="h-96 w-full rounded-lg" />}>
<MaterialsSchema />
</Suspense>

<PointCalculateRules />
</div>
);
}
7 changes: 5 additions & 2 deletions app/global-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 (
Expand Down
16 changes: 8 additions & 8 deletions components/page-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ import { Skeleton } from "./ui/skeleton";

export default function PageHeader({ title, description }: { title: string; description: string }) {
return (
<div>
<h1 className="text-2xl font-bold">{title}</h1>
<p className="text-muted-foreground">{description}</p>
<div className="space-y-2">
<h1 className="text-3xl font-bold tracking-tight">{title}</h1>
<p className="text-base text-muted-foreground">{description}</p>
</div>
);
}

export function PageHeaderSkeleton({ title, description }: { title?: string; description?: string }) {
return (
<div>
{title ? <h1 className="text-2xl font-bold">{title}</h1> : (
<div className="space-y-2">
{title ? <h1 className="text-3xl font-bold tracking-tight">{title}</h1> : (
<Skeleton
className={`h-8 w-64`}
className={`h-9 w-64`}
/>
)}
{description ? <p className="text-muted-foreground">{description}</p> : (
{description ? <p className="text-base text-muted-foreground">{description}</p> : (
<Skeleton
className={`h-4 w-1/3`}
className={`h-5 w-1/2`}
/>
)}
</div>
Expand Down
18 changes: 18 additions & 0 deletions gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
*/
Expand Down
Loading