feat: add github contributionCalendar component

Signed-off-by: zmrlft <2643895326@qq.com>
This commit is contained in:
zmrlft 2025-02-06 20:19:11 +08:00
parent 6b78b31eca
commit 92763df875
7 changed files with 756 additions and 2 deletions

View File

@ -15,6 +15,7 @@
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.0.1",
"sass": "^1.93.2",
"typescript": "^4.6.4",
"vite": "^3.0.7"
}
@ -450,6 +451,316 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@parcel/watcher": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher/-/watcher-2.5.1.tgz",
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.1",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
"@parcel/watcher-freebsd-x64": "2.5.1",
"@parcel/watcher-linux-arm-glibc": "2.5.1",
"@parcel/watcher-linux-arm-musl": "2.5.1",
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
"@parcel/watcher-linux-arm64-musl": "2.5.1",
"@parcel/watcher-linux-x64-glibc": "2.5.1",
"@parcel/watcher-linux-x64-musl": "2.5.1",
"@parcel/watcher-win32-arm64": "2.5.1",
"@parcel/watcher-win32-ia32": "2.5.1",
"@parcel/watcher-win32-x64": "2.5.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/@types/prop-types/-/prop-types-15.7.15.tgz",
@ -510,6 +821,20 @@
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/browserslist": {
"version": "4.26.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/browserslist/-/browserslist-4.26.3.tgz",
@ -565,6 +890,22 @@
],
"license": "CC-BY-4.0"
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/convert-source-map/-/convert-source-map-2.0.0.tgz",
@ -597,6 +938,20 @@
}
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.234",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz",
@ -992,6 +1347,20 @@
"node": ">=6"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/fsevents/-/fsevents-2.3.3.tgz",
@ -1040,6 +1409,13 @@
"node": ">= 0.4"
}
},
"node_modules/immutable": {
"version": "5.1.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/immutable/-/immutable-5.1.3.tgz",
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
"dev": true,
"license": "MIT"
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/is-core-module/-/is-core-module-2.16.1.tgz",
@ -1056,6 +1432,42 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/js-tokens/-/js-tokens-4.0.0.tgz",
@ -1123,6 +1535,21 @@
"node": ">=12"
}
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/ms/-/ms-2.1.3.tgz",
@ -1149,6 +1576,14 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/node-releases": {
"version": "2.0.23",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/node-releases/-/node-releases-2.0.23.tgz",
@ -1170,6 +1605,20 @@
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/postcss/-/postcss-8.5.6.tgz",
@ -1234,6 +1683,20 @@
"node": ">=0.10.0"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/resolve/-/resolve-1.22.10.tgz",
@ -1271,6 +1734,27 @@
"fsevents": "~2.3.2"
}
},
"node_modules/sass": {
"version": "1.93.2",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/sass/-/sass-1.93.2.tgz",
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/scheduler/-/scheduler-0.23.2.tgz",
@ -1321,6 +1805,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/typescript/-/typescript-4.9.5.tgz",

View File

@ -16,7 +16,8 @@
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.0.1",
"sass": "^1.93.2",
"typescript": "^4.6.4",
"vite": "^3.0.7"
}
}
}

View File

@ -1 +1 @@
f26173c7304a0bf8ea5c86eb567e7db2
4d24ccab52bffb5e1bca6aca46e9e53c

View File

