({
+ solvedStatus: ["solved", "unsolved", "not-tried"],
+ difficulty: [
+ QuestionDifficulty.Easy,
+ QuestionDifficulty.Medium,
+ QuestionDifficulty.Hard,
+ QuestionDifficulty.Unspecified,
+ ],
+ });
+
+ const deferredSearch = useDebouncedValue(search, 200);
+
+ const where: QuestionWhereInput = {
+ or: [
+ {
+ titleContainsFold: deferredSearch,
+ },
+ {
+ descriptionContainsFold: deferredSearch,
+ },
+ {
+ categoryContainsFold: deferredSearch,
+ },
+ ],
+ difficultyIn: tags.difficulty,
+ };
+
+ return (
+
+ );
+}
+
+export function ChallengeQuestionsList({
+ where,
+ solvedStatusContains,
+}: {
+ where: QuestionWhereInput;
+ solvedStatusContains: SolvedStatus[];
+}) {
+ const { data, fetchMore } = useSuspenseQuery(LIST_QUESTIONS, {
+ variables: { where },
+ });
+
+ return (
+
+ {data?.questions.edges
+ ?.filter(
+ (question) =>
+ question
+ && question.node
+ && solvedStatusContains.includes(
+ getQuestionSolvedStatus(question.node),
+ ),
+ )
+ .map((question) => {
+ if (!question || !question.node) return null;
+ return ;
+ })}
+
+ {data?.questions.pageInfo.hasNextPage && (
+
+ )}
+
+ );
+}
diff --git a/app/(app)/challenges/model.ts b/app/(app)/challenges/model.ts
new file mode 100644
index 0000000..925e14e
--- /dev/null
+++ b/app/(app)/challenges/model.ts
@@ -0,0 +1,24 @@
+import { QuestionDifficulty } from "@/gql/graphql";
+
+/**
+ * 解題狀態
+ */
+export type SolvedStatus = "solved" | "unsolved" | "not-tried";
+
+/**
+ * 難度
+ */
+export type Difficulty = QuestionDifficulty;
+
+export const solvedStatusTranslation: Record = {
+ solved: "✅ 已經解決",
+ unsolved: "尚未解決",
+ "not-tried": "還沒嘗試",
+};
+
+export const difficultyTranslation: Record = {
+ [QuestionDifficulty.Easy]: "簡單",
+ [QuestionDifficulty.Medium]: "中等",
+ [QuestionDifficulty.Hard]: "困難",
+ [QuestionDifficulty.Unspecified]: "未指定",
+};
diff --git a/app/(app)/challenges/page.tsx b/app/(app)/challenges/page.tsx
index 516b667..c416b3f 100644
--- a/app/(app)/challenges/page.tsx
+++ b/app/(app)/challenges/page.tsx
@@ -1,3 +1,21 @@
+import type { Metadata } from "next";
+import { Suspense } from "react";
+import Header from "./_header";
+import HeaderSkeleton from "./_header/skeleton";
+import ChallengePageContent from "./content";
+
+export const metadata: Metadata = {
+ title: "挑戰題目",
+};
+
export default function ChallengesPage() {
- return ChallengesPage
;
+ return (
+
+ }>
+
+
+
+
+
+ );
}
diff --git a/app/(app)/comments/page.tsx b/app/(app)/comments/page.tsx
index c1228bf..7f2cb14 100644
--- a/app/(app)/comments/page.tsx
+++ b/app/(app)/comments/page.tsx
@@ -1,3 +1,9 @@
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "經驗分享",
+};
+
export default function CommentsPage() {
return CommentsPage
;
}
diff --git a/app/(app)/materials/page.tsx b/app/(app)/materials/page.tsx
index f809f8f..8868b93 100644
--- a/app/(app)/materials/page.tsx
+++ b/app/(app)/materials/page.tsx
@@ -1,3 +1,9 @@
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "補充資料",
+};
+
export default function MaterialsPage() {
return MaterialsPage
;
}
diff --git a/app/(app)/statistics/page.tsx b/app/(app)/statistics/page.tsx
index d2d1a01..aa59291 100644
--- a/app/(app)/statistics/page.tsx
+++ b/app/(app)/statistics/page.tsx
@@ -1,3 +1,9 @@
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "統計資料",
+};
+
export default function StatisticsPage() {
return StatisticsPage
;
}
diff --git a/app/globals.css b/app/globals.css
index 42b49ee..8347438 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -37,7 +37,7 @@
--sidebar-accent-foreground: oklch(0.3729 0.0306 259.7328);
--sidebar-border: oklch(0.8717 0.0093 258.3382);
--sidebar-ring: oklch(0.5854 0.2041 277.1173);
- --font-sans: IBM Plex Sans, ui-sans-serif, sans-serif, system-ui;
+ --font-sans: IBM Plex Sans TC, ui-sans-serif, sans-serif, system-ui;
--font-serif: Merriweather, serif;
--font-mono: JetBrains Mono, monospace;
--radius: 0.5rem;
@@ -91,7 +91,7 @@
--sidebar-accent-foreground: oklch(0.8717 0.0093 258.3382);
--sidebar-border: oklch(0.4461 0.0263 256.8018);
--sidebar-ring: oklch(0.6801 0.1583 276.9349);
- --font-sans: IBM Plex Sans, ui-sans-serif, sans-serif, system-ui;
+ --font-sans: IBM Plex Sans TC, ui-sans-serif, sans-serif, system-ui;
--font-serif: Merriweather, serif;
--font-mono: JetBrains Mono, monospace;
--radius: 0.5rem;
diff --git a/app/layout.tsx b/app/layout.tsx
index a921e90..1216aa0 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -7,7 +7,7 @@ import { ProgressProvider } from "@/providers/use-progress-provider";
import { PreloadResources } from "./preload-resources";
export const metadata: Metadata = {
- title: "資料庫練功坊",
+ title: { template: "%s | 資料庫練功坊", default: "首頁" },
description: "AI 賦能的資料庫練習平台",
};
diff --git a/app/login/page.tsx b/app/login/page.tsx
index d05370a..944544f 100644
--- a/app/login/page.tsx
+++ b/app/login/page.tsx
@@ -1,5 +1,6 @@
import { Logo } from "@/components/logo";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import type { Metadata } from "next";
import Link from "next/link";
import { Suspense } from "react";
import DoYouKnow from "./do-you-know";
@@ -15,6 +16,10 @@ interface LoginPageProps {
}>;
}
+export const metadata: Metadata = {
+ title: "登入",
+};
+
export default async function LoginPage({ searchParams }: LoginPageProps) {
const params = await searchParams;
diff --git a/app/login/status.action.ts b/app/login/status.action.ts
index 87ba659..54a1f22 100644
--- a/app/login/status.action.ts
+++ b/app/login/status.action.ts
@@ -1,10 +1,12 @@
"use server";
+import buildUri from "@/lib/build-uri";
+
export async function getUpstreamLatency(): Promise {
try {
const start = Date.now();
- const response = await fetch("https://api.dbplay.app", { method: "HEAD" });
+ const response = await fetch(buildUri("/"));
if (!response.ok) {
return -1;
}
diff --git a/codegen.ts b/codegen.ts
index ac30bf3..f891559 100644
--- a/codegen.ts
+++ b/codegen.ts
@@ -7,10 +7,14 @@ const config: CodegenConfig = {
generates: {
"./gql/": {
preset: "client",
+ presetConfig: {
+ fragmentMasking: { unmaskFunctionName: "readFragment" },
+ },
config: {
useTypeImports: true,
scalars: {
Time: "string", // ISO8601
+ Cursor: "string",
},
},
},
diff --git a/components/ui/grid-progress.tsx b/components/ui/grid-progress.tsx
new file mode 100644
index 0000000..5864e25
--- /dev/null
+++ b/components/ui/grid-progress.tsx
@@ -0,0 +1,201 @@
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const gridProgressVariants = cva(
+ "inline-grid items-center justify-center",
+ {
+ variants: {
+ size: {
+ sm: "gap-0.5",
+ default: "gap-1",
+ lg: "gap-1.5",
+ },
+ },
+ defaultVariants: {
+ size: "default",
+ },
+ },
+);
+
+const gridItemVariants = cva(
+ "rounded-sm transition-all duration-200 ease-in-out",
+ {
+ variants: {
+ size: {
+ sm: "h-2 w-2",
+ default: "h-3 w-3",
+ lg: "h-4 w-4",
+ },
+ variant: {
+ default: "bg-muted",
+ primary: "bg-muted",
+ success: "bg-muted",
+ warning: "bg-muted",
+ destructive: "bg-muted",
+ },
+ filled: {
+ true: "",
+ false: "",
+ },
+ },
+ compoundVariants: [
+ // 主要填充狀態
+ {
+ variant: "primary",
+ filled: true,
+ className: "bg-primary shadow-sm",
+ },
+ // 成功填充狀態
+ {
+ variant: "success",
+ filled: true,
+ className: "bg-green-500 shadow-sm",
+ },
+ // 警告填充狀態
+ {
+ variant: "warning",
+ filled: true,
+ className: "bg-yellow-500 shadow-sm",
+ },
+ // 危險填充狀態
+ {
+ variant: "destructive",
+ filled: true,
+ className: "bg-red-500 shadow-sm",
+ },
+ // 預設填充狀態
+ {
+ variant: "default",
+ filled: true,
+ className: "bg-foreground shadow-sm",
+ },
+ ],
+ defaultVariants: {
+ size: "default",
+ variant: "default",
+ filled: false,
+ },
+ },
+);
+
+interface GridProgressProps
+ extends
+ Omit, "children">,
+ VariantProps,
+ Pick, "variant">
+{
+ /**
+ * 目前進度值,範圍 0-100
+ */
+ progress: number;
+ /**
+ * 網格列數
+ * @default 2
+ */
+ rows?: number;
+ /**
+ * 網格欄數
+ * @default 5
+ */
+ cols?: number;
+ /**
+ * 是否顯示進度文字
+ * @default false
+ */
+ showProgress?: boolean;
+ /**
+ * 進度文字的自訂格式化函式
+ */
+ progressFormatter?: (progress: number, filledSteps: number, totalSteps: number) => string;
+ /**
+ * 是否啟用懸停效果
+ * @default true
+ */
+ enableHover?: boolean;
+}
+
+const GridProgress = React.forwardRef(
+ ({
+ className,
+ size,
+ variant = "default",
+ progress,
+ rows = 2,
+ cols = 5,
+ showProgress = false,
+ progressFormatter,
+ enableHover = true,
+ ...props
+ }, ref) => {
+ const totalSteps = rows * cols;
+
+ // 確保 progress 在有效範圍內
+ const clampedProgress = Math.max(0, Math.min(100, progress));
+
+ // 計算需要填充的格子數
+ const filledSteps = Math.round((clampedProgress / 100) * totalSteps);
+
+ // 預設的進度文字格式化函式
+ const defaultFormatter = (progress: number, filled: number, total: number) =>
+ `${Math.round(progress)}% (${filled}/${total})`;
+
+ const progressText = progressFormatter
+ ? progressFormatter(clampedProgress, filledSteps, totalSteps)
+ : defaultFormatter(clampedProgress, filledSteps, totalSteps);
+
+ return (
+
+
+ {Array.from({ length: totalSteps }, (_, index) => {
+ const isFilled = index < filledSteps;
+ const row = Math.floor(index / cols) + 1;
+ const col = (index % cols) + 1;
+
+ return (
+
+ );
+ })}
+
+
+ {showProgress && (
+
+ {progressText}
+
+ )}
+
+ );
+ },
+);
+
+GridProgress.displayName = "GridProgress";
+
+export { gridItemVariants, GridProgress, type GridProgressProps, gridProgressVariants };
diff --git a/components/ui/toggle-group.tsx b/components/ui/toggle-group.tsx
new file mode 100644
index 0000000..ad16239
--- /dev/null
+++ b/components/ui/toggle-group.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
+import { type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { toggleVariants } from "@/components/ui/toggle";
+import { cn } from "@/lib/utils";
+
+const ToggleGroupContext = React.createContext<
+ VariantProps
+>({
+ size: "default",
+ variant: "default",
+});
+
+function ToggleGroup({
+ className,
+ variant,
+ size,
+ children,
+ ...props
+}:
+ & React.ComponentProps
+ & VariantProps)
+{
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+function ToggleGroupItem({
+ className,
+ children,
+ variant,
+ size,
+ ...props
+}:
+ & React.ComponentProps
+ & VariantProps)
+{
+ const context = React.useContext(ToggleGroupContext);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export { ToggleGroup, ToggleGroupItem };
diff --git a/components/ui/toggle.tsx b/components/ui/toggle.tsx
new file mode 100644
index 0000000..a02bd21
--- /dev/null
+++ b/components/ui/toggle.tsx
@@ -0,0 +1,63 @@
+"use client";
+
+import * as TogglePrimitive from "@radix-ui/react-toggle";
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const toggleVariants = cva(
+ `
+ inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium
+ whitespace-nowrap transition-[color,box-shadow] outline-none
+ hover:bg-muted hover:text-muted-foreground
+ focus-visible:border-ring focus-visible:ring-[3px]
+ focus-visible:ring-ring/50
+ disabled:pointer-events-none disabled:opacity-50
+ aria-invalid:border-destructive aria-invalid:ring-destructive/20
+ data-[state=on]:bg-accent data-[state=on]:text-accent-foreground
+ dark:aria-invalid:ring-destructive/40
+ [&_svg]:pointer-events-none [&_svg]:shrink-0
+ [&_svg:not([class*='size-'])]:size-4
+ `,
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ outline: `
+ border border-input bg-transparent shadow-xs
+ hover:bg-accent hover:text-accent-foreground
+ `,
+ },
+ size: {
+ default: "h-9 min-w-9 px-2",
+ sm: "h-8 min-w-8 px-1.5",
+ lg: "h-10 min-w-10 px-2.5",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+function Toggle({
+ className,
+ variant,
+ size,
+ ...props
+}:
+ & React.ComponentProps
+ & VariantProps)
+{
+ return (
+
+ );
+}
+
+export { Toggle, toggleVariants };
diff --git a/gql/fragment-masking.ts b/gql/fragment-masking.ts
index 743a364..6306be4 100644
--- a/gql/fragment-masking.ts
+++ b/gql/fragment-masking.ts
@@ -16,46 +16,46 @@ export type FragmentType>
: never;
// return non-nullable if `fragmentType` is non-nullable
-export function useFragment(
+export function readFragment(
_documentNode: DocumentTypeDecoration,
fragmentType: FragmentType>
): TType;
// return nullable if `fragmentType` is undefined
-export function useFragment(
+export function readFragment(
_documentNode: DocumentTypeDecoration,
fragmentType: FragmentType> | undefined
): TType | undefined;
// return nullable if `fragmentType` is nullable
-export function useFragment(
+export function readFragment(
_documentNode: DocumentTypeDecoration,
fragmentType: FragmentType> | null
): TType | null;
// return nullable if `fragmentType` is nullable or undefined
-export function useFragment(
+export function readFragment(
_documentNode: DocumentTypeDecoration,
fragmentType: FragmentType> | null | undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
-export function useFragment(
+export function readFragment(
_documentNode: DocumentTypeDecoration,
fragmentType: Array>>
): Array;
// return array of nullable if `fragmentType` is array of nullable
-export function useFragment(
+export function readFragment(
_documentNode: DocumentTypeDecoration,
fragmentType: Array>> | null | undefined
): Array | null | undefined;
// return readonly array of non-nullable if `fragmentType` is array of non-nullable
-export function useFragment(
+export function readFragment(
_documentNode: DocumentTypeDecoration,
fragmentType: ReadonlyArray>>
): ReadonlyArray;
// return readonly array of nullable if `fragmentType` is array of nullable
-export function useFragment(
+export function readFragment(
_documentNode: DocumentTypeDecoration,
fragmentType: ReadonlyArray>> | null | undefined
): ReadonlyArray | null | undefined;
-export function useFragment(
+export function readFragment(
_documentNode: DocumentTypeDecoration,
fragmentType: FragmentType> | Array>> | ReadonlyArray>> | null | undefined
): TType | Array | ReadonlyArray | null | undefined {
diff --git a/gql/gql.ts b/gql/gql.ts
index 73b7680..c9a6d35 100644
--- a/gql/gql.ts
+++ b/gql/gql.ts
@@ -14,9 +14,17 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
*/
type Documents = {
+ "\n query ChallengeStatisticsQuery {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n }\n }\n }\n": typeof types.ChallengeStatisticsQueryDocument,
+ "\n fragment QuestionCard on Question {\n id\n title\n description\n difficulty\n category\n\n ...QuestionSolvedStatus\n }\n": typeof types.QuestionCardFragmentDoc,
+ "\n fragment QuestionSolvedStatus on Question {\n solved\n attempted\n }\n": typeof types.QuestionSolvedStatusFragmentDoc,
+ "\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 query BasicUserInfo {\n me {\n id\n name\n email\n avatar\n\n group {\n name\n }\n }\n }\n": typeof types.BasicUserInfoDocument,
};
const documents: Documents = {
+ "\n query ChallengeStatisticsQuery {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n }\n }\n }\n": types.ChallengeStatisticsQueryDocument,
+ "\n fragment QuestionCard on Question {\n id\n title\n description\n difficulty\n category\n\n ...QuestionSolvedStatus\n }\n": types.QuestionCardFragmentDoc,
+ "\n fragment QuestionSolvedStatus on Question {\n solved\n attempted\n }\n": types.QuestionSolvedStatusFragmentDoc,
+ "\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 query BasicUserInfo {\n me {\n id\n name\n email\n avatar\n\n group {\n name\n }\n }\n }\n": types.BasicUserInfoDocument,
};
@@ -34,6 +42,22 @@ const documents: Documents = {
*/
export function graphql(source: string): unknown;
+/**
+ * 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 ChallengeStatisticsQuery {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\n }\n }\n }\n"): (typeof documents)["\n query ChallengeStatisticsQuery {\n me {\n submissionStatistics {\n totalQuestions\n solvedQuestions\n attemptedQuestions\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 QuestionCard on Question {\n id\n title\n description\n difficulty\n category\n\n ...QuestionSolvedStatus\n }\n"): (typeof documents)["\n fragment QuestionCard on Question {\n id\n title\n description\n difficulty\n category\n\n ...QuestionSolvedStatus\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 QuestionSolvedStatus on Question {\n solved\n attempted\n }\n"): (typeof documents)["\n fragment QuestionSolvedStatus on Question {\n solved\n attempted\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 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.
*/
diff --git a/gql/graphql.ts b/gql/graphql.ts
index 470d72c..dcfd794 100644
--- a/gql/graphql.ts
+++ b/gql/graphql.ts
@@ -1,7 +1,7 @@
/* eslint-disable */
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type Maybe = T | null;
-export type InputMaybe = Maybe;
+export type InputMaybe = T | null | undefined;
export type Exact = { [K in keyof T]: T[K] };
export type MakeOptional = Omit & { [SubKey in K]?: Maybe };
export type MakeMaybe = Omit & { [SubKey in K]: Maybe };
@@ -18,7 +18,9 @@ export type Scalars = {
* Define a Relay Cursor type:
* https://relay.dev/graphql/connections.htm#sec-Cursor
*/
- Cursor: { input: any; output: any; }
+ Cursor: { input: string; output: string; }
+ /** The builtin Map type */
+ Map: { input: any; output: any; }
/** The builtin Time type */
Time: { input: string; output: string; }
};
@@ -61,6 +63,7 @@ export type CreateQuestionInput = {
difficulty?: InputMaybe;
/** Reference answer */
referenceAnswer: Scalars['String']['input'];
+ submissionIDs?: InputMaybe>;
/** Question title */
title: Scalars['String']['input'];
};
@@ -83,8 +86,11 @@ export type CreateScopeSetInput = {
export type CreateUserInput = {
avatar?: InputMaybe;
email: Scalars['String']['input'];
+ eventIDs?: InputMaybe>;
groupID: Scalars['ID']['input'];
name: Scalars['String']['input'];
+ pointIDs?: InputMaybe>;
+ submissionIDs?: InputMaybe>;
};
export type Database = Node & {
@@ -179,6 +185,99 @@ export type DatabaseWhereInput = {
slugNotIn?: InputMaybe>;
};
+export type Event = Node & {
+ __typename?: 'Event';
+ id: Scalars['ID']['output'];
+ payload?: Maybe;
+ triggeredAt: Scalars['Time']['output'];
+ type: Scalars['String']['output'];
+ user: User;
+ userID: Scalars['ID']['output'];
+};
+
+/** A connection to a list of items. */
+export type EventConnection = {
+ __typename?: 'EventConnection';
+ /** A list of edges. */
+ edges?: Maybe>>;
+ /** Information to aid in pagination. */
+ pageInfo: PageInfo;
+ /** Identifies the total count of items in the connection. */
+ totalCount: Scalars['Int']['output'];
+};
+
+/** An edge in a connection. */
+export type EventEdge = {
+ __typename?: 'EventEdge';
+ /** A cursor for use in pagination. */
+ cursor: Scalars['Cursor']['output'];
+ /** The item at the end of the edge. */
+ node?: Maybe;
+};
+
+/** Ordering options for Event connections */
+export type EventOrder = {
+ /** The ordering direction. */
+ direction?: OrderDirection;
+ /** The field by which to order Events. */
+ field: EventOrderField;
+};
+
+/** Properties by which Event connections can be ordered. */
+export enum EventOrderField {
+ TriggeredAt = 'TRIGGERED_AT'
+}
+
+/**
+ * EventWhereInput is used for filtering Event objects.
+ * Input was generated by ent.
+ */
+export type EventWhereInput = {
+ and?: InputMaybe>;
+ /** user edge predicates */
+ hasUser?: InputMaybe;
+ hasUserWith?: InputMaybe>;
+ /** id field predicates */
+ id?: InputMaybe;
+ idGT?: InputMaybe;
+ idGTE?: InputMaybe;
+ idIn?: InputMaybe>;
+ idLT?: InputMaybe;
+ idLTE?: InputMaybe;
+ idNEQ?: InputMaybe;
+ idNotIn?: InputMaybe>;
+ not?: InputMaybe;
+ or?: InputMaybe>;
+ /** triggered_at field predicates */
+ triggeredAt?: InputMaybe;
+ triggeredAtGT?: InputMaybe;
+ triggeredAtGTE?: InputMaybe;
+ triggeredAtIn?: InputMaybe>;
+ triggeredAtLT?: InputMaybe;
+ triggeredAtLTE?: InputMaybe;
+ triggeredAtNEQ?: InputMaybe;
+ triggeredAtNotIn?: InputMaybe>;
+ /** type field predicates */
+ type?: InputMaybe;
+ typeContains?: InputMaybe;
+ typeContainsFold?: InputMaybe;
+ typeEqualFold?: InputMaybe;
+ typeGT?: InputMaybe;
+ typeGTE?: InputMaybe;
+ typeHasPrefix?: InputMaybe;
+ typeHasSuffix?: InputMaybe;
+ typeIn?: InputMaybe>;
+ typeLT?: InputMaybe;
+ typeLTE?: InputMaybe;
+ typeNEQ?: InputMaybe;
+ typeNotIn?: InputMaybe>;
+ /** user_id field predicates */
+ userID?: InputMaybe;
+ userIDIn?: InputMaybe>;
+ userIDNEQ?: InputMaybe;
+ userIDNotIn?: InputMaybe>;
+};
+
export type Group = Node & {
__typename?: 'Group';
createdAt: Scalars['Time']['output'];
@@ -302,6 +401,8 @@ export type Mutation = {
logoutAll: Scalars['Boolean']['output'];
/** Logout a user from all his devices. */
logoutUser: Scalars['Boolean']['output'];
+ /** Submit your answer to a question. */
+ submitAnswer: SubmissionResult;
/** Update a database. */
updateDatabase: Database;
/** Update a group. */
@@ -374,6 +475,12 @@ export type MutationLogoutUserArgs = {
};
+export type MutationSubmitAnswerArgs = {
+ answer: Scalars['String']['input'];
+ id: Scalars['ID']['input'];
+};
+
+
export type MutationUpdateDatabaseArgs = {
id: Scalars['ID']['input'];
input: UpdateDatabaseInput;
@@ -441,11 +548,117 @@ export type PageInfo = {
startCursor?: Maybe;
};
+export type Point = Node & {
+ __typename?: 'Point';
+ description?: Maybe;
+ grantedAt: Scalars['Time']['output'];
+ id: Scalars['ID']['output'];
+ points: Scalars['Int']['output'];
+ user: User;
+};
+
+/** A connection to a list of items. */
+export type PointConnection = {
+ __typename?: 'PointConnection';
+ /** A list of edges. */
+ edges?: Maybe>>;
+ /** Information to aid in pagination. */
+ pageInfo: PageInfo;
+ /** Identifies the total count of items in the connection. */
+ totalCount: Scalars['Int']['output'];
+};
+
+/** An edge in a connection. */
+export type PointEdge = {
+ __typename?: 'PointEdge';
+ /** A cursor for use in pagination. */
+ cursor: Scalars['Cursor']['output'];
+ /** The item at the end of the edge. */
+ node?: Maybe;
+};
+
+/** Ordering options for Point connections */
+export type PointOrder = {
+ /** The ordering direction. */
+ direction?: OrderDirection;
+ /** The field by which to order Points. */
+ field: PointOrderField;
+};
+
+/** Properties by which Point connections can be ordered. */
+export enum PointOrderField {
+ GrantedAt = 'GRANTED_AT'
+}
+
+/**
+ * PointWhereInput is used for filtering Point objects.
+ * Input was generated by ent.
+ */
+export type PointWhereInput = {
+ and?: InputMaybe>;
+ /** description field predicates */
+ description?: InputMaybe;
+ descriptionContains?: InputMaybe;
+ descriptionContainsFold?: InputMaybe;
+ descriptionEqualFold?: InputMaybe;
+ descriptionGT?: InputMaybe;
+ descriptionGTE?: InputMaybe;
+ descriptionHasPrefix?: InputMaybe;
+ descriptionHasSuffix?: InputMaybe;
+ descriptionIn?: InputMaybe>;
+ descriptionIsNil?: InputMaybe;
+ descriptionLT?: InputMaybe;
+ descriptionLTE?: InputMaybe;
+ descriptionNEQ?: InputMaybe;
+ descriptionNotIn?: InputMaybe>;
+ descriptionNotNil?: InputMaybe;
+ /** granted_at field predicates */
+ grantedAt?: InputMaybe;
+ grantedAtGT?: InputMaybe;
+ grantedAtGTE?: InputMaybe;
+ grantedAtIn?: InputMaybe>;
+ grantedAtLT?: InputMaybe;
+ grantedAtLTE?: InputMaybe;
+ grantedAtNEQ?: InputMaybe;
+ grantedAtNotIn?: InputMaybe>;
+ /** user edge predicates */
+ hasUser?: InputMaybe;
+ hasUserWith?: InputMaybe>;
+ /** id field predicates */
+ id?: InputMaybe;
+ idGT?: InputMaybe;
+ idGTE?: InputMaybe;
+ idIn?: InputMaybe>;
+ idLT?: InputMaybe;
+ idLTE?: InputMaybe;
+ idNEQ?: InputMaybe;
+ idNotIn?: InputMaybe>;
+ not?: InputMaybe;
+ or?: InputMaybe>;
+ /** points field predicates */
+ points?: InputMaybe;
+ pointsGT?: InputMaybe;
+ pointsGTE?: InputMaybe;
+ pointsIn?: InputMaybe>;
+ pointsLT?: InputMaybe;
+ pointsLTE?: InputMaybe;
+ pointsNEQ?: InputMaybe;
+ pointsNotIn?: InputMaybe>;
+};
+
export type Query = {
__typename?: 'Query';
/** Get a database by ID. */
database: Database;
databases: Array;
+ /**
+ * Get an event by ID.
+ *
+ * If you have the "event:read" scope, you can get any event by ID;
+ * otherwise, you can only get your own events.
+ */
+ event: Event;
+ events: EventConnection;
/** Get a group by ID. */
group: Group;
groups: Array;
@@ -454,12 +667,28 @@ export type Query = {
node?: Maybe;
/** Lookup nodes by a list of IDs. */
nodes: Array>;
+ /**
+ * Get a point grant by ID.
+ *
+ * If you have the "point:read" scope, you can get any point grant by ID;
+ * otherwise, you can only get your own point grants.
+ */
+ pointGrant: Point;
+ points: PointConnection;
/** Get a question by ID. */
question: Question;
questions: QuestionConnection;
/** Get a scope set by ID or slug. */
scopeSet: ScopeSet;
scopeSets: Array;
+ /**
+ * Get a submission by ID.
+ *
+ * If you have the "submission:read" scope, you can get any submission by ID;
+ * otherwise, you can only get your own submissions.
+ */
+ submission: Submission;
+ submissions: SubmissionConnection;
/** Get a user by ID. */
user: User;
users: UserConnection;
@@ -471,6 +700,21 @@ export type QueryDatabaseArgs = {
};
+export type QueryEventArgs = {
+ id: Scalars['ID']['input'];
+};
+
+
+export type QueryEventsArgs = {
+ after?: InputMaybe;
+ before?: InputMaybe;
+ first?: InputMaybe;
+ last?: InputMaybe;
+ orderBy?: InputMaybe;
+ where?: InputMaybe;
+};
+
+
export type QueryGroupArgs = {
id: Scalars['ID']['input'];
};
@@ -486,6 +730,21 @@ export type QueryNodesArgs = {
};
+export type QueryPointGrantArgs = {
+ id: Scalars['ID']['input'];
+};
+
+
+export type QueryPointsArgs = {
+ after?: InputMaybe;
+ before?: InputMaybe;
+ first?: InputMaybe;
+ last?: InputMaybe;
+ orderBy?: InputMaybe;
+ where?: InputMaybe;
+};
+
+
export type QueryQuestionArgs = {
id: Scalars['ID']['input'];
};
@@ -506,6 +765,21 @@ export type QueryScopeSetArgs = {
};
+export type QuerySubmissionArgs = {
+ id: Scalars['ID']['input'];
+};
+
+
+export type QuerySubmissionsArgs = {
+ after?: InputMaybe;
+ before?: InputMaybe;
+ first?: InputMaybe;
+ last?: InputMaybe;
+ orderBy?: InputMaybe;
+ where?: InputMaybe;
+};
+
+
export type QueryUserArgs = {
id: Scalars['ID']['input'];
};
@@ -522,6 +796,8 @@ export type QueryUsersArgs = {
export type Question = Node & {
__typename?: 'Question';
+ /** Have you tried to solve the question? */
+ attempted: Scalars['Boolean']['output'];
/** Question category, e.g. 'query' */
category: Scalars['String']['output'];
database: Database;
@@ -532,9 +808,24 @@ export type Question = Node & {
id: Scalars['ID']['output'];
/** Reference answer */
referenceAnswer: Scalars['String']['output'];
- referenceAnswerResult: SqlResponse;
+ referenceAnswerResult: SqlExecutionResult;
+ /** Have you solved the question? */
+ solved: Scalars['Boolean']['output'];
+ submissions: SubmissionConnection;
/** Question title */
title: Scalars['String']['output'];
+ /** List of your submissions for this question. */
+ userSubmissions: Array;
+};
+
+
+export type QuestionSubmissionsArgs = {
+ after?: InputMaybe;
+ before?: InputMaybe;
+ first?: InputMaybe;
+ last?: InputMaybe;
+ orderBy?: InputMaybe;
+ where?: InputMaybe;
};
/** A connection to a list of items. */
@@ -621,6 +912,9 @@ export type QuestionWhereInput = {
/** database edge predicates */
hasDatabase?: InputMaybe;
hasDatabaseWith?: InputMaybe>;
+ /** submissions edge predicates */
+ hasSubmissions?: InputMaybe;
+ hasSubmissionsWith?: InputMaybe>;
/** id field predicates */
id?: InputMaybe;
idGT?: InputMaybe;
@@ -662,6 +956,12 @@ export type QuestionWhereInput = {
titleNotIn?: InputMaybe>;
};
+export type SqlExecutionResult = {
+ __typename?: 'SQLExecutionResult';
+ columns: Array;
+ rows: Array>;
+};
+
export type ScopeSet = Node & {
__typename?: 'ScopeSet';
description?: Maybe;
@@ -735,10 +1035,145 @@ export type ScopeSetWhereInput = {
slugNotIn?: InputMaybe>;
};
-export type SqlResponse = {
- __typename?: 'SqlResponse';
- columns: Array;
- rows: Array>;
+export type SolvedQuestionByDifficulty = {
+ __typename?: 'SolvedQuestionByDifficulty';
+ difficulty: QuestionDifficulty;
+ solvedQuestions: Scalars['Int']['output'];
+};
+
+export type Submission = Node & {
+ __typename?: 'Submission';
+ error?: Maybe;
+ id: Scalars['ID']['output'];
+ queryResult?: Maybe;
+ question: Question;
+ status: SubmissionStatus;
+ submittedAt: Scalars['Time']['output'];
+ submittedCode: Scalars['String']['output'];
+ user: User;
+};
+
+/** A connection to a list of items. */
+export type SubmissionConnection = {
+ __typename?: 'SubmissionConnection';
+ /** A list of edges. */
+ edges?: Maybe>>;
+ /** Information to aid in pagination. */
+ pageInfo: PageInfo;
+ /** Identifies the total count of items in the connection. */
+ totalCount: Scalars['Int']['output'];
+};
+
+/** An edge in a connection. */
+export type SubmissionEdge = {
+ __typename?: 'SubmissionEdge';
+ /** A cursor for use in pagination. */
+ cursor: Scalars['Cursor']['output'];
+ /** The item at the end of the edge. */
+ node?: Maybe;
+};
+
+/** Ordering options for Submission connections */
+export type SubmissionOrder = {
+ /** The ordering direction. */
+ direction?: OrderDirection;
+ /** The field by which to order Submissions. */
+ field: SubmissionOrderField;
+};
+
+/** Properties by which Submission connections can be ordered. */
+export enum SubmissionOrderField {
+ SubmittedAt = 'SUBMITTED_AT'
+}
+
+export type SubmissionResult = {
+ __typename?: 'SubmissionResult';
+ error?: Maybe;
+ result?: Maybe;
+};
+
+export type SubmissionStatistics = {
+ __typename?: 'SubmissionStatistics';
+ attemptedQuestions: Scalars['Int']['output'];
+ solvedQuestionByDifficulty: Array;
+ solvedQuestions: Scalars['Int']['output'];
+ totalQuestions: Scalars['Int']['output'];
+};
+
+/** SubmissionStatus is enum for the field status */
+export enum SubmissionStatus {
+ Failed = 'failed',
+ Pending = 'pending',
+ Success = 'success'
+}
+
+/**
+ * SubmissionWhereInput is used for filtering Submission objects.
+ * Input was generated by ent.
+ */
+export type SubmissionWhereInput = {
+ and?: InputMaybe>;
+ /** error field predicates */
+ error?: InputMaybe;
+ errorContains?: InputMaybe;
+ errorContainsFold?: InputMaybe;
+ errorEqualFold?: InputMaybe;
+ errorGT?: InputMaybe;
+ errorGTE?: InputMaybe;
+ errorHasPrefix?: InputMaybe;
+ errorHasSuffix?: InputMaybe;
+ errorIn?: InputMaybe>;
+ errorIsNil?: InputMaybe;
+ errorLT?: InputMaybe;
+ errorLTE?: InputMaybe;
+ errorNEQ?: InputMaybe;
+ errorNotIn?: InputMaybe>;
+ errorNotNil?: InputMaybe;
+ /** question edge predicates */
+ hasQuestion?: InputMaybe;
+ hasQuestionWith?: InputMaybe>;
+ /** user edge predicates */
+ hasUser?: InputMaybe;
+ hasUserWith?: InputMaybe>;
+ /** id field predicates */
+ id?: InputMaybe;
+ idGT?: InputMaybe;
+ idGTE?: InputMaybe;
+ idIn?: InputMaybe>;
+ idLT?: InputMaybe;
+ idLTE?: InputMaybe;
+ idNEQ?: InputMaybe;
+ idNotIn?: InputMaybe>;
+ not?: InputMaybe;
+ or?: InputMaybe>;
+ /** status field predicates */
+ status?: InputMaybe;
+ statusIn?: InputMaybe