Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
NEXT_PUBLIC_SITE_NAME=freecodecamp-chengdu.github.io
NEXT_PUBLIC_SITE_SUMMARY=Lark project scaffold based on TypeScript, React, Next.js, Bootstrap & Workbox.
NEXT_PUBLIC_LOGO = https://github.com/FreeCodeCamp-Chengdu.png

NEXT_PUBLIC_SENTRY_DSN =
SENTRY_ORG =
Expand Down
91 changes: 71 additions & 20 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
// @ts-check
import { fixupPluginRules } from '@eslint/compat';
import { FlatCompat } from '@eslint/eslintrc';
import cspellPlugin from '@cspell/eslint-plugin';
import eslint from '@eslint/js';
// @ts-expect-error eslint-plugin-next doesn't come with TypeScript definitions
import nextPlugin from '@next/eslint-plugin-next';
import stylistic from '@stylistic/eslint-plugin';
import eslintConfigPrettier from 'eslint-config-prettier';
import reactPlugin from 'eslint-plugin-react';
import react from 'eslint-plugin-react';
import simpleImportSortPlugin from 'eslint-plugin-simple-import-sort';
import globals from 'globals';
import tsEslint from 'typescript-eslint';
import { fileURLToPath } from 'url';

const tsconfigRootDir = fileURLToPath(new URL('.', import.meta.url)),
flatCompat = new FlatCompat();
/**
* @see{@link https://github.com/typescript-eslint/typescript-eslint/blob/main/eslint.config.mjs}
* @see{@link https://github.com/vercel/next.js/issues/71763#issuecomment-2476838298}
*/

const tsconfigRootDir = fileURLToPath(new URL('.', import.meta.url));

