diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 000000000..4a3620353 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,60 @@ +## JSON Schema Website CI/CD Workflow Guidelines + +### Overview + +This document outlines the guidelines for contributing to and maintaining GitHub Actions workflows in the JSON Schema Website project. Adherence to these guidelines ensures consistency, efficiency, and ease of maintenance across our CI/CD processes. + +### General Principles + +- **Change Management**: Modifications to files in this directory are closely monitored. Changes will trigger unauthorized file changes workflow during pull request checks. Only make changes when explicitly advised by a project contributor or maintainer. +- **Documentation and Naming**: Use descriptive, self-explanatory names for workflows, jobs, and steps. Include clear comments within workflow files to explain complex configurations. + +### YAML Workflow File Structure + +Our YAML files are organized based on specific roles and event triggers. When creating or modifying workflows, ensure that: +- The file roles described below are strictly maintained. +- Job sequences within workflows are preserved using [GitHub Action job dependencies](https://docs.github.com/en/actions/using-workflows/using-jobs-in-a-workflow#defining-prerequisite-jobs). + +### File Categorization + +Organize workflow files based on their primary event trigger: + +- **Issue Workflows**: + - [Issue Workflow](./issue.yml): Handles issue-related events such as opening, closing, or labeling issues. + +- **Pull Request Workflows**: + - [CI Workflow](./ci.yml): Runs for all contributors on pull requests, performing code-quality checks, unauthorized file changes detection, and build processes. + - [PR Interaction Workflow](./pull-request-target.yml): Contains workflows specifically for first-time contributors, such as welcome messages. + +### Exceptions to File Categorization + +Separate files may be created for workflows that: + +- Require unique `cron` schedules for periodic execution. + Examples: + - [Link Checker](./link-checker.yml): Periodically checks for broken links in the repository. + - [Mark stale issues and pull requests](./stale-issues-prs.yml): Automatically labels and closes stale issues and PRs. + - [Dependabot](../dependabot.yml): Keeps dependencies up-to-date. + +- Need specific `paths` triggers, activating only when files in particular directories are modified. + Example: + - [New Implementation Commenter](./new-implementation.yml): Adds comments when new implementation files are added. + +- Only work correctly if they have a dedicated file. + Examples: + - [Preview Deployment](./preview-deployment.yml): Deploys preview environments for pull requests. + - [Production Deployment](./production-deployment.yml): Handles production deployments. + - [CodeQL Code Scanning](./codeql.yml): Performs code security analysis. + - [Check PR Dependencies](./pr-dependencies.yml): Enforces dependencies between PRs based on opening comments. + +### Workflow Maintenance + +To ensure the efficiency and reliability of our workflows, follow these maintenance guidelines: + +- **Regular Review**: Review and update workflows at least quarterly to incorporate new features or best practices. +- **Dependency Updates**: Keep workflow dependencies up-to-date by reviewing and applying Dependabot suggestions promptly. +- **Documentation**: Update workflow documentation immediately after any changes or modifications to keep it current. +- **Performance Monitoring**: Regularly check workflow run times and optimize where possible to reduce GitHub Actions usage. +- **Security Checks**: Ensure that workflows using secrets or accessing sensitive data are properly secured and follow least privilege principles. + +These guidelines aim to maintain the integrity and efficiency of our CI/CD processes. Always consult with the team before making significant changes to any workflow. \ No newline at end of file diff --git a/.github/workflows/pull-request.yml b/.github/workflows/ci.yml similarity index 94% rename from .github/workflows/pull-request.yml rename to .github/workflows/ci.yml index 7528a0ce0..f4bab9cad 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,11 @@ -name: PR Workflow +name: CI on: pull_request: types: [opened, reopened, synchronize] jobs: - linting_and_type-checking: - name: Linting, Formatting and Type checking + code-quality-checks: + name: Code Quality Checks runs-on: ubuntu-latest steps: - name: Checkout repository @@ -44,8 +44,8 @@ jobs: - name: Type checking run: yarn run typecheck - Check-Unauthorized-File-Changes: - name: Checks if no unauthorized files are changed + check-unauthorized-file-changes: + name: Check Unauthorized File Changes if: ${{github.actor != 'dependabot[bot]'}} runs-on: ubuntu-latest steps: @@ -94,8 +94,8 @@ jobs: build: name: Build check + needs: code-quality-checks runs-on: ubuntu-latest - needs: linting_and_type-checking steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index e1f1e9f72..9510d6303 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -5,25 +5,25 @@ on: types: ['opened'] jobs: - Issue-Labeler: - name: Adding Label to issue - runs-on: ubuntu-latest - steps: - - uses: Renato66/auto-label@v2.3.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - ignore-comments: true - default-labels: '["Status: Triage"]' + issue-labeler: + name: Adding Label to Issue + runs-on: ubuntu-latest + steps: + - name: Auto Label Issue + uses: Renato66/auto-label@v3.1.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + ignore-comments: true + default-labels: '["Status: Triage"]' - Issue-Greeting: - name: Greeting Message to User - runs-on: ubuntu-latest - steps: - - name: Greeting Message to User - uses: actions/first-interaction@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: | - Welcome to the [JSON Schema](https://json-schema.org/) Community. We are so excited you are here! Thanks a lot for reporting your first issue!! 🎉🎉 Please make sure to take a look to our [contributors guide](https://github.com/json-schema-org/website/blob/main/CONTRIBUTING.md) if you plan on opening a pull request. - For more details check out [README.md](https://github.com/json-schema-org/website?tab=readme-ov-file#-welcome-to-the-json-schema-website) file. - \ No newline at end of file + issue-greeting: + name: Greeting Message to User + runs-on: ubuntu-latest + steps: + - name: Send Greeting Message + uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: | + Welcome to the [JSON Schema](https://json-schema.org/) Community. We are so excited you are here! Thanks a lot for reporting your first issue!! 🎉🎉 Please make sure to take a look at our [contributors guide](https://github.com/json-schema-org/website/blob/main/CONTRIBUTING.md) if you plan on opening a pull request. + For more details, check out the [README.md](https://github.com/json-schema-org/website?tab=readme-ov-file#-welcome-to-the-json-schema-website) file. diff --git a/.github/workflows/links.yml b/.github/workflows/link-checker.yml similarity index 98% rename from .github/workflows/links.yml rename to .github/workflows/link-checker.yml index 6e32ce6da..73c57b9d3 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/link-checker.yml @@ -8,6 +8,7 @@ on: jobs: linkChecker: + name: Check and Report Broken Links runs-on: ubuntu-latest steps: - name: Checkout Repository diff --git a/.github/workflows/new-implementation.yml b/.github/workflows/new-implementation.yml index f54607e71..a4dbe4303 100644 --- a/.github/workflows/new-implementation.yml +++ b/.github/workflows/new-implementation.yml @@ -21,7 +21,7 @@ jobs: pull-requests: write steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v7 with: script: | github.rest.issues.createComment({ diff --git a/.github/workflows/pull-request-target.yml b/.github/workflows/pull-request-target.yml index d54a71298..e663c1337 100644 --- a/.github/workflows/pull-request-target.yml +++ b/.github/workflows/pull-request-target.yml @@ -1,4 +1,4 @@ -name: Pull Request Target Workflow +name: PR Interaction Workflow on: pull_request_target: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale-issues-prs.yml similarity index 100% rename from .github/workflows/stale.yml rename to .github/workflows/stale-issues-prs.yml diff --git a/.github/workflows/sync-contributors.yml b/.github/workflows/sync-contributors.yml new file mode 100644 index 000000000..7325c2910 --- /dev/null +++ b/.github/workflows/sync-contributors.yml @@ -0,0 +1,59 @@ +name : Sync Contributors Data + +on: + schedule: # Run sunday at midnight every week + - cron: '0 0 * * 0' + workflow_dispatch: + +jobs: + sync-contributors-data: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - 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: Fetch Contributors data + uses: actions/github-script@v7 + env: + ORGS: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + with: + github-token: ${{ steps.get_workflow_token.outputs.token }} + script: | + const fs = require('fs'); + + let data = await github.paginate(github.rest.repos.listContributors, { + owner: process.env.ORGS, + repo: process.env.REPO, + per_page: 100, + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + }); + + // Filter the data to get only the required fields + data = data.map(({ login, id, avatar_url, html_url }) => + ({ login, id, avatar_url, html_url })); + + // Store the data in a file + fs.writeFileSync('community.json', JSON.stringify(data, null, 2)); + + - name: Commit changes + env: + GITHUB_APP_TOKEN: ${{ steps.get_workflow_token.outputs.token }} + run: | + git config user.name "the-json-schema-bot[bot]" + git config user.email "the-json-schema-bot[bot]@users.noreply.github.com" + git add community.json + git diff --quiet && git diff --staged --quiet || (git commit -m "chore(community): update community.json" && git push "https://x-access-token:${GITHUB_APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:${GITHUB_REF#refs/heads/}) + + + + diff --git a/.github/workflows/sync-project-roadmap.yml b/.github/workflows/sync-project-roadmap.yml new file mode 100644 index 000000000..238af4819 --- /dev/null +++ b/.github/workflows/sync-project-roadmap.yml @@ -0,0 +1,100 @@ +name : Sync Project Roadmap Data + +on: + schedule: # Run sundat at 00:05 every week + - cron: '5 0 * * 0' + workflow_dispatch: + +jobs: + sync-roadmap-data: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - 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 }} + + # fetch project data and store it in a file + - name: Fetch project data + env: + GH_TOKEN: ${{ steps.get_workflow_token.outputs.token }} # GitHub App token stored in secrets + PROJECT_ID: ${{vars.ROADMAP_PROJECT_ID}} # Project ID + run: | + gh api graphql -f query=' + query($PROJECT_ID : ID!) { + node(id: $PROJECT_ID) { + ... on ProjectV2 { + items(first: 20) { + nodes { + id + fieldValues(first: 8) { + nodes { + ... on ProjectV2ItemFieldTextValue { + text + field { + ... on ProjectV2FieldCommon { + name + } + } + } + ... on ProjectV2ItemFieldDateValue { + date + field { + ... on ProjectV2FieldCommon { + name + } + } + } + ... on ProjectV2ItemFieldSingleSelectValue { + name + field { + ... on ProjectV2FieldCommon { + name + } + } + } + } + } + content { + ... on DraftIssue { + title + body + } + ... on Issue { + title + assignees(first: 10) { + nodes { + login + } + } + } + ... on PullRequest { + title + assignees(first: 10) { + nodes { + login + } + } + } + } + } + } + } + } + }' -f PROJECT_ID=$PROJECT_ID | jq '.data.node.items.nodes' > project_data.json + + # commit updated project data + - name: Commit changes + env: + GITHUB_APP_TOKEN: ${{ steps.get_workflow_token.outputs.token }} + run: | + git config user.name "the-json-schema-bot[bot]" + git config user.email "the-json-schema-bot[bot]@users.noreply.github.com" + git add project_data.json + git diff --quiet && git diff --staged --quiet || (git commit -m "chore(project_data): update project_data.json" && git push "https://x-access-token:${GITHUB_APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:${GITHUB_REF#refs/heads/}) + diff --git a/README.md b/README.md index 45635c591..905896d48 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,19 @@ git submodule init git submodule update ``` +### Setup Enviroment Variables + +1. Create a new `.env` file by copying the contents of the `.env.example` into `.env` file. Use this command: +``` +cp .env.example .env +``` +2. Open .env and fill in your actual values for each variable. + +3. Save the file. + +4. Ensure .env is in your .gitignore. + + #### Install dependencies Install dependencies diff --git a/components/Accordion.tsx b/components/Accordion.tsx new file mode 100644 index 000000000..02b50ab2b --- /dev/null +++ b/components/Accordion.tsx @@ -0,0 +1,94 @@ +import React, { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; + +interface AccordionItem { + question: string; + answer: string; + id: number; +} + +interface AccordionProps { + items: AccordionItem[]; +} + +const Accordion: React.FC = ({ items }) => { + const [activeIndex, setActiveIndex] = useState(null); + const router = useRouter(); + + const handleToggle = (index: number) => { + setActiveIndex((prevIndex) => (prevIndex === index ? null : index)); + }; + + useEffect(() => { + const hash = router.asPath.split('#')[1]; + if (hash) { + const id = parseInt(hash, 10); + const index = items.findIndex((item) => item.id === id); + if (index !== -1) { + setActiveIndex(index); + + setTimeout(() => { + const element = document.getElementById(hash); + if (element) { + const navbarHeight = 150; + const offset = element.offsetTop - navbarHeight; + window.scrollTo({ top: offset, behavior: 'smooth' }); + } + }, 0); + } + } + }, [items, router.asPath]); + + const handleLinkClick = (id: number) => { + const index = items.findIndex((item) => item.id === id); + setActiveIndex(index); + + const newUrl = `#${id}`; + router.push(newUrl, undefined, { shallow: true }); + }; + + return ( +
+ {items.map((item, index) => ( +
+
+ +
handleToggle(index)} + > + + +
+
+ {activeIndex === index && ( +
+ {item.answer} +
+ )} +
+ ))} +
+ ); +}; + +export default Accordion; diff --git a/components/Card.tsx b/components/Card.tsx new file mode 100644 index 000000000..325084a82 --- /dev/null +++ b/components/Card.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import Link from 'next/link'; +import TextTruncate from 'react-text-truncate'; +interface CardProps { + title: string; + body: string; + icon?: string; + link?: string; + image?: string; + extended?: boolean; + headerSize?: 'small' | 'medium' | 'large'; + bodyTextSize?: 'small' | 'medium' | 'large'; +} + +const CardBody = ({ + title, + body, + icon, + link, + image, + extended, + headerSize, + bodyTextSize, +}: CardProps) => { + const headerSizeClasses: Record = { + small: 'text-[0.9rem]', + medium: 'text-[1.3rem]', + large: 'text-[2rem]', + }; + const bodyTextSizeClasses: Record = { + small: 'text-[0.85rem]', + medium: 'text-[1rem]', + large: 'text-[1.5rem]', + }; + return ( +
+
+ {image && } +
+
+ {icon && ( + + {title} + + )} +

+ {title} +

+
+
+

+ {extended && } + {!extended && } +

+ {link && ( +

+ Read More +

+ )} +
+ ); +}; + +const Card: React.FC = ({ + title, + body, + icon, + link, + image, + extended, + headerSize, + bodyTextSize, +}) => { + return ( + <> + {link ? ( + + + + ) : ( + + )} + + ); +}; + +export default Card; diff --git a/components/DarkModeToggle.tsx b/components/DarkModeToggle.tsx index 16ea1e8f8..d45169780 100644 --- a/components/DarkModeToggle.tsx +++ b/components/DarkModeToggle.tsx @@ -2,39 +2,100 @@ import { useTheme } from 'next-themes'; import { useEffect, useState } from 'react'; import React from 'react'; -export default function DarkModeToggle() { - const { theme, setTheme } = useTheme(); - const [isDarkMode, setIsDarkMode] = useState(theme === 'dark'); - const [isClickable, setIsClickable] = useState(true); - const [img, setImg] = useState('/icons/moon.svg'); - - const toggleDarkMode = () => { - if (!isClickable) return; - - setIsClickable(false); - const newTheme = isDarkMode ? 'light' : 'dark'; - setTheme(newTheme); - setIsDarkMode(!isDarkMode); +function ListItem({ + children, + onClick, +}: { + children: React.ReactNode; + onClick: () => void; +}) { + return ( +
+ {children} +
+ ); +} - setTimeout(() => { - setIsClickable(true); - }, 500); - }; +export default function DarkModeToggle() { + const { theme, setTheme, resolvedTheme } = useTheme(); + const [isDarkMode, setIsDarkMode] = useState(false); useEffect(() => { - if (!theme) setTheme('light'); + setIsDarkMode(resolvedTheme === 'dark'); + }, [resolvedTheme]); - const img = theme === 'dark' ? '/icons/sun.svg' : '/icons/moon.svg'; - setImg(img); - }, [theme, setTheme]); + const [showSelect, setShowSelect] = useState(false); + const [activeThemeIcon, setActiveThemeIcon] = useState(''); + useEffect(() => { + switch (theme) { + case 'system': + return setActiveThemeIcon('/icons/theme-switch.svg'); + case 'light': + return setActiveThemeIcon('/icons/sun.svg'); + case 'dark': + return setActiveThemeIcon('/icons/moon.svg'); + } + }, [theme, resolvedTheme]); return ( - +
+ +
{ + setShowSelect(false); + }} + tabIndex={0} + > + setTheme('system')}> + System theme + System + + setTheme('light')}> + System theme + Light + + setTheme('dark')}> + System theme + Dark + +
+
); } diff --git a/components/Faq.tsx b/components/Faq.tsx new file mode 100644 index 000000000..fd0f3f2b7 --- /dev/null +++ b/components/Faq.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import faqData from '../data/faq.json'; +import Accordion from '~/components/Accordion'; + +export default function Faq({ category }: { category: string }) { + const filteredFAQs = faqData.filter((item) => item.category === category); + + return ( +
+
+

+ {category.toUpperCase()} +

+ +
+
+ ); +} diff --git a/components/Layout.tsx b/components/Layout.tsx index fb13a6a65..6581ccca5 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -41,8 +41,8 @@ export default function Layout({ ); useEffect(() => { - // Check if the URL contains "#community" - if (window.location.hash === '#community') { + // Check if the URL contains "community" + if (window.location.hash === 'community') { // Find the anchor element by its ID const target = document.getElementById('community'); @@ -164,26 +164,29 @@ const MainNavigation = () => { const section = useContext(SectionContext); const showMobileNav = useStore((s: any) => s.overlayNavigation === 'docs'); - const { theme } = useTheme(); + const { resolvedTheme, theme } = useTheme(); const [icon, setIcon] = useState(''); const [menu, setMenu] = useState('bg-black'); const [closeMenu, setCLoseMenu] = useState('url("/icons/cancel.svg")'); useEffect(() => { const icon = theme === 'dark' ? 'herobtn' : ''; - const menu = theme === 'dark' ? 'bg-white' : 'bg-black'; - const dataTheme = theme === 'dark' ? 'dark' : 'light'; + const menu = resolvedTheme === 'dark' ? 'bg-white' : 'bg-black'; + const dataTheme = resolvedTheme === 'dark' ? 'dark' : 'light'; const closeMenu = - theme === 'dark' + resolvedTheme === 'dark' ? 'url("/icons/cancel-dark.svg")' : 'url("/icons/cancel.svg")'; document.documentElement.setAttribute('data-theme', dataTheme); - document.documentElement.setAttribute('class', 'keygrad keyshadow'); + document.documentElement.setAttribute( + 'class', + `keygrad keyshadow ${dataTheme}`, + ); setIcon(icon); setMenu(menu); setCLoseMenu(closeMenu); - }, [theme]); + }, [theme, resolvedTheme]); return (
@@ -195,7 +198,7 @@ const MainNavigation = () => { /> @@ -214,12 +217,12 @@ const MainNavigation = () => { /> -
+
{ @@ -298,7 +301,7 @@ const MobileNav = () => { /> @@ -402,16 +405,16 @@ const Footer = () => ( ); const Logo = () => { - const { theme } = useTheme(); + const { resolvedTheme } = useTheme(); const [imageSrc, setImageSrc] = useState('/img/logos/logo-blue.svg'); // Default to match the server-side render useEffect(() => { const src = - theme === 'dark' + resolvedTheme === 'dark' ? '/img/logos/logo-white.svg' : '/img/logos/logo-blue.svg'; setImageSrc(src); - }, [theme]); + }, [resolvedTheme]); return (
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index cfec6dc8a..16ad66826 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -91,8 +91,11 @@ const SegmentSubtitle = ({ label }: { label: string }) => { const getDocsPath = [ '/overview/what-is-jsonschema', '/overview/sponsors', + '/overview/case-studies', '/overview/similar-technologies', + '/overview/use-cases', '/overview/code-of-conduct', + '/overview/faq', ]; const getStartedPath = [ '/learn/json-schema-examples', @@ -140,6 +143,7 @@ const getSpecificationPath = [ '/specification-links', '/specification', ]; + export const SidebarLayout = ({ children }: { children: React.ReactNode }) => { const router = useRouter(); const [open, setOpen] = useState(false); @@ -214,7 +218,6 @@ export const SidebarLayout = ({ children }: { children: React.ReactNode }) => {
-
@@ -281,15 +284,14 @@ export const DocsNav = ({ const rotateR = active.getReference ? 'rotate(180deg)' : 'rotate(0)'; const rotateSpec = active.getSpecification ? 'rotate(180deg)' : 'rotate(0)'; - const { theme } = useTheme(); + const { resolvedTheme } = useTheme(); const [learn_icon, setLearn_icon] = useState(''); const [reference_icon, setReference_icon] = useState(''); const [spec_icon, setSpec_icon] = useState(''); const [overview_icon, setOverview_icon] = useState(''); - useEffect(() => { - if (theme === 'dark') { + if (resolvedTheme === 'dark') { setOverview_icon('/icons/eye-dark.svg'); setLearn_icon('/icons/compass-dark.svg'); setReference_icon('/icons/book-dark.svg'); @@ -300,7 +302,7 @@ export const DocsNav = ({ setReference_icon('/icons/book.svg'); setSpec_icon('/icons/clipboard.svg'); } - }, [theme]); + }, [resolvedTheme]); return (