diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7cbd4a5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,33 @@ + + + + + +**What kind of change does this PR introduce?** + + + +**Issue Number:** + +- Closes #___ +- Related to #___ +- Others? + +**Screenshots/videos:** + + + +**If relevant, did you update the documentation?** + + + +**Summary** + + + + +**Does this PR introduce a breaking change?** + + \ No newline at end of file diff --git a/.github/workflows/link-checker.yml b/.github/workflows/link-checker.yml new file mode 100644 index 0000000..e462d6e --- /dev/null +++ b/.github/workflows/link-checker.yml @@ -0,0 +1,94 @@ +name: Link Checker + +on: + repository_dispatch: + workflow_dispatch: + schedule: + - cron: '0 0 1 * *' # Run at midnight on the first of every month + +jobs: + linkChecker: + name: Check and report broken links + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + # Replace corepack with direct PNPM installation + - name: Install pnpm + run: npm install -g pnpm + + - name: Get Token + uses: actions/create-github-app-token@v1 + id: get_workflow_token + with: + app-id: ${{ vars.APP_ID }} + private-key: ${{ secrets.PRIVATE_KEY }} + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Serve App Locally + run: pnpm run dev & + + - name: Wait for App to Start + run: sleep 20 + + # This will restore the lychee cache + - name: Restore lychee cache + uses: actions/cache@v4 + with: + path: .lycheecache + key: cache-lychee-${{ github.sha }} + restore-keys: cache-lychee- + + # This will run the link checker on all markdown files in the pages directory + - name: Link Checker + id: lychee + uses: lycheeverse/lychee-action@v1 + with: + args: --base https://tour.json-schema.org --verbose --no-progress --accept 200,204,429,403 './content/**/*.mdx' --cache --max-cache-age 1d https://tour.json-schema.org + token: ${{secrets.GITHUB_TOKEN}} + + - name: Install Octokit + run: pnpm add @octokit/core@5.1.0 + + # This will create an issue with the link checker report if it does not exist, otherwise it will update the existing issue. + + - name: Create Issue + if: env.lychee_exit_code != 0 + uses: actions/github-script@v7 + env: + AUTH_TOKEN: ${{ steps.get_workflow_token.outputs.token }} + with: + script: | + const { Octokit } = require("@octokit/core"); + const octokit = new Octokit({ auth: process.env.AUTH_TOKEN }); + const allIssues = await octokit.request('GET /repos/{owner}/{repo}/issues', { + owner: context.repo.owner, + repo: context.repo.repo + }); + + const existingIssue = allIssues.data.find(issue => issue.title === 'Link Checker Report'); + if (existingIssue) { + await octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssue.number, + body: '## Link Checker Report\n\n' + require('fs').readFileSync('./lychee/out.md', 'utf8') + }); + } else { + await octokit.request('POST /repos/{owner}/{repo}/issues', { + owner: context.repo.owner, + repo: context.repo.repo, + title: 'Link Checker Report', + body: '## Link Checker Report\n\n' + require('fs').readFileSync('./lychee/out.md', 'utf8') + }); + } \ No newline at end of file diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml index c113a71..a4c1907 100644 --- a/.github/workflows/nextjs.yml +++ b/.github/workflows/nextjs.yml @@ -56,7 +56,7 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 8 + run_install: false - name: Setup Node uses: actions/setup-node@v4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index adcd3a4..80c59e1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,14 @@ +# Process to Open a Pull Request + +1. Find an issue that is open and has an [Available](https://github.com/json-schema-org/tour/issues?q=is%3Aissue+is%3Aopen+label%3A%22Status%3A+Available%22) tag. +2. Ask for the issue to be assigned to you. (Please do not work on an issue that is not assigned to you) +3. Once the issue is assigned to you, fork the repository. +4. Create a new branch in your forked repository. +5. Make the necessary changes in your forked repository. +6. Create a pull request to the main repository. +7. Wait for the review and approval of the pull request. + + # Guidelines for contributing to the JSON Schema project ## Issues @@ -17,3 +28,4 @@ Most PRs, will be left open for a minimum of 14 days. Minor fixes may be merged ## Conduct All official channels including the mailing list, GitHub organization, Slack server, and IRC channels, follow our [Code of Conduct](https://github.com/json-schema-org/.github/blob/main/CODE_OF_CONDUCT.md). + diff --git a/README.md b/README.md index dccc797..dd2110c 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,105 @@ # Tour of JSON Schema -This repository contains the code for the Tour of JSON Schema project. -https://tour.json-schema.org +Welcome to the **Tour of JSON Schema** project! This repository hosts the codebase for an interactive learning platform designed to help users understand and master JSON Schema. You can access the live version of the project at [https://tour.json-schema.org](https://tour.json-schema.org). -# Development +--- -The project is built using next.js. +## Table of Contents -After cloning the repository, run the following command to install the dependencies: +- [Tour of JSON Schema](#tour-of-json-schema) + - [Table of Contents](#table-of-contents) + - [Development Setup](#development-setup) + - [Prerequisites](#prerequisites) + - [Installation](#installation) + - [Figma Design](#figma-design) + - [Contributing](#contributing) + - [Content Creation](#content-creation) + - [Writing MDX](#writing-mdx) + - [VSCode Extensions](#vscode-extensions) + - [File Structure](#file-structure) + - [MDX Components Guide](#mdx-components-guide) + - [GoodToKnowBox](#goodtoknowbox) + - [CodeSnippet](#codesnippet) + - [SideEditorLink](#sideeditorlink) -```bash -pnpm install -``` +--- -To start the development server, run the following command: +## Development Setup -```bash -pnpm dev -``` +The project is built using **Next.js**, a React framework for building server-rendered applications. Below are the steps to set up your development environment: -> when you run `pnpm dev`, a file named `outline.json` will be created in th `/content` directory. This file is used to generate the table of contents for the website. +### Prerequisites -(make sure you run tests before pushing your changes) -To run the tests, run the following command: +- **Node.js**: Ensure you have Node.js installed (v20 or higher). +- **pnpm**: The project uses `pnpm` as the package manager. -```bash -pnpm test -``` +### Installation +1. Clone the repository: + ```bash + git clone https://github.com/json-schema-org/tour + cd tour-of-json-schema + ``` -# Content +2. Install dependencies: + ```bash + pnpm install + ``` -### Writing MDX +3. Start the development server: + ```bash + pnpm dev + ``` -The content written in [MDX](https://mdxjs.com/), a markdown format that supports JSX syntax. This allows us to embed React components in the docs. See the [GitHub Markdown Guide](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) for a quick overview of markdown syntax. + > **Note**: Running `pnpm dev` will generate a file named `outline.json` in the `/content` directory. This file is used to dynamically generate the table of contents for the website. -### VSCode +4. Run tests before pushing changes: + ```bash + pnpm test + ``` +> [!CAUTION] +> Always run tests before submitting a pull request to ensure your changes do not introduce regressions. -#### Extensions +### Figma Design -We recommend the following extensions for VSCode users: +You can view the Figma design for the project at [this link](https://www.figma.com/design/w8ow79jE7lJucJt2zZTbcz/Tour-of-JSON-Schema?node-id=2303-39&t=CH9j0oDmVft8uTWX-0). -- [MDX](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx): Intellisense and syntax highlighting for MDX. -- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode): Format MDX files on save. +### Contributing -## File Structure +Please Read the [Contributing Guide](CONTRIBUTING.md) for detailed instructions on how to contribute to the project. -the content of each step is stored in the `/content` directory with the following structure: -``` +--- + +## Content Creation + +The content for the Tour of JSON Schema is written in **MDX**, a markdown format that supports JSX syntax. This allows us to embed React components directly into the documentation, making it highly interactive and engaging. + +### Writing MDX +MDX combines the simplicity of Markdown with the power of React. Here are some resources to get started: +- [MDX Documentation](https://mdxjs.com/) +- [GitHub Markdown Guide](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) + +### VSCode Extensions + +For a smoother development experience, we recommend installing the following extensions in Visual Studio Code: + +- **MDX**: Provides syntax highlighting and IntelliSense for MDX files. + - Marketplace Link: [MDX Extension](https://marketplace.visualstudio.com/items?itemName=unifiedjs.vscode-mdx) +- **Prettier**: Automatically formats your MDX files on save. + - Marketplace Link: [Prettier Extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) + +### File Structure + +The content is organized in the `/content` directory with the following structure: + +``` ├── 01-introduction -│ ├── index.mdx +│ ├── index.mdx # Overview of the section │ ├── 01-welcome -│ ├── instructions.mdx -│ ├── code.ts +│ ├── instructions.mdx # Instructions for the step +│ ├── code.ts # Template code and validation logic │ ├── 02-what-is-json-schema │ ├── instructions.mdx │ ├── code.ts @@ -66,7 +111,72 @@ the content of each step is stored in the `/content` directory with the followin │ ├── 02-arrays │ ├── instructions.mdx │ ├── code.ts +``` + +- Each section (e.g., `01-introduction`) has an `index.mdx` file that serves as an overview. +- Each step within a section contains: + - `instructions.mdx`: The content displayed to the user. + - `code.ts`: Contains template code and logic to validate user-provided schemas. + +--- + +## MDX Components Guide + +The project includes custom React components to enhance the interactivity and readability of the content. Below is a guide to the available components: + +### GoodToKnowBox + +A styled box for displaying tips, notes, or additional information. + +**Props:** +- `title` (optional): The title of the box. Defaults to "Good to know". +- `children`: The content of the box. + +**Example:** +```md + + Use `$ref` to reuse schema definitions and keep your JSON Schema DRY. + +``` + +--- + +### CodeSnippet + +A code block with syntax highlighting and optional line highlighting. + +**Props:** +- `highlightLineStart`: The starting line number to highlight. +- `highlightLineEnd` (optional): The ending line number to highlight. Defaults to `highlightLineStart`. +- `startingLineNumber` (optional): The starting line number for the code block. Defaults to `1`. +- `showLineNumbers` (optional): Whether to display line numbers. Defaults to `true`. + +**Example:** +```md + +{ + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" } + } +} + +``` + +--- + +### SideEditorLink + +A link that focuses the editor on the right side of the screen. + +**Props:** +- `text` (optional): Custom text for the link. Defaults to "side editor". +**Example:** +```md + ``` +--- -The instructions.mdx file holds the content the users sees, and the code.ts file holds template code and the logic to validate user provided schemas +Thank you for contributing to the **Tour of JSON Schema** project! Together, we can make JSON Schema more accessible and easier to learn for everyone. \ No newline at end of file diff --git a/app/components/CertificateButton/CertificateButton.module.css b/app/components/CertificateButton/CertificateButton.module.css new file mode 100644 index 0000000..d31d5d0 --- /dev/null +++ b/app/components/CertificateButton/CertificateButton.module.css @@ -0,0 +1,12 @@ +.certificateButton { + font-size: small; + margin-right: 32px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 8px 10px; + border-radius: 8px; + cursor: pointer; + font-weight: bold; +} diff --git a/app/components/CertificateButton/CertificateButton.tsx b/app/components/CertificateButton/CertificateButton.tsx new file mode 100644 index 0000000..6dd8c5a --- /dev/null +++ b/app/components/CertificateButton/CertificateButton.tsx @@ -0,0 +1,113 @@ +import { isTheTourCompleted } from "@/lib/client-functions"; +import React, { useEffect, useState } from "react"; +import styles from "./CertificateButton.module.css"; +import { LockIcon } from "@chakra-ui/icons"; +import { + Box, + Button, + Flex, + Input, + Tooltip, + useDisclosure, + useToast, +} from "@chakra-ui/react"; +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton, +} from "@chakra-ui/react"; +import { googleSheetAPIRoute } from "@/lib/contentVariables"; +const submitCertificateReq = async (name: string, email: string) => { + await fetch(googleSheetAPIRoute, { + method: "POST", + body: JSON.stringify({ + name, + email, + certificateReq: true, + }), + }); +}; + +export default function CertificateButton() { + const { isOpen, onOpen, onClose } = useDisclosure(); + const [isTheTourCompletedState, setIsTheTourCompletedState] = + useState(isTheTourCompleted()); + useEffect(() => { + setIsTheTourCompletedState(isTheTourCompleted()); + }, [isTheTourCompleted()]); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const toast = useToast(); + + return ( + <> + + + + + + + Get Your Certificate + + + We will email you your certificate + + setName(e.target.value)} + value={name} + /> + setEmail(e.target.value)} + value={email} + /> + + + + + + + + + + + ); +} diff --git a/app/components/CertificateButton/index.tsx b/app/components/CertificateButton/index.tsx new file mode 100644 index 0000000..8f51a54 --- /dev/null +++ b/app/components/CertificateButton/index.tsx @@ -0,0 +1 @@ +export { default as default } from "./CertificateButton"; diff --git a/app/components/ChapterItem/ChapterItem.module.css b/app/components/ChapterItem/ChapterItem.module.css index aea3002..751f66f 100644 --- a/app/components/ChapterItem/ChapterItem.module.css +++ b/app/components/ChapterItem/ChapterItem.module.css @@ -94,12 +94,7 @@ .stepItem:hover { text-decoration: underline; } -.activeStep { - border-left-width: 3px; - border-left-color: hsl(var(--primary)); - color: hsl(var(--primary)); - font-weight: bold; -} + .active { background-color: hsl(var(--primary)); color: hsl(var(--background)); @@ -115,3 +110,10 @@ color: hsl(var(--text) / 0.5); border-left-color: hsl(var(--primary)); } + +.activeStep { + border-left-width: 3px; + border-left-color: hsl(var(--primary)); + color: hsl(var(--primary)); + font-weight: bold; +} diff --git a/app/components/ChapterItem/ChapterItem.tsx b/app/components/ChapterItem/ChapterItem.tsx index 0acbf5b..0f58a67 100644 --- a/app/components/ChapterItem/ChapterItem.tsx +++ b/app/components/ChapterItem/ChapterItem.tsx @@ -93,7 +93,7 @@ export default function ChapterItem({
{title}
- + {
    {steps.map((step, stepIndex) => ( diff --git a/app/components/CodeEditor/CodeEditor.tsx b/app/components/CodeEditor/CodeEditor.tsx index 8982e15..3857e1b 100644 --- a/app/components/CodeEditor/CodeEditor.tsx +++ b/app/components/CodeEditor/CodeEditor.tsx @@ -3,16 +3,152 @@ import styles from "./CodeEditor.module.css"; import ctx from "classnames"; import { GeistMono } from "geist/font/mono"; -import Editor, { useMonaco } from "@monaco-editor/react"; +import Editor, { Monaco } from "@monaco-editor/react"; import { Flex, useColorMode } from "@chakra-ui/react"; -import { useEffect, useState } from "react"; -import SmallBtn from "../SmallBtn"; -import { CodeFile } from "@/lib/types"; -import { OutputReducerAction } from "@/lib/reducers"; -import { validateCode } from "@/lib/client-functions"; +import { useEffect, useState, useRef } from "react"; +import MyBtn from "../MyBtn"; +import { tryFormattingCode, validateCode } from "@/lib/client-functions"; import FiChevronRight from "@/app/styles/icons/HiChevronRightGreen"; import { useRouter } from "next/navigation"; -import { useEditorStore } from "@/lib/stores"; +import { useUserSolutionStore, useEditorStore } from "@/lib/stores"; +import { sendGAEvent } from "@next/third-parties/google"; +import { CodeFile, OutputResult } from "@/lib/types"; +import { OutputReducerAction } from "@/lib/reducers"; + +// Custom hook for editor theme setup +const useEditorTheme = (monaco: Monaco, colorMode: "dark" | "light") => { + useEffect(() => { + if (monaco) { + monaco.editor.defineTheme("my-theme", { + base: "vs-dark", + inherit: true, + rules: [], + colors: { + "editor.background": "#1f1f1f", + }, + }); + monaco.editor.setTheme(colorMode === "light" ? "light" : "my-theme"); + } + }, [monaco, colorMode]); +}; + +// Custom hook for keyboard shortcuts +const useValidationShortcut = ( + handleValidate: () => void, + codeString: string, +) => { + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter" && event.shiftKey) { + sendGAEvent("event", "buttonClicked", { + value: "Validate (through shortcut)", + }); + event.preventDefault(); + handleValidate(); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }, [handleValidate, codeString]); +}; + +// Custom hook for code persistence +const useCodePersistence = ( + chapterIndex: number, + stepIndex: number, + codeString: string, + setCodeString: (value: string) => void, + codeFile: CodeFile, +) => { + const userSolutionStore = useUserSolutionStore(); + + // Load saved code + useEffect(() => { + const savedCode = userSolutionStore.getSavedUserSolutionByLesson( + chapterIndex, + stepIndex, + ); + if (savedCode && savedCode !== codeString) { + setCodeString(savedCode); + } + }, [chapterIndex, stepIndex]); + + // Save code changes + useEffect(() => { + userSolutionStore.saveUserSolutionForLesson( + chapterIndex, + stepIndex, + codeString, + ); + }, [codeString, chapterIndex, stepIndex]); + + // Initialize code if no saved solutions + useEffect(() => { + if (Object.keys(userSolutionStore.userSolutionsByLesson).length === 0) { + setCodeString(JSON.stringify(codeFile.code, null, 2)); + } + }, [userSolutionStore]); +}; + +// EditorControls component for the buttons section +const EditorControls = ({ + handleValidate, + isValidating, + resetCode, + nextStepPath, + outputResult, +}: { + handleValidate: () => void; + isValidating: boolean; + resetCode: () => void; + nextStepPath: string | undefined; + outputResult: OutputResult; +}) => { + const router = useRouter(); + + return ( +
    + + + {isValidating ? "Validating ..." : "Validate"} + + + + Reset + + + { + if (nextStepPath) router.push("/" + nextStepPath); + }} + variant={ + outputResult.validityStatus === "valid" ? "default" : "success" + } + isDisabled={!nextStepPath} + size={outputResult.validityStatus === "valid" ? "sm" : "xs"} + > + Next + + +
    + ); +}; export default function CodeEditor({ codeString, @@ -22,6 +158,7 @@ export default function CodeEditor({ nextStepPath, stepIndex, chapterIndex, + outputResult, }: { codeString: string; setCodeString: (codeString: string) => void; @@ -30,46 +167,54 @@ export default function CodeEditor({ nextStepPath: string | undefined; stepIndex: number; chapterIndex: number; + outputResult: OutputResult; }) { const { colorMode } = useColorMode(); const [monaco, setMonaco] = useState(null); - const router = useRouter(); + const [isValidating, setIsValidating] = useState(false); const editorStore = useEditorStore(); + const editorRef = useRef(null); - useEffect(() => { - if (monaco) { - monaco.editor.defineTheme("my-theme", { - base: "vs-dark", - inherit: true, - rules: [], - colors: { - "editor.background": "#1f1f1f", - }, - }); - monaco.editor.setTheme(colorMode === "light" ? "light" : "my-theme"); - } - }, [monaco, colorMode]); - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - // event.preventDefault(); - if (event.key == "Enter" && event.shiftKey) { - event.preventDefault(); // Prevent default behavior - validateCode( - codeString, - codeFile, - dispatchOutput, - stepIndex, - chapterIndex, - ); - } - }; + // Apply custom hooks + useEditorTheme(monaco, colorMode); - document.addEventListener("keydown", handleKeyDown); + const handleValidate = () => { + setIsValidating(true); + setTimeout(() => { + tryFormattingCode(editorRef, setCodeString); + validateCode( + codeString, + codeFile, + dispatchOutput, + stepIndex, + chapterIndex, + ); + setIsValidating(false); + }, 500); + }; + + useValidationShortcut(handleValidate, codeString); + useCodePersistence( + chapterIndex, + stepIndex, + codeString, + setCodeString, + codeFile, + ); + + const resetCode = () => { + setCodeString(JSON.stringify(codeFile.code, null, 2)); + dispatchOutput({ type: "RESET" }); + }; + + const handleEditorMount = (editor: any, monaco: Monaco) => { + setMonaco(monaco); + + editorRef.current = editor; + editorStore.setEditor(editor); + editorStore.setMonaco(monaco); + }; - return () => { - document.removeEventListener("keydown", handleKeyDown); - }; - }, [codeString]); return ( <>
    @@ -78,55 +223,24 @@ export default function CodeEditor({ defaultValue={codeString} theme={colorMode === "light" ? "light" : "my-theme"} value={codeString} - height={"100%"} - onChange={(codeString) => setCodeString(codeString ? codeString : "")} - options={{ minimap: { enabled: false }, fontSize: 14 }} - onMount={(editor, monaco) => { - setMonaco(monaco); - editorStore.setEditor(editor); - editorStore.setMonaco(monaco); + height="100%" + onChange={(codeString) => setCodeString(codeString ?? "")} + options={{ + minimap: { enabled: false }, + fontSize: 14, + formatOnPaste: true, + formatOnType: true, }} + onMount={handleEditorMount} />
    -
    - - - validateCode( - codeString, - codeFile, - dispatchOutput, - stepIndex, - chapterIndex, - ) - } - variant={"default"} - tooltip="Shift + Enter" - > - Validate - - - { - setCodeString(JSON.stringify(codeFile.code, null, 2)); - dispatchOutput({ type: "RESET" }); - }} - variant={"error"} - > - Reset - - - { - if (nextStepPath) router.push("/" + nextStepPath); - }} - variant={"success"} - isDisabled={!nextStepPath} - > - Next - - -
    + ); } diff --git a/app/components/CodeSnippet/CodeSnippet.tsx b/app/components/CodeSnippet/CodeSnippet.tsx index 445b67d..0345b25 100644 --- a/app/components/CodeSnippet/CodeSnippet.tsx +++ b/app/components/CodeSnippet/CodeSnippet.tsx @@ -2,9 +2,7 @@ import SyntaxHighlighter from "react-syntax-highlighter"; import { nightOwl, - atelierCaveLight, arduinoLight, - atelierEstuaryLight, } from "react-syntax-highlighter/dist/esm/styles/hljs"; import styles from "./CodeSnippet.module.css"; import { CSSProperties, useState } from "react"; @@ -38,13 +36,7 @@ export default function CodeSnippet({ } return ( -
    { - e.preventDefault(); - e.clipboardData.setData("text/plain", children); - }} - > +
    { @@ -73,7 +65,7 @@ export default function CodeSnippet({ paddingBlock: "10px", }} wrapLines={true} - wrapLongLines={true} + wrapLongLines={false} lineNumberStyle={{ color: "hsl(var(--text) / 0.6)", paddingRight: "12px", @@ -82,10 +74,12 @@ export default function CodeSnippet({ startingLineNumber={startingLineNumber} lineProps={(lineNumber) => { let style: CSSProperties = { - opacity: colorMode === "dark" ? 0.9 : 1, + display: "block", paddingRight: "16px", paddingLeft: "4px", + opacity: colorMode === "dark" ? 0.9 : 1, }; + if ( highlightLineStart && highlightLineEnd && @@ -101,6 +95,7 @@ export default function CodeSnippet({ }`, }; } + return { style }; }} > diff --git a/app/components/CommunityLinks/CommunityLinks.module.css b/app/components/CommunityLinks/CommunityLinks.module.css index ad1834d..b650470 100644 --- a/app/components/CommunityLinks/CommunityLinks.module.css +++ b/app/components/CommunityLinks/CommunityLinks.module.css @@ -6,12 +6,12 @@ position: relative; } .footerLink { - color: hsl(var(--primary)); + color: hsl(var(--text)); text-decoration: underline; text-underline-offset: 0.15em; - text-decoration-color: hsl(var(--primary) / 0.3); + text-decoration-color: hsl(var(--primary) / 0.5); } .footerLink:hover { - text-decoration-color: hsl(var(--primary)); + text-decoration-color: hsl(var(--text)); } diff --git a/app/components/CommunityLinks/CommunityLinks.tsx b/app/components/CommunityLinks/CommunityLinks.tsx index 8ea3441..ed5ce4d 100644 --- a/app/components/CommunityLinks/CommunityLinks.tsx +++ b/app/components/CommunityLinks/CommunityLinks.tsx @@ -9,7 +9,7 @@ export default function CompanyLogos() { return [ { title: "Github", link: "https://github.com/json-schema-org" }, { title: "Slack", link: "https://json-schema.org/slack" }, - { title: "Twitter", link: "https://x.com/jsonschema" }, + { title: "X", link: "https://x.com/jsonschema" }, { title: "Youtube", link: "https://www.youtube.com/@JSONSchemaOrgOfficial", diff --git a/app/components/ContinueBtn/ContinueBtn.module.css b/app/components/ContinueBtn/ContinueBtn.module.css new file mode 100644 index 0000000..e8626f9 --- /dev/null +++ b/app/components/ContinueBtn/ContinueBtn.module.css @@ -0,0 +1,12 @@ +.continueBtn { + display: flex; + gap: 5px; +} + +.rightIcon { + display: flex; + justify-content: center; + align-items: center; + width: 10px; + height: 10px; +} diff --git a/app/components/ContinueBtn/ContinueBtn.tsx b/app/components/ContinueBtn/ContinueBtn.tsx new file mode 100644 index 0000000..a3ee7e1 --- /dev/null +++ b/app/components/ContinueBtn/ContinueBtn.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { getCheckPoint } from "@/lib/progressSaving"; +import styles from "./ContinueBtn.module.css"; +import RightArrow from "@/app/styles/icons/RightArrow"; +import { Button } from "@chakra-ui/react"; + +export default function ContinueBtn() { + const router = useRouter(); + const checkpoint = getCheckPoint(); + + const handleClick = () => { + const checkpoint = getCheckPoint(); + if (checkpoint) { + router.push(`/${checkpoint}`); + } + }; + + return ( + <> + {checkpoint && ( + + )} + + ); + + return null; +} diff --git a/app/components/ContinueBtn/index.tsx b/app/components/ContinueBtn/index.tsx new file mode 100644 index 0000000..4fc678b --- /dev/null +++ b/app/components/ContinueBtn/index.tsx @@ -0,0 +1 @@ +export { default as default } from "./ContinueBtn"; diff --git a/app/components/EditorNOutput/EditorNOutput.tsx b/app/components/EditorNOutput/EditorNOutput.tsx index c8b5758..05957f7 100644 --- a/app/components/EditorNOutput/EditorNOutput.tsx +++ b/app/components/EditorNOutput/EditorNOutput.tsx @@ -92,6 +92,7 @@ export default function EditorNOutput({ nextStepPath={nextStepPath} stepIndex={stepIndex} chapterIndex={chapterIndex} + outputResult={output} />
    { - await fetch(APIRoute, { + await fetch(googleSheetAPIRoute, { method: "POST", body: JSON.stringify({ feedback: feedback, diff --git a/app/components/HomePageLinks/HomePageLinks.module.css b/app/components/HomePageLinks/HomePageLinks.module.css new file mode 100644 index 0000000..85a7705 --- /dev/null +++ b/app/components/HomePageLinks/HomePageLinks.module.css @@ -0,0 +1,9 @@ +/* grid of 4 rows */ +.HomePageLinks { + display: grid; + grid-template-rows: repeat(4, 1fr); + gap: 8px; + column-gap: 32px; + grid-auto-flow: column; + font-size: large; +} diff --git a/app/components/HomePageLinks/HomePageLinks.tsx b/app/components/HomePageLinks/HomePageLinks.tsx new file mode 100644 index 0000000..e42224c --- /dev/null +++ b/app/components/HomePageLinks/HomePageLinks.tsx @@ -0,0 +1,40 @@ +import { contentManager } from "@/lib/contentManager"; +import styles from "./HomePageLinks.module.css"; +import Link from "next/link"; +import { useMemo } from "react"; +import CommunityLinksStyles from "@/app/components/CommunityLinks/CommunityLinks.module.css"; + +export default function HomePageLinks() { + const outline = contentManager.getOutline(); + const totalChapters = contentManager.getTotalChapters(); + const chapterInfo: { + title: string; + link: string; + }[] = useMemo(() => { + const chapterInfo = []; + for (let i = 0; i < totalChapters; i++) { + const chapter = outline[i]; + chapterInfo.push({ + title: chapter.title, + link: contentManager.getPathWithPrefix( + chapter.steps[0].fullPath, + ) as string, + }); + } + return chapterInfo; + }, []); + + return ( +
    + {chapterInfo.map((chapter, index) => ( + + {index + 1}. {chapter.title} + + ))} +
    + ); +} diff --git a/app/components/HomePageLinks/index.tsx b/app/components/HomePageLinks/index.tsx new file mode 100644 index 0000000..9228814 --- /dev/null +++ b/app/components/HomePageLinks/index.tsx @@ -0,0 +1 @@ +export { default as default } from "./HomePageLinks"; diff --git a/app/components/IconLink/IconLink.tsx b/app/components/IconLink/IconLink.tsx new file mode 100644 index 0000000..322d30c --- /dev/null +++ b/app/components/IconLink/IconLink.tsx @@ -0,0 +1,27 @@ +"use client"; + +import React from "react"; + +export default function Icon() { + const [isDarkMode, setIsDarkMode] = React.useState(false); + const onUpdate = React.useCallback((matcher: MediaQueryList) => { + setIsDarkMode(matcher.matches); + }, []); + + React.useEffect(() => { + const matcher: MediaQueryList = window.matchMedia( + "(prefers-color-scheme: dark)", + ); + matcher.addEventListener("change", () => onUpdate(matcher)); + onUpdate(matcher); + }, []); + + return ( + + ); +} diff --git a/app/components/IconLink/index.tsx b/app/components/IconLink/index.tsx new file mode 100644 index 0000000..850a51c --- /dev/null +++ b/app/components/IconLink/index.tsx @@ -0,0 +1 @@ +export { default as default } from "./IconLink"; diff --git a/app/components/KeyBindings/KeyBindings.module.css b/app/components/KeyBindings/KeyBindings.module.css index 8d0a60b..09f6724 100644 --- a/app/components/KeyBindings/KeyBindings.module.css +++ b/app/components/KeyBindings/KeyBindings.module.css @@ -2,13 +2,13 @@ font-family: monospace; padding: 1px 5px; - font-weight: bold; - border-radius: 6px; - background-color: hsl(var(--text)); + font-weight: 900; + border-radius: 8px; + background-color: hsl(var(--text) / 0.85); text-transform: capitalize; - border: 1px solid hsl(var(--background) / 0.4); - border-bottom: 2px solid hsl(var(--background) / 0.6); - font-size: small; + border: 2px solid hsl(var(--background) / 0.4); + border-bottom: 4px solid hsl(var(--background) / 0.6); + color: hsl(var(--background)); } @@ -17,7 +17,7 @@ flex-direction: row; gap: 6px; align-items: center; - font-size: small; + justify-content: center; } .keyAndPlus { diff --git a/app/components/Mdx/Mdx.tsx b/app/components/Mdx/Mdx.tsx index dd73dea..26ebf41 100644 --- a/app/components/Mdx/Mdx.tsx +++ b/app/components/Mdx/Mdx.tsx @@ -11,7 +11,7 @@ import rehypeMdxCodeProps from "rehype-mdx-code-props"; import { MDXComponents } from "mdx/types"; import CodeSnippet from "../CodeSnippet/CodeSnippet"; import InfoBox from "../InfoBox"; -import GoodToKnowBox from "../GoodToKnowBox/GoodToKnowBox"; +import GoodToKnowBox from "../GoodToKnowBox"; import rehypeExternalLinks from "rehype-external-links"; import styles from "./Mdx.module.css"; import SideEditorLink from "../SideEditorLink"; @@ -41,7 +41,7 @@ function createHeading(level: number): any { as={`h${level}` as As} size={headingSizes[level]} lineHeight={"tallest"} - letterSpacing={level <= 2 ? "tighter" : ""} + letterSpacing={level <= 2 ? "tight" : ""} className={styles.heading} > {children} @@ -84,7 +84,6 @@ export const components: MDXComponents = { }; const customComponents = { - InfoBox, GoodToKnowBox, CodeSnippet, SideEditorLink, diff --git a/app/components/SmallBtn/SmallBtn.tsx b/app/components/MyBtn/MyBtn.tsx similarity index 63% rename from app/components/SmallBtn/SmallBtn.tsx rename to app/components/MyBtn/MyBtn.tsx index a8ed8d6..f8d135c 100644 --- a/app/components/SmallBtn/SmallBtn.tsx +++ b/app/components/MyBtn/MyBtn.tsx @@ -1,24 +1,32 @@ import { Button, Tooltip } from "@chakra-ui/react"; +import { sendGAEvent } from "@next/third-parties/google"; -export default function SmallBtn({ +export default function MyBtn({ children, variant, onClick, isDisabled, tooltip, + size = "xs", }: { children: React.ReactNode; variant: "success" | "error" | "default"; onClick: () => void; isDisabled?: boolean; tooltip?: string; + size?: "xs" | "sm" | "md" | "lg"; }) { return (
    - + { + sendGAEvent("event", "buttonClicked", { + value: "Github Link", + }); + }} + > - diff --git a/app/components/OutlineDrawer/OutlineDrawer.tsx b/app/components/OutlineDrawer/OutlineDrawer.tsx index d3de8d4..523c725 100644 --- a/app/components/OutlineDrawer/OutlineDrawer.tsx +++ b/app/components/OutlineDrawer/OutlineDrawer.tsx @@ -6,11 +6,14 @@ import { DrawerHeader, DrawerCloseButton, DrawerBody, + Button, } from "@chakra-ui/react"; import styles from "./OutlineDrawer.module.css"; import { ContentOutline } from "@/lib/types"; import ChapterItem from "../ChapterItem"; import { isChapterCompleted } from "@/lib/client-functions"; +import Link from "next/link"; +import CertificateButton from "../CertificateButton/CertificateButton"; export default function OutlineDrawer({ isOpen, @@ -39,7 +42,10 @@ export default function OutlineDrawer({ - Outline + + Outline + +