From f34e8ba2742ece07a27e1b9079e40938002ff067 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 2 Nov 2026 14:57:02 +0800 Subject: [PATCH] Fold the manual into the docs as a pseudocode reference The manual's content becomes six guides in a new Pseudocode reference group under /docs, in English and Chinese. /manual and /app/manual redirect to /docs, the Manual nav and footer links go, and the editor's modal now shows the docs guides and remembers the last one opened. Co-Authored-By: Claude Opus 5.5 --- apps/web/next.config.ts | 7 + .../[locale]/(public)/docs/[slug]/page.tsx | 4 +- .../src/app/[locale]/(public)/docs/page.tsx | 24 +- .../[locale]/(public)/lessons/[slug]/page.tsx | 8 +- .../(public)/manual/ManualContent.tsx | 815 ------------------ .../[locale]/(public)/manual/manualText.ts | 208 ----- .../src/app/[locale]/(public)/manual/page.tsx | 135 --- .../[locale]/(public)/practice/[id]/page.tsx | 2 +- apps/web/src/app/[locale]/app/manual/page.tsx | 16 - apps/web/src/app/[locale]/app/page.test.tsx | 19 +- apps/web/src/app/[locale]/app/page.tsx | 54 +- apps/web/src/app/components/Footer.tsx | 1 - apps/web/src/app/components/PublicHeader.tsx | 3 +- .../app/components/docs/ContentArticle.tsx | 9 +- .../docs}/CopyableCodeBlock.tsx | 32 +- .../app/components/docs/DocSectionView.tsx | 2 +- .../web/src/app/components/docs/DocsPanel.tsx | 117 +++ apps/web/src/app/components/docs/unitIcons.ts | 3 +- apps/web/src/app/globals.css | 304 +------ apps/web/src/app/sitemap.test.ts | 3 +- apps/web/src/app/sitemap.ts | 2 - apps/web/src/i18n/config.test.ts | 8 +- apps/web/src/i18n/messages/editor.en.ts | 6 +- apps/web/src/i18n/messages/editor.zh.ts | 6 +- apps/web/src/i18n/messages/en.ts | 78 +- apps/web/src/i18n/messages/zh.ts | 75 +- apps/web/src/lib/docs/docs.test.ts | 8 +- apps/web/src/lib/docs/en/index.ts | 2 + .../src/lib/docs/en/pseudocode-reference.ts | 581 +++++++++++++ apps/web/src/lib/docs/en/pseudoeditor.ts | 37 - apps/web/src/lib/docs/guides.ts | 12 + apps/web/src/lib/docs/index.ts | 22 +- apps/web/src/lib/docs/types.ts | 5 +- apps/web/src/lib/docs/units.ts | 16 + apps/web/src/lib/docs/zh/index.ts | 2 + .../src/lib/docs/zh/pseudocode-reference.ts | 575 ++++++++++++ apps/web/src/lib/docs/zh/pseudoeditor.ts | 36 - apps/web/src/lib/seo-content.ts | 4 +- 38 files changed, 1481 insertions(+), 1760 deletions(-) delete mode 100644 apps/web/src/app/[locale]/(public)/manual/ManualContent.tsx delete mode 100644 apps/web/src/app/[locale]/(public)/manual/manualText.ts delete mode 100644 apps/web/src/app/[locale]/(public)/manual/page.tsx delete mode 100644 apps/web/src/app/[locale]/app/manual/page.tsx rename apps/web/src/app/{[locale]/(public)/manual => components/docs}/CopyableCodeBlock.tsx (84%) create mode 100644 apps/web/src/app/components/docs/DocsPanel.tsx create mode 100644 apps/web/src/lib/docs/en/pseudocode-reference.ts create mode 100644 apps/web/src/lib/docs/guides.ts create mode 100644 apps/web/src/lib/docs/zh/pseudocode-reference.ts diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index b4d229a..39d7cd9 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -72,6 +72,13 @@ const nextConfig: NextConfig = { unoptimized: true, }, poweredByHeader: false, + async redirects() { + // The manual's content now lives in the docs as the pseudocode reference. + return ["/manual", "/app/manual"].flatMap((source) => [ + { source, destination: "/docs", permanent: true }, + { source: `/:locale(en|zh)${source}`, destination: "/:locale/docs", permanent: true }, + ]); + }, async headers() { const immutableAssetHeaders = [ { diff --git a/apps/web/src/app/[locale]/(public)/docs/[slug]/page.tsx b/apps/web/src/app/[locale]/(public)/docs/[slug]/page.tsx index e51c727..3b9727b 100644 --- a/apps/web/src/app/[locale]/(public)/docs/[slug]/page.tsx +++ b/apps/web/src/app/[locale]/(public)/docs/[slug]/page.tsx @@ -4,7 +4,7 @@ import { ContentArticle } from "@/app/components/docs/ContentArticle"; import { localePath, pageMetadata } from "@/i18n/config"; import { getDictionary } from "@/i18n/messages"; import { resolveLocale } from "@/i18n/server"; -import { docs, docPath, getDoc } from "@/lib/docs"; +import { docs, docPath, getDoc, isGuide } from "@/lib/docs"; type PageProps = { params: Promise<{ locale: string; slug: string }> }; @@ -26,6 +26,6 @@ export default async function DocPage({ params }: PageProps) { const { slug } = await params; const doc = getDoc(locale, slug); if (!doc) notFound(); - if (doc.unit !== "pseudoeditor") permanentRedirect(localePath(locale, docPath(doc))); + if (!isGuide(doc)) permanentRedirect(localePath(locale, docPath(doc))); return ; } diff --git a/apps/web/src/app/[locale]/(public)/docs/page.tsx b/apps/web/src/app/[locale]/(public)/docs/page.tsx index f13071b..1c65fd7 100644 --- a/apps/web/src/app/[locale]/(public)/docs/page.tsx +++ b/apps/web/src/app/[locale]/(public)/docs/page.tsx @@ -6,7 +6,7 @@ import { PublicListLink, publicListClassName } from "@/app/components/PublicUi"; import { localePath, localeTags, localeUrl, pageMetadata } from "@/i18n/config"; import { getDictionary } from "@/i18n/messages"; import { resolveLocale, type LocaleParams } from "@/i18n/server"; -import { docPath, getGuides } from "@/lib/docs"; +import { docPath, getGuides, getUnit, guideUnitIds } from "@/lib/docs"; export async function generateMetadata({ params }: LocaleParams): Promise { const locale = await resolveLocale(params); @@ -60,14 +60,20 @@ export default async function DocsIndexPage({ params }: LocaleParams) { -
-

{t.guidesTitle}

-
- {guides.map((doc) => ( - - ))} -
-
+ {guideUnitIds.map((id) => { + const unit = getUnit(locale, id); + return ( +
+

{unit.title}

+

{unit.summary}

+
+ {guides.filter((doc) => doc.unit === id).map((doc) => ( + + ))} +
+
+ ); + })}
diff --git a/apps/web/src/app/[locale]/(public)/lessons/[slug]/page.tsx b/apps/web/src/app/[locale]/(public)/lessons/[slug]/page.tsx index 91992c0..1660e2f 100644 --- a/apps/web/src/app/[locale]/(public)/lessons/[slug]/page.tsx +++ b/apps/web/src/app/[locale]/(public)/lessons/[slug]/page.tsx @@ -4,19 +4,19 @@ import { ContentArticle } from "@/app/components/docs/ContentArticle"; import { pageMetadata } from "@/i18n/config"; import { getDictionary } from "@/i18n/messages"; import { resolveLocale } from "@/i18n/server"; -import { docs, docPath, getDoc } from "@/lib/docs"; +import { docs, docPath, getDoc, isGuide } from "@/lib/docs"; type PageProps = { params: Promise<{ locale: string; slug: string }> }; export function generateStaticParams() { - return docs.filter((doc) => doc.unit !== "pseudoeditor").map((doc) => ({ slug: doc.slug })); + return docs.filter((doc) => !isGuide(doc)).map((doc) => ({ slug: doc.slug })); } export async function generateMetadata({ params }: PageProps): Promise { const locale = await resolveLocale(params); const { slug } = await params; const doc = getDoc(locale, slug); - if (!doc || doc.unit === "pseudoeditor") return {}; + if (!doc || isGuide(doc)) return {}; const base = pageMetadata(locale, docPath(doc), doc.title, doc.description, getDictionary(locale).meta.ogImageAlt); return { ...base, openGraph: { ...base.openGraph, type: "article", modifiedTime: doc.updated } }; } @@ -25,6 +25,6 @@ export default async function LessonPage({ params }: PageProps) { const locale = await resolveLocale(params); const { slug } = await params; const doc = getDoc(locale, slug); - if (!doc || doc.unit === "pseudoeditor") notFound(); + if (!doc || isGuide(doc)) notFound(); return ; } diff --git a/apps/web/src/app/[locale]/(public)/manual/ManualContent.tsx b/apps/web/src/app/[locale]/(public)/manual/ManualContent.tsx deleted file mode 100644 index 291ac46..0000000 --- a/apps/web/src/app/[locale]/(public)/manual/ManualContent.tsx +++ /dev/null @@ -1,815 +0,0 @@ -import Link from "next/link"; -import { defaultLocale, localePath, type Locale } from "@/i18n/config"; -import CopyableCodeBlock from "./CopyableCodeBlock"; -import { manualText } from "./manualText"; - -type Pattern = { - title: string; - whenToUse: string; - logic: string[]; - template: string; - walkthrough: string[]; -}; - -type WorkedProgram = { - title: string; - objective: string; - code: string; - explanation: string[]; - testRun: string[]; -}; - -function ManualCard({ - children, - className = "", - accent = false, - delay = 0, - id, -}: { - children: React.ReactNode; - className?: string; - accent?: boolean; - delay?: number; - id?: string; -}) { - return ( -
-
{children}
-
- ); -} - -function SectionTitle({ children }: { children: React.ReactNode }) { - return

{children}

; -} - -function InfoBadge({ children }: { children: React.ReactNode }) { - return ( -
{children}
- ); -} - -const commandWords = [ - ["Calculate", "Work out from given facts, figures or information."], - ["Compare", "Identify and comment on similarities and differences."], - ["Define", "Give the precise meaning."], - ["Demonstrate", "Show how, or give an example."], - ["Describe", "State points, characteristics and main features."], - ["Evaluate", "Judge quality, importance, amount or value."], - ["Explain", "Give reasons, show relationships, and support with evidence."], - ["Give", "Produce an answer from source material or recall."], - ["Identify", "Name, select or recognise."], - ["Outline", "Set out main points."], - ["Show (that)", "Provide structured evidence leading to a result."], - ["State", "Express in clear terms."], - ["Suggest", "Apply knowledge to give valid proposals or considerations."], -] as const; - -const loopPatterns: Pattern[] = [ - { - title: "Counted Loop with FOR (Fixed Number of Repeats)", - whenToUse: "You know exactly how many times the loop must run.", - logic: [ - "Set loop variable start and end values.", - "Run body once for each value in the range.", - "Update totals/counters each cycle.", - "Exit automatically when range is complete.", - ], - template: `DECLARE Count : INTEGER -DECLARE Total : INTEGER -Total <- 0 - -FOR Count <- 1 TO 10 - Total <- Total + Count -NEXT Count - -OUTPUT "Total = ", Total`, - walkthrough: [ - "Before loop: Total = 0", - "Count=1 -> Total=1", - "Count=2 -> Total=3", - "Count=3 -> Total=6", - "...", - "Count=10 -> Total=55 then loop ends", - ], - }, - { - title: "Pre-condition Loop with WHILE (Unknown Repeats)", - whenToUse: "Repeat while a condition remains TRUE.", - logic: [ - "Condition is checked before each iteration.", - "If condition is FALSE at start, loop runs zero times.", - "Body must change state so condition can eventually become FALSE.", - ], - template: `DECLARE Number : INTEGER -INPUT Number - -WHILE Number > 9 DO - Number <- Number - 9 -ENDWHILE - -OUTPUT Number`, - walkthrough: [ - "Input 28", - "28 > 9 true -> Number becomes 19", - "19 > 9 true -> Number becomes 10", - "10 > 9 true -> Number becomes 1", - "1 > 9 false -> exit", - ], - }, - { - title: "Post-condition Loop with REPEAT UNTIL (Validation Loop)", - whenToUse: "User must do something at least once, then repeat until valid.", - logic: [ - "Body executes first, then condition is tested.", - "Best for input validation and menu retries.", - "Condition should represent the 'valid/finished' state.", - ], - template: `DECLARE Password : STRING - -REPEAT - OUTPUT "Enter password" - INPUT Password -UNTIL Password = "Secret"`, - walkthrough: [ - "If first entry is wrong, loop repeats.", - "If first entry is correct, still valid because one run is guaranteed.", - ], - }, - { - title: "Nested FOR Loops (Tables / Grids / 2D Arrays)", - whenToUse: "Process rows and columns or all combinations of two ranges.", - logic: [ - "Outer loop controls each row/item group.", - "Inner loop completes all columns/items for current outer value.", - "Reset row-level totals before starting inner loop.", - ], - template: `DECLARE Row : INTEGER -DECLARE Column : INTEGER -DECLARE RowTotal : INTEGER -DECLARE GrandTotal : INTEGER -DECLARE Amount : ARRAY[1:5, 1:4] OF INTEGER - -GrandTotal <- 0 -FOR Row <- 1 TO 5 - RowTotal <- 0 - FOR Column <- 1 TO 4 - RowTotal <- RowTotal + Amount[Row, Column] - NEXT Column - OUTPUT "Row ", Row, " total = ", RowTotal - GrandTotal <- GrandTotal + RowTotal -NEXT Row - -OUTPUT "Grand total = ", GrandTotal`, - walkthrough: [ - "Row 1 processes columns 1..4, then outputs row 1 total.", - "Row 2 starts fresh with RowTotal reset to 0.", - "After final row, GrandTotal contains sum of all cells.", - ], - }, -]; - -const workedPrograms: WorkedProgram[] = [ - { - title: "Program A: Grade Counter with Selection + Loop", - objective: "Read 5 marks, count how many are passes (>= 50), and show class average.", - code: `DECLARE Index : INTEGER -DECLARE Mark : INTEGER -DECLARE Total : INTEGER -DECLARE PassCount : INTEGER -DECLARE Average : REAL - -Total <- 0 -PassCount <- 0 - -FOR Index <- 1 TO 5 - OUTPUT "Enter mark ", Index - INPUT Mark - Total <- Total + Mark - - IF Mark >= 50 - THEN - PassCount <- PassCount + 1 - ENDIF -NEXT Index - -Average <- Total / 5 -OUTPUT "Passes = ", PassCount -OUTPUT "Average = ", Average`, - explanation: [ - "This is count-controlled because there are exactly 5 marks.", - "Total accumulates all marks; PassCount tracks a condition inside the loop.", - "IF branch is optional per iteration; only true marks increment PassCount.", - "Average is computed once, after loop completes.", - ], - testRun: [ - "Input marks: 42, 50, 74, 21, 90", - "Total = 277", - "PassCount = 3", - "Average = 55.4", - ], - }, - { - title: "Program B: Input Validation Menu with REPEAT UNTIL", - objective: "Accept only menu choices 1 to 4.", - code: `DECLARE Choice : INTEGER - -REPEAT - OUTPUT "1.View 2.Add 3.Delete 4.Exit" - INPUT Choice -UNTIL Choice >= 1 AND Choice <= 4 - -OUTPUT "Accepted choice: ", Choice`, - explanation: [ - "REPEAT UNTIL is ideal because user must be prompted at least once.", - "Condition states what valid means, not what invalid means.", - "Using AND ensures both lower and upper bounds are respected.", - ], - testRun: [ - "Input: 8 -> invalid, repeats", - "Input: 0 -> invalid, repeats", - "Input: 3 -> valid, exits", - ], - }, - { - title: "Program C: Search in Array with Flag", - objective: "Find whether a target name exists in StudentNames[1:30].", - code: `DECLARE StudentNames : ARRAY[1:30] OF STRING -DECLARE Index : INTEGER -DECLARE Target : STRING -DECLARE Found : BOOLEAN - -INPUT Target -Found <- FALSE - -FOR Index <- 1 TO 30 - IF StudentNames[Index] = Target - THEN - Found <- TRUE - ENDIF -NEXT Index - -IF Found = TRUE - THEN - OUTPUT "Found" - ELSE - OUTPUT "Not found" -ENDIF`, - explanation: [ - "Found starts FALSE and flips TRUE when match appears.", - "This variant checks all items; examiners usually accept this clear style.", - "Final IF uses the flag to output one message.", - ], - testRun: [ - 'Target = "Ali" and appears at index 7 -> Found becomes TRUE', - "Output: Found", - ], - }, - { - title: "Program D: Procedure + Function Together", - objective: "Show reuse: a procedure for display and a function for calculation.", - code: `PROCEDURE PrintLine(Count : INTEGER) - DECLARE Index : INTEGER - FOR Index <- 1 TO Count - OUTPUT "-" - NEXT Index -ENDPROCEDURE - -FUNCTION SumSquare(A : INTEGER, B : INTEGER) RETURNS INTEGER - RETURN A * A + B * B -ENDFUNCTION - -DECLARE Answer : INTEGER -CALL PrintLine(10) -Answer <- SumSquare(3, 4) -OUTPUT "Answer = ", Answer`, - explanation: [ - "Procedure is called using CALL because it is a full statement.", - "Function returns a value, so it appears in an expression.", - "Keeping display logic and calculation logic separate improves clarity.", - ], - testRun: [ - "PrintLine outputs 10 dashes.", - "SumSquare(3,4) returns 25.", - "Output: Answer = 25", - ], - }, -]; - -const fullStructureExample = `// ================================================== -// EXAM-STYLE PSEUDOCODE STRUCTURE -// ================================================== -// 1) Declarations and constants -DECLARE Index : INTEGER -DECLARE Value : INTEGER -DECLARE Total : INTEGER -CONSTANT MaxItems <- 5 - -// 2) Initialisation -Total <- 0 - -// 3) Input + processing -FOR Index <- 1 TO MaxItems - INPUT Value - Total <- Total + Value -NEXT Index - -// 4) Output -OUTPUT "Total = ", Total`; - -const fileHandlingExample = `DECLARE FileName : STRING -DECLARE LineText : STRING - -FileName <- "Scores.txt" -OPENFILE FileName FOR READ - -READFILE FileName, LineText -OUTPUT "First line was: ", LineText - -CLOSEFILE FileName`; - -const routineSyntaxExample = `LENGTH("Happy Days") -LCASE('W') -UCASE("Happy") -SUBSTRING("Happy Days", 1, 5) -ROUND(15.6789, 2) -RANDOM() - -Value <- ROUND(RANDOM() * 6, 0)`; - -const traceTableExample = `DECLARE N : INTEGER -DECLARE Fact : INTEGER -DECLARE I : INTEGER - -INPUT N -Fact <- 1 -FOR I <- 1 TO N - Fact <- Fact * I -NEXT I -OUTPUT Fact`; - -interface ManualContentProps { - onClose?: () => void; - isModal?: boolean; - locale?: Locale; -} - -export default function ManualContent({ onClose, isModal = false, locale = defaultLocale }: ManualContentProps) { - const t = manualText(locale); - return ( -
- {isModal ? ( - - ) : null} -
- {/* Hero Card */} - -
-
- {t("Cambridge 0478 Guide")} -

- {t("Detailed Pseudocode Guidelines")} -

-

- {t("These guidelines are based on Cambridge IGCSE Computer Science (0478) syllabus 2026-2028 pseudocode conventions from the assessment details section (pages 35-49). They focus on practical writing: structuring logic, choosing correct control flow, and building exam-style solutions with clear, readable pseudocode.")} -

-
- {!isModal ? ( - - - {t("Open editor")} - - ) : null} -
- -
-
-
-

{t("Important notation note")}

-

- {t("Official Cambridge examples use a left-arrow assignment symbol. In this compiler/editor, write assignment as")} {"<-"}{t(".")} -

-
-
-
-
-

{t("Editor support")}

-

- {t("Settings has an Exam syntax control for Cambridge IGCSE, Cambridge A Level, IB, OCR, and AQA. Completions follow the board you pick, including DIV, MOD, LENGTH, and the extra routines that board uses.")} -

-
-
-
-
- - {/* Navigation */} - - {t("Quick Navigation")} -
- {[ - { href: "#workflow", label: "Writing Workflow", num: "1" }, - { href: "#syntax", label: "Core Syntax + Routines", num: "2" }, - { href: "#loops", label: "Loop Logic Deep Dive", num: "3" }, - { href: "#patterns", label: "Reusable Coding Patterns", num: "4" }, - { href: "#worked", label: "Fully Worked Programs", num: "5" }, - { href: "#trace", label: "Trace Tables", num: "6" }, - { href: "#files", label: "File Handling", num: "7" }, - { href: "#exam", label: "Exam Command Words", num: "8" }, - ].map((item) => ( - - - - {item.num} - - {t(item.label)} - - ))} -
-
- - {/* Section 1 */} - - {t("Writing Workflow")} -

- {t("Use this process every time you solve a question to avoid logic errors.")} -

-
    - {[ - "Read the problem and identify required inputs and outputs.", - "Declare every variable/array with correct data type.", - "Initialize counters/totals/flags before loops.", - "Choose selection and loop types based on the problem conditions.", - "Write processing logic in small, readable blocks.", - "Output final results after processing.", - "Dry-run with sample values and edge cases.", - ].map((step, i) => ( -
  1. - - {i + 1} - - {t(step)} -
  2. - ))} -
- -
- - {/* Section 2 */} - - {t("Core Syntax Rules")} -
- {[ - { - title: "Formatting and Names", - points: [ - "Keywords in UPPER CASE: IF, FOR, WHILE, PROCEDURE.", - "Identifiers in PascalCase and starting with capital letter.", - "No underscore in identifier names.", - "Use comments with //.", - ], - }, - { - title: "Data Types and Constants", - points: [ - "INTEGER, REAL, CHAR, STRING, BOOLEAN.", - "DECLARE Name : TYPE for variables.", - "CONSTANT Name <- literal for constants.", - "Constant value must be a literal, not an expression.", - ], - }, - { - title: "Operators", - points: [ - "Arithmetic: + - * / ^", - "Integer division routines: DIV(a,b), MOD(a,b)", - "Relational: = < <= > >= <>", - "Logical: AND OR NOT", - ], - }, - { - title: "Common Library Routines", - points: [ - "LENGTH(StringValue) returns the number of characters in a string.", - "LCASE(StringOrChar) and UCASE(StringOrChar) change case.", - "SUBSTRING(StringValue, Start, Length) uses a 1-based start position.", - "ROUND(RealValue, Places) rounds a real number to a chosen number of decimal places.", - "RANDOM() returns a random number between 0 and 1 inclusive.", - ], - }, - ].map((card) => ( -
-
-

{t(card.title)}

-
    - {card.points.map((p) => ( -
  • - - {t(p)} -
  • - ))} -
-
-
- ))} -
-
-
-

{t("Official Routine Syntax")}

- -

- {t("Cambridge specifically defines")} {t("ROUND(Value, Places)")}{" "}{t("and")}{" "}{t("RANDOM()")}{t(", and both routines are available in this editor's autocomplete.")} -

-
-
-
- - {/* Section 3 */} - - {t("Loop Logic Deep Dive")} -

- {t("Students lose marks by choosing the wrong loop type or by writing loops that never terminate. Understand the condition timing and state changes in every cycle.")} -

-
- {[ - { - title: "FOR Loop", - points: [ - "Use FOR when iteration count is known.", - "Bounds are inclusive, so FOR I <- 1 TO 5 runs 5 times.", - "STEP can be positive or negative.", - ], - }, - { - title: "WHILE Loop", - points: [ - "Condition checked before loop body.", - "Can execute zero times if initial condition is FALSE.", - "Update variables inside loop so condition can become FALSE.", - ], - }, - { - title: "REPEAT UNTIL", - points: [ - "Loop body executes before condition check.", - "Always executes at least once.", - "Use UNTIL valid condition for input validation patterns.", - ], - }, - ].map((card) => ( -
-
-

{t(card.title)}

-
    - {card.points.map((p) => ( -
  • - - {t(p)} -
  • - ))} -
-
-
- ))} -
-
-
-

{t("Termination Checklist")}

-
    - {[ - "Does each iteration change state?", - "Can condition eventually become FALSE (WHILE) or TRUE (UNTIL)?", - "Are bounds correct (no off-by-one mistakes)?", - "Have you initialized counters/totals before the loop?", - ].map((p) => ( -
  • - - {t(p)} -
  • - ))} -
-
-
-
- - {/* Section 4 */} - - {t("Reusable Coding Patterns")} -
- {loopPatterns.map((pattern, i) => ( -
-
-
- - {i + 1} - -

{t(pattern.title)}

-
-

- {t("When to use:")} {t(pattern.whenToUse)} -

-
    - {pattern.logic.map((item) => ( -
  • - - {t(item)} -
  • - ))} -
- -
-

{t("Logic walkthrough")}

-
    - {pattern.walkthrough.map((step) => ( -
  • - - {t(step)} -
  • - ))} -
-
-
-
- ))} -
-
- - {/* Section 5 */} - - {t("Fully Worked Programs")} -

- {t("These are full exam-style answers showing declarations, control flow, and output.")} -

-
- {workedPrograms.map((program, i) => ( -
-
-
- - {String.fromCharCode(65 + i)} - -

{t(program.title)}

-
-

- {t("Objective:")} {t(program.objective)} -

- -
-
-

{t("How the logic works")}

-
    - {program.explanation.map((line) => ( -
  • - - {t(line)} -
  • - ))} -
-
-
-

{t("Sample run")}

-
    - {program.testRun.map((line) => ( -
  • - - {t(line)} -
  • - ))} -
-
-
-
-
- ))} -
-
- - {/* Section 6 */} - - {t("Trace Tables and Dry Runs")} -

- {t("In exams, a fast dry-run catches most logical mistakes. Track key variables after each iteration.")} -

- -
- - - - - - - - - - {[ - [1, 1, 1], - [2, 1, 2], - [3, 2, 6], - [4, 6, 24], - [5, 24, 120], - ].map(([i, before, after]) => ( - - - - - - ))} - -
{t("I")}{t("Fact before")}{t("Fact after Fact <- Fact * I")}
{i}{before}{after}
-
-

- {t("For N = 5, expected output is 120. If your trace table and output disagree, debug the loop boundaries and initial value.")} -

-
- - {/* Section 7 */} - - {t("File Handling Guide")} -
- {[ - "Declare file name and data variables.", - "OPENFILE with FOR READ or FOR WRITE.", - "Use READFILE/WRITEFILE operations.", - "CLOSEFILE when finished.", - ].map((step, i) => ( -
-
- - {i + 1} - -

{t(step)}

-
-
- ))} -
- -
-
-

- {t("Avoid opening the same file in both READ and WRITE simultaneously, and always close files even in short algorithms.")} -

-
-
-
- - {/* Section 8 */} - - {t("Exam Command Words")} -
- - - - - - - - - {commandWords.map(([word, meaning]) => ( - - - - - ))} - -
{t("Command Word")}{t("Meaning in Answers")}
{t(word)}{t(meaning)}
-
-
- - {/* Final Checklist */} - - {t("Final Checklist Before Submitting")} -
- {[ - "All variables/arrays declared with correct data types.", - "Initial values set for counters, totals, and flags.", - "Correct loop choice: FOR vs WHILE vs REPEAT UNTIL.", - "All IF/CASE/loop blocks properly ended.", - "Procedure calls use CALL; function calls appear in expressions.", - "Outputs match the question requirements exactly.", - "Built-in routines such as ROUND() and RANDOM() use the correct syntax.", - "Dry-run tested with normal and edge-case inputs.", - ].map((item, i) => ( -
- - - - {t(item)} -
- ))} -
-
-
-
- ); -} diff --git a/apps/web/src/app/[locale]/(public)/manual/manualText.ts b/apps/web/src/app/[locale]/(public)/manual/manualText.ts deleted file mode 100644 index 48a63cd..0000000 --- a/apps/web/src/app/[locale]/(public)/manual/manualText.ts +++ /dev/null @@ -1,208 +0,0 @@ -import type { Locale } from "@/i18n/config"; - -/** - * Chinese text for ManualContent, keyed by the English string it replaces. - * English stays inline in ManualContent; missing keys fall back to English. - */ -const manualZh: Record = { - "Cambridge 0478 Guide": "Cambridge 0478 指南", - "Detailed Pseudocode Guidelines": "伪代码详细指南", - "These guidelines are based on Cambridge IGCSE Computer Science (0478) syllabus 2026-2028 pseudocode conventions from the assessment details section (pages 35-49). They focus on practical writing: structuring logic, choosing correct control flow, and building exam-style solutions with clear, readable pseudocode.": "本指南依据 Cambridge IGCSE Computer Science(0478)2026–2028 年考纲中“评估详情”部分(第 35–49 页)的伪代码规范编写。内容侧重实际作答:组织逻辑、选择正确的控制结构,以及用清晰易读的伪代码写出符合考试要求的答案。", - "Open editor": "打开编辑器", - "Important notation note": "重要符号说明", - "Official Cambridge examples use a left-arrow assignment symbol. In this compiler/editor, write assignment as": "Cambridge 官方示例使用向左箭头表示赋值。在本编译器/编辑器中,请将赋值写成", - ".": "。", - "Editor support": "编辑器支持", - "Settings has an Exam syntax control for Cambridge IGCSE, Cambridge A Level, IB, OCR, and AQA. Completions follow the board you pick, including DIV, MOD, LENGTH, and the extra routines that board uses.": "设置中的考试语法选项支持 Cambridge IGCSE、Cambridge A Level、IB、OCR 和 AQA。自动补全会跟随你选择的考试局,包括 DIV、MOD、LENGTH 以及该考试局特有的例程。", - "Quick Navigation": "快速导航", - "Writing Workflow": "编写步骤", - "Core Syntax + Routines": "核心语法与函数", - "Loop Logic Deep Dive": "深入理解循环逻辑", - "Reusable Coding Patterns": "可复用的编程模式", - "Fully Worked Programs": "完整示例程序", - "Trace Tables": "跟踪表", - "File Handling": "文件处理", - "Exam Command Words": "考试指令词", - "Use this process every time you solve a question to avoid logic errors.": "每次解题都按以下步骤进行,可以减少逻辑错误。", - "Read the problem and identify required inputs and outputs.": "阅读题目,确定所需的输入和输出。", - "Declare every variable/array with correct data type.": "用正确的数据类型声明每个变量和数组。", - "Initialize counters/totals/flags before loops.": "在循环开始前初始化计数器、累加值和标志。", - "Choose selection and loop types based on the problem conditions.": "根据题目条件选择合适的选择结构和循环类型。", - "Write processing logic in small, readable blocks.": "将处理逻辑写成简短、易读的代码块。", - "Output final results after processing.": "处理完成后输出最终结果。", - "Dry-run with sample values and edge cases.": "用示例值和边界情况进行手动运行。", - "Pseudocode": "伪代码", - "Copy": "复制", - "Core Syntax Rules": "核心语法规则", - "Formatting and Names": "格式与命名", - "Keywords in UPPER CASE: IF, FOR, WHILE, PROCEDURE.": "关键字使用大写:IF、FOR、WHILE、PROCEDURE。", - "Identifiers in PascalCase and starting with capital letter.": "标识符使用 PascalCase,并以大写字母开头。", - "No underscore in identifier names.": "标识符名称中不要使用下划线。", - "Use comments with //.": "使用 // 编写注释。", - "Data Types and Constants": "数据类型与常量", - "INTEGER, REAL, CHAR, STRING, BOOLEAN.": "INTEGER、REAL、CHAR、STRING、BOOLEAN。", - "DECLARE Name : TYPE for variables.": "变量使用 DECLARE Name : TYPE 声明。", - "CONSTANT Name <- literal for constants.": "常量使用 CONSTANT Name <- literal 声明。", - "Constant value must be a literal, not an expression.": "常量值必须是字面量,不能是表达式。", - "Operators": "运算符", - "Arithmetic: + - * / ^": "算术运算:+ - * / ^", - "Integer division routines: DIV(a,b), MOD(a,b)": "整数除法函数:DIV(a,b)、MOD(a,b)", - "Relational: = < <= > >= <>": "关系运算:= < <= > >= <>", - "Logical: AND OR NOT": "逻辑运算:AND OR NOT", - "Common Library Routines": "常用库函数", - "LENGTH(StringValue) returns the number of characters in a string.": "LENGTH(StringValue) 返回字符串中的字符数。", - "LCASE(StringOrChar) and UCASE(StringOrChar) change case.": "LCASE(StringOrChar) 和 UCASE(StringOrChar) 转换字母大小写。", - "SUBSTRING(StringValue, Start, Length) uses a 1-based start position.": "SUBSTRING(StringValue, Start, Length) 的起始位置从 1 开始计数。", - "ROUND(RealValue, Places) rounds a real number to a chosen number of decimal places.": "ROUND(RealValue, Places) 将实数四舍五入到指定的小数位数。", - "RANDOM() returns a random number between 0 and 1 inclusive.": "RANDOM() 返回 0 到 1 之间的随机数,包括 0 和 1。", - "Official Routine Syntax": "官方函数语法", - "Cambridge specifically defines": "Cambridge 明确定义了", - "and": "和", - ", and both routines are available in this editor's autocomplete.": ",本编辑器的自动补全也支持这两个函数。", - "Students lose marks by choosing the wrong loop type or by writing loops that never terminate. Understand the condition timing and state changes in every cycle.": "选择了错误的循环类型,或写出无法终止的循环,都会失分。你需要理解每次循环在何时检查条件,以及状态如何变化。", - "FOR Loop": "FOR 循环", - "Use FOR when iteration count is known.": "已知循环次数时使用 FOR。", - "Bounds are inclusive, so FOR I <- 1 TO 5 runs 5 times.": "上下界都包括在内,因此 FOR I <- 1 TO 5 会运行 5 次。", - "STEP can be positive or negative.": "STEP 可以是正数或负数。", - "WHILE Loop": "WHILE 循环", - "Condition checked before loop body.": "在执行循环体之前检查条件。", - "Can execute zero times if initial condition is FALSE.": "如果初始条件为 FALSE,循环可能一次也不执行。", - "Update variables inside loop so condition can become FALSE.": "在循环内更新变量,使条件最终能够变为 FALSE。", - "Loop body executes before condition check.": "先执行循环体,再检查条件。", - "Always executes at least once.": "循环体至少执行一次。", - "Use UNTIL valid condition for input validation patterns.": "进行输入校验时,用 UNTIL 后的条件表示输入有效。", - "Termination Checklist": "终止检查清单", - "Does each iteration change state?": "每次循环是否都会改变状态?", - "Can condition eventually become FALSE (WHILE) or TRUE (UNTIL)?": "条件最终能否变为 FALSE(WHILE)或 TRUE(UNTIL)?", - "Are bounds correct (no off-by-one mistakes)?": "循环边界是否正确,没有差一错误?", - "Have you initialized counters/totals before the loop?": "是否在循环前初始化了计数器和累加值?", - "Counted Loop with FOR (Fixed Number of Repeats)": "使用 FOR 的计数循环(固定重复次数)", - "When to use:": "适用场景:", - "You know exactly how many times the loop must run.": "你明确知道循环需要执行多少次。", - "Set loop variable start and end values.": "设置循环变量的起始值和结束值。", - "Run body once for each value in the range.": "对范围内的每个值执行一次循环体。", - "Update totals/counters each cycle.": "每轮循环更新累加值或计数器。", - "Exit automatically when range is complete.": "处理完整个范围后自动退出。", - "Logic walkthrough": "逻辑逐步讲解", - "Before loop: Total = 0": "循环前:Total = 0", - "Count=10 -> Total=55 then loop ends": "Count=10 -> Total=55,然后循环结束", - "Pre-condition Loop with WHILE (Unknown Repeats)": "使用 WHILE 的前测条件循环(重复次数未知)", - "Repeat while a condition remains TRUE.": "条件保持 TRUE 时重复执行。", - "Condition is checked before each iteration.": "每轮循环开始前检查条件。", - "If condition is FALSE at start, loop runs zero times.": "如果开始时条件为 FALSE,循环会执行零次。", - "Body must change state so condition can eventually become FALSE.": "循环体必须改变状态,使条件最终能够变为 FALSE。", - "Input 28": "输入 28", - "28 > 9 true -> Number becomes 19": "28 > 9 为 true -> Number 变为 19", - "19 > 9 true -> Number becomes 10": "19 > 9 为 true -> Number 变为 10", - "10 > 9 true -> Number becomes 1": "10 > 9 为 true -> Number 变为 1", - "1 > 9 false -> exit": "1 > 9 为 false -> 退出", - "Post-condition Loop with REPEAT UNTIL (Validation Loop)": "使用 REPEAT UNTIL 的后测条件循环(输入校验循环)", - "User must do something at least once, then repeat until valid.": "用户必须先操作至少一次,再重复到输入有效为止。", - "Body executes first, then condition is tested.": "先执行循环体,再测试条件。", - "Best for input validation and menu retries.": "最适合输入校验和菜单重试。", - "Condition should represent the 'valid/finished' state.": "条件应表示“有效/已完成”的状态。", - "If first entry is wrong, loop repeats.": "如果第一次输入错误,循环会重复。", - "If first entry is correct, still valid because one run is guaranteed.": "如果第一次输入正确,程序也能正常运行,因为循环保证至少执行一次。", - "Nested FOR Loops (Tables / Grids / 2D Arrays)": "嵌套 FOR 循环(表格、网格和二维数组)", - "Process rows and columns or all combinations of two ranges.": "处理行与列,或两个范围内所有值的组合。", - "Outer loop controls each row/item group.": "外层循环控制每一行或每一组项目。", - "Inner loop completes all columns/items for current outer value.": "内层循环处理当前外层值对应的所有列或项目。", - "Reset row-level totals before starting inner loop.": "开始内层循环前,重置每行的累加值。", - "Row 1 processes columns 1..4, then outputs row 1 total.": "第 1 行处理第 1 到第 4 列,然后输出第 1 行的总和。", - "Row 2 starts fresh with RowTotal reset to 0.": "第 2 行开始时,RowTotal 重新设为 0。", - "After final row, GrandTotal contains sum of all cells.": "处理完最后一行后,GrandTotal 保存所有单元格的总和。", - "These are full exam-style answers showing declarations, control flow, and output.": "以下是完整的考试风格答案,展示了声明、控制结构和输出。", - "Program A: Grade Counter with Selection + Loop": "程序 A:使用选择结构和循环统计成绩", - "Objective:": "目标:", - "Read 5 marks, count how many are passes (>= 50), and show class average.": "读取 5 个分数,统计及格(>= 50)人数,并显示全班平均分。", - "How the logic works": "逻辑说明", - "This is count-controlled because there are exactly 5 marks.": "恰好有 5 个分数,因此这是计数控制的循环。", - "Total accumulates all marks; PassCount tracks a condition inside the loop.": "Total 累加所有分数;PassCount 记录循环中满足条件的次数。", - "IF branch is optional per iteration; only true marks increment PassCount.": "每轮循环不一定执行 IF 分支,只有分数满足条件时才增加 PassCount。", - "Average is computed once, after loop completes.": "循环完成后,只计算一次 Average。", - "Sample run": "示例运行", - "Input marks: 42, 50, 74, 21, 90": "输入分数:42、50、74、21、90", - "Program B: Input Validation Menu with REPEAT UNTIL": "程序 B:使用 REPEAT UNTIL 校验菜单输入", - "Accept only menu choices 1 to 4.": "只接受 1 到 4 的菜单选项。", - "REPEAT UNTIL is ideal because user must be prompted at least once.": "用户至少需要看到一次提示,因此适合使用 REPEAT UNTIL。", - "Condition states what valid means, not what invalid means.": "条件说明什么是有效输入,而不是什么是无效输入。", - "Using AND ensures both lower and upper bounds are respected.": "使用 AND 可以同时检查下限和上限。", - "Input: 8 -> invalid, repeats": "输入:8 -> 无效,重复", - "Input: 0 -> invalid, repeats": "输入:0 -> 无效,重复", - "Input: 3 -> valid, exits": "输入:3 -> 有效,退出", - "Program C: Search in Array with Flag": "程序 C:使用标志在数组中搜索", - "Find whether a target name exists in StudentNames[1:30].": "查找目标姓名是否存在于 StudentNames[1:30] 中。", - "Found starts FALSE and flips TRUE when match appears.": "Found 初始为 FALSE,找到匹配项时变为 TRUE。", - "This variant checks all items; examiners usually accept this clear style.": "这个写法会检查全部元素;阅卷员通常接受这种清晰的写法。", - "Final IF uses the flag to output one message.": "最后的 IF 使用该标志输出一条消息。", - "Target = \"Ali\" and appears at index 7 -> Found becomes TRUE": "Target = \"Ali\",并且它出现在索引 7 -> Found 变为 TRUE", - "Output: Found": "输出:Found", - "Program D: Procedure + Function Together": "程序 D:结合使用过程与函数", - "Show reuse: a procedure for display and a function for calculation.": "展示代码复用:用过程负责显示,用函数负责计算。", - "Procedure is called using CALL because it is a full statement.": "过程调用是一个完整语句,因此使用 CALL。", - "Function returns a value, so it appears in an expression.": "函数会返回值,因此可以出现在表达式中。", - "Keeping display logic and calculation logic separate improves clarity.": "将显示逻辑与计算逻辑分开,代码会更清晰。", - "PrintLine outputs 10 dashes.": "PrintLine 输出 10 条短横线。", - "SumSquare(3,4) returns 25.": "SumSquare(3,4) 返回 25。", - "Output: Answer = 25": "输出:Answer = 25", - "Trace Tables and Dry Runs": "跟踪表与手动运行", - "In exams, a fast dry-run catches most logical mistakes. Track key variables after each iteration.": "考试中,快速手动运行程序能发现大多数逻辑错误。每轮循环后记录关键变量。", - "Fact before": "执行前的 Fact", - "Fact after Fact <- Fact * I": "执行 Fact <- Fact * I 后的 Fact", - "For N = 5, expected output is 120. If your trace table and output disagree, debug the loop boundaries and initial value.": "当 N = 5 时,预期输出为 120。如果跟踪表与输出不一致,请检查循环边界和初始值。", - "File Handling Guide": "文件处理指南", - "Declare file name and data variables.": "声明文件名和数据变量。", - "OPENFILE with FOR READ or FOR WRITE.": "使用 OPENFILE 和 FOR READ 或 FOR WRITE 打开文件。", - "Use READFILE/WRITEFILE operations.": "使用 READFILE/WRITEFILE 操作。", - "CLOSEFILE when finished.": "完成后使用 CLOSEFILE 关闭文件。", - "Avoid opening the same file in both READ and WRITE simultaneously, and always close files even in short algorithms.": "不要同时以 READ 和 WRITE 模式打开同一个文件。即使算法很短,也要始终关闭文件。", - "Command Word": "指令词", - "Meaning in Answers": "答题要求", - "Calculate": "Calculate(计算)", - "Work out from given facts, figures or information.": "根据给出的事实、数据或信息得出结果。", - "Compare": "Compare(比较)", - "Identify and comment on similarities and differences.": "找出相同点和不同点,并作出说明。", - "Define": "Define(定义)", - "Give the precise meaning.": "给出准确含义。", - "Demonstrate": "Demonstrate(演示)", - "Show how, or give an example.": "说明做法或给出示例。", - "Describe": "Describe(描述)", - "State points, characteristics and main features.": "陈述要点、特性和主要特征。", - "Evaluate": "Evaluate(评价)", - "Judge quality, importance, amount or value.": "判断质量、重要性、数量或价值。", - "Explain": "Explain(解释)", - "Give reasons, show relationships, and support with evidence.": "给出原因,说明关系,并用证据支持。", - "Give": "Give(给出)", - "Produce an answer from source material or recall.": "根据材料或记忆写出答案。", - "Identify": "Identify(识别)", - "Name, select or recognise.": "说出、选择或辨认。", - "Outline": "Outline(概述)", - "Set out main points.": "列出主要要点。", - "Show (that)": "Show (that)(证明)", - "Provide structured evidence leading to a result.": "提供有条理的证据并推导出结果。", - "State": "State(陈述)", - "Express in clear terms.": "用清楚明确的语言表达。", - "Suggest": "Suggest(建议)", - "Apply knowledge to give valid proposals or considerations.": "运用知识提出合理的方案或考虑因素。", - "Final Checklist Before Submitting": "提交前的最终检查清单", - "All variables/arrays declared with correct data types.": "所有变量和数组都已用正确的数据类型声明。", - "Initial values set for counters, totals, and flags.": "已设置计数器、累加值和标志的初始值。", - "Correct loop choice: FOR vs WHILE vs REPEAT UNTIL.": "循环选择正确:FOR、WHILE 或 REPEAT UNTIL。", - "All IF/CASE/loop blocks properly ended.": "所有 IF、CASE 和循环代码块都正确结束。", - "Procedure calls use CALL; function calls appear in expressions.": "调用过程时使用 CALL;函数调用出现在表达式中。", - "Outputs match the question requirements exactly.": "输出与题目要求完全一致。", - "Built-in routines such as ROUND() and RANDOM() use the correct syntax.": "ROUND() 和 RANDOM() 等内置函数使用正确的语法。", - "Dry-run tested with normal and edge-case inputs.": "已用普通输入和边界情况进行手动运行。", - "Close": "关闭", - "Copied": "已复制", - "Copy code to clipboard": "复制代码到剪贴板", - "Code copied to clipboard": "代码已复制到剪贴板", - "Clipboard access is unavailable here.": "此处无法访问剪贴板。", -}; - -export type ManualText = (english: string) => string; - -export function manualText(locale: Locale): ManualText { - if (locale === "zh") return (english) => manualZh[english] ?? english; - return (english) => english; -} diff --git a/apps/web/src/app/[locale]/(public)/manual/page.tsx b/apps/web/src/app/[locale]/(public)/manual/page.tsx deleted file mode 100644 index d1484c2..0000000 --- a/apps/web/src/app/[locale]/(public)/manual/page.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import type { Metadata } from "next"; -import Link from "next/link"; -import { ArrowRight, BookOpen, CheckCircle2, Code2, FileText, GitBranch, Play } from "lucide-react"; -import { PublicHeader } from "@/app/components/PublicHeader"; -import { localePath, localeTags, localeUrl, pageMetadata } from "@/i18n/config"; -import { getDictionary } from "@/i18n/messages"; -import { resolveLocale, type LocaleParams } from "@/i18n/server"; -import { productName } from "@/lib/seo-content"; - -export async function generateMetadata({ params }: LocaleParams): Promise { - const locale = await resolveLocale(params); - const t = getDictionary(locale).meta; - return pageMetadata(locale, "/manual", t.manualTitle, t.manualDescription, t.ogImageAlt); -} - -const sectionIcons = [Code2, GitBranch, FileText, BookOpen]; - -const exampleCode = [ - `DECLARE Number : INTEGER -DECLARE Total : INTEGER -Total <- 0 - -FOR Number <- 1 TO 5 - Total <- Total + Number -NEXT Number - -OUTPUT "Total = ", Total`, - `DECLARE Choice : INTEGER - -REPEAT - OUTPUT "Choose 1 to 4" - INPUT Choice -UNTIL Choice >= 1 AND Choice <= 4 - -OUTPUT "Accepted"`, -]; - -export default async function ManualPage({ params }: LocaleParams) { - const locale = await resolveLocale(params); - const dict = getDictionary(locale); - const t = dict.manual; - - const structuredData = { - "@context": "https://schema.org", - "@type": ["TechArticle", "LearningResource"], - headline: `${productName} ${dict.meta.manualTitle}`, - description: dict.meta.manualDescription, - url: localeUrl(locale, "/manual"), - inLanguage: localeTags[locale], - learningResourceType: "reference", - educationalLevel: "secondary", - audience: { "@type": "EducationalAudience", educationalRole: "student" }, - isAccessibleForFree: true, - }; - - return ( -
-