diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 58bb732..2ddd536 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index f0106ca..4ebf6b9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 1ab71cf..c8c70e2 100644 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -f26173c7304a0bf8ea5c86eb567e7db2 \ No newline at end of file +4d24ccab52bffb5e1bca6aca46e9e53c \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a6e56f9..98e34fa 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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 (
{resultText}
+
+ +
diff --git a/frontend/src/components/ContributionCalendar.module.scss b/frontend/src/components/ContributionCalendar.module.scss new file mode 100644 index 0000000..d60fbf0 --- /dev/null +++ b/frontend/src/components/ContributionCalendar.module.scss @@ -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; +} \ No newline at end of file diff --git a/frontend/src/components/ContributionCalendar.tsx b/frontend/src/components/ContributionCalendar.tsx new file mode 100644 index 0000000..da8e38e --- /dev/null +++ b/frontend/src/components/ContributionCalendar.tsx @@ -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 }, ...]; + * + */ +type Props = { + contributions: OneDay[]; + className?: string; +} & React.HTMLAttributes; + +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( + + {MONTH[date.getMonth()]} + , + ); + } + return ( + + ); + }); + + // 第一格不一定是周日,此时前面会有空白,需要设置下起始行。 + 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 ( +
+ {renderedMonths} + Mon + Wed + Fri + +
{tiles}
+ +
+ {total} contributions in the last year +
+
+ Less + + + + + + More +
+
+ ); +} + +// 里头需要循环 365 次,耗时 3ms,还是用 memo 包装下吧。 +export default React.memo(ContributionCalendar); \ No newline at end of file diff --git a/frontend/src/css/mixins.scss b/frontend/src/css/mixins.scss new file mode 100644 index 0000000..8550559 --- /dev/null +++ b/frontend/src/css/mixins.scss @@ -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; \ No newline at end of file