export default tsEslint.config(
// register all of the plugins up-front
{
plugins: {
'@typescript-eslint': tsEslint.plugin,
react: fixupPluginRules(reactPlugin),
'@cspell': cspellPlugin,
'@stylistic': stylistic,
'simple-import-sort': simpleImportSortPlugin,
'@typescript-eslint': tsEslint.plugin,
react,
'@next/next': nextPlugin,
},
},
{
Expand All @@ -34,7 +42,6 @@ export default tsEslint.config(
// extends ...
eslint.configs.recommended,
...tsEslint.configs.recommended,
...flatCompat.extends('plugin:@next/next/core-web-vitals'),

// base config
{
Expand All @@ -47,13 +54,65 @@ export default tsEslint.config(
},
},
rules: {
// spellchecker
'@cspell/spellchecker': [
'warn',
{
cspell: {
language: 'en',
dictionaries: [
'typescript',
'node',
'html',
'css',
'bash',
'npm',
'pnpm',
],
},
},
],
// stylistic
'@stylistic/padding-line-between-statements': [
'error',
{ blankLine: 'always', prev: '*', next: 'return' },
{ blankLine: 'always', prev: 'directive', next: '*' },
{ blankLine: 'any', prev: 'directive', next: 'directive' },
{
blankLine: 'always',
prev: '*',
next: ['enum', 'interface', 'type'],
},
],
'arrow-body-style': ['error', 'as-needed'],
'no-empty-pattern': 'warn',
'no-console': ['error', { allow: ['warn', 'error', 'info'] }],
'no-restricted-syntax': [
'error',
{
selector: "TSPropertySignature[key.name='children']",
message:
'Please use PropsWithChildren<T> instead of defining children manually',
},
],
'consistent-return': 'warn',
'prefer-destructuring': ['error', { object: true, array: true }],
// simple-import-sort
'simple-import-sort/exports': 'error',
'simple-import-sort/imports': 'error',
// TypeScript
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-unsafe-declaration-merging': 'warn',
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
// React
'react/no-unescaped-entities': 'off',
'react/self-closing-comp': ['error', { component: true, html: true }],
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never' },
],
'react/jsx-no-target-blank': 'warn',
'react/jsx-sort-props': [
'error',
Expand All @@ -63,19 +122,11 @@ export default tsEslint.config(
noSortAlphabetically: true,
},
],
// Next.js
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs['core-web-vitals'].rules,
'@next/next/no-sync-scripts': 'warn',
},
},
{
files: ['**/*.js'],
extends: [tsEslint.configs.disableTypeChecked],
rules: {
// turn off other type-aware rules
'@typescript-eslint/internal/no-poorly-typed-ts-props': 'off',

// turn off rules that don't apply to JS code
'@typescript-eslint/explicit-function-return-type': 'off',
},
},
eslintConfigPrettier,
);
42 changes: 23 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,64 +9,68 @@
"dependencies": {
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/mdx": "^15.2.1",
"@sentry/nextjs": "^9.4.0",
"@next/mdx": "^15.2.3",
"@sentry/nextjs": "^9.6.0",
"copy-webpack-plugin": "^13.0.0",
"core-js": "^3.41.0",
"file-type": "^20.4.0",
"file-type": "^20.4.1",
"idea-react": "^2.0.0-rc.8",
"koajax": "^3.1.1",
"less": "^4.2.2",
"less-loader": "^12.2.0",
"lodash": "^4.17.21",
"marked": "^15.0.7",
"mime": "^4.0.6",
"mobx": "^6.13.6",
"mobx": "^6.13.7",
"mobx-github": "^0.3.5",
"mobx-i18n": "^0.6.0",
"mobx-lark": "^2.0.0",
"mobx-lark": "^2.1.0",
"mobx-react": "^9.2.0",
"mobx-restful": "^2.1.0",
"mobx-restful-table": "^2.0.1",
"next": "^15.2.1",
"mobx-restful-table": "^2.0.2",
"next": "^15.2.3",
"next-pwa": "~5.6.0",
"next-ssr-middleware": "^0.8.9",
"next-ssr-middleware": "^0.8.10",
"next-with-less": "^3.0.1",
"react": "^19.0.0",
"react-bootstrap": "^2.10.9",
"react-dom": "^19.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.1",
"remark-mdx-frontmatter": "^5.0.0",
"undici": "^7.4.0",
"undici": "^7.5.0",
"web-utility": "^4.4.3",
"webpack": "^5.98.0"
"webpack": "^5.98.0",
"yaml": "^2.7.0"
},
"devDependencies": {
"@babel/plugin-proposal-decorators": "^7.25.9",
"@babel/plugin-transform-typescript": "^7.26.8",
"@babel/preset-react": "^7.26.3",
"@cspell/eslint-plugin": "^8.17.5",
"@eslint/compat": "^1.2.7",
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "^9.21.0",
"@eslint/js": "^9.22.0",
"@next/eslint-plugin-next": "^15.2.3",
"@softonus/prettier-plugin-duplicate-remover": "^1.1.2",
"@stylistic/eslint-plugin": "^4.2.0",
"@types/eslint-config-prettier": "^6.11.3",
"@types/lodash": "^4.17.16",
"@types/next-pwa": "^5.6.9",
"@types/node": "^22.13.9",
"@types/react": "^19.0.10",
"eslint": "^9.21.0",
"eslint-config-next": "^15.2.1",
"eslint-config-prettier": "^10.0.2",
"@types/node": "^22.13.10",
"@types/react": "^19.0.11",
"eslint": "^9.22.0",
"eslint-config-next": "^15.2.3",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^16.0.0",
"husky": "^9.1.7",
"lint-staged": "^15.4.3",
"lint-staged": "^15.5.0",
"prettier": "^3.5.3",
"prettier-plugin-css-order": "^2.1.2",
"typescript": "~5.8.2",
"typescript-eslint": "^8.26.0"
"typescript-eslint": "^8.26.1"
},
"resolutions": {
"next": "$next"
Expand All @@ -89,7 +93,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint && tsc --noEmit",
"lint": "next lint --fix && tsc --noEmit",
"test": "lint-staged && npm run lint",
"pack-image": "docker build -t freecodecamp-chengdu/freecodecamp-chengdu.github.io:latest .",
"container": "docker rm -f freecodecamp-chengdu.github.io && docker run --name freecodecamp-chengdu.github.io -p 3000:3000 -d freecodecamp-chengdu/freecodecamp-chengdu.github.io:latest"
Expand Down
2 changes: 1 addition & 1 deletion pages/_document.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default function Document() {
<link rel="icon" href="/favicon.ico" />

<link rel="manifest" href="/manifest.json" />
<script src="https://polyfill.web-cell.dev/feature/PWAManifest.js"></script>
<script src="https://polyfill.web-cell.dev/feature/PWAManifest.js" />

<link
rel="stylesheet"
Expand Down
1 change: 1 addition & 0 deletions pages/api/Lark/bitable/v1/[...slug].ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ export default proxyLark((URI, data) => {

filterData(record.fields);
}

return data;
});
29 changes: 1 addition & 28 deletions pages/api/Lark/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { marked } from 'marked';
import {
LarkApp,
LarkData,
normalizeText,
TableCellLocation,
normalizeTextArray,
TableCellText,
TableCellValue,
} from 'mobx-lark';

import { safeAPI } from '../core';
Expand All @@ -16,34 +14,9 @@ export const lark = new LarkApp({
secret: process.env.LARK_APP_SECRET!,
});

export interface TableFormViewItem
extends Record<'name' | 'description' | 'shared_url', string>,
Record<'shared' | 'submit_limit_once', boolean> {
shared_limit: 'tenant_editable';
}
export type LarkFormData = LarkData<{ form: TableFormViewItem }>;

export const normalizeTextArray = (list: TableCellText[]) =>
list.reduce(
(sum, item) => {
if (item.text === ',') sum.push('');
else sum[sum.length - 1] += normalizeText(item);

return sum;
},
[''],
);

export const normalizeMarkdownArray = (list: TableCellText[]) =>
normalizeTextArray(list).map(text => marked(text) as string);

export function coordinateOf(location: TableCellValue): [number, number] {
const [longitude, latitude] =
(location as TableCellLocation)?.location.split(',') || [];

return [+latitude, +longitude];
}

export const proxyLark = <T extends LarkData>(
dataFilter?: (path: string, data: T) => T,
) =>
Expand Down
75 changes: 75 additions & 0 deletions pages/api/core.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { HTTPError } from 'koajax';
import { DataObject } from 'mobx-restful';
import { NextApiRequest, NextApiResponse } from 'next';
import { ProxyAgent, setGlobalDispatcher } from 'undici';
import { parse } from 'yaml';

const { HTTP_PROXY } = process.env;

Expand All @@ -20,6 +22,7 @@ export function safeAPI(handler: NextAPI): NextAPI {
console.error(error);

res.status(400);

return res.send({ message: (error as Error).message });
}
const { message, response } = error;
Expand All @@ -42,3 +45,75 @@ export function safeAPI(handler: NextAPI): NextAPI {
}
};
}