@ -2,6 +2,7 @@ import {useState} from 'react';
import logo from './assets/images/logo-universal.png';
import './App.css';
import {Greet} from "../wailsjs/go/main/App";
import ContributionCalendar, { OneDay } from './components/ContributionCalendar';
function App() {
const [resultText, setResultText] = useState("Please enter your name below 👇");
@ -13,10 +14,26 @@ function App() {
Greet(name).then(updateResultText);
}
// 先随便拼 365 天的假数据
const fakeData: OneDay[] = Array.from({ length: 365 }, (_, i) => {
const date = new Date();
date.setDate(date.getDate() - (365 - i));
return {
date: date.toISOString().slice(0, 10), // YYYY-MM-DD
count: Math.floor(Math.random() * 5),
level: Math.floor(Math.random() * 5) as 0 | 1 | 2 | 3 | 4,
};
});
return (
<div id="App">
<img src={logo} id="logo" alt="logo"/>
<div id="result" className="result">{resultText}</div>
<div>
<ContributionCalendar contributions={fakeData}></ContributionCalendar>
</div>
<div id="input" className="input-box">
<input id="name" className="input" onChange={updateName} autoComplete="off" name="input" type="text"/>
<button className="btn" onClick={greet}>Greet</button>

View File

@ -0,0 +1,79 @@
@import "../css/mixins.scss";
.container {
display: grid;
grid-template-columns: auto repeat(53, 10px);
grid-template-rows: auto repeat(7, 10px) auto;
gap: 3px;
width: fit-content;
font-size: 12px;
padding: 14px;
border: solid 1px #D1D9E0;
border-radius: 0.375rem;
@include mobile-layout {
display: none; /* 太长手机显示不下 */
}
}
.month {
grid-row: 1/2;
margin-bottom: -3px;
}
.week {
grid-row: 3;
grid-column: 1/2;
line-height: 10px;
margin-right: 3px;
& + .week {
grid-row: 5;
}
& + .week + .week {
grid-row: 7;
}
}
.tiles {
grid-column: 2/55;
grid-row: 2/9;
display: grid;
grid-auto-flow: column;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
}
.tile {
display: block;
width: 10px;
height: 10px;
border-radius: 2px;
outline: 1px solid rgba(27, 35, 36, 0.06);
outline-offset: -1px;
&[data-level="0"] { background: #EBEDF0; }
&[data-level="1"] { background: #9be9a8; }
&[data-level="2"] { background: #40C463; }
&[data-level="3"] { background: #30a14e; }
&[data-level="4"] { background: #216e39; }
}
.total {
grid-column: 2/30;
margin-top: 4px;
}
.legend {
grid-column: 30/53;
margin-top: 4px;
display: flex;
gap: 5px;
justify-content: right;
align-items: center;
}

View File

@ -0,0 +1,136 @@
import React from "react";
import clsx from "clsx";
import styles from "./ContributionCalendar.module.scss";
const MONTH = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
// 生成气泡提示的内容,主要就是处理英语就的复数词尾,中文就没这破事。
export type OneDay = { level: number; count: number; date: string };
function getTooltip(oneDay: OneDay, date: Date) {
const s = date.toISOString().split("T")[0];
switch (oneDay.count) {
case 0:
return `No contributions on ${s}`;
case 1:
return `1 contribution on ${s}`;
default:
return `${oneDay.count} contributions on ${s}`;
}
}
/**
* 仿 GitHub /script/fetch-contributions.js
*
* @example
* const data = [{ level: 1, count: 5, date: 1728272654618 }, ...];
* <ContributionCalendar contributions={data}/>
*/
type Props = {
contributions: OneDay[];
className?: string;
} & React.HTMLAttributes<HTMLDivElement>;
function ContributionCalendar({ contributions, className, ...rest }: Props) {
if (!contributions || contributions.length === 0) return null;
const firstDate = new Date(contributions[0].date);
const startRow = firstDate.getDay();
const months: (React.ReactElement | undefined)[] = [];
let total = 0;
let latestMonth = -1;
const tiles = contributions.map((c, i) => {
const date = new Date(c.date);
const month = date.getMonth();
total += c.count;
// 在星期天的月份出现变化的列上面显示月份。
if (date.getDay() === 0 && month !== latestMonth) {
// 计算月份对应的列,从 1 开始、左上角格子留空所以 +2
const gridColumn = 2 + Math.floor((i + startRow) / 7);
latestMonth = month;
months.push(
<span
className={styles.month}
key={i}
style={{ gridColumn }}
>
{MONTH[date.getMonth()]}
</span>,
);
}
return (
<i
className={styles.tile}
key={i}
data-level={c.level}
title={getTooltip(c, date)}
/>
);
});
// 第一格不一定是周日,此时前面会有空白,需要设置下起始行。
if (tiles.length > 0) {
tiles[0] = React.cloneElement(tiles[0], {
style: { gridRow: startRow + 1 },
});
}
// 如果第一格不是周日,则首月可能跑到第二列,需要再检查下。
// Safely adjust months. Use optional chaining and avoid mutating props directly.
if (months.length > 0) {
const first = months[0];
if (first && MONTH[firstDate.getMonth()] === (first.props && first.props.children)) {
// create a new element with adjusted style instead of mutating props
months[0] = React.cloneElement(first, {
style: { ...(first.props.style || {}), gridColumn: 2 },
});
}
}
if (months.length > 1 && months[0] && months[1]) {
const m0 = months[0];
const m1 = months[1];
const g0 = m0?.props?.style?.gridColumn as number | undefined;
const g1 = m1?.props?.style?.gridColumn as number | undefined;
if (typeof g0 === 'number' && typeof g1 === 'number' && g1 - g0 < 3) {
months[0] = undefined;
}
}
const last = months.at(-1);
if (last && last.props && last.props.style && typeof last.props.style.gridColumn === 'number') {
if (last.props.style.gridColumn > 53) {
months[months.length - 1] = undefined;
}
}
const renderedMonths = months.filter(Boolean) as React.ReactElement[];
return (
<div {...rest} className={clsx(styles.container, className)}>
{renderedMonths}
<span className={styles.week}>Mon</span>
<span className={styles.week}>Wed</span>
<span className={styles.week}>Fri</span>
<div className={styles.tiles}>{tiles}</div>
<div className={styles.total}>
{total} contributions in the last year
</div>
<div className={styles.legend}>
Less
<i className={styles.tile} data-level={0}/>
<i className={styles.tile} data-level={1}/>
<i className={styles.tile} data-level={2}/>
<i className={styles.tile} data-level={3}/>
<i className={styles.tile} data-level={4}/>
More
</div>
</div>
);
}
// 里头需要循环 365 次,耗时 3ms还是用 memo 包装下吧。
export default React.memo(ContributionCalendar);

View File

@ -0,0 +1,23 @@
@mixin mobile-layout {
@media screen and (max-width: 768px) {
@content;
}
}
@mixin pc-layout {
@media screen and (min-width: 768px) {
@content;
}
}
@mixin tablet-layout {
@media screen and (max-width: 1200px) {
@content;
}
}
/* 手机屏下的文字区域左右边距 */
$margin-mobile: 12px;
/* 限制最大内容宽度,人的视角有限,文字内容太长不好阅读。*/
$max-content: 900px;