@burdenoff/microfe-technospam 2026.1101.5 → 2026.1102.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-types.d.ts +7468 -141
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/node_modules/@codemirror/autocomplete/dist/index.js +527 -0
- package/dist/node_modules/@codemirror/autocomplete/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/commands/dist/index.js +1108 -0
- package/dist/node_modules/@codemirror/commands/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/lang-go/dist/index.js +206 -0
- package/dist/node_modules/@codemirror/lang-go/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/lang-javascript/dist/index.js +321 -0
- package/dist/node_modules/@codemirror/lang-javascript/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/lang-python/dist/index.js +265 -0
- package/dist/node_modules/@codemirror/lang-python/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/language/dist/index.js +957 -0
- package/dist/node_modules/@codemirror/language/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/legacy-modes/mode/ruby.js +164 -0
- package/dist/node_modules/@codemirror/legacy-modes/mode/ruby.js.map +1 -0
- package/dist/node_modules/@codemirror/legacy-modes/mode/shell.js +109 -0
- package/dist/node_modules/@codemirror/legacy-modes/mode/shell.js.map +1 -0
- package/dist/node_modules/@codemirror/state/dist/index.js +1739 -0
- package/dist/node_modules/@codemirror/state/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/streamparser/dist/index.js +70 -0
- package/dist/node_modules/@codemirror/streamparser/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/view/dist/index.js +5427 -0
- package/dist/node_modules/@codemirror/view/dist/index.js.map +1 -0
- package/dist/node_modules/@lezer/common/dist/index.js +896 -0
- package/dist/node_modules/@lezer/common/dist/index.js.map +1 -0
- package/dist/node_modules/@lezer/go/dist/index.js +215 -0
- package/dist/node_modules/@lezer/go/dist/index.js.map +1 -0
- package/dist/node_modules/@lezer/highlight/dist/index.js +395 -0
- package/dist/node_modules/@lezer/highlight/dist/index.js.map +1 -0
- package/dist/node_modules/@lezer/javascript/dist/index.js +437 -0
- package/dist/node_modules/@lezer/javascript/dist/index.js.map +1 -0
- package/dist/node_modules/@lezer/lr/dist/index.js +842 -0
- package/dist/node_modules/@lezer/lr/dist/index.js.map +1 -0
- package/dist/node_modules/@lezer/python/dist/index.js +352 -0
- package/dist/node_modules/@lezer/python/dist/index.js.map +1 -0
- package/dist/node_modules/@marijn/find-cluster-break/src/index.js +66 -0
- package/dist/node_modules/@marijn/find-cluster-break/src/index.js.map +1 -0
- package/dist/node_modules/style-mod/src/style-mod.js +74 -0
- package/dist/node_modules/style-mod/src/style-mod.js.map +1 -0
- package/dist/node_modules/w3c-keyname/index.js +90 -0
- package/dist/node_modules/w3c-keyname/index.js.map +1 -0
- package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
- package/dist/stories/journeys/Problems.stories.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.d.ts +39 -0
- package/dist/technospam/api/technospamApi.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.js +120 -86
- package/dist/technospam/api/technospamApi.js.map +1 -1
- package/dist/technospam/components/CertificateVerifyLink.d.ts +11 -0
- package/dist/technospam/components/CertificateVerifyLink.d.ts.map +1 -0
- package/dist/technospam/components/CertificateVerifyLink.js +63 -0
- package/dist/technospam/components/CertificateVerifyLink.js.map +1 -0
- package/dist/technospam/components/SubmissionViews.d.ts +6 -2
- package/dist/technospam/components/SubmissionViews.d.ts.map +1 -1
- package/dist/technospam/components/SubmissionViews.js +6 -5
- package/dist/technospam/components/SubmissionViews.js.map +1 -1
- package/dist/technospam/components/TechnoSpamErrorBoundary.d.ts +6 -0
- package/dist/technospam/components/TechnoSpamErrorBoundary.d.ts.map +1 -1
- package/dist/technospam/components/TechnoSpamErrorBoundary.js +2 -2
- package/dist/technospam/components/TechnoSpamErrorBoundary.js.map +1 -1
- package/dist/technospam/components/editor/CodeEditor.d.ts +26 -0
- package/dist/technospam/components/editor/CodeEditor.d.ts.map +1 -0
- package/dist/technospam/components/editor/CodeEditor.js +102 -0
- package/dist/technospam/components/editor/CodeEditor.js.map +1 -0
- package/dist/technospam/components/editor/RunOutputPanel.d.ts +14 -0
- package/dist/technospam/components/editor/RunOutputPanel.d.ts.map +1 -0
- package/dist/technospam/components/editor/RunOutputPanel.js +127 -0
- package/dist/technospam/components/editor/RunOutputPanel.js.map +1 -0
- package/dist/technospam/components/editor/editorLanguages.d.ts +11 -0
- package/dist/technospam/components/editor/editorLanguages.d.ts.map +1 -0
- package/dist/technospam/components/editor/editorLanguages.js +30 -0
- package/dist/technospam/components/editor/editorLanguages.js.map +1 -0
- package/dist/technospam/components/editor/editorTheme.d.ts +11 -0
- package/dist/technospam/components/editor/editorTheme.d.ts.map +1 -0
- package/dist/technospam/components/editor/editorTheme.js +120 -0
- package/dist/technospam/components/editor/editorTheme.js.map +1 -0
- package/dist/technospam/components/index.d.ts +1 -0
- package/dist/technospam/components/index.d.ts.map +1 -1
- package/dist/technospam/components/index.js +1 -0
- package/dist/technospam/hooks/useSolutionRunner.d.ts +35 -0
- package/dist/technospam/hooks/useSolutionRunner.d.ts.map +1 -0
- package/dist/technospam/hooks/useSolutionRunner.js +66 -0
- package/dist/technospam/hooks/useSolutionRunner.js.map +1 -0
- package/dist/technospam/hooks/useTechnoSpamAnalytics.d.ts +1 -1
- package/dist/technospam/hooks/useTechnoSpamAnalytics.d.ts.map +1 -1
- package/dist/technospam/hooks/useTechnoSpamAnalytics.js.map +1 -1
- package/dist/technospam/index.d.ts +2 -0
- package/dist/technospam/index.d.ts.map +1 -1
- package/dist/technospam/index.js +2 -0
- package/dist/technospam/lib/certificateVerify.d.ts +71 -0
- package/dist/technospam/lib/certificateVerify.d.ts.map +1 -0
- package/dist/technospam/lib/certificateVerify.js +55 -0
- package/dist/technospam/lib/certificateVerify.js.map +1 -0
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js +416 -263
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +236 -231
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.js +104 -96
- package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
- package/dist/technospam/pages/public/CertificateVerifyPage.d.ts +4 -0
- package/dist/technospam/pages/public/CertificateVerifyPage.d.ts.map +1 -0
- package/dist/technospam/pages/public/CertificateVerifyPage.js +257 -0
- package/dist/technospam/pages/public/CertificateVerifyPage.js.map +1 -0
- package/package.json +14 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"technospamApi.js","names":[],"sources":["../../../src/technospam/api/technospamApi.ts"],"sourcesContent":["/**\n * TechnoSpam data layer — Apollo operations + typed hooks over the\n * wspace-technospam-svc subgraph (Tec* fields), reached through the host\n * shell's default workspace ApolloProvider (defaultGateway: \"workspace\").\n *\n * Every page consumes real backend data through these hooks. There is NO mock\n * data. List queries are cursor-paginated: page 1 omits `cursor` entirely\n * (passing cursor: null makes the Prisma cursor resolver 500), \"load more\"\n * passes the previous page's `pageInfo.cursor`.\n */\nimport { useEffect, useMemo, useState } from 'react';\nimport { CombinedGraphQLErrors, gql, type ApolloClient, type ErrorLike } from '@apollo/client';\nimport { useApolloClient, useLazyQuery, useMutation, useQuery } from '@apollo/client/react';\n\nimport { notify } from '../lib/toast';\nimport { userMessageFromError } from '../lib/errorMessage';\nimport {\n type BuiMyCertificateRow,\n type BuiMyEnrollmentRow,\n forTechnospamCourse,\n mapRows,\n toTecCertificate,\n toTecEnrollment,\n} from './canonicalLearning';\n\n/* ------------------------------------------------------------------ enums */\n\nexport type TecCourseLevel = 'BEGINNER' | 'INTERMEDIATE' | 'ADVANCED' | 'EXPERT';\nexport type TecCourseStatus = 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';\nexport type TecLessonKind = 'VIDEO' | 'ARTICLE' | 'INTERACTIVE' | 'QUIZ' | 'PROJECT';\nexport type TecEnrollmentStatus = 'ENROLLED' | 'IN_PROGRESS' | 'COMPLETED' | 'DROPPED';\n/** EXPIRED only arrives from the canonical Buildmyiq certificate (BOFF-10481). */\nexport type TecCertificateStatus = 'ISSUED' | 'REVOKED' | 'EXPIRED';\nexport type TecProblemDifficulty = 'EASY' | 'MEDIUM' | 'HARD';\nexport type TecProblemStatus = 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';\nexport type TecInterviewFrequency = 'LOW' | 'MEDIUM' | 'HIGH';\nexport type TecSubmissionVerdict =\n | 'PENDING'\n | 'ACCEPTED'\n | 'WRONG_ANSWER'\n | 'TIME_LIMIT_EXCEEDED'\n | 'RUNTIME_ERROR'\n | 'COMPILE_ERROR';\nexport type TecMentorStatus = 'PENDING' | 'ACTIVE' | 'PAUSED' | 'INACTIVE';\nexport type TecSessionStatus = 'REQUESTED' | 'CONFIRMED' | 'COMPLETED' | 'CANCELLED' | 'NO_SHOW';\nexport type TecHackathonStatus =\n 'DRAFT' | 'REGISTRATION_OPEN' | 'IN_PROGRESS' | 'JUDGING' | 'COMPLETED' | 'CANCELLED';\nexport type TecTeamSubmissionStatus = 'DRAFT' | 'SUBMITTED' | 'DISQUALIFIED' | 'WINNER';\n\n/* ------------------------------------------------------------------ types */\n\nexport interface TecCourse {\n id: string;\n slug: string;\n title: string;\n summary?: string | null;\n topic?: string | null;\n level: TecCourseLevel;\n status: TecCourseStatus;\n tags: string[];\n estimatedMinutes: number;\n instructorId?: string | null;\n /** BOFF-4416: true when this course requires a paid TechnoSpam plan. */\n isPremiumOnly: boolean;\n /**\n * BOFF-4416: computed for the calling workspace — true when this course\n * is `isPremiumOnly` and the workspace has no paid plan. When true, every\n * non-preview lesson's `content` is already null in the API response.\n */\n isLocked: boolean;\n /** BOFF-10481: the canonical Buildmyiq course id; null until the course is mirrored. */\n buiCourseId?: string | null;\n publishedAt?: string | null;\n createdAt: string;\n updatedAt: string;\n lessons?: TecLesson[];\n enrollments?: TecEnrollment[];\n}\n\nexport interface TecLesson {\n id: string;\n courseId: string;\n title: string;\n kind: TecLessonKind;\n content?: string | null;\n position: number;\n estimatedMinutes: number;\n isPreview: boolean;\n /**\n * BOFF-4416: true when this lesson's `content` is gated behind a paid\n * TechnoSpam plan (never true for `isPreview` lessons). `content` is\n * already null in the API response whenever this is true.\n */\n isLocked: boolean;\n /** BOFF-4107 — INTERACTIVE lessons: language slug the workspace runs (e.g. \"python\"). */\n language?: string | null;\n /** INTERACTIVE lessons: code pre-filled in the editor (null when locked). */\n starterCode?: string | null;\n /** INTERACTIVE lessons: default stdin for a run (null when locked). */\n sampleInput?: string | null;\n /** INTERACTIVE lessons: output a correct program prints; a matching run completes the lesson. */\n expectedOutput?: string | null;\n /** QUIZ lessons: questions without the answer key (empty when locked). */\n quizQuestions?: TecQuizQuestion[];\n /** True when the signed-in learner has completed this lesson. */\n completedByMe?: boolean;\n /** Federated wspace-files-svc File (VIDEO lessons); null when locked. */\n file?: {\n id: string;\n name?: string | null;\n mimeType?: string | null;\n downloadUrl?: string | null;\n } | null;\n course?: TecCourse | null;\n createdAt: string;\n updatedAt: string;\n}\n\n/** BOFF-4107: a quiz question as a learner sees it (no answer key). */\nexport interface TecQuizQuestion {\n prompt: string;\n options: string[];\n}\n\n/** BOFF-4107: a quiz question with its answer key (lesson-author view). */\nexport interface TecQuizQuestionKey extends TecQuizQuestion {\n correctOption: number;\n explanation?: string | null;\n}\n\nexport type TecLessonRunStatus = 'COMPLETED' | 'WARMING' | 'UNAVAILABLE';\n\nexport interface TecLessonCodeRun {\n lessonId: string;\n status: TecLessonRunStatus;\n language: string;\n stdout: string;\n stderr: string;\n exitCode: number;\n durationMs: number;\n timedOut: boolean;\n memoryLimitExceeded: boolean;\n compileFailed: boolean;\n matchedExpected?: boolean | null;\n lessonCompleted: boolean;\n enrollment?: TecEnrollment | null;\n}\n\nexport interface TecQuizResult {\n lessonId: string;\n score: number;\n correctCount: number;\n totalCount: number;\n passed: boolean;\n passingScore: number;\n results: {\n index: number;\n correct: boolean;\n selectedOption?: number | null;\n correctOption: number;\n explanation?: string | null;\n }[];\n enrollment?: TecEnrollment | null;\n}\n\nexport interface TecEnrollment {\n id: string;\n courseId: string;\n learnerId: string;\n status: TecEnrollmentStatus;\n progressPct: number;\n lessonsCompleted: number;\n startedAt: string;\n lastActiveAt?: string | null;\n completedAt?: string | null;\n createdAt: string;\n updatedAt: string;\n course?: TecCourse | null;\n /** BOFF-4107: first lesson not yet completed; null when every lesson is done. */\n nextLessonId?: string | null;\n /** BOFF-10481: the canonical Buildmyiq enrollment this view was read from. */\n buiEnrollmentId?: string | null;\n}\n\nexport interface TecCertificate {\n id: string;\n courseId: string;\n learnerId: string;\n serial: string;\n score: number;\n status: TecCertificateStatus;\n issuedAt: string;\n revokedAt?: string | null;\n revokeReason?: string | null;\n createdAt: string;\n course?: TecCourse | null;\n /** BOFF-10481: the canonical Buildmyiq certificate this view was read from. */\n buiCertificateId?: string | null;\n}\n\nexport interface TecProblem {\n id: string;\n slug: string;\n title: string;\n statement?: string | null;\n topic?: string | null;\n difficulty: TecProblemDifficulty;\n status: TecProblemStatus;\n tags: string[];\n languages: string[];\n companies: string[];\n roles: string[];\n interviewFrequency?: TecInterviewFrequency | null;\n points: number;\n starterCode?: string | null;\n /** BOFF-4416: true when this problem requires a paid TechnoSpam plan. */\n isPremiumOnly: boolean;\n /**\n * BOFF-4416: computed for the calling workspace — true when this problem\n * is `isPremiumOnly` and the workspace has no paid plan. `statement` /\n * `starterCode` are already null and `publicTestCases` already empty in\n * the API response whenever this is true.\n */\n isLocked: boolean;\n publishedAt?: string | null;\n createdAt: string;\n updatedAt: string;\n submissions?: TecSubmission[];\n /** Worked-example fixtures the grader runs against; PUBLIC cases only. */\n publicTestCases?: TecProblemTestCase[];\n}\n\n/** Visibility of a problem test case. Mirrors TecTestCaseVisibility. */\nexport type TecTestCaseVisibility = 'PUBLIC' | 'HIDDEN';\n\n/** Per-test-case outcome of a graded submission. Mirrors TecTestCaseStatus. */\nexport type TecTestCaseStatus =\n 'PASSED' | 'WRONG_ANSWER' | 'RUNTIME_ERROR' | 'TIME_LIMIT_EXCEEDED' | 'COMPILE_ERROR' | 'SKIPPED';\n\n/**\n * A worked-example fixture shown on the problem page. Only PUBLIC cases are\n * ever returned to a learner, so `input`/`expectedOutput` are always present\n * in practice; they stay optional because the schema allows null for the\n * author-facing redacted shape.\n */\nexport interface TecProblemTestCase {\n id: string;\n problemId: string;\n name: string;\n ordering: number;\n visibility: TecTestCaseVisibility;\n timeLimitMs: number;\n input?: string | null;\n expectedOutput?: string | null;\n}\n\n/** Grading detail for one test case of a submission. */\nexport interface TecTestCaseResult {\n testCaseId: string;\n name: string;\n visibility: TecTestCaseVisibility;\n status: TecTestCaseStatus;\n passed: boolean;\n durationMs: number;\n input?: string | null;\n expectedOutput?: string | null;\n actualOutput?: string | null;\n}\n\nexport interface TecSubmission {\n id: string;\n problemId: string;\n learnerId: string;\n language: string;\n code: string;\n verdict: TecSubmissionVerdict;\n runtimeMs: number;\n pointsAwarded: number;\n passedCount: number;\n totalCount: number;\n stderr?: string | null;\n testResults: TecTestCaseResult[];\n judgedAt?: string | null;\n createdAt: string;\n updatedAt: string;\n problem?: TecProblem | null;\n}\n\nexport interface TecMentor {\n id: string;\n userId: string;\n headline: string;\n role?: string | null;\n bio?: string | null;\n expertise: string[];\n hourlyRate: number;\n currency: string;\n status: TecMentorStatus;\n isAcceptingMentees: boolean;\n ratingAvg: number;\n sessionsCount: number;\n createdAt: string;\n updatedAt: string;\n sessions?: TecMentorSession[];\n}\n\nexport interface TecMentorSession {\n id: string;\n mentorId: string;\n menteeId: string;\n topic: string;\n agenda?: string | null;\n scheduledAt: string;\n durationMinutes: number;\n status: TecSessionStatus;\n meetingUrl?: string | null;\n /** Snapshotted from the mentor's rate at booking time (smallest currency unit). */\n costAmount: number;\n currency: string;\n platformFeeAmount: number;\n rating?: number | null;\n feedback?: string | null;\n completedAt?: string | null;\n cancelledAt?: string | null;\n createdAt: string;\n updatedAt: string;\n mentor?: TecMentor | null;\n}\n\n/** Read-only pricing preview for a not-yet-booked session (see tecCalculateSessionCost). */\nexport interface TecSessionCostEstimate {\n mentorId: string;\n durationMinutes: number;\n hourlyRate: number;\n currency: string;\n costAmount: number;\n platformFeeAmount: number;\n platformFeeBps: number;\n netAmount: number;\n}\n\n/** Calculation/ledger-only record of what a mentor is owed for a completed session (see tecMyPayouts). */\nexport type TecPayoutStatus = 'PENDING' | 'PAID';\n\nexport interface TecMentorPayout {\n id: string;\n mentorId: string;\n sessionId: string;\n grossAmount: number;\n platformFeeAmount: number;\n netAmount: number;\n currency: string;\n status: TecPayoutStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface TecHackathon {\n id: string;\n slug: string;\n title: string;\n theme?: string | null;\n description?: string | null;\n status: TecHackathonStatus;\n startAt: string;\n endAt: string;\n submissionDeadline?: string | null;\n prizePool: number;\n currency: string;\n maxTeamSize: number;\n tags: string[];\n createdAt: string;\n updatedAt: string;\n teams?: TecHackathonTeam[];\n}\n\nexport interface TecHackathonTeam {\n id: string;\n hackathonId: string;\n name: string;\n captainId: string;\n memberIds: string[];\n projectName?: string | null;\n projectUrl?: string | null;\n repoUrl?: string | null;\n pitch?: string | null;\n status: TecTeamSubmissionStatus;\n score: number;\n submittedAt?: string | null;\n createdAt: string;\n updatedAt: string;\n hackathon?: TecHackathon | null;\n}\n\nexport interface TecPageInfo {\n cursor?: string | null;\n hasMore: boolean;\n}\n\nexport interface PaginationInput {\n limit?: number;\n cursor?: string | null;\n}\n\n/** Build a pagination arg that NEVER sends cursor:null (page-1 500 guard). */\nexport function pageArg(limit = 50, cursor?: string | null): { limit: number; cursor?: string } {\n return cursor ? { limit, cursor } : { limit };\n}\n\n/* -------------------------------------------------------------- fragments */\n\nconst COURSE_FIELDS = gql`\n fragment CourseFields on TecCourse {\n id\n slug\n title\n summary\n topic\n level\n status\n tags\n estimatedMinutes\n instructorId\n isPremiumOnly\n isLocked\n buiCourseId\n publishedAt\n createdAt\n updatedAt\n }\n`;\n\nconst LESSON_FIELDS = gql`\n fragment LessonFields on TecLesson {\n id\n courseId\n title\n kind\n content\n position\n estimatedMinutes\n isPreview\n isLocked\n language\n starterCode\n sampleInput\n expectedOutput\n quizQuestions {\n prompt\n options\n }\n completedByMe\n createdAt\n updatedAt\n }\n`;\n\nconst ENROLLMENT_FIELDS = gql`\n fragment EnrollmentFields on TecEnrollment {\n id\n courseId\n learnerId\n status\n progressPct\n lessonsCompleted\n startedAt\n lastActiveAt\n completedAt\n createdAt\n updatedAt\n }\n`;\n\nconst PROBLEM_FIELDS = gql`\n fragment ProblemFields on TecProblem {\n id\n slug\n title\n statement\n topic\n difficulty\n status\n tags\n languages\n companies\n roles\n interviewFrequency\n points\n starterCode\n isPremiumOnly\n isLocked\n publishedAt\n createdAt\n updatedAt\n }\n`;\n\nconst SUBMISSION_FIELDS = gql`\n fragment SubmissionFields on TecSubmission {\n id\n problemId\n learnerId\n language\n code\n verdict\n runtimeMs\n pointsAwarded\n passedCount\n totalCount\n stderr\n testResults {\n testCaseId\n name\n visibility\n status\n passed\n durationMs\n input\n expectedOutput\n actualOutput\n }\n judgedAt\n createdAt\n updatedAt\n }\n`;\n\nconst PROBLEM_TEST_CASE_FIELDS = gql`\n fragment ProblemTestCaseFields on TecProblemTestCase {\n id\n problemId\n name\n ordering\n visibility\n timeLimitMs\n input\n expectedOutput\n }\n`;\n\nconst MENTOR_FIELDS = gql`\n fragment MentorFields on TecMentor {\n id\n userId\n headline\n role\n bio\n expertise\n hourlyRate\n currency\n status\n isAcceptingMentees\n ratingAvg\n sessionsCount\n createdAt\n updatedAt\n }\n`;\n\nconst SESSION_FIELDS = gql`\n fragment SessionFields on TecMentorSession {\n id\n mentorId\n menteeId\n topic\n agenda\n scheduledAt\n durationMinutes\n status\n meetingUrl\n costAmount\n currency\n platformFeeAmount\n rating\n feedback\n completedAt\n cancelledAt\n createdAt\n updatedAt\n }\n`;\n\nconst HACKATHON_FIELDS = gql`\n fragment HackathonFields on TecHackathon {\n id\n slug\n title\n theme\n description\n status\n startAt\n endAt\n submissionDeadline\n prizePool\n currency\n maxTeamSize\n tags\n createdAt\n updatedAt\n }\n`;\n\nconst TEAM_FIELDS = gql`\n fragment TeamFields on TecHackathonTeam {\n id\n hackathonId\n name\n captainId\n memberIds\n projectName\n projectUrl\n repoUrl\n pitch\n status\n score\n submittedAt\n createdAt\n updatedAt\n }\n`;\n\nconst CERTIFICATE_FIELDS = gql`\n fragment CertificateFields on TecCertificate {\n id\n courseId\n learnerId\n serial\n score\n status\n issuedAt\n revokedAt\n revokeReason\n createdAt\n }\n`;\n\n/* --------------------------------------------------------------- queries */\n\nconst LIST_COURSES = gql`\n ${COURSE_FIELDS}\n query TecListCourses(\n $level: TecCourseLevel\n $status: TecCourseStatus\n $pagination: PaginationInput\n ) {\n tecListCourses(level: $level, status: $status, pagination: $pagination) {\n items {\n ...CourseFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst GET_COURSE = gql`\n ${COURSE_FIELDS}\n ${LESSON_FIELDS}\n ${ENROLLMENT_FIELDS}\n query TecCourse($id: ID!) {\n tecCourse(id: $id) {\n ...CourseFields\n lessons {\n ...LessonFields\n }\n enrollments {\n ...EnrollmentFields\n }\n }\n }\n`;\n\nconst LIST_ENROLLMENTS = gql`\n ${ENROLLMENT_FIELDS}\n ${COURSE_FIELDS}\n query TecListEnrollments(\n $learnerId: ID\n $courseId: ID\n $pagination: PaginationInput\n $startDate: DateTime\n $endDate: DateTime\n ) {\n tecListEnrollments(\n learnerId: $learnerId\n courseId: $courseId\n pagination: $pagination\n startDate: $startDate\n endDate: $endDate\n ) {\n items {\n ...EnrollmentFields\n course {\n ...CourseFields\n }\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst LIST_CERTIFICATES = gql`\n ${CERTIFICATE_FIELDS}\n ${COURSE_FIELDS}\n query TecListCertificates($learnerId: ID, $courseId: ID) {\n tecListCertificates(learnerId: $learnerId, courseId: $courseId) {\n ...CertificateFields\n course {\n ...CourseFields\n }\n }\n }\n`;\n\n/**\n * The calling learner's OWN learning records, read from the canonical\n * Buildmyiq self-scoped fields (BOFF-10481, OVL-24 phase 2c). They are\n * `@rbac(scopeType: \"self\")`: the resolver forces learnerId = ctx.actor.id, so\n * no learnerId is ever sent, and `source: TECHNOSPAM` keeps the workspace's\n * native Buildmyiq records out. The Technospam view rides on the overlay\n * fields; canonicalLearning.ts folds each row into the Tec* view model.\n *\n * `courseId` takes the CANONICAL course id (TecCourse.buiCourseId).\n */\nconst MY_ENROLLMENTS = gql`\n ${COURSE_FIELDS}\n query BuiMyTechnospamEnrollments($courseId: ID, $pagination: PaginationInput) {\n buiMyEnrollments(source: TECHNOSPAM, courseId: $courseId, pagination: $pagination) {\n items {\n id\n status\n progressPct\n enrolledAt\n startedAt\n lastActiveAt\n completedAt\n createdAt\n updatedAt\n technospamNextLessonId\n technospamEnrollment {\n id\n courseId\n learnerId\n lessonsCompleted\n course {\n ...CourseFields\n }\n }\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst MY_CERTIFICATES = gql`\n ${COURSE_FIELDS}\n query BuiMyTechnospamCertificates($courseId: ID, $pagination: PaginationInput) {\n buiMyCertificates(source: TECHNOSPAM, courseId: $courseId, pagination: $pagination) {\n items {\n id\n serial\n status\n issuedAt\n revokedAt\n finalScore\n createdAt\n technospamRevokeReason\n technospamCertificate {\n id\n courseId\n learnerId\n score\n course {\n ...CourseFields\n }\n }\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst LIST_PROBLEMS = gql`\n ${PROBLEM_FIELDS}\n query TecListProblems(\n $difficulty: TecProblemDifficulty\n $status: TecProblemStatus\n $topic: String\n $company: String\n $role: String\n $interviewFrequency: TecInterviewFrequency\n $pagination: PaginationInput\n ) {\n tecListProblems(\n difficulty: $difficulty\n status: $status\n topic: $topic\n company: $company\n role: $role\n interviewFrequency: $interviewFrequency\n pagination: $pagination\n ) {\n items {\n ...ProblemFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst GET_PROBLEM = gql`\n ${PROBLEM_FIELDS}\n ${SUBMISSION_FIELDS}\n ${PROBLEM_TEST_CASE_FIELDS}\n query TecProblem($id: ID!) {\n tecProblem(id: $id) {\n ...ProblemFields\n publicTestCases {\n ...ProblemTestCaseFields\n }\n submissions {\n ...SubmissionFields\n }\n }\n }\n`;\n\nconst REGRADE_SUBMISSION = gql`\n ${SUBMISSION_FIELDS}\n mutation TecRegradeSubmission($id: ID!) {\n tecRegradeSubmission(id: $id) {\n ...SubmissionFields\n }\n }\n`;\n\nconst LIST_SUBMISSIONS = gql`\n ${SUBMISSION_FIELDS}\n ${PROBLEM_FIELDS}\n query TecListSubmissions(\n $learnerId: ID\n $problemId: ID\n $pagination: PaginationInput\n $startDate: DateTime\n $endDate: DateTime\n ) {\n tecListSubmissions(\n learnerId: $learnerId\n problemId: $problemId\n pagination: $pagination\n startDate: $startDate\n endDate: $endDate\n ) {\n items {\n ...SubmissionFields\n problem {\n ...ProblemFields\n }\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst LIST_MENTORS = gql`\n ${MENTOR_FIELDS}\n query TecListMentors($status: TecMentorStatus, $pagination: PaginationInput) {\n tecListMentors(status: $status, pagination: $pagination) {\n items {\n ...MentorFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst GET_MENTOR = gql`\n ${MENTOR_FIELDS}\n ${SESSION_FIELDS}\n query TecMentor($id: ID!) {\n tecMentor(id: $id) {\n ...MentorFields\n sessions {\n ...SessionFields\n }\n }\n }\n`;\n\nconst LIST_SESSIONS = gql`\n ${SESSION_FIELDS}\n ${MENTOR_FIELDS}\n query TecListSessions(\n $mentorId: ID\n $menteeId: ID\n $status: TecSessionStatus\n $pagination: PaginationInput\n ) {\n tecListSessions(\n mentorId: $mentorId\n menteeId: $menteeId\n status: $status\n pagination: $pagination\n ) {\n items {\n ...SessionFields\n mentor {\n ...MentorFields\n }\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst CALCULATE_SESSION_COST = gql`\n query TecCalculateSessionCost($mentorId: ID!, $durationMinutes: Int) {\n tecCalculateSessionCost(mentorId: $mentorId, durationMinutes: $durationMinutes) {\n mentorId\n durationMinutes\n hourlyRate\n currency\n costAmount\n platformFeeAmount\n platformFeeBps\n netAmount\n }\n }\n`;\n\nconst PAYOUT_FIELDS = gql`\n fragment PayoutFields on TecMentorPayout {\n id\n mentorId\n sessionId\n grossAmount\n platformFeeAmount\n netAmount\n currency\n status\n createdAt\n updatedAt\n }\n`;\n\nconst MY_PAYOUTS = gql`\n ${PAYOUT_FIELDS}\n query TecMyPayouts($status: TecPayoutStatus, $pagination: PaginationInput) {\n tecMyPayouts(status: $status, pagination: $pagination) {\n items {\n ...PayoutFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst LIST_HACKATHONS = gql`\n ${HACKATHON_FIELDS}\n query TecListHackathons($status: TecHackathonStatus, $pagination: PaginationInput) {\n tecListHackathons(status: $status, pagination: $pagination) {\n items {\n ...HackathonFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst GET_HACKATHON = gql`\n ${HACKATHON_FIELDS}\n ${TEAM_FIELDS}\n query TecHackathon($id: ID!) {\n tecHackathon(id: $id) {\n ...HackathonFields\n teams {\n ...TeamFields\n }\n }\n }\n`;\n\nconst LIST_TEAMS = gql`\n ${TEAM_FIELDS}\n query TecListTeams($hackathonId: ID!, $pagination: PaginationInput) {\n tecListTeams(hackathonId: $hackathonId, pagination: $pagination) {\n items {\n ...TeamFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst LIST_LESSONS = gql`\n ${LESSON_FIELDS}\n query TecListLessons($courseId: ID!, $pagination: PaginationInput) {\n tecListLessons(courseId: $courseId, pagination: $pagination) {\n items {\n ...LessonFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\n/* ------------------------------------------------------- response typings */\n\ntype Conn<K extends string, T> = Record<K, { items: T[]; pageInfo: TecPageInfo }>;\n\n/* ---------------------------------------------------------- query hooks */\n\nexport const useCoursesQuery = (vars: {\n level?: TecCourseLevel;\n status?: TecCourseStatus;\n pagination?: { limit: number; cursor?: string };\n}) =>\n useQuery<Conn<'tecListCourses', TecCourse>>(LIST_COURSES, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useCourseQuery = (id: string) =>\n useQuery<{ tecCourse: TecCourse | null }>(GET_COURSE, {\n variables: { id },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useEnrollmentsQuery = (vars: {\n learnerId?: string;\n courseId?: string;\n startDate?: string;\n endDate?: string;\n}) =>\n useQuery<Conn<'tecListEnrollments', TecEnrollment>>(LIST_ENROLLMENTS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\n/**\n * Cross-learner certificate read (instructor/admin). Requires the `list-all`\n * action — learner surfaces want `useMyCertificatesQuery` instead.\n */\nexport const useCertificatesQuery = (vars: { learnerId?: string; courseId?: string }) =>\n useQuery<{ tecListCertificates: TecCertificate[] }>(LIST_CERTIFICATES, {\n variables: vars,\n fetchPolicy: 'cache-and-network',\n });\n\n/**\n * Which of the learner's own records to read. `buiCourseId` narrows on the\n * server (canonical id); `technospamCourseId` narrows on the client, so a\n * course page gets the right rows even before its canonical id is known.\n */\nexport interface MyLearningScope {\n buiCourseId?: string | null;\n technospamCourseId?: string;\n limit?: number;\n skip?: boolean;\n}\n\n/** The signed-in learner's own TechnoSpam enrollments (canonical, self-scoped). */\nexport function useMyEnrollmentsQuery(scope: MyLearningScope = {}) {\n const result = useQuery<{\n buiMyEnrollments: { items: BuiMyEnrollmentRow[]; pageInfo: TecPageInfo };\n }>(MY_ENROLLMENTS, {\n variables: {\n ...(scope.buiCourseId ? { courseId: scope.buiCourseId } : {}),\n pagination: pageArg(scope.limit ?? 100),\n },\n skip: scope.skip,\n fetchPolicy: 'cache-and-network',\n });\n const items = result.data?.buiMyEnrollments?.items;\n const enrollments = useMemo(\n () => forTechnospamCourse(mapRows(items, toTecEnrollment), scope.technospamCourseId),\n [items, scope.technospamCourseId]\n );\n return { ...result, enrollments };\n}\n\n/** The signed-in learner's own TechnoSpam certificates (canonical, self-scoped). */\nexport function useMyCertificatesQuery(scope: MyLearningScope = {}) {\n const result = useQuery<{\n buiMyCertificates: { items: BuiMyCertificateRow[]; pageInfo: TecPageInfo };\n }>(MY_CERTIFICATES, {\n variables: {\n ...(scope.buiCourseId ? { courseId: scope.buiCourseId } : {}),\n pagination: pageArg(scope.limit ?? 100),\n },\n skip: scope.skip,\n fetchPolicy: 'cache-and-network',\n });\n const items = result.data?.buiMyCertificates?.items;\n const certificates = useMemo(\n () => forTechnospamCourse(mapRows(items, toTecCertificate), scope.technospamCourseId),\n [items, scope.technospamCourseId]\n );\n return { ...result, certificates };\n}\n\nexport const useProblemsQuery = (vars: {\n difficulty?: TecProblemDifficulty;\n status?: TecProblemStatus;\n topic?: string;\n company?: string;\n role?: string;\n interviewFrequency?: TecInterviewFrequency;\n pagination?: { limit: number; cursor?: string };\n}) =>\n useQuery<Conn<'tecListProblems', TecProblem>>(LIST_PROBLEMS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useProblemQuery = (id: string) =>\n useQuery<{ tecProblem: TecProblem | null }>(GET_PROBLEM, {\n variables: { id },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useSubmissionsQuery = (vars: {\n learnerId?: string;\n problemId?: string;\n startDate?: string;\n endDate?: string;\n}) =>\n useQuery<Conn<'tecListSubmissions', TecSubmission>>(LIST_SUBMISSIONS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\n/**\n * Slim submissions page for deriving the learner's solved problems: only the\n * fields the lookup needs, so walking every page stays cheap.\n */\nconst LIST_SOLVED_SUBMISSIONS = gql`\n query TecListSolvedSubmissions($learnerId: ID!, $pagination: PaginationInput) {\n tecListSubmissions(learnerId: $learnerId, pagination: $pagination) {\n items {\n id\n problemId\n verdict\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\n/** The svc clamps list limits to 100 (`MAX_LIMIT`). */\nexport const SOLVED_SCAN_PAGE_SIZE = 100;\n/** Upper bound on pages walked (2,000 submissions) so a runaway cursor cannot loop forever. */\nexport const SOLVED_SCAN_MAX_PAGES = 20;\n\nconst NO_SOLVED: ReadonlySet<string> = new Set<string>();\n\ntype SolvedSubmissionsPage = {\n tecListSubmissions: {\n items: Array<Pick<TecSubmission, 'id' | 'problemId' | 'verdict'>>;\n pageInfo: { cursor?: string | null; hasMore: boolean };\n };\n};\n\n/**\n * Ids of the problems the learner has an ACCEPTED submission for (BOFF-4077).\n *\n * The first page of `tecListSubmissions` is not enough: a learner with more\n * than one page of submissions would see older solves reported as unsolved.\n * This follows the cursor across every page (bounded by\n * `SOLVED_SCAN_MAX_PAGES`), publishing the set as each page lands. Without a\n * learner id it returns an empty set rather than listing the whole\n * workspace's submissions, which would mark other learners' solves as ours.\n */\nexport function useSolvedProblemIds(learnerId: string | undefined): ReadonlySet<string> {\n const client = useApolloClient();\n const [solved, setSolved] = useState<{ learnerId: string; ids: ReadonlySet<string> } | null>(\n null\n );\n\n useEffect(() => {\n if (!learnerId) return undefined;\n let cancelled = false;\n const ids = new Set<string>();\n void (async () => {\n let cursor: string | undefined;\n for (let page = 0; page < SOLVED_SCAN_MAX_PAGES; page += 1) {\n let conn: SolvedSubmissionsPage['tecListSubmissions'] | undefined;\n try {\n const res = await client.query<SolvedSubmissionsPage>({\n query: LIST_SOLVED_SUBMISSIONS,\n variables: { learnerId, pagination: pageArg(SOLVED_SCAN_PAGE_SIZE, cursor) },\n fetchPolicy: 'network-only',\n });\n conn = res.data?.tecListSubmissions;\n } catch {\n // A failed page keeps whatever was already found; the column only\n // under-reports, it never claims a solve that did not happen.\n return;\n }\n if (cancelled || !conn) return;\n for (const submission of conn.items) {\n if (submission.verdict === 'ACCEPTED') ids.add(submission.problemId);\n }\n setSolved({ learnerId, ids: new Set(ids) });\n if (!conn.pageInfo.hasMore || !conn.pageInfo.cursor) return;\n cursor = conn.pageInfo.cursor;\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [client, learnerId]);\n\n return learnerId && solved?.learnerId === learnerId ? solved.ids : NO_SOLVED;\n}\n\nexport const useMentorsQuery = (vars: {\n status?: TecMentorStatus;\n pagination?: { limit: number; cursor?: string };\n}) =>\n useQuery<Conn<'tecListMentors', TecMentor>>(LIST_MENTORS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useMentorQuery = (id: string) =>\n useQuery<{ tecMentor: TecMentor | null }>(GET_MENTOR, {\n variables: { id },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useSessionsQuery = (vars: {\n mentorId?: string;\n menteeId?: string;\n status?: TecSessionStatus;\n pagination?: { limit: number; cursor?: string };\n}) =>\n useQuery<Conn<'tecListSessions', TecMentorSession>>(LIST_SESSIONS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\n/**\n * Live pricing preview for a not-yet-booked session -- refetches whenever\n * mentorId/durationMinutes change (Apollo re-runs on variable change), so\n * the booking form always shows the cost that would actually be snapshotted\n * onto the session by tecBookSession. `skip` while mentorId is empty.\n */\nexport const useSessionCostEstimate = (mentorId: string, durationMinutes: number) =>\n useQuery<{ tecCalculateSessionCost: TecSessionCostEstimate }>(CALCULATE_SESSION_COST, {\n variables: { mentorId, durationMinutes },\n skip: !mentorId || !Number.isFinite(durationMinutes) || durationMinutes <= 0,\n fetchPolicy: 'cache-and-network',\n });\n\n/** The calling mentor's own payout ledger (calculation-only -- see BOFF-4433). */\nexport const useMyPayoutsQuery = (\n vars: { status?: TecPayoutStatus; pagination?: { limit: number; cursor?: string } } = {}\n) =>\n useQuery<Conn<'tecMyPayouts', TecMentorPayout>>(MY_PAYOUTS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useHackathonsQuery = (vars: {\n status?: TecHackathonStatus;\n pagination?: { limit: number; cursor?: string };\n}) =>\n useQuery<Conn<'tecListHackathons', TecHackathon>>(LIST_HACKATHONS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useHackathonQuery = (id: string) =>\n useQuery<{ tecHackathon: TecHackathon | null }>(GET_HACKATHON, {\n variables: { id },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useTeamsQuery = (\n hackathonId: string,\n pagination?: { limit: number; cursor?: string }\n) =>\n useQuery<Conn<'tecListTeams', TecHackathonTeam>>(LIST_TEAMS, {\n variables: { hackathonId, pagination: pagination ?? pageArg(50) },\n skip: !hackathonId,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useLessonsQuery = (\n courseId: string,\n pagination?: { limit: number; cursor?: string }\n) =>\n useQuery<Conn<'tecListLessons', TecLesson>>(LIST_LESSONS, {\n variables: { courseId, pagination: pagination ?? pageArg(50) },\n skip: !courseId,\n fetchPolicy: 'cache-and-network',\n });\n\n/* ------------------------------------------------------------ mutations */\n\nconst onMutationError = (error: ErrorLike): void => {\n notify.error('Something went wrong', userMessageFromError(error));\n};\n\nconst REFETCH = {\n courses: 'TecListCourses',\n enrollments: 'TecListEnrollments',\n /** BOFF-10481: the learner's own records, read from the canonical Buildmyiq fields. */\n myEnrollments: 'BuiMyTechnospamEnrollments',\n myCertificates: 'BuiMyTechnospamCertificates',\n problems: 'TecListProblems',\n submissions: 'TecListSubmissions',\n mentors: 'TecListMentors',\n sessions: 'TecListSessions',\n hackathons: 'TecListHackathons',\n} as const;\n\nconst ENROLL = gql`\n ${ENROLLMENT_FIELDS}\n mutation TecEnroll($input: TecEnrollInput!) {\n tecEnroll(input: $input) {\n ...EnrollmentFields\n }\n }\n`;\n\nconst SUBMIT_SOLUTION = gql`\n ${SUBMISSION_FIELDS}\n mutation TecSubmitSolution($input: TecSubmitSolutionInput!) {\n tecSubmitSolution(input: $input) {\n ...SubmissionFields\n }\n }\n`;\n\nconst BOOK_SESSION = gql`\n ${SESSION_FIELDS}\n mutation TecBookSession($input: TecBookSessionInput!) {\n tecBookSession(input: $input) {\n ...SessionFields\n }\n }\n`;\n\nconst CANCEL_SESSION = gql`\n ${SESSION_FIELDS}\n mutation TecCancelSession($id: ID!) {\n tecCancelSession(id: $id) {\n ...SessionFields\n }\n }\n`;\n\nconst BECOME_MENTOR = gql`\n ${MENTOR_FIELDS}\n mutation TecBecomeMentor($input: TecCreateMentorInput!) {\n tecBecomeMentor(input: $input) {\n ...MentorFields\n }\n }\n`;\n\nconst REGISTER_TEAM = gql`\n ${TEAM_FIELDS}\n mutation TecRegisterTeam($input: TecRegisterTeamInput!) {\n tecRegisterTeam(input: $input) {\n ...TeamFields\n }\n }\n`;\n\nconst SUBMIT_PROJECT = gql`\n ${TEAM_FIELDS}\n mutation TecSubmitProject($input: TecSubmitProjectInput!) {\n tecSubmitProject(input: $input) {\n ...TeamFields\n }\n }\n`;\n\n/* ------- admin / authoring mutations ------- */\n\nconst CREATE_COURSE = gql`\n ${COURSE_FIELDS}\n mutation TecCreateCourse($input: TecCreateCourseInput!) {\n tecCreateCourse(input: $input) {\n ...CourseFields\n }\n }\n`;\n\nconst PUBLISH_COURSE = gql`\n ${COURSE_FIELDS}\n mutation TecPublishCourse($id: ID!) {\n tecPublishCourse(id: $id) {\n ...CourseFields\n }\n }\n`;\n\nconst ARCHIVE_COURSE = gql`\n mutation TecArchiveCourse($id: ID!) {\n tecArchiveCourse(id: $id)\n }\n`;\n\nconst CREATE_LESSON = gql`\n ${LESSON_FIELDS}\n mutation TecCreateLesson($input: TecCreateLessonInput!) {\n tecCreateLesson(input: $input) {\n ...LessonFields\n }\n }\n`;\n\nconst UPDATE_LESSON = gql`\n ${LESSON_FIELDS}\n mutation TecUpdateLesson($id: ID!, $input: TecUpdateLessonInput!) {\n tecUpdateLesson(id: $id, input: $input) {\n ...LessonFields\n }\n }\n`;\n\nconst CREATE_PROBLEM = gql`\n ${PROBLEM_FIELDS}\n mutation TecCreateProblem($input: TecCreateProblemInput!) {\n tecCreateProblem(input: $input) {\n ...ProblemFields\n }\n }\n`;\n\nconst PUBLISH_PROBLEM = gql`\n ${PROBLEM_FIELDS}\n mutation TecPublishProblem($id: ID!) {\n tecPublishProblem(id: $id) {\n ...ProblemFields\n }\n }\n`;\n\nconst CREATE_HACKATHON = gql`\n ${HACKATHON_FIELDS}\n mutation TecCreateHackathon($input: TecCreateHackathonInput!) {\n tecCreateHackathon(input: $input) {\n ...HackathonFields\n }\n }\n`;\n\nconst OPEN_REGISTRATION = gql`\n ${HACKATHON_FIELDS}\n mutation TecOpenRegistration($id: ID!) {\n tecOpenRegistration(id: $id) {\n ...HackathonFields\n }\n }\n`;\n\nconst APPROVE_MENTOR = gql`\n ${MENTOR_FIELDS}\n mutation TecApproveMentor($id: ID!) {\n tecApproveMentor(id: $id) {\n ...MentorFields\n }\n }\n`;\n\nconst SCORE_TEAM = gql`\n ${TEAM_FIELDS}\n mutation TecScoreTeam($id: ID!, $score: Int!, $winner: Boolean) {\n tecScoreTeam(id: $id, score: $score, winner: $winner) {\n ...TeamFields\n }\n }\n`;\n\nconst CONFIRM_SESSION = gql`\n ${SESSION_FIELDS}\n mutation TecConfirmSession($id: ID!, $meetingUrl: String) {\n tecConfirmSession(id: $id, meetingUrl: $meetingUrl) {\n ...SessionFields\n }\n }\n`;\n\nconst COMPLETE_SESSION = gql`\n ${SESSION_FIELDS}\n mutation TecCompleteSession($id: ID!, $rating: Int, $feedback: String) {\n tecCompleteSession(id: $id, rating: $rating, feedback: $feedback) {\n ...SessionFields\n }\n }\n`;\n\nconst GRADE_SUBMISSION = gql`\n ${SUBMISSION_FIELDS}\n mutation TecGradeSubmission($id: ID!, $verdict: TecSubmissionVerdict!, $runtimeMs: Int) {\n tecGradeSubmission(id: $id, verdict: $verdict, runtimeMs: $runtimeMs) {\n ...SubmissionFields\n }\n }\n`;\n\nconst ISSUE_CERTIFICATE = gql`\n ${CERTIFICATE_FIELDS}\n mutation TecIssueCertificate($courseId: ID!, $learnerId: ID!, $score: Int!) {\n tecIssueCertificate(courseId: $courseId, learnerId: $learnerId, score: $score) {\n ...CertificateFields\n }\n }\n`;\n\nconst REVOKE_CERTIFICATE = gql`\n ${CERTIFICATE_FIELDS}\n mutation TecRevokeCertificate($id: ID!, $reason: String) {\n tecRevokeCertificate(id: $id, reason: $reason) {\n ...CertificateFields\n }\n }\n`;\n\nconst START_HACKATHON = gql`\n ${HACKATHON_FIELDS}\n mutation TecStartHackathon($id: ID!) {\n tecStartHackathon(id: $id) {\n ...HackathonFields\n }\n }\n`;\n\nconst START_JUDGING = gql`\n ${HACKATHON_FIELDS}\n mutation TecStartJudging($id: ID!) {\n tecStartJudging(id: $id) {\n ...HackathonFields\n }\n }\n`;\n\nconst COMPLETE_HACKATHON = gql`\n ${HACKATHON_FIELDS}\n mutation TecCompleteHackathon($id: ID!) {\n tecCompleteHackathon(id: $id) {\n ...HackathonFields\n }\n }\n`;\n\n/* ---------------------------------------------------------- mutation hooks */\n\nexport const useEnroll = () =>\n useMutation<{ tecEnroll: TecEnrollment }>(ENROLL, {\n onError: onMutationError,\n refetchQueries: [REFETCH.enrollments, REFETCH.myEnrollments, REFETCH.courses],\n });\n\nexport const useSubmitSolution = () =>\n useMutation<{ tecSubmitSolution: TecSubmission }>(SUBMIT_SOLUTION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.submissions],\n });\n\n/**\n * Re-run the grader for a submission. Used by the problem page when a verdict\n * came back PENDING (grader unavailable at submit time) so a learner can retry\n * without resubmitting identical code.\n */\nexport const useRegradeSubmission = () =>\n useMutation<{ tecRegradeSubmission: TecSubmission }>(REGRADE_SUBMISSION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.submissions],\n });\n\nexport const useBookSession = () =>\n useMutation<{ tecBookSession: TecMentorSession }>(BOOK_SESSION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.sessions, REFETCH.mentors],\n });\n\nexport const useCancelSession = () =>\n useMutation<{ tecCancelSession: TecMentorSession }>(CANCEL_SESSION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.sessions],\n });\n\nexport const useBecomeMentor = () =>\n useMutation<{ tecBecomeMentor: TecMentor }>(BECOME_MENTOR, {\n onError: onMutationError,\n refetchQueries: [REFETCH.mentors],\n });\n\nexport const useRegisterTeam = () =>\n useMutation<{ tecRegisterTeam: TecHackathonTeam }>(REGISTER_TEAM, {\n onError: onMutationError,\n refetchQueries: [REFETCH.hackathons],\n });\n\nexport const useSubmitProject = () =>\n useMutation<{ tecSubmitProject: TecHackathonTeam }>(SUBMIT_PROJECT, {\n onError: onMutationError,\n });\n\nexport const useCreateCourse = () =>\n useMutation<{ tecCreateCourse: TecCourse }>(CREATE_COURSE, {\n onError: onMutationError,\n refetchQueries: [REFETCH.courses],\n });\n\nexport const usePublishCourse = () =>\n useMutation<{ tecPublishCourse: TecCourse }>(PUBLISH_COURSE, {\n onError: onMutationError,\n refetchQueries: [REFETCH.courses],\n });\n\nexport const useArchiveCourse = () =>\n useMutation<{ tecArchiveCourse: boolean }>(ARCHIVE_COURSE, {\n onError: onMutationError,\n refetchQueries: [REFETCH.courses],\n });\n\nexport const useCreateLesson = () =>\n useMutation<{ tecCreateLesson: TecLesson }>(CREATE_LESSON, { onError: onMutationError });\n\n/**\n * Edit an already-authored lesson (title/kind/content/minutes/preview/position).\n * Every field on TecUpdateLessonInput is optional — send only what changed.\n */\nexport const useUpdateLesson = () =>\n useMutation<{ tecUpdateLesson: TecLesson }>(UPDATE_LESSON, { onError: onMutationError });\n\nexport const useCreateProblem = () =>\n useMutation<{ tecCreateProblem: TecProblem }>(CREATE_PROBLEM, {\n onError: onMutationError,\n refetchQueries: [REFETCH.problems],\n });\n\nexport const usePublishProblem = () =>\n useMutation<{ tecPublishProblem: TecProblem }>(PUBLISH_PROBLEM, {\n onError: onMutationError,\n refetchQueries: [REFETCH.problems],\n });\n\nexport const useCreateHackathon = () =>\n useMutation<{ tecCreateHackathon: TecHackathon }>(CREATE_HACKATHON, {\n onError: onMutationError,\n refetchQueries: [REFETCH.hackathons],\n });\n\nexport const useOpenRegistration = () =>\n useMutation<{ tecOpenRegistration: TecHackathon }>(OPEN_REGISTRATION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.hackathons],\n });\n\nexport const useApproveMentor = () =>\n useMutation<{ tecApproveMentor: TecMentor }>(APPROVE_MENTOR, {\n onError: onMutationError,\n refetchQueries: [REFETCH.mentors],\n });\n\nexport const useScoreTeam = () =>\n useMutation<{ tecScoreTeam: TecHackathonTeam }>(SCORE_TEAM, { onError: onMutationError });\n\nexport const useConfirmSession = () =>\n useMutation<{ tecConfirmSession: TecMentorSession }>(CONFIRM_SESSION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.sessions],\n });\n\nexport const useCompleteSession = () =>\n useMutation<{ tecCompleteSession: TecMentorSession }>(COMPLETE_SESSION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.sessions],\n });\n\nexport const useGradeSubmission = () =>\n useMutation<{ tecGradeSubmission: TecSubmission }>(GRADE_SUBMISSION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.submissions],\n });\n\nexport const useIssueCertificate = () =>\n useMutation<{ tecIssueCertificate: TecCertificate }>(ISSUE_CERTIFICATE, {\n onError: onMutationError,\n });\n\nexport const useRevokeCertificate = () =>\n useMutation<{ tecRevokeCertificate: TecCertificate }>(REVOKE_CERTIFICATE, {\n onError: onMutationError,\n });\n\nexport const useStartHackathon = () =>\n useMutation<{ tecStartHackathon: TecHackathon }>(START_HACKATHON, {\n onError: onMutationError,\n refetchQueries: [REFETCH.hackathons],\n });\n\nexport const useStartJudging = () =>\n useMutation<{ tecStartJudging: TecHackathon }>(START_JUDGING, {\n onError: onMutationError,\n refetchQueries: [REFETCH.hackathons],\n });\n\nexport const useCompleteHackathon = () =>\n useMutation<{ tecCompleteHackathon: TecHackathon }>(COMPLETE_HACKATHON, {\n onError: onMutationError,\n refetchQueries: [REFETCH.hackathons],\n });\n\n/* ============================================================ Study Planner\n * BOFF-4415 — deterministic, rule-based goal -> plan scheduler. NOT an LLM\n * feature; see the backend StudyPlanService honesty note. Every query/\n * mutation below is self-scoped (learnerId comes from the authenticated\n * actor server-side) so no learnerId variable is ever sent from the client.\n * ========================================================================= */\n\nexport type TecStudyPlanStatus = 'ACTIVE' | 'COMPLETED' | 'ARCHIVED';\nexport type TecStudyPlanStepKind = 'COURSE' | 'PROBLEM';\nexport type TecStudyPlanStepStatus = 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'SKIPPED';\n\nexport interface TecStudyPlanStep {\n id: string;\n order: number;\n kind: TecStudyPlanStepKind;\n targetId: string;\n title: string;\n scheduledWeek: number;\n status: TecStudyPlanStepStatus;\n rationale: string;\n estimatedMinutes: number;\n completedAt?: string | null;\n}\n\nexport interface TecStudyPlan {\n id: string;\n goal: string;\n topic?: string | null;\n timeframeWeeks: number;\n status: TecStudyPlanStatus;\n targetCompletionDate?: string | null;\n progressPct: number;\n totalSteps: number;\n completedSteps: number;\n createdAt: string;\n steps: TecStudyPlanStep[];\n}\n\nconst STUDY_PLAN_STEP_FIELDS = gql`\n fragment StudyPlanStepFields on TecStudyPlanStep {\n id\n order\n kind\n targetId\n title\n scheduledWeek\n status\n rationale\n estimatedMinutes\n completedAt\n }\n`;\n\nconst STUDY_PLAN_FIELDS = gql`\n ${STUDY_PLAN_STEP_FIELDS}\n fragment StudyPlanFields on TecStudyPlan {\n id\n goal\n topic\n timeframeWeeks\n status\n targetCompletionDate\n progressPct\n totalSteps\n completedSteps\n createdAt\n steps {\n ...StudyPlanStepFields\n }\n }\n`;\n\nconst LIST_MY_STUDY_PLANS = gql`\n ${STUDY_PLAN_FIELDS}\n query TecMyStudyPlans {\n tecMyStudyPlans {\n items {\n ...StudyPlanFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst GENERATE_STUDY_PLAN = gql`\n ${STUDY_PLAN_FIELDS}\n mutation TecGenerateStudyPlan($input: TecGenerateStudyPlanInput!) {\n tecGenerateStudyPlan(input: $input) {\n ...StudyPlanFields\n }\n }\n`;\n\nconst UPDATE_STUDY_PLAN_STEP_STATUS = gql`\n ${STUDY_PLAN_STEP_FIELDS}\n mutation TecUpdateStudyPlanStepStatus($input: TecUpdateStudyPlanStepStatusInput!) {\n tecUpdateStudyPlanStepStatus(input: $input) {\n ...StudyPlanStepFields\n }\n }\n`;\n\nconst ARCHIVE_STUDY_PLAN = gql`\n mutation TecArchiveStudyPlan($id: ID!) {\n tecArchiveStudyPlan(id: $id)\n }\n`;\n\nexport const useMyStudyPlansQuery = () =>\n useQuery<{ tecMyStudyPlans: { items: TecStudyPlan[]; pageInfo: TecPageInfo } }>(\n LIST_MY_STUDY_PLANS,\n {\n fetchPolicy: 'cache-and-network',\n }\n );\n\nexport const useGenerateStudyPlan = () =>\n useMutation<{ tecGenerateStudyPlan: TecStudyPlan }>(GENERATE_STUDY_PLAN, {\n onError: onMutationError,\n refetchQueries: ['TecMyStudyPlans'],\n });\n\nexport const useUpdateStudyPlanStepStatus = () =>\n useMutation<{ tecUpdateStudyPlanStepStatus: TecStudyPlanStep }>(UPDATE_STUDY_PLAN_STEP_STATUS, {\n onError: onMutationError,\n refetchQueries: ['TecMyStudyPlans'],\n });\n\nexport const useArchiveStudyPlan = () =>\n useMutation<{ tecArchiveStudyPlan: boolean }>(ARCHIVE_STUDY_PLAN, {\n onError: onMutationError,\n refetchQueries: ['TecMyStudyPlans'],\n });\n\n/* ================================================================ Portfolio\n * BOFF-4432 — learner-curated showcase of real completed hackathon\n * projects, certificates, and courses. `tecPublicPortfolio` is a public,\n * unauthenticated field (@rbac public: true) reached through the same\n * gateway; there is no separate unauthenticated *page* wired up yet (see\n * ApplicationsPortfolioPagePage's deferred-scope note).\n * ========================================================================= */\n\nexport type TecPortfolioItemKind = 'HACKATHON_PROJECT' | 'CERTIFICATE' | 'COURSE_COMPLETION';\n\nexport interface TecPortfolioItem {\n id: string;\n kind: TecPortfolioItemKind;\n sourceId: string;\n title: string;\n description?: string | null;\n link?: string | null;\n completionDate: string;\n order: number;\n}\n\nexport interface TecPortfolio {\n id: string;\n isPublished: boolean;\n publicSlug?: string | null;\n publishedAt?: string | null;\n items: TecPortfolioItem[];\n}\n\nconst PORTFOLIO_ITEM_FIELDS = gql`\n fragment PortfolioItemFields on TecPortfolioItem {\n id\n kind\n sourceId\n title\n description\n link\n completionDate\n order\n }\n`;\n\nconst PORTFOLIO_FIELDS = gql`\n ${PORTFOLIO_ITEM_FIELDS}\n fragment PortfolioFields on TecPortfolio {\n id\n isPublished\n publicSlug\n publishedAt\n items {\n ...PortfolioItemFields\n }\n }\n`;\n\nconst GET_MY_PORTFOLIO = gql`\n ${PORTFOLIO_FIELDS}\n query TecMyPortfolio {\n tecMyPortfolio {\n ...PortfolioFields\n }\n }\n`;\n\nconst ADD_PORTFOLIO_ITEM = gql`\n ${PORTFOLIO_ITEM_FIELDS}\n mutation TecAddPortfolioItem($input: TecAddPortfolioItemInput!) {\n tecAddPortfolioItem(input: $input) {\n ...PortfolioItemFields\n }\n }\n`;\n\nconst REORDER_PORTFOLIO_ITEMS = gql`\n ${PORTFOLIO_FIELDS}\n mutation TecReorderPortfolioItems($input: TecReorderPortfolioItemsInput!) {\n tecReorderPortfolioItems(input: $input) {\n ...PortfolioFields\n }\n }\n`;\n\nconst REMOVE_PORTFOLIO_ITEM = gql`\n mutation TecRemovePortfolioItem($id: ID!) {\n tecRemovePortfolioItem(id: $id)\n }\n`;\n\nconst PUBLISH_PORTFOLIO = gql`\n ${PORTFOLIO_FIELDS}\n mutation TecPublishPortfolio {\n tecPublishPortfolio {\n ...PortfolioFields\n }\n }\n`;\n\nconst UNPUBLISH_PORTFOLIO = gql`\n ${PORTFOLIO_FIELDS}\n mutation TecUnpublishPortfolio {\n tecUnpublishPortfolio {\n ...PortfolioFields\n }\n }\n`;\n\nexport const useMyPortfolioQuery = () =>\n useQuery<{ tecMyPortfolio: TecPortfolio }>(GET_MY_PORTFOLIO, {\n fetchPolicy: 'cache-and-network',\n });\n\nconst REFETCH_PORTFOLIO = ['TecMyPortfolio'];\n\nexport const useAddPortfolioItem = () =>\n useMutation<{ tecAddPortfolioItem: TecPortfolioItem }>(ADD_PORTFOLIO_ITEM, {\n onError: onMutationError,\n refetchQueries: REFETCH_PORTFOLIO,\n });\n\nexport const useReorderPortfolioItems = () =>\n useMutation<{ tecReorderPortfolioItems: TecPortfolio }>(REORDER_PORTFOLIO_ITEMS, {\n onError: onMutationError,\n });\n\nexport const useRemovePortfolioItem = () =>\n useMutation<{ tecRemovePortfolioItem: boolean }>(REMOVE_PORTFOLIO_ITEM, {\n onError: onMutationError,\n refetchQueries: REFETCH_PORTFOLIO,\n });\n\nexport const usePublishPortfolio = () =>\n useMutation<{ tecPublishPortfolio: TecPortfolio }>(PUBLISH_PORTFOLIO, {\n onError: onMutationError,\n refetchQueries: REFETCH_PORTFOLIO,\n });\n\nexport const useUnpublishPortfolio = () =>\n useMutation<{ tecUnpublishPortfolio: TecPortfolio }>(UNPUBLISH_PORTFOLIO, {\n onError: onMutationError,\n refetchQueries: REFETCH_PORTFOLIO,\n });\n\n/* ------------------------------------------------ TechnoSage review (BOFF-3841) */\n\nexport type TecReviewSource = 'AI' | 'RULES';\n\n/**\n * TechnoSage's explanation of one of the caller's own graded submissions.\n * It explains the grader's verdict and never changes it. `source` says who\n * wrote it: a model (AI) or the grader output alone (RULES).\n */\nexport interface TecSubmissionReview {\n id: string;\n submissionId: string;\n problemId: string;\n verdict: TecSubmissionVerdict;\n summary: string;\n failureExplanation: string;\n conceptsToRevisit: string[];\n suggestedProblems: Array<Pick<TecProblem, 'id' | 'title' | 'difficulty' | 'topic'>>;\n source: TecReviewSource;\n model: string | null;\n createdAt: string;\n}\n\nconst SUBMISSION_REVIEW_FIELDS = gql`\n fragment SubmissionReviewFields on TecSubmissionReview {\n id\n submissionId\n problemId\n verdict\n summary\n failureExplanation\n conceptsToRevisit\n suggestedProblems {\n id\n title\n difficulty\n topic\n }\n source\n model\n createdAt\n }\n`;\n\nconst MY_SUBMISSION_REVIEW = gql`\n ${SUBMISSION_REVIEW_FIELDS}\n query TecMySubmissionReview($submissionId: ID!) {\n tecMySubmissionReview(submissionId: $submissionId) {\n ...SubmissionReviewFields\n }\n }\n`;\n\nconst REVIEW_SUBMISSION = gql`\n ${SUBMISSION_REVIEW_FIELDS}\n mutation TecReviewSubmission($submissionId: ID!) {\n tecReviewSubmission(submissionId: $submissionId) {\n ...SubmissionReviewFields\n }\n }\n`;\n\n/** The stored review of a submission, if the learner already asked for one. */\nexport const useMySubmissionReviewQuery = (submissionId: string | null) =>\n useQuery<{ tecMySubmissionReview: TecSubmissionReview | null }>(MY_SUBMISSION_REVIEW, {\n variables: { submissionId: submissionId ?? '' },\n skip: !submissionId,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useReviewSubmission = () =>\n useMutation<{ tecReviewSubmission: TecSubmissionReview }>(REVIEW_SUBMISSION, {\n onError: onMutationError,\n refetchQueries: ['TecMySubmissionReview'],\n });\n\n/* ------------------------------------------------------- pagination helper */\n\nexport interface PageLimit {\n limit: number;\n more: () => void;\n}\n\n/**\n * Simple, robust \"load more\". The backend paginates with `take = limit + 1`\n * (offset is ignored; only `cursor` shifts the window), so a larger `limit`\n * simply fetches more rows from the top and `pageInfo.hasMore` reports whether\n * still more exist. Growing the limit avoids cursor-merge/fetchMore complexity\n * and never sends `cursor: null` (the page-1 500 guard). Pass `limit` into the\n * query's `pageArg` and render <LoadMore hasMore={...} onClick={more} />.\n */\nexport function usePageLimit(step = 24, initial = 24): PageLimit {\n const [limit, setLimit] = useState(initial);\n return { limit, more: () => setLimit((l) => l + step) };\n}\n\n/* ------------------------------------------------- lesson player (BOFF-4107) */\n\nconst GET_LESSON_PLAYER = gql`\n ${LESSON_FIELDS}\n ${COURSE_FIELDS}\n query TecLessonPlayer($lessonId: ID!, $courseId: ID!) {\n tecLesson(id: $lessonId) {\n ...LessonFields\n file {\n id\n name\n mimeType\n downloadUrl\n }\n }\n tecCourse(id: $courseId) {\n ...CourseFields\n lessons {\n id\n courseId\n title\n kind\n position\n estimatedMinutes\n isPreview\n isLocked\n completedByMe\n }\n }\n }\n`;\n\nconst LESSON_QUIZ_KEY = gql`\n query TecLessonQuizKey($lessonId: ID!) {\n tecLessonQuizKey(lessonId: $lessonId) {\n prompt\n options\n correctOption\n explanation\n }\n }\n`;\n\nconst COMPLETE_LESSON = gql`\n ${ENROLLMENT_FIELDS}\n mutation TecCompleteLesson($lessonId: ID!) {\n tecCompleteLesson(lessonId: $lessonId) {\n ...EnrollmentFields\n nextLessonId\n }\n }\n`;\n\nconst SUBMIT_LESSON_QUIZ = gql`\n ${ENROLLMENT_FIELDS}\n mutation TecSubmitLessonQuiz($lessonId: ID!, $answers: [Int!]!) {\n tecSubmitLessonQuiz(lessonId: $lessonId, answers: $answers) {\n lessonId\n score\n correctCount\n totalCount\n passed\n passingScore\n results {\n index\n correct\n selectedOption\n correctOption\n explanation\n }\n enrollment {\n ...EnrollmentFields\n nextLessonId\n }\n }\n }\n`;\n\nconst RUN_LESSON_CODE = gql`\n ${ENROLLMENT_FIELDS}\n mutation TecRunLessonCode($input: TecRunLessonCodeInput!) {\n tecRunLessonCode(input: $input) {\n lessonId\n status\n language\n stdout\n stderr\n exitCode\n durationMs\n timedOut\n memoryLimitExceeded\n compileFailed\n matchedExpected\n lessonCompleted\n enrollment {\n ...EnrollmentFields\n nextLessonId\n }\n }\n }\n`;\n\nexport interface LessonPlayerData {\n tecLesson: TecLesson | null;\n tecCourse: TecCourse | null;\n}\n\nexport const useLessonPlayerQuery = (courseId: string, lessonId: string) =>\n useQuery<LessonPlayerData>(GET_LESSON_PLAYER, {\n variables: { courseId, lessonId },\n skip: !courseId || !lessonId,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useLessonQuizKeyQuery = (lessonId: string, skip = false) =>\n useQuery<{ tecLessonQuizKey: TecQuizQuestionKey[] }>(LESSON_QUIZ_KEY, {\n variables: { lessonId },\n skip: skip || !lessonId,\n fetchPolicy: 'network-only',\n });\n\nconst LESSON_PROGRESS_REFETCH = [\n 'TecLessonPlayer',\n 'TecCourse',\n REFETCH.myEnrollments,\n REFETCH.enrollments,\n];\n\nexport const useCompleteLesson = () =>\n useMutation<{ tecCompleteLesson: TecEnrollment }>(COMPLETE_LESSON, {\n onError: onMutationError,\n refetchQueries: LESSON_PROGRESS_REFETCH,\n });\n\nexport const useSubmitLessonQuiz = () =>\n useMutation<{ tecSubmitLessonQuiz: TecQuizResult }>(SUBMIT_LESSON_QUIZ, {\n onError: onMutationError,\n refetchQueries: LESSON_PROGRESS_REFETCH,\n });\n\n/**\n * Runs code for an INTERACTIVE lesson. No refetch here: the page refetches\n * only when a run actually completed the lesson, so polling a warming\n * sandbox does not reload the page each time.\n */\nexport const useRunLessonCode = () =>\n useMutation<{ tecRunLessonCode: TecLessonCodeRun }>(RUN_LESSON_CODE, {\n onError: onMutationError,\n });\n\n/* ------------------------------------------- offline lessons (BOFF-4418) */\n\n/**\n * Fetch one lesson (with its course outline) straight from the network, for\n * saving a course to the device. Returns null when the lesson cannot be read.\n */\nexport async function fetchLessonForOffline(\n client: ApolloClient,\n courseId: string,\n lessonId: string\n): Promise<LessonPlayerData | null> {\n try {\n const result = await client.query<LessonPlayerData>({\n query: GET_LESSON_PLAYER,\n variables: { courseId, lessonId },\n fetchPolicy: 'network-only',\n });\n return result.data ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * GraphQL error codes that mean the server refused this completion for good\n * (lesson removed, learner not enrolled, lesson needs a quiz pass or code run).\n * Every other GraphQL error — an expired session (`UNAUTHENTICATED`), a\n * permission or quota check, a server fault — may succeed later, so the\n * completion stays queued.\n */\nconst TERMINAL_COMPLETION_ERROR_CODES: ReadonlySet<string> = new Set([\n 'NOT_FOUND',\n 'BAD_USER_INPUT',\n]);\n\n/** True when every GraphQL error in a failed completion is terminal. */\nexport function isTerminalCompletionError(error: unknown): boolean {\n if (!CombinedGraphQLErrors.is(error) || error.errors.length === 0) return false;\n return error.errors.every((graphQLError) => {\n const code = graphQLError.extensions?.['code'];\n return typeof code === 'string' && TERMINAL_COMPLETION_ERROR_CODES.has(code);\n });\n}\n\n/**\n * Send a lesson completion that was queued while offline: `rejected` when the\n * server refused it for good (see {@link isTerminalCompletionError}), `retry`\n * for anything that may succeed later (no connection, gateway down, expired\n * session).\n */\nexport async function sendQueuedLessonCompletion(\n client: ApolloClient,\n lessonId: string\n): Promise<'sent' | 'rejected' | 'retry'> {\n try {\n const result = await client.mutate<{ tecCompleteLesson: TecEnrollment }>({\n mutation: COMPLETE_LESSON,\n variables: { lessonId },\n refetchQueries: LESSON_PROGRESS_REFETCH,\n });\n return result.data?.tecCompleteLesson ? 'sent' : 'rejected';\n } catch (error) {\n return isTerminalCompletionError(error) ? 'rejected' : 'retry';\n }\n}\n\n/* ----------------------------------------- enterprise connectors (BOFF-4430) */\n\nexport type TecCertificateVerificationStatus = 'VALID' | 'REVOKED' | 'NOT_FOUND';\n\nexport interface TecCertificateVerification {\n serial: string;\n status: TecCertificateVerificationStatus;\n valid: boolean;\n issuer: string;\n courseTitle?: string | null;\n courseLevel?: string | null;\n score?: number | null;\n issuedAt?: string | null;\n revokedAt?: string | null;\n verifiedAt: string;\n}\n\n/** One xAPI 1.0.3 statement as exported by the service (LRS-ready JSON). */\nexport type TecXapiStatement = Record<string, unknown> & { id: string; timestamp: string };\n\nexport interface TecXapiStatementPage {\n statements: TecXapiStatement[];\n count: number;\n hasMore: boolean;\n nextSince?: string | null;\n version: string;\n}\n\n/** Most serials one batch verification accepts (mirrors the service cap). */\nexport const MAX_VERIFY_BATCH = 25;\n\nconst VERIFY_CERTIFICATES = gql`\n query TecVerifyCertificates($serials: [String!]!) {\n tecVerifyCertificates(serials: $serials) {\n serial\n status\n valid\n issuer\n courseTitle\n courseLevel\n score\n issuedAt\n revokedAt\n verifiedAt\n }\n }\n`;\n\nconst EXPORT_XAPI_STATEMENTS = gql`\n query TecExportXapiStatements($courseId: ID, $since: DateTime, $until: DateTime, $limit: Int) {\n tecExportXapiStatements(courseId: $courseId, since: $since, until: $until, limit: $limit) {\n statements\n count\n hasMore\n nextSince\n version\n }\n }\n`;\n\n/**\n * Certificate verification — the same public query ATS partners call. Run on\n * demand (skip until serials are submitted) and always against the network,\n * so a revocation shows up immediately.\n */\nexport const useVerifyCertificatesQuery = (serials: string[]) =>\n useQuery<{ tecVerifyCertificates: TecCertificateVerification[] }>(VERIFY_CERTIFICATES, {\n variables: { serials },\n skip: serials.length === 0,\n fetchPolicy: 'network-only',\n });\n\nexport interface XapiExportVars {\n courseId?: string;\n since?: string;\n until?: string;\n limit?: number;\n}\n\n/**\n * xAPI export (admin; `list-all` on technospam-enrollment). Returns an\n * imperative runner that follows `nextSince` until the window is exhausted\n * or `maxStatements` is reached, de-duplicating by statement id (pages\n * overlap on one timestamp by design).\n */\nexport function useXapiExport() {\n const [running, setRunning] = useState(false);\n const [error, setError] = useState<ErrorLike | null>(null);\n const [loadPage] = useLazyQuery<{ tecExportXapiStatements: TecXapiStatementPage }>(\n EXPORT_XAPI_STATEMENTS,\n { fetchPolicy: 'network-only' }\n );\n\n const run = async (vars: XapiExportVars, maxStatements = 5000) => {\n setRunning(true);\n setError(null);\n const seen = new Map<string, TecXapiStatement>();\n let since = vars.since;\n let truncated = false;\n try {\n for (;;) {\n const res = await loadPage({ variables: { ...vars, since, limit: vars.limit ?? 500 } });\n if (res.error) throw res.error;\n const page = res.data?.tecExportXapiStatements;\n if (!page) break;\n for (const statement of page.statements) seen.set(statement.id, statement);\n if (!page.hasMore || !page.nextSince) break;\n if (seen.size >= maxStatements) {\n truncated = true;\n break;\n }\n since = page.nextSince;\n }\n return { statements: [...seen.values()], truncated };\n } catch (err) {\n setError(err as ErrorLike);\n notify.error('Export failed', userMessageFromError(err));\n return null;\n } finally {\n setRunning(false);\n }\n };\n\n return { run, running, error };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"technospamApi.js","names":[],"sources":["../../../src/technospam/api/technospamApi.ts"],"sourcesContent":["/**\n * TechnoSpam data layer — Apollo operations + typed hooks over the\n * wspace-technospam-svc subgraph (Tec* fields), reached through the host\n * shell's default workspace ApolloProvider (defaultGateway: \"workspace\").\n *\n * Every page consumes real backend data through these hooks. There is NO mock\n * data. List queries are cursor-paginated: page 1 omits `cursor` entirely\n * (passing cursor: null makes the Prisma cursor resolver 500), \"load more\"\n * passes the previous page's `pageInfo.cursor`.\n */\nimport { useEffect, useMemo, useState } from 'react';\nimport { CombinedGraphQLErrors, gql, type ApolloClient, type ErrorLike } from '@apollo/client';\nimport { useApolloClient, useLazyQuery, useMutation, useQuery } from '@apollo/client/react';\n\nimport { notify } from '../lib/toast';\nimport { userMessageFromError } from '../lib/errorMessage';\nimport {\n type BuiMyCertificateRow,\n type BuiMyEnrollmentRow,\n forTechnospamCourse,\n mapRows,\n toTecCertificate,\n toTecEnrollment,\n} from './canonicalLearning';\n\n/* ------------------------------------------------------------------ enums */\n\nexport type TecCourseLevel = 'BEGINNER' | 'INTERMEDIATE' | 'ADVANCED' | 'EXPERT';\nexport type TecCourseStatus = 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';\nexport type TecLessonKind = 'VIDEO' | 'ARTICLE' | 'INTERACTIVE' | 'QUIZ' | 'PROJECT';\nexport type TecEnrollmentStatus = 'ENROLLED' | 'IN_PROGRESS' | 'COMPLETED' | 'DROPPED';\n/** EXPIRED only arrives from the canonical Buildmyiq certificate (BOFF-10481). */\nexport type TecCertificateStatus = 'ISSUED' | 'REVOKED' | 'EXPIRED';\nexport type TecProblemDifficulty = 'EASY' | 'MEDIUM' | 'HARD';\nexport type TecProblemStatus = 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';\nexport type TecInterviewFrequency = 'LOW' | 'MEDIUM' | 'HIGH';\nexport type TecSubmissionVerdict =\n | 'PENDING'\n | 'ACCEPTED'\n | 'WRONG_ANSWER'\n | 'TIME_LIMIT_EXCEEDED'\n | 'RUNTIME_ERROR'\n | 'COMPILE_ERROR';\nexport type TecMentorStatus = 'PENDING' | 'ACTIVE' | 'PAUSED' | 'INACTIVE';\nexport type TecSessionStatus = 'REQUESTED' | 'CONFIRMED' | 'COMPLETED' | 'CANCELLED' | 'NO_SHOW';\nexport type TecHackathonStatus =\n 'DRAFT' | 'REGISTRATION_OPEN' | 'IN_PROGRESS' | 'JUDGING' | 'COMPLETED' | 'CANCELLED';\nexport type TecTeamSubmissionStatus = 'DRAFT' | 'SUBMITTED' | 'DISQUALIFIED' | 'WINNER';\n\n/* ------------------------------------------------------------------ types */\n\nexport interface TecCourse {\n id: string;\n slug: string;\n title: string;\n summary?: string | null;\n topic?: string | null;\n level: TecCourseLevel;\n status: TecCourseStatus;\n tags: string[];\n estimatedMinutes: number;\n instructorId?: string | null;\n /** BOFF-4416: true when this course requires a paid TechnoSpam plan. */\n isPremiumOnly: boolean;\n /**\n * BOFF-4416: computed for the calling workspace — true when this course\n * is `isPremiumOnly` and the workspace has no paid plan. When true, every\n * non-preview lesson's `content` is already null in the API response.\n */\n isLocked: boolean;\n /** BOFF-10481: the canonical Buildmyiq course id; null until the course is mirrored. */\n buiCourseId?: string | null;\n publishedAt?: string | null;\n createdAt: string;\n updatedAt: string;\n lessons?: TecLesson[];\n enrollments?: TecEnrollment[];\n}\n\nexport interface TecLesson {\n id: string;\n courseId: string;\n title: string;\n kind: TecLessonKind;\n content?: string | null;\n position: number;\n estimatedMinutes: number;\n isPreview: boolean;\n /**\n * BOFF-4416: true when this lesson's `content` is gated behind a paid\n * TechnoSpam plan (never true for `isPreview` lessons). `content` is\n * already null in the API response whenever this is true.\n */\n isLocked: boolean;\n /** BOFF-4107 — INTERACTIVE lessons: language slug the workspace runs (e.g. \"python\"). */\n language?: string | null;\n /** INTERACTIVE lessons: code pre-filled in the editor (null when locked). */\n starterCode?: string | null;\n /** INTERACTIVE lessons: default stdin for a run (null when locked). */\n sampleInput?: string | null;\n /** INTERACTIVE lessons: output a correct program prints; a matching run completes the lesson. */\n expectedOutput?: string | null;\n /** QUIZ lessons: questions without the answer key (empty when locked). */\n quizQuestions?: TecQuizQuestion[];\n /** True when the signed-in learner has completed this lesson. */\n completedByMe?: boolean;\n /** Federated wspace-files-svc File (VIDEO lessons); null when locked. */\n file?: {\n id: string;\n name?: string | null;\n mimeType?: string | null;\n downloadUrl?: string | null;\n } | null;\n course?: TecCourse | null;\n createdAt: string;\n updatedAt: string;\n}\n\n/** BOFF-4107: a quiz question as a learner sees it (no answer key). */\nexport interface TecQuizQuestion {\n prompt: string;\n options: string[];\n}\n\n/** BOFF-4107: a quiz question with its answer key (lesson-author view). */\nexport interface TecQuizQuestionKey extends TecQuizQuestion {\n correctOption: number;\n explanation?: string | null;\n}\n\nexport type TecLessonRunStatus = 'COMPLETED' | 'WARMING' | 'UNAVAILABLE';\n\nexport interface TecLessonCodeRun {\n lessonId: string;\n status: TecLessonRunStatus;\n language: string;\n stdout: string;\n stderr: string;\n exitCode: number;\n durationMs: number;\n timedOut: boolean;\n memoryLimitExceeded: boolean;\n compileFailed: boolean;\n matchedExpected?: boolean | null;\n lessonCompleted: boolean;\n enrollment?: TecEnrollment | null;\n}\n\nexport interface TecQuizResult {\n lessonId: string;\n score: number;\n correctCount: number;\n totalCount: number;\n passed: boolean;\n passingScore: number;\n results: {\n index: number;\n correct: boolean;\n selectedOption?: number | null;\n correctOption: number;\n explanation?: string | null;\n }[];\n enrollment?: TecEnrollment | null;\n}\n\nexport interface TecEnrollment {\n id: string;\n courseId: string;\n learnerId: string;\n status: TecEnrollmentStatus;\n progressPct: number;\n lessonsCompleted: number;\n startedAt: string;\n lastActiveAt?: string | null;\n completedAt?: string | null;\n createdAt: string;\n updatedAt: string;\n course?: TecCourse | null;\n /** BOFF-4107: first lesson not yet completed; null when every lesson is done. */\n nextLessonId?: string | null;\n /** BOFF-10481: the canonical Buildmyiq enrollment this view was read from. */\n buiEnrollmentId?: string | null;\n}\n\nexport interface TecCertificate {\n id: string;\n courseId: string;\n learnerId: string;\n serial: string;\n score: number;\n status: TecCertificateStatus;\n issuedAt: string;\n revokedAt?: string | null;\n revokeReason?: string | null;\n createdAt: string;\n course?: TecCourse | null;\n /** BOFF-10481: the canonical Buildmyiq certificate this view was read from. */\n buiCertificateId?: string | null;\n}\n\nexport interface TecProblem {\n id: string;\n slug: string;\n title: string;\n statement?: string | null;\n topic?: string | null;\n difficulty: TecProblemDifficulty;\n status: TecProblemStatus;\n tags: string[];\n languages: string[];\n companies: string[];\n roles: string[];\n interviewFrequency?: TecInterviewFrequency | null;\n points: number;\n starterCode?: string | null;\n /** BOFF-4416: true when this problem requires a paid TechnoSpam plan. */\n isPremiumOnly: boolean;\n /**\n * BOFF-4416: computed for the calling workspace — true when this problem\n * is `isPremiumOnly` and the workspace has no paid plan. `statement` /\n * `starterCode` are already null and `publicTestCases` already empty in\n * the API response whenever this is true.\n */\n isLocked: boolean;\n publishedAt?: string | null;\n createdAt: string;\n updatedAt: string;\n submissions?: TecSubmission[];\n /** Worked-example fixtures the grader runs against; PUBLIC cases only. */\n publicTestCases?: TecProblemTestCase[];\n}\n\n/** Visibility of a problem test case. Mirrors TecTestCaseVisibility. */\nexport type TecTestCaseVisibility = 'PUBLIC' | 'HIDDEN';\n\n/** Per-test-case outcome of a graded submission. Mirrors TecTestCaseStatus. */\nexport type TecTestCaseStatus =\n 'PASSED' | 'WRONG_ANSWER' | 'RUNTIME_ERROR' | 'TIME_LIMIT_EXCEEDED' | 'COMPILE_ERROR' | 'SKIPPED';\n\n/**\n * A worked-example fixture shown on the problem page. Only PUBLIC cases are\n * ever returned to a learner, so `input`/`expectedOutput` are always present\n * in practice; they stay optional because the schema allows null for the\n * author-facing redacted shape.\n */\nexport interface TecProblemTestCase {\n id: string;\n problemId: string;\n name: string;\n ordering: number;\n visibility: TecTestCaseVisibility;\n timeLimitMs: number;\n input?: string | null;\n expectedOutput?: string | null;\n}\n\n/** Grading detail for one test case of a submission. */\nexport interface TecTestCaseResult {\n testCaseId: string;\n name: string;\n visibility: TecTestCaseVisibility;\n status: TecTestCaseStatus;\n passed: boolean;\n durationMs: number;\n input?: string | null;\n expectedOutput?: string | null;\n actualOutput?: string | null;\n}\n\n/** Outcome of one tecRunSolution call (BOFF-4078). */\nexport type TecSolutionRunStatus = 'COMPLETED' | 'WARMING' | 'UNAVAILABLE';\n\n/** Raw output of a run against custom stdin. */\nexport interface TecSolutionRunOutput {\n stdin: string;\n stdout: string;\n stderr: string;\n exitCode: number;\n durationMs: number;\n timedOut: boolean;\n memoryLimitExceeded: boolean;\n compileFailed: boolean;\n}\n\n/**\n * A scratch run of the learner's code (the editor's Run button): never stored\n * as a submission and never awards points. Without custom stdin it runs the\n * PUBLIC sample cases only.\n */\nexport interface TecSolutionRun {\n problemId: string;\n language: string;\n status: TecSolutionRunStatus;\n verdict?: TecSubmissionVerdict | null;\n runtimeMs: number;\n passedCount: number;\n totalCount: number;\n stderr?: string | null;\n testResults: TecTestCaseResult[];\n customOutput?: TecSolutionRunOutput | null;\n}\n\nexport interface TecSubmission {\n id: string;\n problemId: string;\n learnerId: string;\n language: string;\n code: string;\n verdict: TecSubmissionVerdict;\n runtimeMs: number;\n pointsAwarded: number;\n passedCount: number;\n totalCount: number;\n stderr?: string | null;\n testResults: TecTestCaseResult[];\n judgedAt?: string | null;\n createdAt: string;\n updatedAt: string;\n problem?: TecProblem | null;\n}\n\nexport interface TecMentor {\n id: string;\n userId: string;\n headline: string;\n role?: string | null;\n bio?: string | null;\n expertise: string[];\n hourlyRate: number;\n currency: string;\n status: TecMentorStatus;\n isAcceptingMentees: boolean;\n ratingAvg: number;\n sessionsCount: number;\n createdAt: string;\n updatedAt: string;\n sessions?: TecMentorSession[];\n}\n\nexport interface TecMentorSession {\n id: string;\n mentorId: string;\n menteeId: string;\n topic: string;\n agenda?: string | null;\n scheduledAt: string;\n durationMinutes: number;\n status: TecSessionStatus;\n meetingUrl?: string | null;\n /** Snapshotted from the mentor's rate at booking time (smallest currency unit). */\n costAmount: number;\n currency: string;\n platformFeeAmount: number;\n rating?: number | null;\n feedback?: string | null;\n completedAt?: string | null;\n cancelledAt?: string | null;\n createdAt: string;\n updatedAt: string;\n mentor?: TecMentor | null;\n}\n\n/** Read-only pricing preview for a not-yet-booked session (see tecCalculateSessionCost). */\nexport interface TecSessionCostEstimate {\n mentorId: string;\n durationMinutes: number;\n hourlyRate: number;\n currency: string;\n costAmount: number;\n platformFeeAmount: number;\n platformFeeBps: number;\n netAmount: number;\n}\n\n/** Calculation/ledger-only record of what a mentor is owed for a completed session (see tecMyPayouts). */\nexport type TecPayoutStatus = 'PENDING' | 'PAID';\n\nexport interface TecMentorPayout {\n id: string;\n mentorId: string;\n sessionId: string;\n grossAmount: number;\n platformFeeAmount: number;\n netAmount: number;\n currency: string;\n status: TecPayoutStatus;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface TecHackathon {\n id: string;\n slug: string;\n title: string;\n theme?: string | null;\n description?: string | null;\n status: TecHackathonStatus;\n startAt: string;\n endAt: string;\n submissionDeadline?: string | null;\n prizePool: number;\n currency: string;\n maxTeamSize: number;\n tags: string[];\n createdAt: string;\n updatedAt: string;\n teams?: TecHackathonTeam[];\n}\n\nexport interface TecHackathonTeam {\n id: string;\n hackathonId: string;\n name: string;\n captainId: string;\n memberIds: string[];\n projectName?: string | null;\n projectUrl?: string | null;\n repoUrl?: string | null;\n pitch?: string | null;\n status: TecTeamSubmissionStatus;\n score: number;\n submittedAt?: string | null;\n createdAt: string;\n updatedAt: string;\n hackathon?: TecHackathon | null;\n}\n\nexport interface TecPageInfo {\n cursor?: string | null;\n hasMore: boolean;\n}\n\nexport interface PaginationInput {\n limit?: number;\n cursor?: string | null;\n}\n\n/** Build a pagination arg that NEVER sends cursor:null (page-1 500 guard). */\nexport function pageArg(limit = 50, cursor?: string | null): { limit: number; cursor?: string } {\n return cursor ? { limit, cursor } : { limit };\n}\n\n/* -------------------------------------------------------------- fragments */\n\nconst COURSE_FIELDS = gql`\n fragment CourseFields on TecCourse {\n id\n slug\n title\n summary\n topic\n level\n status\n tags\n estimatedMinutes\n instructorId\n isPremiumOnly\n isLocked\n buiCourseId\n publishedAt\n createdAt\n updatedAt\n }\n`;\n\nconst LESSON_FIELDS = gql`\n fragment LessonFields on TecLesson {\n id\n courseId\n title\n kind\n content\n position\n estimatedMinutes\n isPreview\n isLocked\n language\n starterCode\n sampleInput\n expectedOutput\n quizQuestions {\n prompt\n options\n }\n completedByMe\n createdAt\n updatedAt\n }\n`;\n\nconst ENROLLMENT_FIELDS = gql`\n fragment EnrollmentFields on TecEnrollment {\n id\n courseId\n learnerId\n status\n progressPct\n lessonsCompleted\n startedAt\n lastActiveAt\n completedAt\n createdAt\n updatedAt\n }\n`;\n\nconst PROBLEM_FIELDS = gql`\n fragment ProblemFields on TecProblem {\n id\n slug\n title\n statement\n topic\n difficulty\n status\n tags\n languages\n companies\n roles\n interviewFrequency\n points\n starterCode\n isPremiumOnly\n isLocked\n publishedAt\n createdAt\n updatedAt\n }\n`;\n\nconst SUBMISSION_FIELDS = gql`\n fragment SubmissionFields on TecSubmission {\n id\n problemId\n learnerId\n language\n code\n verdict\n runtimeMs\n pointsAwarded\n passedCount\n totalCount\n stderr\n testResults {\n testCaseId\n name\n visibility\n status\n passed\n durationMs\n input\n expectedOutput\n actualOutput\n }\n judgedAt\n createdAt\n updatedAt\n }\n`;\n\nconst PROBLEM_TEST_CASE_FIELDS = gql`\n fragment ProblemTestCaseFields on TecProblemTestCase {\n id\n problemId\n name\n ordering\n visibility\n timeLimitMs\n input\n expectedOutput\n }\n`;\n\nconst MENTOR_FIELDS = gql`\n fragment MentorFields on TecMentor {\n id\n userId\n headline\n role\n bio\n expertise\n hourlyRate\n currency\n status\n isAcceptingMentees\n ratingAvg\n sessionsCount\n createdAt\n updatedAt\n }\n`;\n\nconst SESSION_FIELDS = gql`\n fragment SessionFields on TecMentorSession {\n id\n mentorId\n menteeId\n topic\n agenda\n scheduledAt\n durationMinutes\n status\n meetingUrl\n costAmount\n currency\n platformFeeAmount\n rating\n feedback\n completedAt\n cancelledAt\n createdAt\n updatedAt\n }\n`;\n\nconst HACKATHON_FIELDS = gql`\n fragment HackathonFields on TecHackathon {\n id\n slug\n title\n theme\n description\n status\n startAt\n endAt\n submissionDeadline\n prizePool\n currency\n maxTeamSize\n tags\n createdAt\n updatedAt\n }\n`;\n\nconst TEAM_FIELDS = gql`\n fragment TeamFields on TecHackathonTeam {\n id\n hackathonId\n name\n captainId\n memberIds\n projectName\n projectUrl\n repoUrl\n pitch\n status\n score\n submittedAt\n createdAt\n updatedAt\n }\n`;\n\nconst CERTIFICATE_FIELDS = gql`\n fragment CertificateFields on TecCertificate {\n id\n courseId\n learnerId\n serial\n score\n status\n issuedAt\n revokedAt\n revokeReason\n createdAt\n }\n`;\n\n/* --------------------------------------------------------------- queries */\n\nconst LIST_COURSES = gql`\n ${COURSE_FIELDS}\n query TecListCourses(\n $level: TecCourseLevel\n $status: TecCourseStatus\n $pagination: PaginationInput\n ) {\n tecListCourses(level: $level, status: $status, pagination: $pagination) {\n items {\n ...CourseFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst GET_COURSE = gql`\n ${COURSE_FIELDS}\n ${LESSON_FIELDS}\n ${ENROLLMENT_FIELDS}\n query TecCourse($id: ID!) {\n tecCourse(id: $id) {\n ...CourseFields\n lessons {\n ...LessonFields\n }\n enrollments {\n ...EnrollmentFields\n }\n }\n }\n`;\n\nconst LIST_ENROLLMENTS = gql`\n ${ENROLLMENT_FIELDS}\n ${COURSE_FIELDS}\n query TecListEnrollments(\n $learnerId: ID\n $courseId: ID\n $pagination: PaginationInput\n $startDate: DateTime\n $endDate: DateTime\n ) {\n tecListEnrollments(\n learnerId: $learnerId\n courseId: $courseId\n pagination: $pagination\n startDate: $startDate\n endDate: $endDate\n ) {\n items {\n ...EnrollmentFields\n course {\n ...CourseFields\n }\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst LIST_CERTIFICATES = gql`\n ${CERTIFICATE_FIELDS}\n ${COURSE_FIELDS}\n query TecListCertificates($learnerId: ID, $courseId: ID) {\n tecListCertificates(learnerId: $learnerId, courseId: $courseId) {\n ...CertificateFields\n course {\n ...CourseFields\n }\n }\n }\n`;\n\n/**\n * The calling learner's OWN learning records, read from the canonical\n * Buildmyiq self-scoped fields (BOFF-10481, OVL-24 phase 2c). They are\n * `@rbac(scopeType: \"self\")`: the resolver forces learnerId = ctx.actor.id, so\n * no learnerId is ever sent, and `source: TECHNOSPAM` keeps the workspace's\n * native Buildmyiq records out. The Technospam view rides on the overlay\n * fields; canonicalLearning.ts folds each row into the Tec* view model.\n *\n * `courseId` takes the CANONICAL course id (TecCourse.buiCourseId).\n */\nconst MY_ENROLLMENTS = gql`\n ${COURSE_FIELDS}\n query BuiMyTechnospamEnrollments($courseId: ID, $pagination: PaginationInput) {\n buiMyEnrollments(source: TECHNOSPAM, courseId: $courseId, pagination: $pagination) {\n items {\n id\n status\n progressPct\n enrolledAt\n startedAt\n lastActiveAt\n completedAt\n createdAt\n updatedAt\n technospamNextLessonId\n technospamEnrollment {\n id\n courseId\n learnerId\n lessonsCompleted\n course {\n ...CourseFields\n }\n }\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst MY_CERTIFICATES = gql`\n ${COURSE_FIELDS}\n query BuiMyTechnospamCertificates($courseId: ID, $pagination: PaginationInput) {\n buiMyCertificates(source: TECHNOSPAM, courseId: $courseId, pagination: $pagination) {\n items {\n id\n serial\n status\n issuedAt\n revokedAt\n finalScore\n createdAt\n technospamRevokeReason\n technospamCertificate {\n id\n courseId\n learnerId\n score\n course {\n ...CourseFields\n }\n }\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst LIST_PROBLEMS = gql`\n ${PROBLEM_FIELDS}\n query TecListProblems(\n $difficulty: TecProblemDifficulty\n $status: TecProblemStatus\n $topic: String\n $company: String\n $role: String\n $interviewFrequency: TecInterviewFrequency\n $pagination: PaginationInput\n ) {\n tecListProblems(\n difficulty: $difficulty\n status: $status\n topic: $topic\n company: $company\n role: $role\n interviewFrequency: $interviewFrequency\n pagination: $pagination\n ) {\n items {\n ...ProblemFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst GET_PROBLEM = gql`\n ${PROBLEM_FIELDS}\n ${SUBMISSION_FIELDS}\n ${PROBLEM_TEST_CASE_FIELDS}\n query TecProblem($id: ID!) {\n tecProblem(id: $id) {\n ...ProblemFields\n publicTestCases {\n ...ProblemTestCaseFields\n }\n submissions {\n ...SubmissionFields\n }\n }\n }\n`;\n\nconst REGRADE_SUBMISSION = gql`\n ${SUBMISSION_FIELDS}\n mutation TecRegradeSubmission($id: ID!) {\n tecRegradeSubmission(id: $id) {\n ...SubmissionFields\n }\n }\n`;\n\nconst LIST_SUBMISSIONS = gql`\n ${SUBMISSION_FIELDS}\n ${PROBLEM_FIELDS}\n query TecListSubmissions(\n $learnerId: ID\n $problemId: ID\n $pagination: PaginationInput\n $startDate: DateTime\n $endDate: DateTime\n ) {\n tecListSubmissions(\n learnerId: $learnerId\n problemId: $problemId\n pagination: $pagination\n startDate: $startDate\n endDate: $endDate\n ) {\n items {\n ...SubmissionFields\n problem {\n ...ProblemFields\n }\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst LIST_MENTORS = gql`\n ${MENTOR_FIELDS}\n query TecListMentors($status: TecMentorStatus, $pagination: PaginationInput) {\n tecListMentors(status: $status, pagination: $pagination) {\n items {\n ...MentorFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst GET_MENTOR = gql`\n ${MENTOR_FIELDS}\n ${SESSION_FIELDS}\n query TecMentor($id: ID!) {\n tecMentor(id: $id) {\n ...MentorFields\n sessions {\n ...SessionFields\n }\n }\n }\n`;\n\nconst LIST_SESSIONS = gql`\n ${SESSION_FIELDS}\n ${MENTOR_FIELDS}\n query TecListSessions(\n $mentorId: ID\n $menteeId: ID\n $status: TecSessionStatus\n $pagination: PaginationInput\n ) {\n tecListSessions(\n mentorId: $mentorId\n menteeId: $menteeId\n status: $status\n pagination: $pagination\n ) {\n items {\n ...SessionFields\n mentor {\n ...MentorFields\n }\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst CALCULATE_SESSION_COST = gql`\n query TecCalculateSessionCost($mentorId: ID!, $durationMinutes: Int) {\n tecCalculateSessionCost(mentorId: $mentorId, durationMinutes: $durationMinutes) {\n mentorId\n durationMinutes\n hourlyRate\n currency\n costAmount\n platformFeeAmount\n platformFeeBps\n netAmount\n }\n }\n`;\n\nconst PAYOUT_FIELDS = gql`\n fragment PayoutFields on TecMentorPayout {\n id\n mentorId\n sessionId\n grossAmount\n platformFeeAmount\n netAmount\n currency\n status\n createdAt\n updatedAt\n }\n`;\n\nconst MY_PAYOUTS = gql`\n ${PAYOUT_FIELDS}\n query TecMyPayouts($status: TecPayoutStatus, $pagination: PaginationInput) {\n tecMyPayouts(status: $status, pagination: $pagination) {\n items {\n ...PayoutFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst LIST_HACKATHONS = gql`\n ${HACKATHON_FIELDS}\n query TecListHackathons($status: TecHackathonStatus, $pagination: PaginationInput) {\n tecListHackathons(status: $status, pagination: $pagination) {\n items {\n ...HackathonFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst GET_HACKATHON = gql`\n ${HACKATHON_FIELDS}\n ${TEAM_FIELDS}\n query TecHackathon($id: ID!) {\n tecHackathon(id: $id) {\n ...HackathonFields\n teams {\n ...TeamFields\n }\n }\n }\n`;\n\nconst LIST_TEAMS = gql`\n ${TEAM_FIELDS}\n query TecListTeams($hackathonId: ID!, $pagination: PaginationInput) {\n tecListTeams(hackathonId: $hackathonId, pagination: $pagination) {\n items {\n ...TeamFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst LIST_LESSONS = gql`\n ${LESSON_FIELDS}\n query TecListLessons($courseId: ID!, $pagination: PaginationInput) {\n tecListLessons(courseId: $courseId, pagination: $pagination) {\n items {\n ...LessonFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\n/* ------------------------------------------------------- response typings */\n\ntype Conn<K extends string, T> = Record<K, { items: T[]; pageInfo: TecPageInfo }>;\n\n/* ---------------------------------------------------------- query hooks */\n\nexport const useCoursesQuery = (vars: {\n level?: TecCourseLevel;\n status?: TecCourseStatus;\n pagination?: { limit: number; cursor?: string };\n}) =>\n useQuery<Conn<'tecListCourses', TecCourse>>(LIST_COURSES, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useCourseQuery = (id: string) =>\n useQuery<{ tecCourse: TecCourse | null }>(GET_COURSE, {\n variables: { id },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useEnrollmentsQuery = (vars: {\n learnerId?: string;\n courseId?: string;\n startDate?: string;\n endDate?: string;\n}) =>\n useQuery<Conn<'tecListEnrollments', TecEnrollment>>(LIST_ENROLLMENTS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\n/**\n * Cross-learner certificate read (instructor/admin). Requires the `list-all`\n * action — learner surfaces want `useMyCertificatesQuery` instead.\n */\nexport const useCertificatesQuery = (vars: { learnerId?: string; courseId?: string }) =>\n useQuery<{ tecListCertificates: TecCertificate[] }>(LIST_CERTIFICATES, {\n variables: vars,\n fetchPolicy: 'cache-and-network',\n });\n\n/**\n * Which of the learner's own records to read. `buiCourseId` narrows on the\n * server (canonical id); `technospamCourseId` narrows on the client, so a\n * course page gets the right rows even before its canonical id is known.\n */\nexport interface MyLearningScope {\n buiCourseId?: string | null;\n technospamCourseId?: string;\n limit?: number;\n skip?: boolean;\n}\n\n/** The signed-in learner's own TechnoSpam enrollments (canonical, self-scoped). */\nexport function useMyEnrollmentsQuery(scope: MyLearningScope = {}) {\n const result = useQuery<{\n buiMyEnrollments: { items: BuiMyEnrollmentRow[]; pageInfo: TecPageInfo };\n }>(MY_ENROLLMENTS, {\n variables: {\n ...(scope.buiCourseId ? { courseId: scope.buiCourseId } : {}),\n pagination: pageArg(scope.limit ?? 100),\n },\n skip: scope.skip,\n fetchPolicy: 'cache-and-network',\n });\n const items = result.data?.buiMyEnrollments?.items;\n const enrollments = useMemo(\n () => forTechnospamCourse(mapRows(items, toTecEnrollment), scope.technospamCourseId),\n [items, scope.technospamCourseId]\n );\n return { ...result, enrollments };\n}\n\n/** The signed-in learner's own TechnoSpam certificates (canonical, self-scoped). */\nexport function useMyCertificatesQuery(scope: MyLearningScope = {}) {\n const result = useQuery<{\n buiMyCertificates: { items: BuiMyCertificateRow[]; pageInfo: TecPageInfo };\n }>(MY_CERTIFICATES, {\n variables: {\n ...(scope.buiCourseId ? { courseId: scope.buiCourseId } : {}),\n pagination: pageArg(scope.limit ?? 100),\n },\n skip: scope.skip,\n fetchPolicy: 'cache-and-network',\n });\n const items = result.data?.buiMyCertificates?.items;\n const certificates = useMemo(\n () => forTechnospamCourse(mapRows(items, toTecCertificate), scope.technospamCourseId),\n [items, scope.technospamCourseId]\n );\n return { ...result, certificates };\n}\n\nexport const useProblemsQuery = (vars: {\n difficulty?: TecProblemDifficulty;\n status?: TecProblemStatus;\n topic?: string;\n company?: string;\n role?: string;\n interviewFrequency?: TecInterviewFrequency;\n pagination?: { limit: number; cursor?: string };\n}) =>\n useQuery<Conn<'tecListProblems', TecProblem>>(LIST_PROBLEMS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useProblemQuery = (id: string) =>\n useQuery<{ tecProblem: TecProblem | null }>(GET_PROBLEM, {\n variables: { id },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useSubmissionsQuery = (vars: {\n learnerId?: string;\n problemId?: string;\n startDate?: string;\n endDate?: string;\n}) =>\n useQuery<Conn<'tecListSubmissions', TecSubmission>>(LIST_SUBMISSIONS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\n/**\n * Slim submissions page for deriving the learner's solved problems: only the\n * fields the lookup needs, so walking every page stays cheap.\n */\nconst LIST_SOLVED_SUBMISSIONS = gql`\n query TecListSolvedSubmissions($learnerId: ID!, $pagination: PaginationInput) {\n tecListSubmissions(learnerId: $learnerId, pagination: $pagination) {\n items {\n id\n problemId\n verdict\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\n/** The svc clamps list limits to 100 (`MAX_LIMIT`). */\nexport const SOLVED_SCAN_PAGE_SIZE = 100;\n/** Upper bound on pages walked (2,000 submissions) so a runaway cursor cannot loop forever. */\nexport const SOLVED_SCAN_MAX_PAGES = 20;\n\nconst NO_SOLVED: ReadonlySet<string> = new Set<string>();\n\ntype SolvedSubmissionsPage = {\n tecListSubmissions: {\n items: Array<Pick<TecSubmission, 'id' | 'problemId' | 'verdict'>>;\n pageInfo: { cursor?: string | null; hasMore: boolean };\n };\n};\n\n/**\n * Ids of the problems the learner has an ACCEPTED submission for (BOFF-4077).\n *\n * The first page of `tecListSubmissions` is not enough: a learner with more\n * than one page of submissions would see older solves reported as unsolved.\n * This follows the cursor across every page (bounded by\n * `SOLVED_SCAN_MAX_PAGES`), publishing the set as each page lands. Without a\n * learner id it returns an empty set rather than listing the whole\n * workspace's submissions, which would mark other learners' solves as ours.\n */\nexport function useSolvedProblemIds(learnerId: string | undefined): ReadonlySet<string> {\n const client = useApolloClient();\n const [solved, setSolved] = useState<{ learnerId: string; ids: ReadonlySet<string> } | null>(\n null\n );\n\n useEffect(() => {\n if (!learnerId) return undefined;\n let cancelled = false;\n const ids = new Set<string>();\n void (async () => {\n let cursor: string | undefined;\n for (let page = 0; page < SOLVED_SCAN_MAX_PAGES; page += 1) {\n let conn: SolvedSubmissionsPage['tecListSubmissions'] | undefined;\n try {\n const res = await client.query<SolvedSubmissionsPage>({\n query: LIST_SOLVED_SUBMISSIONS,\n variables: { learnerId, pagination: pageArg(SOLVED_SCAN_PAGE_SIZE, cursor) },\n fetchPolicy: 'network-only',\n });\n conn = res.data?.tecListSubmissions;\n } catch {\n // A failed page keeps whatever was already found; the column only\n // under-reports, it never claims a solve that did not happen.\n return;\n }\n if (cancelled || !conn) return;\n for (const submission of conn.items) {\n if (submission.verdict === 'ACCEPTED') ids.add(submission.problemId);\n }\n setSolved({ learnerId, ids: new Set(ids) });\n if (!conn.pageInfo.hasMore || !conn.pageInfo.cursor) return;\n cursor = conn.pageInfo.cursor;\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [client, learnerId]);\n\n return learnerId && solved?.learnerId === learnerId ? solved.ids : NO_SOLVED;\n}\n\nexport const useMentorsQuery = (vars: {\n status?: TecMentorStatus;\n pagination?: { limit: number; cursor?: string };\n}) =>\n useQuery<Conn<'tecListMentors', TecMentor>>(LIST_MENTORS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useMentorQuery = (id: string) =>\n useQuery<{ tecMentor: TecMentor | null }>(GET_MENTOR, {\n variables: { id },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useSessionsQuery = (vars: {\n mentorId?: string;\n menteeId?: string;\n status?: TecSessionStatus;\n pagination?: { limit: number; cursor?: string };\n}) =>\n useQuery<Conn<'tecListSessions', TecMentorSession>>(LIST_SESSIONS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\n/**\n * Live pricing preview for a not-yet-booked session -- refetches whenever\n * mentorId/durationMinutes change (Apollo re-runs on variable change), so\n * the booking form always shows the cost that would actually be snapshotted\n * onto the session by tecBookSession. `skip` while mentorId is empty.\n */\nexport const useSessionCostEstimate = (mentorId: string, durationMinutes: number) =>\n useQuery<{ tecCalculateSessionCost: TecSessionCostEstimate }>(CALCULATE_SESSION_COST, {\n variables: { mentorId, durationMinutes },\n skip: !mentorId || !Number.isFinite(durationMinutes) || durationMinutes <= 0,\n fetchPolicy: 'cache-and-network',\n });\n\n/** The calling mentor's own payout ledger (calculation-only -- see BOFF-4433). */\nexport const useMyPayoutsQuery = (\n vars: { status?: TecPayoutStatus; pagination?: { limit: number; cursor?: string } } = {}\n) =>\n useQuery<Conn<'tecMyPayouts', TecMentorPayout>>(MY_PAYOUTS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useHackathonsQuery = (vars: {\n status?: TecHackathonStatus;\n pagination?: { limit: number; cursor?: string };\n}) =>\n useQuery<Conn<'tecListHackathons', TecHackathon>>(LIST_HACKATHONS, {\n variables: { pagination: pageArg(50), ...vars },\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useHackathonQuery = (id: string) =>\n useQuery<{ tecHackathon: TecHackathon | null }>(GET_HACKATHON, {\n variables: { id },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useTeamsQuery = (\n hackathonId: string,\n pagination?: { limit: number; cursor?: string }\n) =>\n useQuery<Conn<'tecListTeams', TecHackathonTeam>>(LIST_TEAMS, {\n variables: { hackathonId, pagination: pagination ?? pageArg(50) },\n skip: !hackathonId,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useLessonsQuery = (\n courseId: string,\n pagination?: { limit: number; cursor?: string }\n) =>\n useQuery<Conn<'tecListLessons', TecLesson>>(LIST_LESSONS, {\n variables: { courseId, pagination: pagination ?? pageArg(50) },\n skip: !courseId,\n fetchPolicy: 'cache-and-network',\n });\n\n/* ------------------------------------------------------------ mutations */\n\nconst onMutationError = (error: ErrorLike): void => {\n notify.error('Something went wrong', userMessageFromError(error));\n};\n\nconst REFETCH = {\n courses: 'TecListCourses',\n enrollments: 'TecListEnrollments',\n /** BOFF-10481: the learner's own records, read from the canonical Buildmyiq fields. */\n myEnrollments: 'BuiMyTechnospamEnrollments',\n myCertificates: 'BuiMyTechnospamCertificates',\n problems: 'TecListProblems',\n submissions: 'TecListSubmissions',\n mentors: 'TecListMentors',\n sessions: 'TecListSessions',\n hackathons: 'TecListHackathons',\n} as const;\n\nconst ENROLL = gql`\n ${ENROLLMENT_FIELDS}\n mutation TecEnroll($input: TecEnrollInput!) {\n tecEnroll(input: $input) {\n ...EnrollmentFields\n }\n }\n`;\n\nconst SUBMIT_SOLUTION = gql`\n ${SUBMISSION_FIELDS}\n mutation TecSubmitSolution($input: TecSubmitSolutionInput!) {\n tecSubmitSolution(input: $input) {\n ...SubmissionFields\n }\n }\n`;\n\nconst RUN_SOLUTION = gql`\n mutation TecRunSolution($input: TecRunSolutionInput!) {\n tecRunSolution(input: $input) {\n problemId\n language\n status\n verdict\n runtimeMs\n passedCount\n totalCount\n stderr\n testResults {\n testCaseId\n name\n visibility\n status\n passed\n durationMs\n input\n expectedOutput\n actualOutput\n }\n customOutput {\n stdin\n stdout\n stderr\n exitCode\n durationMs\n timedOut\n memoryLimitExceeded\n compileFailed\n }\n }\n }\n`;\n\nconst BOOK_SESSION = gql`\n ${SESSION_FIELDS}\n mutation TecBookSession($input: TecBookSessionInput!) {\n tecBookSession(input: $input) {\n ...SessionFields\n }\n }\n`;\n\nconst CANCEL_SESSION = gql`\n ${SESSION_FIELDS}\n mutation TecCancelSession($id: ID!) {\n tecCancelSession(id: $id) {\n ...SessionFields\n }\n }\n`;\n\nconst BECOME_MENTOR = gql`\n ${MENTOR_FIELDS}\n mutation TecBecomeMentor($input: TecCreateMentorInput!) {\n tecBecomeMentor(input: $input) {\n ...MentorFields\n }\n }\n`;\n\nconst REGISTER_TEAM = gql`\n ${TEAM_FIELDS}\n mutation TecRegisterTeam($input: TecRegisterTeamInput!) {\n tecRegisterTeam(input: $input) {\n ...TeamFields\n }\n }\n`;\n\nconst SUBMIT_PROJECT = gql`\n ${TEAM_FIELDS}\n mutation TecSubmitProject($input: TecSubmitProjectInput!) {\n tecSubmitProject(input: $input) {\n ...TeamFields\n }\n }\n`;\n\n/* ------- admin / authoring mutations ------- */\n\nconst CREATE_COURSE = gql`\n ${COURSE_FIELDS}\n mutation TecCreateCourse($input: TecCreateCourseInput!) {\n tecCreateCourse(input: $input) {\n ...CourseFields\n }\n }\n`;\n\nconst PUBLISH_COURSE = gql`\n ${COURSE_FIELDS}\n mutation TecPublishCourse($id: ID!) {\n tecPublishCourse(id: $id) {\n ...CourseFields\n }\n }\n`;\n\nconst ARCHIVE_COURSE = gql`\n mutation TecArchiveCourse($id: ID!) {\n tecArchiveCourse(id: $id)\n }\n`;\n\nconst CREATE_LESSON = gql`\n ${LESSON_FIELDS}\n mutation TecCreateLesson($input: TecCreateLessonInput!) {\n tecCreateLesson(input: $input) {\n ...LessonFields\n }\n }\n`;\n\nconst UPDATE_LESSON = gql`\n ${LESSON_FIELDS}\n mutation TecUpdateLesson($id: ID!, $input: TecUpdateLessonInput!) {\n tecUpdateLesson(id: $id, input: $input) {\n ...LessonFields\n }\n }\n`;\n\nconst CREATE_PROBLEM = gql`\n ${PROBLEM_FIELDS}\n mutation TecCreateProblem($input: TecCreateProblemInput!) {\n tecCreateProblem(input: $input) {\n ...ProblemFields\n }\n }\n`;\n\nconst PUBLISH_PROBLEM = gql`\n ${PROBLEM_FIELDS}\n mutation TecPublishProblem($id: ID!) {\n tecPublishProblem(id: $id) {\n ...ProblemFields\n }\n }\n`;\n\nconst CREATE_HACKATHON = gql`\n ${HACKATHON_FIELDS}\n mutation TecCreateHackathon($input: TecCreateHackathonInput!) {\n tecCreateHackathon(input: $input) {\n ...HackathonFields\n }\n }\n`;\n\nconst OPEN_REGISTRATION = gql`\n ${HACKATHON_FIELDS}\n mutation TecOpenRegistration($id: ID!) {\n tecOpenRegistration(id: $id) {\n ...HackathonFields\n }\n }\n`;\n\nconst APPROVE_MENTOR = gql`\n ${MENTOR_FIELDS}\n mutation TecApproveMentor($id: ID!) {\n tecApproveMentor(id: $id) {\n ...MentorFields\n }\n }\n`;\n\nconst SCORE_TEAM = gql`\n ${TEAM_FIELDS}\n mutation TecScoreTeam($id: ID!, $score: Int!, $winner: Boolean) {\n tecScoreTeam(id: $id, score: $score, winner: $winner) {\n ...TeamFields\n }\n }\n`;\n\nconst CONFIRM_SESSION = gql`\n ${SESSION_FIELDS}\n mutation TecConfirmSession($id: ID!, $meetingUrl: String) {\n tecConfirmSession(id: $id, meetingUrl: $meetingUrl) {\n ...SessionFields\n }\n }\n`;\n\nconst COMPLETE_SESSION = gql`\n ${SESSION_FIELDS}\n mutation TecCompleteSession($id: ID!, $rating: Int, $feedback: String) {\n tecCompleteSession(id: $id, rating: $rating, feedback: $feedback) {\n ...SessionFields\n }\n }\n`;\n\nconst GRADE_SUBMISSION = gql`\n ${SUBMISSION_FIELDS}\n mutation TecGradeSubmission($id: ID!, $verdict: TecSubmissionVerdict!, $runtimeMs: Int) {\n tecGradeSubmission(id: $id, verdict: $verdict, runtimeMs: $runtimeMs) {\n ...SubmissionFields\n }\n }\n`;\n\nconst ISSUE_CERTIFICATE = gql`\n ${CERTIFICATE_FIELDS}\n mutation TecIssueCertificate($courseId: ID!, $learnerId: ID!, $score: Int!) {\n tecIssueCertificate(courseId: $courseId, learnerId: $learnerId, score: $score) {\n ...CertificateFields\n }\n }\n`;\n\nconst REVOKE_CERTIFICATE = gql`\n ${CERTIFICATE_FIELDS}\n mutation TecRevokeCertificate($id: ID!, $reason: String) {\n tecRevokeCertificate(id: $id, reason: $reason) {\n ...CertificateFields\n }\n }\n`;\n\nconst START_HACKATHON = gql`\n ${HACKATHON_FIELDS}\n mutation TecStartHackathon($id: ID!) {\n tecStartHackathon(id: $id) {\n ...HackathonFields\n }\n }\n`;\n\nconst START_JUDGING = gql`\n ${HACKATHON_FIELDS}\n mutation TecStartJudging($id: ID!) {\n tecStartJudging(id: $id) {\n ...HackathonFields\n }\n }\n`;\n\nconst COMPLETE_HACKATHON = gql`\n ${HACKATHON_FIELDS}\n mutation TecCompleteHackathon($id: ID!) {\n tecCompleteHackathon(id: $id) {\n ...HackathonFields\n }\n }\n`;\n\n/* ---------------------------------------------------------- mutation hooks */\n\nexport const useEnroll = () =>\n useMutation<{ tecEnroll: TecEnrollment }>(ENROLL, {\n onError: onMutationError,\n refetchQueries: [REFETCH.enrollments, REFETCH.myEnrollments, REFETCH.courses],\n });\n\nexport const useSubmitSolution = () =>\n useMutation<{ tecSubmitSolution: TecSubmission }>(SUBMIT_SOLUTION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.submissions],\n });\n\n/**\n * Run a solution against the problem's sample cases (or custom stdin) without\n * submitting it (BOFF-4078). Nothing is stored, so there is nothing to refetch.\n */\nexport const useRunSolution = () =>\n useMutation<{ tecRunSolution: TecSolutionRun }>(RUN_SOLUTION, {\n onError: onMutationError,\n });\n\n/**\n * Re-run the grader for a submission. Used by the problem page when a verdict\n * came back PENDING (grader unavailable at submit time) so a learner can retry\n * without resubmitting identical code.\n */\nexport const useRegradeSubmission = () =>\n useMutation<{ tecRegradeSubmission: TecSubmission }>(REGRADE_SUBMISSION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.submissions],\n });\n\nexport const useBookSession = () =>\n useMutation<{ tecBookSession: TecMentorSession }>(BOOK_SESSION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.sessions, REFETCH.mentors],\n });\n\nexport const useCancelSession = () =>\n useMutation<{ tecCancelSession: TecMentorSession }>(CANCEL_SESSION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.sessions],\n });\n\nexport const useBecomeMentor = () =>\n useMutation<{ tecBecomeMentor: TecMentor }>(BECOME_MENTOR, {\n onError: onMutationError,\n refetchQueries: [REFETCH.mentors],\n });\n\nexport const useRegisterTeam = () =>\n useMutation<{ tecRegisterTeam: TecHackathonTeam }>(REGISTER_TEAM, {\n onError: onMutationError,\n refetchQueries: [REFETCH.hackathons],\n });\n\nexport const useSubmitProject = () =>\n useMutation<{ tecSubmitProject: TecHackathonTeam }>(SUBMIT_PROJECT, {\n onError: onMutationError,\n });\n\nexport const useCreateCourse = () =>\n useMutation<{ tecCreateCourse: TecCourse }>(CREATE_COURSE, {\n onError: onMutationError,\n refetchQueries: [REFETCH.courses],\n });\n\nexport const usePublishCourse = () =>\n useMutation<{ tecPublishCourse: TecCourse }>(PUBLISH_COURSE, {\n onError: onMutationError,\n refetchQueries: [REFETCH.courses],\n });\n\nexport const useArchiveCourse = () =>\n useMutation<{ tecArchiveCourse: boolean }>(ARCHIVE_COURSE, {\n onError: onMutationError,\n refetchQueries: [REFETCH.courses],\n });\n\nexport const useCreateLesson = () =>\n useMutation<{ tecCreateLesson: TecLesson }>(CREATE_LESSON, { onError: onMutationError });\n\n/**\n * Edit an already-authored lesson (title/kind/content/minutes/preview/position).\n * Every field on TecUpdateLessonInput is optional — send only what changed.\n */\nexport const useUpdateLesson = () =>\n useMutation<{ tecUpdateLesson: TecLesson }>(UPDATE_LESSON, { onError: onMutationError });\n\nexport const useCreateProblem = () =>\n useMutation<{ tecCreateProblem: TecProblem }>(CREATE_PROBLEM, {\n onError: onMutationError,\n refetchQueries: [REFETCH.problems],\n });\n\nexport const usePublishProblem = () =>\n useMutation<{ tecPublishProblem: TecProblem }>(PUBLISH_PROBLEM, {\n onError: onMutationError,\n refetchQueries: [REFETCH.problems],\n });\n\nexport const useCreateHackathon = () =>\n useMutation<{ tecCreateHackathon: TecHackathon }>(CREATE_HACKATHON, {\n onError: onMutationError,\n refetchQueries: [REFETCH.hackathons],\n });\n\nexport const useOpenRegistration = () =>\n useMutation<{ tecOpenRegistration: TecHackathon }>(OPEN_REGISTRATION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.hackathons],\n });\n\nexport const useApproveMentor = () =>\n useMutation<{ tecApproveMentor: TecMentor }>(APPROVE_MENTOR, {\n onError: onMutationError,\n refetchQueries: [REFETCH.mentors],\n });\n\nexport const useScoreTeam = () =>\n useMutation<{ tecScoreTeam: TecHackathonTeam }>(SCORE_TEAM, { onError: onMutationError });\n\nexport const useConfirmSession = () =>\n useMutation<{ tecConfirmSession: TecMentorSession }>(CONFIRM_SESSION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.sessions],\n });\n\nexport const useCompleteSession = () =>\n useMutation<{ tecCompleteSession: TecMentorSession }>(COMPLETE_SESSION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.sessions],\n });\n\nexport const useGradeSubmission = () =>\n useMutation<{ tecGradeSubmission: TecSubmission }>(GRADE_SUBMISSION, {\n onError: onMutationError,\n refetchQueries: [REFETCH.submissions],\n });\n\nexport const useIssueCertificate = () =>\n useMutation<{ tecIssueCertificate: TecCertificate }>(ISSUE_CERTIFICATE, {\n onError: onMutationError,\n });\n\nexport const useRevokeCertificate = () =>\n useMutation<{ tecRevokeCertificate: TecCertificate }>(REVOKE_CERTIFICATE, {\n onError: onMutationError,\n });\n\nexport const useStartHackathon = () =>\n useMutation<{ tecStartHackathon: TecHackathon }>(START_HACKATHON, {\n onError: onMutationError,\n refetchQueries: [REFETCH.hackathons],\n });\n\nexport const useStartJudging = () =>\n useMutation<{ tecStartJudging: TecHackathon }>(START_JUDGING, {\n onError: onMutationError,\n refetchQueries: [REFETCH.hackathons],\n });\n\nexport const useCompleteHackathon = () =>\n useMutation<{ tecCompleteHackathon: TecHackathon }>(COMPLETE_HACKATHON, {\n onError: onMutationError,\n refetchQueries: [REFETCH.hackathons],\n });\n\n/* ============================================================ Study Planner\n * BOFF-4415 — deterministic, rule-based goal -> plan scheduler. NOT an LLM\n * feature; see the backend StudyPlanService honesty note. Every query/\n * mutation below is self-scoped (learnerId comes from the authenticated\n * actor server-side) so no learnerId variable is ever sent from the client.\n * ========================================================================= */\n\nexport type TecStudyPlanStatus = 'ACTIVE' | 'COMPLETED' | 'ARCHIVED';\nexport type TecStudyPlanStepKind = 'COURSE' | 'PROBLEM';\nexport type TecStudyPlanStepStatus = 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'SKIPPED';\n\nexport interface TecStudyPlanStep {\n id: string;\n order: number;\n kind: TecStudyPlanStepKind;\n targetId: string;\n title: string;\n scheduledWeek: number;\n status: TecStudyPlanStepStatus;\n rationale: string;\n estimatedMinutes: number;\n completedAt?: string | null;\n}\n\nexport interface TecStudyPlan {\n id: string;\n goal: string;\n topic?: string | null;\n timeframeWeeks: number;\n status: TecStudyPlanStatus;\n targetCompletionDate?: string | null;\n progressPct: number;\n totalSteps: number;\n completedSteps: number;\n createdAt: string;\n steps: TecStudyPlanStep[];\n}\n\nconst STUDY_PLAN_STEP_FIELDS = gql`\n fragment StudyPlanStepFields on TecStudyPlanStep {\n id\n order\n kind\n targetId\n title\n scheduledWeek\n status\n rationale\n estimatedMinutes\n completedAt\n }\n`;\n\nconst STUDY_PLAN_FIELDS = gql`\n ${STUDY_PLAN_STEP_FIELDS}\n fragment StudyPlanFields on TecStudyPlan {\n id\n goal\n topic\n timeframeWeeks\n status\n targetCompletionDate\n progressPct\n totalSteps\n completedSteps\n createdAt\n steps {\n ...StudyPlanStepFields\n }\n }\n`;\n\nconst LIST_MY_STUDY_PLANS = gql`\n ${STUDY_PLAN_FIELDS}\n query TecMyStudyPlans {\n tecMyStudyPlans {\n items {\n ...StudyPlanFields\n }\n pageInfo {\n cursor\n hasMore\n }\n }\n }\n`;\n\nconst GENERATE_STUDY_PLAN = gql`\n ${STUDY_PLAN_FIELDS}\n mutation TecGenerateStudyPlan($input: TecGenerateStudyPlanInput!) {\n tecGenerateStudyPlan(input: $input) {\n ...StudyPlanFields\n }\n }\n`;\n\nconst UPDATE_STUDY_PLAN_STEP_STATUS = gql`\n ${STUDY_PLAN_STEP_FIELDS}\n mutation TecUpdateStudyPlanStepStatus($input: TecUpdateStudyPlanStepStatusInput!) {\n tecUpdateStudyPlanStepStatus(input: $input) {\n ...StudyPlanStepFields\n }\n }\n`;\n\nconst ARCHIVE_STUDY_PLAN = gql`\n mutation TecArchiveStudyPlan($id: ID!) {\n tecArchiveStudyPlan(id: $id)\n }\n`;\n\nexport const useMyStudyPlansQuery = () =>\n useQuery<{ tecMyStudyPlans: { items: TecStudyPlan[]; pageInfo: TecPageInfo } }>(\n LIST_MY_STUDY_PLANS,\n {\n fetchPolicy: 'cache-and-network',\n }\n );\n\nexport const useGenerateStudyPlan = () =>\n useMutation<{ tecGenerateStudyPlan: TecStudyPlan }>(GENERATE_STUDY_PLAN, {\n onError: onMutationError,\n refetchQueries: ['TecMyStudyPlans'],\n });\n\nexport const useUpdateStudyPlanStepStatus = () =>\n useMutation<{ tecUpdateStudyPlanStepStatus: TecStudyPlanStep }>(UPDATE_STUDY_PLAN_STEP_STATUS, {\n onError: onMutationError,\n refetchQueries: ['TecMyStudyPlans'],\n });\n\nexport const useArchiveStudyPlan = () =>\n useMutation<{ tecArchiveStudyPlan: boolean }>(ARCHIVE_STUDY_PLAN, {\n onError: onMutationError,\n refetchQueries: ['TecMyStudyPlans'],\n });\n\n/* ================================================================ Portfolio\n * BOFF-4432 — learner-curated showcase of real completed hackathon\n * projects, certificates, and courses. `tecPublicPortfolio` is a public,\n * unauthenticated field (@rbac public: true) reached through the same\n * gateway; there is no separate unauthenticated *page* wired up yet (see\n * ApplicationsPortfolioPagePage's deferred-scope note).\n * ========================================================================= */\n\nexport type TecPortfolioItemKind = 'HACKATHON_PROJECT' | 'CERTIFICATE' | 'COURSE_COMPLETION';\n\nexport interface TecPortfolioItem {\n id: string;\n kind: TecPortfolioItemKind;\n sourceId: string;\n title: string;\n description?: string | null;\n link?: string | null;\n completionDate: string;\n order: number;\n}\n\nexport interface TecPortfolio {\n id: string;\n isPublished: boolean;\n publicSlug?: string | null;\n publishedAt?: string | null;\n items: TecPortfolioItem[];\n}\n\nconst PORTFOLIO_ITEM_FIELDS = gql`\n fragment PortfolioItemFields on TecPortfolioItem {\n id\n kind\n sourceId\n title\n description\n link\n completionDate\n order\n }\n`;\n\nconst PORTFOLIO_FIELDS = gql`\n ${PORTFOLIO_ITEM_FIELDS}\n fragment PortfolioFields on TecPortfolio {\n id\n isPublished\n publicSlug\n publishedAt\n items {\n ...PortfolioItemFields\n }\n }\n`;\n\nconst GET_MY_PORTFOLIO = gql`\n ${PORTFOLIO_FIELDS}\n query TecMyPortfolio {\n tecMyPortfolio {\n ...PortfolioFields\n }\n }\n`;\n\nconst ADD_PORTFOLIO_ITEM = gql`\n ${PORTFOLIO_ITEM_FIELDS}\n mutation TecAddPortfolioItem($input: TecAddPortfolioItemInput!) {\n tecAddPortfolioItem(input: $input) {\n ...PortfolioItemFields\n }\n }\n`;\n\nconst REORDER_PORTFOLIO_ITEMS = gql`\n ${PORTFOLIO_FIELDS}\n mutation TecReorderPortfolioItems($input: TecReorderPortfolioItemsInput!) {\n tecReorderPortfolioItems(input: $input) {\n ...PortfolioFields\n }\n }\n`;\n\nconst REMOVE_PORTFOLIO_ITEM = gql`\n mutation TecRemovePortfolioItem($id: ID!) {\n tecRemovePortfolioItem(id: $id)\n }\n`;\n\nconst PUBLISH_PORTFOLIO = gql`\n ${PORTFOLIO_FIELDS}\n mutation TecPublishPortfolio {\n tecPublishPortfolio {\n ...PortfolioFields\n }\n }\n`;\n\nconst UNPUBLISH_PORTFOLIO = gql`\n ${PORTFOLIO_FIELDS}\n mutation TecUnpublishPortfolio {\n tecUnpublishPortfolio {\n ...PortfolioFields\n }\n }\n`;\n\nexport const useMyPortfolioQuery = () =>\n useQuery<{ tecMyPortfolio: TecPortfolio }>(GET_MY_PORTFOLIO, {\n fetchPolicy: 'cache-and-network',\n });\n\nconst REFETCH_PORTFOLIO = ['TecMyPortfolio'];\n\nexport const useAddPortfolioItem = () =>\n useMutation<{ tecAddPortfolioItem: TecPortfolioItem }>(ADD_PORTFOLIO_ITEM, {\n onError: onMutationError,\n refetchQueries: REFETCH_PORTFOLIO,\n });\n\nexport const useReorderPortfolioItems = () =>\n useMutation<{ tecReorderPortfolioItems: TecPortfolio }>(REORDER_PORTFOLIO_ITEMS, {\n onError: onMutationError,\n });\n\nexport const useRemovePortfolioItem = () =>\n useMutation<{ tecRemovePortfolioItem: boolean }>(REMOVE_PORTFOLIO_ITEM, {\n onError: onMutationError,\n refetchQueries: REFETCH_PORTFOLIO,\n });\n\nexport const usePublishPortfolio = () =>\n useMutation<{ tecPublishPortfolio: TecPortfolio }>(PUBLISH_PORTFOLIO, {\n onError: onMutationError,\n refetchQueries: REFETCH_PORTFOLIO,\n });\n\nexport const useUnpublishPortfolio = () =>\n useMutation<{ tecUnpublishPortfolio: TecPortfolio }>(UNPUBLISH_PORTFOLIO, {\n onError: onMutationError,\n refetchQueries: REFETCH_PORTFOLIO,\n });\n\n/* ------------------------------------------------ TechnoSage review (BOFF-3841) */\n\nexport type TecReviewSource = 'AI' | 'RULES';\n\n/**\n * TechnoSage's explanation of one of the caller's own graded submissions.\n * It explains the grader's verdict and never changes it. `source` says who\n * wrote it: a model (AI) or the grader output alone (RULES).\n */\nexport interface TecSubmissionReview {\n id: string;\n submissionId: string;\n problemId: string;\n verdict: TecSubmissionVerdict;\n summary: string;\n failureExplanation: string;\n conceptsToRevisit: string[];\n suggestedProblems: Array<Pick<TecProblem, 'id' | 'title' | 'difficulty' | 'topic'>>;\n source: TecReviewSource;\n model: string | null;\n createdAt: string;\n}\n\nconst SUBMISSION_REVIEW_FIELDS = gql`\n fragment SubmissionReviewFields on TecSubmissionReview {\n id\n submissionId\n problemId\n verdict\n summary\n failureExplanation\n conceptsToRevisit\n suggestedProblems {\n id\n title\n difficulty\n topic\n }\n source\n model\n createdAt\n }\n`;\n\nconst MY_SUBMISSION_REVIEW = gql`\n ${SUBMISSION_REVIEW_FIELDS}\n query TecMySubmissionReview($submissionId: ID!) {\n tecMySubmissionReview(submissionId: $submissionId) {\n ...SubmissionReviewFields\n }\n }\n`;\n\nconst REVIEW_SUBMISSION = gql`\n ${SUBMISSION_REVIEW_FIELDS}\n mutation TecReviewSubmission($submissionId: ID!) {\n tecReviewSubmission(submissionId: $submissionId) {\n ...SubmissionReviewFields\n }\n }\n`;\n\n/** The stored review of a submission, if the learner already asked for one. */\nexport const useMySubmissionReviewQuery = (submissionId: string | null) =>\n useQuery<{ tecMySubmissionReview: TecSubmissionReview | null }>(MY_SUBMISSION_REVIEW, {\n variables: { submissionId: submissionId ?? '' },\n skip: !submissionId,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useReviewSubmission = () =>\n useMutation<{ tecReviewSubmission: TecSubmissionReview }>(REVIEW_SUBMISSION, {\n onError: onMutationError,\n refetchQueries: ['TecMySubmissionReview'],\n });\n\n/* ------------------------------------------------------- pagination helper */\n\nexport interface PageLimit {\n limit: number;\n more: () => void;\n}\n\n/**\n * Simple, robust \"load more\". The backend paginates with `take = limit + 1`\n * (offset is ignored; only `cursor` shifts the window), so a larger `limit`\n * simply fetches more rows from the top and `pageInfo.hasMore` reports whether\n * still more exist. Growing the limit avoids cursor-merge/fetchMore complexity\n * and never sends `cursor: null` (the page-1 500 guard). Pass `limit` into the\n * query's `pageArg` and render <LoadMore hasMore={...} onClick={more} />.\n */\nexport function usePageLimit(step = 24, initial = 24): PageLimit {\n const [limit, setLimit] = useState(initial);\n return { limit, more: () => setLimit((l) => l + step) };\n}\n\n/* ------------------------------------------------- lesson player (BOFF-4107) */\n\nconst GET_LESSON_PLAYER = gql`\n ${LESSON_FIELDS}\n ${COURSE_FIELDS}\n query TecLessonPlayer($lessonId: ID!, $courseId: ID!) {\n tecLesson(id: $lessonId) {\n ...LessonFields\n file {\n id\n name\n mimeType\n downloadUrl\n }\n }\n tecCourse(id: $courseId) {\n ...CourseFields\n lessons {\n id\n courseId\n title\n kind\n position\n estimatedMinutes\n isPreview\n isLocked\n completedByMe\n }\n }\n }\n`;\n\nconst LESSON_QUIZ_KEY = gql`\n query TecLessonQuizKey($lessonId: ID!) {\n tecLessonQuizKey(lessonId: $lessonId) {\n prompt\n options\n correctOption\n explanation\n }\n }\n`;\n\nconst COMPLETE_LESSON = gql`\n ${ENROLLMENT_FIELDS}\n mutation TecCompleteLesson($lessonId: ID!) {\n tecCompleteLesson(lessonId: $lessonId) {\n ...EnrollmentFields\n nextLessonId\n }\n }\n`;\n\nconst SUBMIT_LESSON_QUIZ = gql`\n ${ENROLLMENT_FIELDS}\n mutation TecSubmitLessonQuiz($lessonId: ID!, $answers: [Int!]!) {\n tecSubmitLessonQuiz(lessonId: $lessonId, answers: $answers) {\n lessonId\n score\n correctCount\n totalCount\n passed\n passingScore\n results {\n index\n correct\n selectedOption\n correctOption\n explanation\n }\n enrollment {\n ...EnrollmentFields\n nextLessonId\n }\n }\n }\n`;\n\nconst RUN_LESSON_CODE = gql`\n ${ENROLLMENT_FIELDS}\n mutation TecRunLessonCode($input: TecRunLessonCodeInput!) {\n tecRunLessonCode(input: $input) {\n lessonId\n status\n language\n stdout\n stderr\n exitCode\n durationMs\n timedOut\n memoryLimitExceeded\n compileFailed\n matchedExpected\n lessonCompleted\n enrollment {\n ...EnrollmentFields\n nextLessonId\n }\n }\n }\n`;\n\nexport interface LessonPlayerData {\n tecLesson: TecLesson | null;\n tecCourse: TecCourse | null;\n}\n\nexport const useLessonPlayerQuery = (courseId: string, lessonId: string) =>\n useQuery<LessonPlayerData>(GET_LESSON_PLAYER, {\n variables: { courseId, lessonId },\n skip: !courseId || !lessonId,\n fetchPolicy: 'cache-and-network',\n });\n\nexport const useLessonQuizKeyQuery = (lessonId: string, skip = false) =>\n useQuery<{ tecLessonQuizKey: TecQuizQuestionKey[] }>(LESSON_QUIZ_KEY, {\n variables: { lessonId },\n skip: skip || !lessonId,\n fetchPolicy: 'network-only',\n });\n\nconst LESSON_PROGRESS_REFETCH = [\n 'TecLessonPlayer',\n 'TecCourse',\n REFETCH.myEnrollments,\n REFETCH.enrollments,\n];\n\nexport const useCompleteLesson = () =>\n useMutation<{ tecCompleteLesson: TecEnrollment }>(COMPLETE_LESSON, {\n onError: onMutationError,\n refetchQueries: LESSON_PROGRESS_REFETCH,\n });\n\nexport const useSubmitLessonQuiz = () =>\n useMutation<{ tecSubmitLessonQuiz: TecQuizResult }>(SUBMIT_LESSON_QUIZ, {\n onError: onMutationError,\n refetchQueries: LESSON_PROGRESS_REFETCH,\n });\n\n/**\n * Runs code for an INTERACTIVE lesson. No refetch here: the page refetches\n * only when a run actually completed the lesson, so polling a warming\n * sandbox does not reload the page each time.\n */\nexport const useRunLessonCode = () =>\n useMutation<{ tecRunLessonCode: TecLessonCodeRun }>(RUN_LESSON_CODE, {\n onError: onMutationError,\n });\n\n/* ------------------------------------------- offline lessons (BOFF-4418) */\n\n/**\n * Fetch one lesson (with its course outline) straight from the network, for\n * saving a course to the device. Returns null when the lesson cannot be read.\n */\nexport async function fetchLessonForOffline(\n client: ApolloClient,\n courseId: string,\n lessonId: string\n): Promise<LessonPlayerData | null> {\n try {\n const result = await client.query<LessonPlayerData>({\n query: GET_LESSON_PLAYER,\n variables: { courseId, lessonId },\n fetchPolicy: 'network-only',\n });\n return result.data ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * GraphQL error codes that mean the server refused this completion for good\n * (lesson removed, learner not enrolled, lesson needs a quiz pass or code run).\n * Every other GraphQL error — an expired session (`UNAUTHENTICATED`), a\n * permission or quota check, a server fault — may succeed later, so the\n * completion stays queued.\n */\nconst TERMINAL_COMPLETION_ERROR_CODES: ReadonlySet<string> = new Set([\n 'NOT_FOUND',\n 'BAD_USER_INPUT',\n]);\n\n/** True when every GraphQL error in a failed completion is terminal. */\nexport function isTerminalCompletionError(error: unknown): boolean {\n if (!CombinedGraphQLErrors.is(error) || error.errors.length === 0) return false;\n return error.errors.every((graphQLError) => {\n const code = graphQLError.extensions?.['code'];\n return typeof code === 'string' && TERMINAL_COMPLETION_ERROR_CODES.has(code);\n });\n}\n\n/**\n * Send a lesson completion that was queued while offline: `rejected` when the\n * server refused it for good (see {@link isTerminalCompletionError}), `retry`\n * for anything that may succeed later (no connection, gateway down, expired\n * session).\n */\nexport async function sendQueuedLessonCompletion(\n client: ApolloClient,\n lessonId: string\n): Promise<'sent' | 'rejected' | 'retry'> {\n try {\n const result = await client.mutate<{ tecCompleteLesson: TecEnrollment }>({\n mutation: COMPLETE_LESSON,\n variables: { lessonId },\n refetchQueries: LESSON_PROGRESS_REFETCH,\n });\n return result.data?.tecCompleteLesson ? 'sent' : 'rejected';\n } catch (error) {\n return isTerminalCompletionError(error) ? 'rejected' : 'retry';\n }\n}\n\n/* ----------------------------------------- enterprise connectors (BOFF-4430) */\n\nexport type TecCertificateVerificationStatus = 'VALID' | 'REVOKED' | 'NOT_FOUND';\n\nexport interface TecCertificateVerification {\n serial: string;\n status: TecCertificateVerificationStatus;\n valid: boolean;\n issuer: string;\n courseTitle?: string | null;\n courseLevel?: string | null;\n score?: number | null;\n issuedAt?: string | null;\n revokedAt?: string | null;\n verifiedAt: string;\n}\n\n/** One xAPI 1.0.3 statement as exported by the service (LRS-ready JSON). */\nexport type TecXapiStatement = Record<string, unknown> & { id: string; timestamp: string };\n\nexport interface TecXapiStatementPage {\n statements: TecXapiStatement[];\n count: number;\n hasMore: boolean;\n nextSince?: string | null;\n version: string;\n}\n\n/** Most serials one batch verification accepts (mirrors the service cap). */\nexport const MAX_VERIFY_BATCH = 25;\n\nconst VERIFY_CERTIFICATES = gql`\n query TecVerifyCertificates($serials: [String!]!) {\n tecVerifyCertificates(serials: $serials) {\n serial\n status\n valid\n issuer\n courseTitle\n courseLevel\n score\n issuedAt\n revokedAt\n verifiedAt\n }\n }\n`;\n\nconst EXPORT_XAPI_STATEMENTS = gql`\n query TecExportXapiStatements($courseId: ID, $since: DateTime, $until: DateTime, $limit: Int) {\n tecExportXapiStatements(courseId: $courseId, since: $since, until: $until, limit: $limit) {\n statements\n count\n hasMore\n nextSince\n version\n }\n }\n`;\n\n/**\n * Certificate verification — the same public query ATS partners call. Run on\n * demand (skip until serials are submitted) and always against the network,\n * so a revocation shows up immediately.\n */\nexport const useVerifyCertificatesQuery = (serials: string[]) =>\n useQuery<{ tecVerifyCertificates: TecCertificateVerification[] }>(VERIFY_CERTIFICATES, {\n variables: { serials },\n skip: serials.length === 0,\n fetchPolicy: 'network-only',\n });\n\nexport interface XapiExportVars {\n courseId?: string;\n since?: string;\n until?: string;\n limit?: number;\n}\n\n/**\n * xAPI export (admin; `list-all` on technospam-enrollment). Returns an\n * imperative runner that follows `nextSince` until the window is exhausted\n * or `maxStatements` is reached, de-duplicating by statement id (pages\n * overlap on one timestamp by design).\n */\nexport function useXapiExport() {\n const [running, setRunning] = useState(false);\n const [error, setError] = useState<ErrorLike | null>(null);\n const [loadPage] = useLazyQuery<{ tecExportXapiStatements: TecXapiStatementPage }>(\n EXPORT_XAPI_STATEMENTS,\n { fetchPolicy: 'network-only' }\n );\n\n const run = async (vars: XapiExportVars, maxStatements = 5000) => {\n setRunning(true);\n setError(null);\n const seen = new Map<string, TecXapiStatement>();\n let since = vars.since;\n let truncated = false;\n try {\n for (;;) {\n const res = await loadPage({ variables: { ...vars, since, limit: vars.limit ?? 500 } });\n if (res.error) throw res.error;\n const page = res.data?.tecExportXapiStatements;\n if (!page) break;\n for (const statement of page.statements) seen.set(statement.id, statement);\n if (!page.hasMore || !page.nextSince) break;\n if (seen.size >= maxStatements) {\n truncated = true;\n break;\n }\n since = page.nextSince;\n }\n return { statements: [...seen.values()], truncated };\n } catch (err) {\n setError(err as ErrorLike);\n notify.error('Export failed', userMessageFromError(err));\n return null;\n } finally {\n setRunning(false);\n }\n };\n\n return { run, running, error };\n}\n"],"mappings":";;;;;;;AAsbA,SAAgB,EAAQ,IAAQ,IAAI,GAA4D;AAC9F,QAAO,IAAS;EAAE;EAAO;EAAQ,GAAG,EAAE,UAAO;;AAK/C,IAAM,IAAgB,CAAG;;;;;;;;;;;;;;;;;;;GAqBnB,IAAgB,CAAG;;;;;;;;;;;;;;;;;;;;;;;GAyBnB,IAAoB,CAAG;;;;;;;;;;;;;;GAgBvB,IAAiB,CAAG;;;;;;;;;;;;;;;;;;;;;;GAwBpB,IAAoB,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BvB,KAA2B,CAAG;;;;;;;;;;;GAa9B,IAAgB,CAAG;;;;;;;;;;;;;;;;;GAmBnB,IAAiB,CAAG;;;;;;;;;;;;;;;;;;;;;GAuBpB,IAAmB,CAAG;;;;;;;;;;;;;;;;;;GAoBtB,IAAc,CAAG;;;;;;;;;;;;;;;;;GAmBjB,IAAqB,CAAG;;;;;;;;;;;;;GAiBxB,KAAe,CAAG;IACpB,EAAc;;;;;;;;;;;;;;;;GAkBZ,KAAa,CAAG;IAClB,EAAc;IACd,EAAc;IACd,EAAkB;;;;;;;;;;;;GAchB,KAAmB,CAAG;IACxB,EAAkB;IAClB,EAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BZ,KAAoB,CAAG;IACzB,EAAmB;IACnB,EAAc;;;;;;;;;GAqBZ,KAAiB,CAAG;IACtB,EAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCZ,KAAkB,CAAG;IACvB,EAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BZ,KAAgB,CAAG;IACrB,EAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8Bb,KAAc,CAAG;IACnB,EAAe;IACf,EAAkB;IAClB,GAAyB;;;;;;;;;;;;GAcvB,KAAqB,CAAG;IAC1B,EAAkB;;;;;;GAQhB,KAAmB,CAAG;IACxB,EAAkB;IAClB,EAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6Bb,IAAe,CAAG;IACpB,EAAc;;;;;;;;;;;;GAcZ,IAAa,CAAG;IAClB,EAAc;IACd,EAAe;;;;;;;;;GAWb,IAAgB,CAAG;IACrB,EAAe;IACf,EAAc;;;;;;;;;;;;;;;;;;;;;;;;;GA2BZ,IAAyB,CAAG;;;;;;;;;;;;;;AA8Bf,CAAG;IAfA,CAAG;;;;;;;;;;;;;EAgBP;;;;;;;;;;;;;AAclB,IAAM,IAAkB,CAAG;IACvB,EAAiB;;;;;;;;;;;;GAcf,IAAgB,CAAG;IACrB,EAAiB;IACjB,EAAY;;;;;;;;;GAWV,IAAa,CAAG;IAClB,EAAY;;;;;;;;;;;;GAcV,IAAe,CAAG;IACpB,EAAc;;;;;;;;;;;;GAoBL,KAAmB,MAK9B,EAA4C,IAAc;CACxD,WAAW;EAAE,YAAY,EAAQ,GAAG;EAAE,GAAG;EAAM;CAC/C,aAAa;CACd,CAAC,EAES,KAAkB,MAC7B,EAA0C,IAAY;CACpD,WAAW,EAAE,OAAI;CACjB,MAAM,CAAC;CACP,aAAa;CACd,CAAC,EAES,KAAuB,MAMlC,EAAoD,IAAkB;CACpE,WAAW;EAAE,YAAY,EAAQ,GAAG;EAAE,GAAG;EAAM;CAC/C,aAAa;CACd,CAAC,EAMS,KAAwB,MACnC,EAAoD,IAAmB;CACrE,WAAW;CACX,aAAa;CACd,CAAC;AAeJ,SAAgB,EAAsB,IAAyB,EAAE,EAAE;CACjE,IAAM,IAAS,EAEZ,IAAgB;EACjB,WAAW;GACT,GAAI,EAAM,cAAc,EAAE,UAAU,EAAM,aAAa,GAAG,EAAE;GAC5D,YAAY,EAAQ,EAAM,SAAS,IAAI;GACxC;EACD,MAAM,EAAM;EACZ,aAAa;EACd,CAAC,EACI,IAAQ,EAAO,MAAM,kBAAkB,OACvC,IAAc,QACZ,EAAoB,EAAQ,GAAO,EAAgB,EAAE,EAAM,mBAAmB,EACpF,CAAC,GAAO,EAAM,mBAAmB,CAClC;AACD,QAAO;EAAE,GAAG;EAAQ;EAAa;;AAInC,SAAgB,EAAuB,IAAyB,EAAE,EAAE;CAClE,IAAM,IAAS,EAEZ,IAAiB;EAClB,WAAW;GACT,GAAI,EAAM,cAAc,EAAE,UAAU,EAAM,aAAa,GAAG,EAAE;GAC5D,YAAY,EAAQ,EAAM,SAAS,IAAI;GACxC;EACD,MAAM,EAAM;EACZ,aAAa;EACd,CAAC,EACI,IAAQ,EAAO,MAAM,mBAAmB,OACxC,IAAe,QACb,EAAoB,EAAQ,GAAO,EAAiB,EAAE,EAAM,mBAAmB,EACrF,CAAC,GAAO,EAAM,mBAAmB,CAClC;AACD,QAAO;EAAE,GAAG;EAAQ;EAAc;;AAGpC,IAAa,KAAoB,MAS/B,EAA8C,IAAe;CAC3D,WAAW;EAAE,YAAY,EAAQ,GAAG;EAAE,GAAG;EAAM;CAC/C,aAAa;CACd,CAAC,EAES,KAAmB,MAC9B,EAA4C,IAAa;CACvD,WAAW,EAAE,OAAI;CACjB,MAAM,CAAC;CACP,aAAa;CACd,CAAC,EAES,KAAuB,MAMlC,EAAoD,IAAkB;CACpE,WAAW;EAAE,YAAY,EAAQ,GAAG;EAAE,GAAG;EAAM;CAC/C,aAAa;CACd,CAAC,EAME,IAA0B,CAAG;;;;;;;;;;;;;;GAqB7B,qBAAiC,IAAI,KAAa;AAmBxD,SAAgB,GAAoB,GAAoD;CACtF,IAAM,IAAS,IAAiB,EAC1B,CAAC,GAAQ,KAAa,EAC1B,KACD;AAoCD,QAlCA,QAAgB;AACd,MAAI,CAAC,EAAW;EAChB,IAAI,IAAY,IACV,oBAAM,IAAI,KAAa;AA0B7B,UAzBM,YAAY;GAChB,IAAI;AACJ,QAAK,IAAI,IAAO,GAAG,IAAA,IAA8B,KAAQ,GAAG;IAC1D,IAAI;AACJ,QAAI;AAMF,UALY,MAAM,EAAO,MAA6B;MACpD,OAAO;MACP,WAAW;OAAE;OAAW,YAAY,EAAA,KAA+B,EAAO;OAAE;MAC5E,aAAa;MACd,CAAC,EACS,MAAM;YACX;AAGN;;AAEF,QAAI,KAAa,CAAC,EAAM;AACxB,SAAK,IAAM,KAAc,EAAK,MAC5B,CAAI,EAAW,YAAY,cAAY,EAAI,IAAI,EAAW,UAAU;AAGtE,QADA,EAAU;KAAE;KAAW,KAAK,IAAI,IAAI,EAAI;KAAE,CAAC,EACvC,CAAC,EAAK,SAAS,WAAW,CAAC,EAAK,SAAS,OAAQ;AACrD,QAAS,EAAK,SAAS;;MAEvB,QACS;AACX,OAAY;;IAEb,CAAC,GAAQ,EAAU,CAAC,EAEhB,KAAa,GAAQ,cAAc,IAAY,EAAO,MAAM;;AAGrE,IAAa,MAAmB,MAI9B,EAA4C,GAAc;CACxD,WAAW;EAAE,YAAY,EAAQ,GAAG;EAAE,GAAG;EAAM;CAC/C,aAAa;CACd,CAAC,EAES,MAAkB,MAC7B,EAA0C,GAAY;CACpD,WAAW,EAAE,OAAI;CACjB,MAAM,CAAC;CACP,aAAa;CACd,CAAC,EAES,MAAoB,MAM/B,EAAoD,GAAe;CACjE,WAAW;EAAE,YAAY,EAAQ,GAAG;EAAE,GAAG;EAAM;CAC/C,aAAa;CACd,CAAC,EAQS,MAA0B,GAAkB,MACvD,EAA8D,GAAwB;CACpF,WAAW;EAAE;EAAU;EAAiB;CACxC,MAAM,CAAC,KAAY,CAAC,OAAO,SAAS,EAAgB,IAAI,KAAmB;CAC3E,aAAa;CACd,CAAC,EAWS,MAAsB,MAIjC,EAAkD,GAAiB;CACjE,WAAW;EAAE,YAAY,EAAQ,GAAG;EAAE,GAAG;EAAM;CAC/C,aAAa;CACd,CAAC,EAES,MAAqB,MAChC,EAAgD,GAAe;CAC7D,WAAW,EAAE,OAAI;CACjB,MAAM,CAAC;CACP,aAAa;CACd,CAAC,EAES,MACX,GACA,MAEA,EAAiD,GAAY;CAC3D,WAAW;EAAE;EAAa,YAAY,KAAc,EAAQ,GAAG;EAAE;CACjE,MAAM,CAAC;CACP,aAAa;CACd,CAAC,EAES,MACX,GACA,MAEA,EAA4C,GAAc;CACxD,WAAW;EAAE;EAAU,YAAY,KAAc,EAAQ,GAAG;EAAE;CAC9D,MAAM,CAAC;CACP,aAAa;CACd,CAAC,EAIE,KAAmB,MAA2B;AAClD,GAAO,MAAM,wBAAwB,EAAqB,EAAM,CAAC;GAG7D,IAAU;CACd,SAAS;CACT,aAAa;CAEb,eAAe;CACf,gBAAgB;CAChB,UAAU;CACV,aAAa;CACb,SAAS;CACT,UAAU;CACV,YAAY;CACb,EAEK,KAAS,CAAG;IACd,EAAkB;;;;;;GAQhB,KAAkB,CAAG;IACvB,EAAkB;;;;;;GAQhB,KAAe,CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoClB,KAAe,CAAG;IACpB,EAAe;;;;;;GAQb,KAAiB,CAAG;IACtB,EAAe;;;;;;GAQb,KAAgB,CAAG;IACrB,EAAc;;;;;;GAQZ,KAAgB,CAAG;IACrB,EAAY;;;;;;GAQV,KAAiB,CAAG;IACtB,EAAY;;;;;;GAUV,KAAgB,CAAG;IACrB,EAAc;;;;;;GAQZ,KAAiB,CAAG;IACtB,EAAc;;;;;;GAQZ,KAAiB,CAAG;;;;GAMpB,KAAgB,CAAG;IACrB,EAAc;;;;;;GAQZ,KAAgB,CAAG;IACrB,EAAc;;;;;;GAQZ,KAAiB,CAAG;IACtB,EAAe;;;;;;GAQb,KAAkB,CAAG;IACvB,EAAe;;;;;;GAQb,KAAmB,CAAG;IACxB,EAAiB;;;;;;GAQf,KAAoB,CAAG;IACzB,EAAiB;;;;;;GAQf,KAAiB,CAAG;IACtB,EAAc;;;;;;GAQZ,KAAa,CAAG;IAClB,EAAY;;;;;;GAQV,KAAkB,CAAG;IACvB,EAAe;;;;;;GAQb,KAAmB,CAAG;IACxB,EAAe;;;;;;GAQb,KAAmB,CAAG;IACxB,EAAkB;;;;;;GAQhB,KAAoB,CAAG;IACzB,EAAmB;;;;;;;AAQI,CAAG;IAC1B,EAAmB;;;;;;;AAQvB,IAAM,KAAkB,CAAG;IACvB,EAAiB;;;;;;GAQf,KAAgB,CAAG;IACrB,EAAiB;;;;;;GAQf,KAAqB,CAAG;IAC1B,EAAiB;;;;;;GAUR,WACX,EAA0C,IAAQ;CAChD,SAAS;CACT,gBAAgB;EAAC,EAAQ;EAAa,EAAQ;EAAe,EAAQ;EAAQ;CAC9E,CAAC,EAES,WACX,EAAkD,IAAiB;CACjE,SAAS;CACT,gBAAgB,CAAC,EAAQ,YAAY;CACtC,CAAC,EAMS,WACX,EAAgD,IAAc,EAC5D,SAAS,GACV,CAAC,EAOS,WACX,EAAqD,IAAoB;CACvE,SAAS;CACT,gBAAgB,CAAC,EAAQ,YAAY;CACtC,CAAC,EAES,WACX,EAAkD,IAAc;CAC9D,SAAS;CACT,gBAAgB,CAAC,EAAQ,UAAU,EAAQ,QAAQ;CACpD,CAAC,EAES,WACX,EAAoD,IAAgB;CAClE,SAAS;CACT,gBAAgB,CAAC,EAAQ,SAAS;CACnC,CAAC,EAES,WACX,EAA4C,IAAe;CACzD,SAAS;CACT,gBAAgB,CAAC,EAAQ,QAAQ;CAClC,CAAC,EAES,WACX,EAAmD,IAAe;CAChE,SAAS;CACT,gBAAgB,CAAC,EAAQ,WAAW;CACrC,CAAC,EAES,WACX,EAAoD,IAAgB,EAClE,SAAS,GACV,CAAC,EAES,WACX,EAA4C,IAAe;CACzD,SAAS;CACT,gBAAgB,CAAC,EAAQ,QAAQ;CAClC,CAAC,EAES,WACX,EAA6C,IAAgB;CAC3D,SAAS;CACT,gBAAgB,CAAC,EAAQ,QAAQ;CAClC,CAAC,EAES,WACX,EAA2C,IAAgB;CACzD,SAAS;CACT,gBAAgB,CAAC,EAAQ,QAAQ;CAClC,CAAC,EAES,WACX,EAA4C,IAAe,EAAE,SAAS,GAAiB,CAAC,EAM7E,WACX,EAA4C,IAAe,EAAE,SAAS,GAAiB,CAAC,EAE7E,WACX,EAA8C,IAAgB;CAC5D,SAAS;CACT,gBAAgB,CAAC,EAAQ,SAAS;CACnC,CAAC,EAES,WACX,EAA+C,IAAiB;CAC9D,SAAS;CACT,gBAAgB,CAAC,EAAQ,SAAS;CACnC,CAAC,EAES,WACX,EAAkD,IAAkB;CAClE,SAAS;CACT,gBAAgB,CAAC,EAAQ,WAAW;CACrC,CAAC,EAES,WACX,EAAmD,IAAmB;CACpE,SAAS;CACT,gBAAgB,CAAC,EAAQ,WAAW;CACrC,CAAC,EAES,WACX,EAA6C,IAAgB;CAC3D,SAAS;CACT,gBAAgB,CAAC,EAAQ,QAAQ;CAClC,CAAC,EAES,WACX,EAAgD,IAAY,EAAE,SAAS,GAAiB,CAAC,EAE9E,WACX,EAAqD,IAAiB;CACpE,SAAS;CACT,gBAAgB,CAAC,EAAQ,SAAS;CACnC,CAAC,EAES,WACX,EAAsD,IAAkB;CACtE,SAAS;CACT,gBAAgB,CAAC,EAAQ,SAAS;CACnC,CAAC,EAES,WACX,EAAmD,IAAkB;CACnE,SAAS;CACT,gBAAgB,CAAC,EAAQ,YAAY;CACtC,CAAC,EAES,WACX,EAAqD,IAAmB,EACtE,SAAS,GACV,CAAC,EAOS,WACX,EAAiD,IAAiB;CAChE,SAAS;CACT,gBAAgB,CAAC,EAAQ,WAAW;CACrC,CAAC,EAES,WACX,EAA+C,IAAe;CAC5D,SAAS;CACT,gBAAgB,CAAC,EAAQ,WAAW;CACrC,CAAC,EAES,WACX,EAAoD,IAAoB;CACtE,SAAS;CACT,gBAAgB,CAAC,EAAQ,WAAW;CACrC,CAAC,EAwCE,IAAyB,CAAG;;;;;;;;;;;;;GAe5B,IAAoB,CAAG;IACzB,EAAuB;;;;;;;;;;;;;;;;GAkBrB,KAAsB,CAAG;IAC3B,EAAkB;;;;;;;;;;;;GAchB,KAAsB,CAAG;IAC3B,EAAkB;;;;;;GAQhB,KAAgC,CAAG;IACrC,EAAuB;;;;;;GAQrB,KAAqB,CAAG;;;;GAMjB,WACX,EACE,IACA,EACE,aAAa,qBACd,CACF,EAEU,WACX,EAAoD,IAAqB;CACvE,SAAS;CACT,gBAAgB,CAAC,kBAAkB;CACpC,CAAC,EAES,WACX,EAAgE,IAA+B;CAC7F,SAAS;CACT,gBAAgB,CAAC,kBAAkB;CACpC,CAAC,EAES,WACX,EAA8C,IAAoB;CAChE,SAAS;CACT,gBAAgB,CAAC,kBAAkB;CACpC,CAAC,EA+BE,IAAwB,CAAG;;;;;;;;;;;GAa3B,IAAmB,CAAG;IACxB,EAAsB;;;;;;;;;;GAYpB,KAAmB,CAAG;IACxB,EAAiB;;;;;;GAQf,KAAqB,CAAG;IAC1B,EAAsB;;;;;;;AAQM,CAAG;IAC/B,EAAiB;;;;;;;AAQrB,IAAM,KAAwB,CAAG;;;;GAM3B,KAAoB,CAAG;IACzB,EAAiB;;;;;;GAQf,KAAsB,CAAG;IAC3B,EAAiB;;;;;;GAQR,WACX,EAA2C,IAAkB,EAC3D,aAAa,qBACd,CAAC,EAEE,IAAoB,CAAC,iBAAiB,EAE/B,WACX,EAAuD,IAAoB;CACzE,SAAS;CACT,gBAAgB;CACjB,CAAC,EAOS,WACX,EAAiD,IAAuB;CACtE,SAAS;CACT,gBAAgB;CACjB,CAAC,EAES,WACX,EAAmD,IAAmB;CACpE,SAAS;CACT,gBAAgB;CACjB,CAAC,EAES,WACX,EAAqD,IAAqB;CACxE,SAAS;CACT,gBAAgB;CACjB,CAAC,EAyBE,IAA2B,CAAG;;;;;;;;;;;;;;;;;;;GAqB9B,KAAuB,CAAG;IAC5B,EAAyB;;;;;;GAQvB,KAAoB,CAAG;IACzB,EAAyB;;;;;;GAShB,MAA8B,MACzC,EAAgE,IAAsB;CACpF,WAAW,EAAE,cAAc,KAAgB,IAAI;CAC/C,MAAM,CAAC;CACP,aAAa;CACd,CAAC,EAES,WACX,EAA0D,IAAmB;CAC3E,SAAS;CACT,gBAAgB,CAAC,wBAAwB;CAC1C,CAAC;AAiBJ,SAAgB,GAAa,IAAO,IAAI,IAAU,IAAe;CAC/D,IAAM,CAAC,GAAO,KAAY,EAAS,EAAQ;AAC3C,QAAO;EAAE;EAAO,YAAY,GAAU,MAAM,IAAI,EAAK;EAAE;;AAKzD,IAAM,IAAoB,CAAG;IACzB,EAAc;IACd,EAAc;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BZ,KAAkB,CAAG;;;;;;;;;GAWrB,IAAkB,CAAG;IACvB,EAAkB;;;;;;;GAShB,KAAqB,CAAG;IAC1B,EAAkB;;;;;;;;;;;;;;;;;;;;;;GAwBhB,KAAkB,CAAG;IACvB,EAAkB;;;;;;;;;;;;;;;;;;;;;GA4BT,MAAwB,GAAkB,MACrD,EAA2B,GAAmB;CAC5C,WAAW;EAAE;EAAU;EAAU;CACjC,MAAM,CAAC,KAAY,CAAC;CACpB,aAAa;CACd,CAAC,EAES,KAAyB,GAAkB,IAAO,OAC7D,EAAqD,IAAiB;CACpE,WAAW,EAAE,aAAU;CACvB,MAAM,KAAQ,CAAC;CACf,aAAa;CACd,CAAC,EAEE,IAA0B;CAC9B;CACA;CACA,EAAQ;CACR,EAAQ;CACT,EAEY,WACX,EAAkD,GAAiB;CACjE,SAAS;CACT,gBAAgB;CACjB,CAAC,EAES,WACX,EAAoD,IAAoB;CACtE,SAAS;CACT,gBAAgB;CACjB,CAAC,EAOS,WACX,EAAoD,IAAiB,EACnE,SAAS,GACV,CAAC;AAQJ,eAAsB,GACpB,GACA,GACA,GACkC;AAClC,KAAI;AAMF,UALe,MAAM,EAAO,MAAwB;GAClD,OAAO;GACP,WAAW;IAAE;IAAU;IAAU;GACjC,aAAa;GACd,CAAC,EACY,QAAQ;SAChB;AACN,SAAO;;;AAWX,IAAM,KAAuD,IAAI,IAAI,CACnE,aACA,iBACD,CAAC;AAGF,SAAgB,GAA0B,GAAyB;AAEjE,QADI,CAAC,EAAsB,GAAG,EAAM,IAAI,EAAM,OAAO,WAAW,IAAU,KACnE,EAAM,OAAO,OAAO,MAAiB;EAC1C,IAAM,IAAO,EAAa,YAAa;AACvC,SAAO,OAAO,KAAS,YAAY,GAAgC,IAAI,EAAK;GAC5E;;AASJ,eAAsB,GACpB,GACA,GACwC;AACxC,KAAI;AAMF,UALe,MAAM,EAAO,OAA6C;GACvE,UAAU;GACV,WAAW,EAAE,aAAU;GACvB,gBAAgB;GACjB,CAAC,EACY,MAAM,oBAAoB,SAAS;UAC1C,GAAO;AACd,SAAO,GAA0B,EAAM,GAAG,aAAa;;;AAmC3D,IAAM,KAAsB,CAAG;;;;;;;;;;;;;;;GAiBzB,KAAyB,CAAG;;;;;;;;;;GAiBrB,MAA8B,MACzC,EAAkE,IAAqB;CACrF,WAAW,EAAE,YAAS;CACtB,MAAM,EAAQ,WAAW;CACzB,aAAa;CACd,CAAC;AAeJ,SAAgB,KAAgB;CAC9B,IAAM,CAAC,GAAS,KAAc,EAAS,GAAM,EACvC,CAAC,GAAO,KAAY,EAA2B,KAAK,EACpD,CAAC,KAAY,GACjB,IACA,EAAE,aAAa,gBAAgB,CAChC;AAgCD,QAAO;EAAE,KA9BG,OAAO,GAAsB,IAAgB,QAAS;AAEhE,GADA,EAAW,GAAK,EAChB,EAAS,KAAK;GACd,IAAM,oBAAO,IAAI,KAA+B,EAC5C,IAAQ,EAAK,OACb,IAAY;AAChB,OAAI;AACF,aAAS;KACP,IAAM,IAAM,MAAM,EAAS,EAAE,WAAW;MAAE,GAAG;MAAM;MAAO,OAAO,EAAK,SAAS;MAAK,EAAE,CAAC;AACvF,SAAI,EAAI,MAAO,OAAM,EAAI;KACzB,IAAM,IAAO,EAAI,MAAM;AACvB,SAAI,CAAC,EAAM;AACX,UAAK,IAAM,KAAa,EAAK,WAAY,GAAK,IAAI,EAAU,IAAI,EAAU;AAC1E,SAAI,CAAC,EAAK,WAAW,CAAC,EAAK,UAAW;AACtC,SAAI,EAAK,QAAQ,GAAe;AAC9B,UAAY;AACZ;;AAEF,SAAQ,EAAK;;AAEf,WAAO;KAAE,YAAY,CAAC,GAAG,EAAK,QAAQ,CAAC;KAAE;KAAW;YAC7C,GAAK;AAGZ,WAFA,EAAS,EAAiB,EAC1B,EAAO,MAAM,iBAAiB,EAAqB,EAAI,CAAC,EACjD;aACC;AACR,MAAW,GAAM;;;EAIP;EAAS;EAAO"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The public verification link for one certificate (BOFF-10576): the URL a
|
|
3
|
+
* learner shares with a recruiter, who opens it without an account. Shows
|
|
4
|
+
* the link itself, a copy button and an open-in-new-tab button.
|
|
5
|
+
*/
|
|
6
|
+
export declare function CertificateVerifyLink({ serial, compact, className, }: {
|
|
7
|
+
serial: string;
|
|
8
|
+
compact?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=CertificateVerifyLink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CertificateVerifyLink.d.ts","sourceRoot":"","sources":["../../../src/technospam/components/CertificateVerifyLink.tsx"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,MAAM,EACN,OAAe,EACf,SAAS,GACV,EAAE;IACD,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,+BA0DA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { cn as e } from "../utils/cn.js";
|
|
2
|
+
import { notify as t } from "../lib/toast.js";
|
|
3
|
+
import { certificateVerifyLink as n } from "../lib/certificateVerify.js";
|
|
4
|
+
import { Copy as r, ExternalLink as i } from "lucide-react";
|
|
5
|
+
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
6
|
+
import { Button as s } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
//#region src/technospam/components/CertificateVerifyLink.tsx
|
|
8
|
+
function c({ serial: c, compact: l = !1, className: u }) {
|
|
9
|
+
let d = n(c), f = async () => {
|
|
10
|
+
try {
|
|
11
|
+
await navigator.clipboard.writeText(d), t.success("Verification link copied", "Share it with a recruiter or employer.");
|
|
12
|
+
} catch {
|
|
13
|
+
t.error("Could not copy the link", d);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
return /* @__PURE__ */ o("div", {
|
|
17
|
+
className: e("flex min-w-0 flex-col gap-1.5", u),
|
|
18
|
+
"data-testid": `tec-cert-verify-link-${c}`,
|
|
19
|
+
children: [l ? null : /* @__PURE__ */ a("span", {
|
|
20
|
+
className: "text-[11px] font-medium uppercase tracking-wide text-text-muted",
|
|
21
|
+
children: "Public verification link"
|
|
22
|
+
}), /* @__PURE__ */ o("div", {
|
|
23
|
+
className: "flex min-w-0 items-center gap-1.5",
|
|
24
|
+
children: [
|
|
25
|
+
/* @__PURE__ */ a("a", {
|
|
26
|
+
href: d,
|
|
27
|
+
target: "_blank",
|
|
28
|
+
rel: "noopener noreferrer",
|
|
29
|
+
className: "min-w-0 flex-1 truncate font-mono text-xs text-text-link hover:underline",
|
|
30
|
+
onClick: (e) => e.stopPropagation(),
|
|
31
|
+
children: d
|
|
32
|
+
}),
|
|
33
|
+
/* @__PURE__ */ a(s, {
|
|
34
|
+
type: "button",
|
|
35
|
+
size: "sm",
|
|
36
|
+
variant: "ghost",
|
|
37
|
+
"aria-label": "Copy verification link",
|
|
38
|
+
onClick: (e) => {
|
|
39
|
+
e.stopPropagation(), f();
|
|
40
|
+
},
|
|
41
|
+
children: /* @__PURE__ */ a(r, { size: 14 })
|
|
42
|
+
}),
|
|
43
|
+
/* @__PURE__ */ a(s, {
|
|
44
|
+
asChild: !0,
|
|
45
|
+
size: "sm",
|
|
46
|
+
variant: "ghost",
|
|
47
|
+
children: /* @__PURE__ */ a("a", {
|
|
48
|
+
href: d,
|
|
49
|
+
target: "_blank",
|
|
50
|
+
rel: "noopener noreferrer",
|
|
51
|
+
"aria-label": "Open verification page",
|
|
52
|
+
onClick: (e) => e.stopPropagation(),
|
|
53
|
+
children: /* @__PURE__ */ a(i, { size: 14 })
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
]
|
|
57
|
+
})]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { c as CertificateVerifyLink };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=CertificateVerifyLink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CertificateVerifyLink.js","names":[],"sources":["../../../src/technospam/components/CertificateVerifyLink.tsx"],"sourcesContent":["import { Copy, ExternalLink } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { certificateVerifyLink } from '../lib/certificateVerify';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/cn';\n\n/**\n * The public verification link for one certificate (BOFF-10576): the URL a\n * learner shares with a recruiter, who opens it without an account. Shows\n * the link itself, a copy button and an open-in-new-tab button.\n */\nexport function CertificateVerifyLink({\n serial,\n compact = false,\n className,\n}: {\n serial: string;\n compact?: boolean;\n className?: string;\n}) {\n const link = certificateVerifyLink(serial);\n\n const copy = async () => {\n try {\n await navigator.clipboard.writeText(link);\n notify.success('Verification link copied', 'Share it with a recruiter or employer.');\n } catch {\n notify.error('Could not copy the link', link);\n }\n };\n\n return (\n <div\n className={cn('flex min-w-0 flex-col gap-1.5', className)}\n data-testid={`tec-cert-verify-link-${serial}`}\n >\n {compact ? null : (\n <span className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Public verification link\n </span>\n )}\n <div className=\"flex min-w-0 items-center gap-1.5\">\n <a\n href={link}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"min-w-0 flex-1 truncate font-mono text-xs text-text-link hover:underline\"\n onClick={(event) => event.stopPropagation()}\n >\n {link}\n </a>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n aria-label=\"Copy verification link\"\n onClick={(event) => {\n event.stopPropagation();\n void copy();\n }}\n >\n <Copy size={14} />\n </Button>\n <Button asChild size=\"sm\" variant=\"ghost\">\n <a\n href={link}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label=\"Open verification page\"\n onClick={(event) => event.stopPropagation()}\n >\n <ExternalLink size={14} />\n </a>\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAWA,SAAgB,EAAsB,EACpC,WACA,aAAU,IACV,gBAKC;CACD,IAAM,IAAO,EAAsB,EAAO,EAEpC,IAAO,YAAY;AACvB,MAAI;AAEF,GADA,MAAM,UAAU,UAAU,UAAU,EAAK,EACzC,EAAO,QAAQ,4BAA4B,yCAAyC;UAC9E;AACN,KAAO,MAAM,2BAA2B,EAAK;;;AAIjD,QACE,kBAAC,OAAD;EACE,WAAW,EAAG,iCAAiC,EAAU;EACzD,eAAa,wBAAwB;YAFvC,CAIG,IAAU,OACT,kBAAC,QAAD;GAAM,WAAU;aAAkE;GAE3E,CAAA,EAET,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,KAAD;KACE,MAAM;KACN,QAAO;KACP,KAAI;KACJ,WAAU;KACV,UAAU,MAAU,EAAM,iBAAiB;eAE1C;KACC,CAAA;IACJ,kBAAC,GAAD;KACE,MAAK;KACL,MAAK;KACL,SAAQ;KACR,cAAW;KACX,UAAU,MAAU;AAEb,MADL,EAAM,iBAAiB,EAClB,GAAM;;eAGb,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;KACX,CAAA;IACT,kBAAC,GAAD;KAAQ,SAAA;KAAQ,MAAK;KAAK,SAAQ;eAChC,kBAAC,KAAD;MACE,MAAM;MACN,QAAO;MACP,KAAI;MACJ,cAAW;MACX,UAAU,MAAU,EAAM,iBAAiB;gBAE3C,kBAAC,GAAD,EAAc,MAAM,IAAM,CAAA;MACxB,CAAA;KACG,CAAA;IACL;KACF"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
import { TecSubmission } from '../api/technospamApi';
|
|
3
3
|
|
|
4
|
+
/** The grading fields the panel renders — a graded submission or a sample run. */
|
|
5
|
+
export type GradedResult = Pick<TecSubmission, 'verdict' | 'passedCount' | 'totalCount' | 'runtimeMs' | 'stderr' | 'testResults'>;
|
|
4
6
|
/**
|
|
5
7
|
* A dark, editor-window styled code surface: mac-style traffic-light dots plus a
|
|
6
8
|
* language label header over a scrollable monospace body so read-only code reads
|
|
@@ -24,8 +26,10 @@ export declare function IoBlock({ label, value }: {
|
|
|
24
26
|
* row here only ever carries its name and status — never the input, the
|
|
25
27
|
* expected output, or what the submitted program actually printed.
|
|
26
28
|
*/
|
|
27
|
-
export declare function TestResultsPanel({ submission, actualOutputLabel, }: {
|
|
28
|
-
submission:
|
|
29
|
+
export declare function TestResultsPanel({ submission, actualOutputLabel, caseNoun, }: {
|
|
30
|
+
submission: GradedResult;
|
|
29
31
|
actualOutputLabel?: string;
|
|
32
|
+
/** What the cases are called in the summary line, e.g. "sample test cases". */
|
|
33
|
+
caseNoun?: string;
|
|
30
34
|
}): import("react").JSX.Element | null;
|
|
31
35
|
//# sourceMappingURL=SubmissionViews.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubmissionViews.d.ts","sourceRoot":"","sources":["../../../src/technospam/components/SubmissionViews.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMvC,OAAO,KAAK,EAAE,aAAa,EAAqB,MAAM,sBAAsB,CAAC;AAE7E;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,QAAQ,EACR,aAAa,GACd,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,SAAS,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,+BAuBA;AAED,2EAA2E;AAC3E,wBAAgB,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,+BASjF;AAaD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,iBAAiC,
|
|
1
|
+
{"version":3,"file":"SubmissionViews.d.ts","sourceRoot":"","sources":["../../../src/technospam/components/SubmissionViews.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMvC,OAAO,KAAK,EAAE,aAAa,EAAqB,MAAM,sBAAsB,CAAC;AAE7E,kFAAkF;AAClF,MAAM,MAAM,YAAY,GAAG,IAAI,CAC7B,aAAa,EACb,SAAS,GAAG,aAAa,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,GAAG,aAAa,CAClF,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,QAAQ,EACR,aAAa,GACd,EAAE;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,SAAS,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,+BAuBA;AAED,2EAA2E;AAC3E,wBAAgB,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,+BASjF;AAaD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,iBAAiC,EACjC,QAAuB,GACxB,EAAE;IACD,UAAU,EAAE,YAAY,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,sCA0DA"}
|
|
@@ -55,9 +55,9 @@ function m(e) {
|
|
|
55
55
|
className: "text-status-error-fg"
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
|
-
function h({ submission: r, actualOutputLabel: a = "Your output" }) {
|
|
59
|
-
let
|
|
60
|
-
return
|
|
58
|
+
function h({ submission: r, actualOutputLabel: a = "Your output", caseNoun: s = "test cases" }) {
|
|
59
|
+
let c = r.testResults ?? [];
|
|
60
|
+
return c.length === 0 ? null : /* @__PURE__ */ u("div", {
|
|
61
61
|
className: "space-y-3",
|
|
62
62
|
children: [
|
|
63
63
|
/* @__PURE__ */ u("div", {
|
|
@@ -71,7 +71,8 @@ function h({ submission: r, actualOutputLabel: a = "Your output" }) {
|
|
|
71
71
|
r.passedCount,
|
|
72
72
|
" of ",
|
|
73
73
|
r.totalCount,
|
|
74
|
-
"
|
|
74
|
+
" ",
|
|
75
|
+
s
|
|
75
76
|
]
|
|
76
77
|
}),
|
|
77
78
|
/* @__PURE__ */ u("span", {
|
|
@@ -87,7 +88,7 @@ function h({ submission: r, actualOutputLabel: a = "Your output" }) {
|
|
|
87
88
|
}),
|
|
88
89
|
/* @__PURE__ */ l("div", {
|
|
89
90
|
className: "divide-y divide-border-seam overflow-hidden rounded-xl border border-border-seam",
|
|
90
|
-
children:
|
|
91
|
+
children: c.map((n) => {
|
|
91
92
|
let r = m(n.status);
|
|
92
93
|
return /* @__PURE__ */ u("div", {
|
|
93
94
|
className: "space-y-2 bg-bg-surface px-4 py-3",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubmissionViews.js","names":[],"sources":["../../../src/technospam/components/SubmissionViews.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { CheckCircle2, Clock, Code2, EyeOff, MinusCircle, XCircle } from 'lucide-react';\nimport { Badge } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { humanizeEnum } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport type { TecSubmission, TecTestCaseStatus } from '../api/technospamApi';\n\n/**\n * A dark, editor-window styled code surface: mac-style traffic-light dots plus a\n * language label header over a scrollable monospace body so read-only code reads\n * like a real IDE panel rather than a flat block.\n */\nexport function CodePanel({\n label,\n children,\n bodyClassName,\n}: {\n label: string;\n children: ReactNode;\n bodyClassName?: string;\n}) {\n return (\n <div className=\"overflow-hidden rounded-xl border border-border-strong bg-bg-sunken shadow-sm\">\n <div className=\"flex items-center justify-between gap-2 border-b border-border-seam bg-bg-canvas/40 px-4 py-2\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-error-border\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-warning-border\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-success-border\" />\n </div>\n <span className=\"inline-flex items-center gap-1.5 font-mono text-xs text-text-secondary\">\n <Code2 size={13} /> {label}\n </span>\n </div>\n <pre\n className={cn(\n 'overflow-auto p-4 font-mono text-xs leading-relaxed text-text-primary',\n bodyClassName\n )}\n >\n {children}\n </pre>\n </div>\n );\n}\n\n/** One labelled monospace input/output block inside a test-case result. */\nexport function IoBlock({ label, value }: { label: string; value?: string | null }) {\n return (\n <div className=\"min-w-0\">\n <p className=\"mb-1 text-xs font-medium uppercase tracking-wide text-text-muted\">{label}</p>\n <pre className=\"overflow-auto rounded-lg bg-bg-sunken px-3 py-2 font-mono text-xs leading-relaxed text-text-primary\">\n {value?.length ? value : '—'}\n </pre>\n </div>\n );\n}\n\n/** Icon + tone for a per-test-case status, using semantic tokens only. */\nfunction testCaseStatusTone(status: TecTestCaseStatus): { icon: ReactNode; className: string } {\n if (status === 'PASSED') {\n return { icon: <CheckCircle2 size={14} />, className: 'text-status-success-fg' };\n }\n if (status === 'SKIPPED') {\n return { icon: <MinusCircle size={14} />, className: 'text-text-muted' };\n }\n return { icon: <XCircle size={14} />, className: 'text-status-error-fg' };\n}\n\n/**\n * Per-test-case grading detail for a graded submission — shared by the learner's\n * problem page and the authoring console's grading panel.\n *\n * The backend redacts HIDDEN fixtures before persisting results, so a hidden\n * row here only ever carries its name and status — never the input, the\n * expected output, or what the submitted program actually printed.\n */\nexport function TestResultsPanel({\n submission,\n actualOutputLabel = 'Your output',\n}: {\n submission:
|
|
1
|
+
{"version":3,"file":"SubmissionViews.js","names":[],"sources":["../../../src/technospam/components/SubmissionViews.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { CheckCircle2, Clock, Code2, EyeOff, MinusCircle, XCircle } from 'lucide-react';\nimport { Badge } from '@burdenoff/fe-libs/ui';\nimport { StatusBadge } from './StatusBadge';\nimport { humanizeEnum } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport type { TecSubmission, TecTestCaseStatus } from '../api/technospamApi';\n\n/** The grading fields the panel renders — a graded submission or a sample run. */\nexport type GradedResult = Pick<\n TecSubmission,\n 'verdict' | 'passedCount' | 'totalCount' | 'runtimeMs' | 'stderr' | 'testResults'\n>;\n\n/**\n * A dark, editor-window styled code surface: mac-style traffic-light dots plus a\n * language label header over a scrollable monospace body so read-only code reads\n * like a real IDE panel rather than a flat block.\n */\nexport function CodePanel({\n label,\n children,\n bodyClassName,\n}: {\n label: string;\n children: ReactNode;\n bodyClassName?: string;\n}) {\n return (\n <div className=\"overflow-hidden rounded-xl border border-border-strong bg-bg-sunken shadow-sm\">\n <div className=\"flex items-center justify-between gap-2 border-b border-border-seam bg-bg-canvas/40 px-4 py-2\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-error-border\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-warning-border\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-success-border\" />\n </div>\n <span className=\"inline-flex items-center gap-1.5 font-mono text-xs text-text-secondary\">\n <Code2 size={13} /> {label}\n </span>\n </div>\n <pre\n className={cn(\n 'overflow-auto p-4 font-mono text-xs leading-relaxed text-text-primary',\n bodyClassName\n )}\n >\n {children}\n </pre>\n </div>\n );\n}\n\n/** One labelled monospace input/output block inside a test-case result. */\nexport function IoBlock({ label, value }: { label: string; value?: string | null }) {\n return (\n <div className=\"min-w-0\">\n <p className=\"mb-1 text-xs font-medium uppercase tracking-wide text-text-muted\">{label}</p>\n <pre className=\"overflow-auto rounded-lg bg-bg-sunken px-3 py-2 font-mono text-xs leading-relaxed text-text-primary\">\n {value?.length ? value : '—'}\n </pre>\n </div>\n );\n}\n\n/** Icon + tone for a per-test-case status, using semantic tokens only. */\nfunction testCaseStatusTone(status: TecTestCaseStatus): { icon: ReactNode; className: string } {\n if (status === 'PASSED') {\n return { icon: <CheckCircle2 size={14} />, className: 'text-status-success-fg' };\n }\n if (status === 'SKIPPED') {\n return { icon: <MinusCircle size={14} />, className: 'text-text-muted' };\n }\n return { icon: <XCircle size={14} />, className: 'text-status-error-fg' };\n}\n\n/**\n * Per-test-case grading detail for a graded submission — shared by the learner's\n * problem page and the authoring console's grading panel.\n *\n * The backend redacts HIDDEN fixtures before persisting results, so a hidden\n * row here only ever carries its name and status — never the input, the\n * expected output, or what the submitted program actually printed.\n */\nexport function TestResultsPanel({\n submission,\n actualOutputLabel = 'Your output',\n caseNoun = 'test cases',\n}: {\n submission: GradedResult;\n actualOutputLabel?: string;\n /** What the cases are called in the summary line, e.g. \"sample test cases\". */\n caseNoun?: string;\n}) {\n const results = submission.testResults ?? [];\n if (results.length === 0) return null;\n\n return (\n <div className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n <StatusBadge status={submission.verdict} />\n <span className=\"text-sm text-text-secondary\">\n Passed {submission.passedCount} of {submission.totalCount} {caseNoun}\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {submission.runtimeMs} ms\n </span>\n </div>\n\n <div className=\"divide-y divide-border-seam overflow-hidden rounded-xl border border-border-seam\">\n {results.map((result) => {\n const tone = testCaseStatusTone(result.status);\n return (\n <div key={result.testCaseId} className=\"space-y-2 bg-bg-surface px-4 py-3\">\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-1\">\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 text-sm font-medium',\n tone.className\n )}\n >\n {tone.icon} {result.name}\n </span>\n <Badge variant=\"outline\">{humanizeEnum(result.status)}</Badge>\n {result.visibility === 'HIDDEN' ? (\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <EyeOff size={12} /> hidden test\n </span>\n ) : null}\n <span className=\"ml-auto text-xs text-text-muted\">{result.durationMs} ms</span>\n </div>\n\n {result.visibility === 'PUBLIC' && result.status !== 'SKIPPED' ? (\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-3\">\n <IoBlock label=\"Input\" value={result.input} />\n <IoBlock label=\"Expected\" value={result.expectedOutput} />\n <IoBlock label={actualOutputLabel} value={result.actualOutput} />\n </div>\n ) : null}\n </div>\n );\n })}\n </div>\n\n {submission.stderr ? (\n <CodePanel label=\"stderr\" bodyClassName=\"max-h-48\">\n {submission.stderr}\n </CodePanel>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAmBA,SAAgB,EAAU,EACxB,UACA,aACA,oBAKC;AACD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD,EAAM,WAAU,mDAAoD,CAAA;KACpE,kBAAC,QAAD,EAAM,WAAU,qDAAsD,CAAA;KACtE,kBAAC,QAAD,EAAM,WAAU,qDAAsD,CAAA;KAClE;OACN,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;;KAAE;KAChB;MACH;MACN,kBAAC,OAAD;GACE,WAAW,EACT,yEACA,EACD;GAEA;GACG,CAAA,CACF;;;AAKV,SAAgB,EAAQ,EAAE,UAAO,YAAmD;AAClF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,KAAD;GAAG,WAAU;aAAoE;GAAU,CAAA,EAC3F,kBAAC,OAAD;GAAK,WAAU;aACZ,GAAO,SAAS,IAAQ;GACrB,CAAA,CACF;;;AAKV,SAAS,EAAmB,GAAmE;AAO7F,QANI,MAAW,WACN;EAAE,MAAM,kBAAC,GAAD,EAAc,MAAM,IAAM,CAAA;EAAE,WAAW;EAA0B,GAE9E,MAAW,YACN;EAAE,MAAM,kBAAC,GAAD,EAAa,MAAM,IAAM,CAAA;EAAE,WAAW;EAAmB,GAEnE;EAAE,MAAM,kBAAC,GAAD,EAAS,MAAM,IAAM,CAAA;EAAE,WAAW;EAAwB;;AAW3E,SAAgB,EAAiB,EAC/B,eACA,uBAAoB,eACpB,cAAW,gBAMV;CACD,IAAM,IAAU,EAAW,eAAe,EAAE;AAG5C,QAFI,EAAQ,WAAW,IAAU,OAG/B,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAa,QAAQ,EAAW,SAAW,CAAA;KAC3C,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OAA8C;OACpC,EAAW;OAAY;OAAK,EAAW;OAAW;OAAE;OACvD;;KACP,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OACE,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;;OAAE,EAAW;OAAU;OACrC;;KACH;;GAEN,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAQ,KAAK,MAAW;KACvB,IAAM,IAAO,EAAmB,EAAO,OAAO;AAC9C,YACE,kBAAC,OAAD;MAA6B,WAAU;gBAAvC,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,QAAD;SACE,WAAW,EACT,wDACA,EAAK,UACN;mBAJH;UAMG,EAAK;UAAK;UAAE,EAAO;UACf;;QACP,kBAAC,GAAD;SAAO,SAAQ;mBAAW,EAAa,EAAO,OAAO;SAAS,CAAA;QAC7D,EAAO,eAAe,WACrB,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CACE,kBAAC,GAAD,EAAQ,MAAM,IAAM,CAAA,EAAA,eACf;aACL;QACJ,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CAAmD,EAAO,YAAW,MAAU;;QAC3E;UAEL,EAAO,eAAe,YAAY,EAAO,WAAW,YACnD,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD;SAAS,OAAM;SAAQ,OAAO,EAAO;SAAS,CAAA;QAC9C,kBAAC,GAAD;SAAS,OAAM;SAAW,OAAO,EAAO;SAAkB,CAAA;QAC1D,kBAAC,GAAD;SAAS,OAAO;SAAmB,OAAO,EAAO;SAAgB,CAAA;QAC7D;WACJ,KACA;QA1BI,EAAO,WA0BX;MAER;IACE,CAAA;GAEL,EAAW,SACV,kBAAC,GAAD;IAAW,OAAM;IAAS,eAAc;cACrC,EAAW;IACF,CAAA,GACV;GACA"}
|
|
@@ -3,6 +3,12 @@ import { Component, ReactNode } from 'react';
|
|
|
3
3
|
interface Props {
|
|
4
4
|
children: ReactNode;
|
|
5
5
|
title?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Rendered instead of the default error view when a child throws. Used for
|
|
8
|
+
* small, recoverable regions (for example a lazily loaded editor chunk that
|
|
9
|
+
* failed to download) where a working fallback beats a page-level error.
|
|
10
|
+
*/
|
|
11
|
+
fallback?: ReactNode;
|
|
6
12
|
}
|
|
7
13
|
interface State {
|
|
8
14
|
hasError: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TechnoSpamErrorBoundary.d.ts","sourceRoot":"","sources":["../../../src/technospam/components/TechnoSpamErrorBoundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGlD,UAAU,KAAK;IACb,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"TechnoSpamErrorBoundary.d.ts","sourceRoot":"","sources":["../../../src/technospam/components/TechnoSpamErrorBoundary.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGlD,UAAU,KAAK;IACb,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,UAAU,KAAK;IACb,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,qBAAa,uBAAwB,SAAQ,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;gBACtD,KAAK,EAAE,KAAK;IAKxB,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK;IAIpD,MAAM,IAAI,SAAS;CAcpB"}
|