export interface ArticleMeta {
name: string;
path?: string;
meta?: DataObject;
subs: ArticleMeta[];
}

const MDX_pattern = /\.mdx?$/;

export async function frontMatterOf(path: string) {
const { readFile } = await import('fs/promises');

const file = await readFile(path, 'utf-8');

const [, frontMatter] = file.match(/^---[\r\n]([\s\S]+?[\r\n])---/) || [];

return frontMatter && parse(frontMatter);
}

export async function* pageListOf(
path: string,
prefix = 'pages',
): AsyncGenerator<ArticleMeta> {
const { readdir } = await import('fs/promises');

const list = await readdir(prefix + path, { withFileTypes: true });

for (const node of list) {
let { name, path } = node;

if (name.startsWith('.')) continue;

const isMDX = MDX_pattern.test(name);

name = name.replace(MDX_pattern, '');
path = `${path}/${name}`.replace(new RegExp(`^${prefix}`), '');

if (node.isFile())
if (isMDX) {
const article: ArticleMeta = { name, path, subs: [] };
try {
const meta = await frontMatterOf(`${node.path}/${node.name}`);

if (meta) article.meta = meta;
} catch (error) {
console.error(error);
}
yield article;
} else continue;

if (!node.isDirectory()) continue;

const subs = await Array.fromAsync(pageListOf(path, prefix));

if (subs[0]) yield { name, subs };
}
}

export type TreeNode<K extends string> = {
[key in K]: TreeNode<K>[];
};

export function* traverseTree<K extends string>(
tree: TreeNode<K>,
key: K,
): Generator<TreeNode<K>> {
for (const node of tree[key] || []) {
yield node;
yield* traverseTree(node, key);
}
}
Loading