diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 029b077a306a..4a605c7cb02c 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -1,41 +1,74 @@ -name: App Deployment +name: Deploy to EC2 on: + workflow_dispatch: # allow manual run push: - branches: [ master ] -env: - PUBLIC_API_URL: "https://api.roadmap.sh" - PUBLIC_EDITOR_APP_URL: "https://draw.roadmap.sh" - PUBLIC_AVATAR_BASE_URL: "https://dodrc8eu8m09s.cloudfront.net/avatars" - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CI: true + branches: + - master jobs: - build: + deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: actions/setup-node@v1 - with: - node-version: 18 - - name: Prepare Draw Repository - run: | - git clone https://${{ secrets.GH_PAT }}@github.com/roadmapsh/web-draw.git .temp/web-draw --depth 1 - - uses: pnpm/action-setup@v2.2.2 - with: - version: 7.13.4 - - name: Setup Environment - run: | - pnpm install - - name: Generate meta and build - run: | - npm run generate-renderer - npm run build - touch ./dist/.nojekyll - echo 'roadmap.sh' > ./dist/CNAME - - name: Deploy to GH Pages - run: | - git config user.email "kamranahmed.se@gmail.com" - git config user.name "Kamran Ahmed" - git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git - npm run deploy + - name: Checkout code + uses: actions/checkout@v2 + with: + fetch-depth: 2 + - uses: actions/setup-node@v1 + with: + node-version: 20 + - uses: pnpm/action-setup@v3.0.0 + with: + version: 8.15.6 + + # -------------------- + # Setup configuration + # -------------------- + - name: Prepare configuration files + run: | + git clone https://${{ secrets.GH_PAT }}@github.com/roadmapsh/infra-config.git configuration --depth 1 + - name: Copy configuration files + run: | + cp configuration/dist/github/developer-roadmap.env .env + + # -------------------- + # Prepare the build + # -------------------- + - name: Install dependencies + run: | + pnpm install + - name: Generate build + run: | + git clone https://${{ secrets.GH_PAT }}@github.com/roadmapsh/web-draw.git .temp/web-draw --depth 1 + npm run generate-renderer + npm run build + + # -------------------- + # Deploy to EC2 + # -------------------- + - uses: webfactory/ssh-agent@v0.7.0 + with: + ssh-private-key: ${{ secrets.EC2_PRIVATE_KEY }} + - name: Deploy app to EC2 + run: | + rsync -apvz --delete --no-times --exclude "configuration" -e "ssh -o StrictHostKeyChecking=no" -p ./ ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/var/www/roadmap.sh/ + - name: Restart PM2 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USERNAME }} + key: ${{ secrets.EC2_PRIVATE_KEY }} + script: | + cd /var/www/roadmap.sh + sudo pm2 restart web-roadmap + + # -------------------- + # Clear Cloudfront Caching + # -------------------- + - name: Clear Cloudfront Caching + run: | + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.GH_PAT }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/roadmapsh/infra-ansible/actions/workflows/playbook.yml/dispatches \ + -d '{ "ref":"master", "inputs": { "playbook": "roadmap_web.yml", "tags": "cloudfront", "is_verbose": false } }' \ No newline at end of file diff --git a/astro.config.mjs b/astro.config.mjs index b2dd8091375c..f4d152ab68e5 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,10 +1,10 @@ // https://astro.build/config import sitemap from '@astrojs/sitemap'; import tailwind from '@astrojs/tailwind'; +import node from '@astrojs/node'; import compress from 'astro-compress'; import { defineConfig } from 'astro/config'; import rehypeExternalLinks from 'rehype-external-links'; -import { fileURLToPath } from 'node:url'; import { serializeSitemap, shouldIndexPage } from './sitemap.mjs'; import react from '@astrojs/react'; @@ -41,9 +41,11 @@ export default defineConfig({ ], ], }, - build: { - format: 'file', - }, + output: 'hybrid', + adapter: node({ + mode: 'standalone', + }), + trailingSlash: 'never', integrations: [ tailwind({ config: { diff --git a/package.json b/package.json index 0ec606decd3c..401e083bba8b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:e2e": "playwright test" }, "dependencies": { + "@astrojs/node": "^8.2.1", "@astrojs/react": "^3.0.10", "@astrojs/sitemap": "^3.0.5", "@astrojs/tailwind": "^5.1.0", @@ -34,6 +35,7 @@ "astro": "^4.4.0", "astro-compress": "^2.2.10", "clsx": "^2.1.0", + "dayjs": "^1.11.10", "dom-to-image": "^2.6.0", "dracula-prism": "^2.1.16", "gray-matter": "^4.0.3", @@ -48,8 +50,10 @@ "npm-check-updates": "^16.14.15", "prismjs": "^1.29.0", "react": "^18.2.0", + "react-calendar-heatmap": "^1.9.0", "react-confetti": "^6.1.0", "react-dom": "^18.2.0", + "react-tooltip": "^5.26.3", "reactflow": "^11.10.4", "rehype-external-links": "^3.0.0", "remark-parse": "^11.0.0", @@ -69,6 +73,7 @@ "@types/dom-to-image": "^2.6.7", "@types/js-cookie": "^3.0.6", "@types/prismjs": "^1.26.3", + "@types/react-calendar-heatmap": "^1.6.7", "csv-parser": "^3.0.0", "gh-pages": "^6.1.1", "js-yaml": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74211416361a..0452e828ffec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,9 +5,12 @@ settings: excludeLinksFromLockfile: false dependencies: + '@astrojs/node': + specifier: ^8.2.1 + version: 8.2.1(astro@4.4.0) '@astrojs/react': specifier: ^3.0.10 - version: 3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3) + version: 3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3) '@astrojs/sitemap': specifier: ^3.0.5 version: 3.0.5 @@ -22,10 +25,10 @@ dependencies: version: 0.7.1(nanostores@0.9.5)(react@18.2.0) '@resvg/resvg-js': specifier: ^2.6.0 - version: 2.6.0 + version: 2.6.2 '@types/react': specifier: ^18.2.56 - version: 18.2.59 + version: 18.2.58 '@types/react-dom': specifier: ^18.2.19 version: 18.2.19 @@ -38,6 +41,9 @@ dependencies: clsx: specifier: ^2.1.0 version: 2.1.0 + dayjs: + specifier: ^1.11.10 + version: 1.11.10 dom-to-image: specifier: ^2.6.0 version: 2.6.0 @@ -80,15 +86,21 @@ dependencies: react: specifier: ^18.2.0 version: 18.2.0 + react-calendar-heatmap: + specifier: ^1.9.0 + version: 1.9.0(react@18.2.0) react-confetti: specifier: ^6.1.0 version: 6.1.0(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-tooltip: + specifier: ^5.26.3 + version: 5.26.3(react-dom@18.2.0)(react@18.2.0) reactflow: specifier: ^11.10.4 - version: 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) + version: 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) rehype-external-links: specifier: ^3.0.0 version: 3.0.0 @@ -121,7 +133,7 @@ dependencies: version: 11.0.4 zustand: specifier: ^4.5.1 - version: 4.5.1(@types/react@18.2.59)(react@18.2.0) + version: 4.5.1(@types/react@18.2.58)(react@18.2.0) devDependencies: '@playwright/test': @@ -139,6 +151,9 @@ devDependencies: '@types/prismjs': specifier: ^1.26.3 version: 1.26.3 + '@types/react-calendar-heatmap': + specifier: ^1.6.7 + version: 1.6.7 csv-parser: specifier: ^3.0.0 version: 3.0.0 @@ -211,6 +226,18 @@ packages: - supports-color dev: false + /@astrojs/node@8.2.1(astro@4.4.0): + resolution: {integrity: sha512-n3VWx34V5te6g/Jm2rbpXzTdpCW86CmstaGbsPutOs6VaXvvWwk+ZibA/bFl7XgNpxqQ5d6Pqacnsn+xkZ/Kag==} + peerDependencies: + astro: ^4.2.0 + dependencies: + astro: 4.4.0 + send: 0.18.0 + server-destroy: 1.0.1 + transitivePeerDependencies: + - supports-color + dev: false + /@astrojs/prism@3.0.0: resolution: {integrity: sha512-g61lZupWq1bYbcBnYZqdjndShr/J3l/oFobBKPA3+qMat146zce3nz2kdO4giGbhYDt4gYdhmoBz0vZJ4sIurQ==} engines: {node: '>=18.14.1'} @@ -218,7 +245,7 @@ packages: prismjs: 1.29.0 dev: false - /@astrojs/react@3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3): + /@astrojs/react@3.0.10(@types/react-dom@18.2.19)(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0)(vite@5.1.3): resolution: {integrity: sha512-uGRIwKMAn7tva2vxXMyoVIGxWFr0rjZ8ZWIlkTG/vIpnAjD2nM8Cz6B8j7yzj176jvl6gZ6xTbTVPm09aeK0Yw==} engines: {node: '>=18.14.1'} peerDependencies: @@ -227,7 +254,7 @@ packages: react: ^17.0.2 || ^18.0.0 react-dom: ^17.0.2 || ^18.0.0 dependencies: - '@types/react': 18.2.59 + '@types/react': 18.2.58 '@types/react-dom': 18.2.19 '@vitejs/plugin-react': 4.2.1(vite@5.1.3) react: 18.2.0 @@ -757,6 +784,23 @@ packages: tslib: 2.6.2 dev: false + /@floating-ui/core@1.6.0: + resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} + dependencies: + '@floating-ui/utils': 0.2.1 + dev: false + + /@floating-ui/dom@1.6.3: + resolution: {integrity: sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==} + dependencies: + '@floating-ui/core': 1.6.0 + '@floating-ui/utils': 0.2.1 + dev: false + + /@floating-ui/utils@0.2.1: + resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} + dev: false + /@gar/promisify@1.1.3: resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} dev: false @@ -1135,39 +1179,39 @@ packages: config-chain: 1.1.13 dev: false - /@reactflow/background@11.3.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/background@11.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-byj/G9pEC8tN0wT/ptcl/LkEP/BBfa33/SvBkqE4XwyofckqF87lKp573qGlisfnsijwAbpDlf81PuFL41So4Q==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/controls@11.2.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/controls@11.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-e8nWplbYfOn83KN1BrxTXS17+enLyFnjZPbyDgHSRLtI5ZGPKF/8iRXV+VXb2LFVzlu4Wh3la/pkxtfP/0aguA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/core@11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/core@11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-j3i9b2fsTX/sBbOm+RmNzYEFWbNx4jGWGuGooh2r1jQaE2eV+TLJgiG/VNOp0q5mBl9f6g1IXs3Gm86S9JfcGw==} peerDependencies: react: '>=17' @@ -1183,19 +1227,19 @@ packages: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/minimap@11.7.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/minimap@11.7.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-le95jyTtt3TEtJ1qa7tZ5hyM4S7gaEQkW43cixcMOZLu33VAdc2aCpJg/fXcRrrf7moN2Mbl9WIMNXUKsp5ILA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) '@types/d3-selection': 3.0.10 '@types/d3-zoom': 3.0.8 classcat: 5.0.4 @@ -1203,48 +1247,48 @@ packages: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-resizer@2.2.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/node-resizer@2.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-HfickMm0hPDIHt9qH997nLdgLt0kayQyslKE0RS/GZvZ4UMQJlx/NRRyj5y47Qyg0NnC66KYOQWDM9LLzRTnUg==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 d3-drag: 3.0.0 d3-selection: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-toolbar@1.3.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/node-toolbar@1.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-VmgxKmToax4sX1biZ9LXA7cj/TBJ+E5cklLGwquCCVVxh+lxpZGTBF3a5FJGVHiUNBBtFsC8ldcSZIK4cAlQww==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.5.1(@types/react@18.2.59)(react@18.2.0) + zustand: 4.5.1(@types/react@18.2.58)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@resvg/resvg-js-android-arm-eabi@2.6.0: - resolution: {integrity: sha512-lJnZ/2P5aMocrFMW7HWhVne5gH82I8xH6zsfH75MYr4+/JOaVcGCTEQ06XFohGMdYRP3v05SSPLPvTM/RHjxfA==} + /@resvg/resvg-js-android-arm-eabi@2.6.2: + resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} engines: {node: '>= 10'} cpu: [arm] os: [android] @@ -1252,8 +1296,8 @@ packages: dev: false optional: true - /@resvg/resvg-js-android-arm64@2.6.0: - resolution: {integrity: sha512-N527f529bjMwYWShZYfBD60dXA4Fux+D695QsHQ93BDYZSHUoOh1CUGUyICevnTxs7VgEl98XpArmUWBZQVMfQ==} + /@resvg/resvg-js-android-arm64@2.6.2: + resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} engines: {node: '>= 10'} cpu: [arm64] os: [android] @@ -1261,8 +1305,8 @@ packages: dev: false optional: true - /@resvg/resvg-js-darwin-arm64@2.6.0: - resolution: {integrity: sha512-MabUKLVayEwlPo0mIqAmMt+qESN8LltCvv5+GLgVga1avpUrkxj/fkU1TKm8kQegutUjbP/B0QuMuUr0uhF8ew==} + /@resvg/resvg-js-darwin-arm64@2.6.2: + resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -1270,8 +1314,8 @@ packages: dev: false optional: true - /@resvg/resvg-js-darwin-x64@2.6.0: - resolution: {integrity: sha512-zrFetdnSw/suXjmyxSjfDV7i61hahv6DDG6kM7BYN2yJ3Es5+BZtqYZTcIWogPJedYKmzN1YTMWGd/3f0ubFiA==} + /@resvg/resvg-js-darwin-x64@2.6.2: + resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -1279,8 +1323,8 @@ packages: dev: false optional: true - /@resvg/resvg-js-linux-arm-gnueabihf@2.6.0: - resolution: {integrity: sha512-sH4gxXt7v7dGwjGyzLwn7SFGvwZG6DQqLaZ11MmzbCwd9Zosy1TnmrMJfn6TJ7RHezmQMgBPi18bl55FZ1AT4A==} + /@resvg/resvg-js-linux-arm-gnueabihf@2.6.2: + resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} engines: {node: '>= 10'} cpu: [arm] os: [linux] @@ -1288,8 +1332,8 @@ packages: dev: false optional: true - /@resvg/resvg-js-linux-arm64-gnu@2.6.0: - resolution: {integrity: sha512-fCyMncqCJtrlANADIduYF4IfnWQ295UKib7DAxFXQhBsM9PLDTpizr0qemZcCNadcwSVHnAIzL4tliZhCM8P6A==} + /@resvg/resvg-js-linux-arm64-gnu@2.6.2: + resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1297,8 +1341,8 @@ packages: dev: false optional: true - /@resvg/resvg-js-linux-arm64-musl@2.6.0: - resolution: {integrity: sha512-ouLjTgBQHQyxLht4FdMPTvuY8xzJigM9EM2Tlu0llWkN1mKyTQrvYWi6TA6XnKdzDJHy7ZLpWpjZi7F5+Pg+Vg==} + /@resvg/resvg-js-linux-arm64-musl@2.6.2: + resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1306,8 +1350,8 @@ packages: dev: false optional: true - /@resvg/resvg-js-linux-x64-gnu@2.6.0: - resolution: {integrity: sha512-n3zC8DWsvxC1AwxpKFclIPapDFibs5XdIRoV/mcIlxlh0vseW1F49b97F33BtJQRmlntsqqN6GMMqx8byB7B+Q==} + /@resvg/resvg-js-linux-x64-gnu@2.6.2: + resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1315,8 +1359,8 @@ packages: dev: false optional: true - /@resvg/resvg-js-linux-x64-musl@2.6.0: - resolution: {integrity: sha512-n4tasK1HOlAxdTEROgYA1aCfsEKk0UOFDNd/AQTTZlTmCbHKXPq+O8npaaKlwXquxlVK8vrkcWbksbiGqbCAcw==} + /@resvg/resvg-js-linux-x64-musl@2.6.2: + resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1324,8 +1368,8 @@ packages: dev: false optional: true - /@resvg/resvg-js-win32-arm64-msvc@2.6.0: - resolution: {integrity: sha512-X2+EoBJFwDI5LDVb51Sk7ldnVLitMGr9WwU/i21i3fAeAXZb3hM16k67DeTy16OYkT2dk/RfU1tP1wG+rWbz2Q==} + /@resvg/resvg-js-win32-arm64-msvc@2.6.2: + resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -1333,8 +1377,8 @@ packages: dev: false optional: true - /@resvg/resvg-js-win32-ia32-msvc@2.6.0: - resolution: {integrity: sha512-L7oevWjQoUgK5W1fCKn0euSVemhDXVhrjtwqpc7MwBKKimYeiOshO1Li1pa8bBt5PESahenhWgdB6lav9O0fEg==} + /@resvg/resvg-js-win32-ia32-msvc@2.6.2: + resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -1342,8 +1386,8 @@ packages: dev: false optional: true - /@resvg/resvg-js-win32-x64-msvc@2.6.0: - resolution: {integrity: sha512-8lJlghb+Unki5AyKgsnFbRJwkEj9r1NpwyuBG8yEJiG1W9eEGl03R3I7bsVa3haof/3J1NlWf0rzSa1G++A2iw==} + /@resvg/resvg-js-win32-x64-msvc@2.6.2: + resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1351,22 +1395,22 @@ packages: dev: false optional: true - /@resvg/resvg-js@2.6.0: - resolution: {integrity: sha512-Tf3YpbBKcQn991KKcw/vg7vZf98v01seSv6CVxZBbRkL/xyjnoYB6KgrFL6zskT1A4dWC/vg77KyNOW+ePaNlA==} + /@resvg/resvg-js@2.6.2: + resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} engines: {node: '>= 10'} optionalDependencies: - '@resvg/resvg-js-android-arm-eabi': 2.6.0 - '@resvg/resvg-js-android-arm64': 2.6.0 - '@resvg/resvg-js-darwin-arm64': 2.6.0 - '@resvg/resvg-js-darwin-x64': 2.6.0 - '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.0 - '@resvg/resvg-js-linux-arm64-gnu': 2.6.0 - '@resvg/resvg-js-linux-arm64-musl': 2.6.0 - '@resvg/resvg-js-linux-x64-gnu': 2.6.0 - '@resvg/resvg-js-linux-x64-musl': 2.6.0 - '@resvg/resvg-js-win32-arm64-msvc': 2.6.0 - '@resvg/resvg-js-win32-ia32-msvc': 2.6.0 - '@resvg/resvg-js-win32-x64-msvc': 2.6.0 + '@resvg/resvg-js-android-arm-eabi': 2.6.2 + '@resvg/resvg-js-android-arm64': 2.6.2 + '@resvg/resvg-js-darwin-arm64': 2.6.2 + '@resvg/resvg-js-darwin-x64': 2.6.2 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 + '@resvg/resvg-js-linux-arm64-musl': 2.6.2 + '@resvg/resvg-js-linux-x64-gnu': 2.6.2 + '@resvg/resvg-js-linux-x64-musl': 2.6.2 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 + '@resvg/resvg-js-win32-x64-msvc': 2.6.2 dev: false /@rollup/rollup-android-arm-eabi@4.9.6: @@ -1867,21 +1911,25 @@ packages: /@types/prop-types@15.7.11: resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} - dev: false + + /@types/react-calendar-heatmap@1.6.7: + resolution: {integrity: sha512-xWBS9iOvw+aCidPk8QwCH69OCO7jnj6/9TjooqGQ9W+rA5m1aw36GjQMlSYKAg86otDeg9dzA+hSAIcvw/y9Rg==} + dependencies: + '@types/react': 18.2.58 + dev: true /@types/react-dom@18.2.19: resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==} dependencies: - '@types/react': 18.2.59 + '@types/react': 18.2.58 dev: false - /@types/react@18.2.59: - resolution: {integrity: sha512-DE+F6BYEC8VtajY85Qr7mmhTd/79rJKIHCg99MU9SWPB4xvLb6D1za2vYflgZfmPqQVEr6UqJTnLXEwzpVPuOg==} + /@types/react@18.2.58: + resolution: {integrity: sha512-TaGvMNhxvG2Q0K0aYxiKfNDS5m5ZsoIBBbtfUorxdH4NGSXIlYvZxLJI+9Dd3KjeB3780bciLyAb7ylO8pLhPw==} dependencies: '@types/prop-types': 15.7.11 '@types/scheduler': 0.16.8 csstype: 3.1.3 - dev: false /@types/sax@1.2.7: resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} @@ -1891,7 +1939,6 @@ packages: /@types/scheduler@0.16.8: resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} - dev: false /@types/unist@2.0.10: resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} @@ -2465,6 +2512,10 @@ packages: resolution: {integrity: sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==} dev: false + /classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + dev: false + /clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} @@ -2529,7 +2580,6 @@ packages: /color-string@1.9.1: resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - requiresBuild: true dependencies: color-name: 1.1.4 simple-swizzle: 0.2.2 @@ -2709,7 +2759,6 @@ packages: /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - dev: false /csv-parser@3.0.0: resolution: {integrity: sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==} @@ -2784,6 +2833,21 @@ packages: d3-transition: 3.0.1(d3-selection@3.0.0) dev: false + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2834,11 +2898,21 @@ packages: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + /dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} dev: false + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + /detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} @@ -2949,6 +3023,10 @@ packages: /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + /electron-to-chromium@1.4.640: resolution: {integrity: sha512-z/6oZ/Muqk4BaE7P69bXhUhpJbUM9ZJeka43ZwxsDshKtePns4mhBlh8bU5+yrnOnz3fhG82XLzGUXazOmsWnA==} dev: false @@ -2967,6 +3045,11 @@ packages: /emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + /encoding@0.1.13: resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} requiresBuild: true @@ -3066,6 +3149,11 @@ packages: '@types/estree': 1.0.5 dev: false + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + /event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -3247,6 +3335,11 @@ packages: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} dev: false + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} requiresBuild: true @@ -3646,6 +3739,17 @@ packages: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} dev: false + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + /http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} @@ -3775,7 +3879,6 @@ packages: /is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - requiresBuild: true dev: false /is-binary-path@2.1.0: @@ -4486,6 +4589,10 @@ packages: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} dev: true + /memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: false @@ -4760,6 +4867,12 @@ packages: mime-db: 1.52.0 dev: true + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + /mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -4901,6 +5014,10 @@ packages: hasBin: true dev: false + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: false @@ -5185,6 +5302,13 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -5698,6 +5822,14 @@ packages: sisteransi: 1.0.5 dev: false + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + dev: false + /property-information@6.4.0: resolution: {integrity: sha512-9t5qARVofg2xQqKtytzt+lZ4d1Qvj8t5B8fEwXK6qOfgRLgH/b13QlgEyDh033NOS31nXeFbYv7CLUDG1CeifQ==} dev: false @@ -5747,6 +5879,11 @@ packages: engines: {node: '>=10'} dev: false + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + /rc-config-loader@4.1.3: resolution: {integrity: sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==} dependencies: @@ -5768,6 +5905,16 @@ packages: strip-json-comments: 2.0.1 dev: false + /react-calendar-heatmap@1.9.0(react@18.2.0): + resolution: {integrity: sha512-mGed9any6QLOVckxwxC/eeP9s9wE8mTUW/FCE0V27xF9WOaCGuOftGSRH8DSDoSwgzMSVF6uuH7M1xvc+aZ8sg==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + memoize-one: 5.2.1 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + /react-confetti@6.1.0(react@18.2.0): resolution: {integrity: sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==} engines: {node: '>=10.18'} @@ -5788,11 +5935,27 @@ packages: scheduler: 0.23.0 dev: false + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + dev: false + /react-refresh@0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} engines: {node: '>=0.10.0'} dev: false + /react-tooltip@5.26.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-MpYAws8CEHUd/RC4GaDCdoceph/T4KHM5vS5Dbk8FOmLMvvIht2ymP2htWdrke7K6lqPO8rz8+bnwWUIXeDlzg==} + peerDependencies: + react: '>=16.14.0' + react-dom: '>=16.14.0' + dependencies: + '@floating-ui/dom': 1.6.3 + classnames: 2.5.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -5800,18 +5963,18 @@ packages: loose-envify: 1.4.0 dev: false - /reactflow@11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0): + /reactflow@11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-0CApYhtYicXEDg/x2kvUHiUk26Qur8lAtTtiSlptNKuyEuGti6P1y5cS32YGaUoDMoCqkm/m+jcKkfMOvSCVRA==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/background': 11.3.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/controls': 11.2.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/core': 11.10.4(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/minimap': 11.7.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-resizer': 2.2.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-toolbar': 1.3.9(@types/react@18.2.59)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/background': 11.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/controls': 11.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.4(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/minimap': 11.7.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/node-resizer': 2.2.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/node-toolbar': 1.3.9(@types/react@18.2.58)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -6186,6 +6349,27 @@ packages: lru-cache: 6.0.0 dev: false + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + /server-destroy@1.0.1: resolution: {integrity: sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==} dev: false @@ -6194,6 +6378,10 @@ packages: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: false + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + /sharp@0.32.6: resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} engines: {node: '>=14.15.0'} @@ -6300,7 +6488,6 @@ packages: /simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - requiresBuild: true dependencies: is-arrayish: 0.3.2 dev: false @@ -6421,6 +6608,11 @@ packages: minipass: 3.3.6 dev: false + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + /stdin-discarder@0.1.0: resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6706,6 +6898,11 @@ packages: dependencies: is-number: 7.0.0 + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: true @@ -7228,7 +7425,7 @@ packages: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false - /zustand@4.5.1(@types/react@18.2.59)(react@18.2.0): + /zustand@4.5.1(@types/react@18.2.58)(react@18.2.0): resolution: {integrity: sha512-XlauQmH64xXSC1qGYNv00ODaQ3B+tNPoy22jv2diYiP4eoDKr9LA+Bh5Bc3gplTrFdb6JVI+N4kc1DZ/tbtfPg==} engines: {node: '>=12.7.0'} peerDependencies: @@ -7243,7 +7440,7 @@ packages: react: optional: true dependencies: - '@types/react': 18.2.59 + '@types/react': 18.2.58 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) dev: false diff --git a/scripts/generate-renderer.sh b/scripts/generate-renderer.sh index e8de75e92ffd..faf9519e5bdd 100644 --- a/scripts/generate-renderer.sh +++ b/scripts/generate-renderer.sh @@ -29,4 +29,4 @@ done # ignore the worktree changes for the editor directory -git update-index --assume-unchanged editor/readonly-editor.tsx \ No newline at end of file +git update-index --assume-unchanged editor/readonly-editor.tsx || true \ No newline at end of file diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 000000000000..8917e95be09d --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,154 @@ +import Cookies from 'js-cookie'; +import { TOKEN_COOKIE_NAME } from '../lib/jwt.ts'; +import type { APIContext } from 'astro'; + +type HttpOptionsType = RequestInit | { headers: Record }; + +type AppResponse = Record; + +export type FetchError = { + status: number; + message: string; +}; + +export type AppError = { + status: number; + message: string; + errors?: { message: string; location: string }[]; +}; + +export type ApiReturn = { + response?: ResponseType; + error?: ErrorType | FetchError; +}; + +export function api(context: APIContext) { + const token = context.cookies.get(TOKEN_COOKIE_NAME)?.value; + + async function apiCall( + url: string, + options?: HttpOptionsType, + ): Promise> { + try { + const response = await fetch(url, { + credentials: 'include', + ...options, + headers: new Headers({ + 'Content-Type': 'application/json', + Accept: 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(options?.headers ?? {}), + }), + }); + + // @ts-ignore + const doesAcceptHtml = options?.headers?.['Accept'] === 'text/html'; + + const data = doesAcceptHtml + ? await response.text() + : await response.json(); + + if (response.ok) { + return { + response: data as ResponseType, + error: undefined, + }; + } + + // Logout user if token is invalid + if (data.status === 401) { + context.cookies.delete(TOKEN_COOKIE_NAME); + context.redirect(context.request.url); + + return { response: undefined, error: data as ErrorType }; + } + + if (data.status === 403) { + return { response: undefined, error: data as ErrorType }; + } + + return { + response: undefined, + error: data as ErrorType, + }; + } catch (error: any) { + return { + response: undefined, + error: { + status: 0, + message: error.message, + }, + }; + } + } + + return { + get: function apiGet( + url: string, + queryParams?: Record, + options?: HttpOptionsType, + ): Promise> { + const searchParams = new URLSearchParams(queryParams).toString(); + const queryUrl = searchParams ? `${url}?${searchParams}` : url; + + return apiCall(queryUrl, { + ...options, + method: 'GET', + }); + }, + post: async function apiPost< + ResponseType = AppResponse, + ErrorType = AppError, + >( + url: string, + body: Record, + options?: HttpOptionsType, + ): Promise> { + return apiCall(url, { + ...options, + method: 'POST', + body: JSON.stringify(body), + }); + }, + patch: async function apiPatch< + ResponseType = AppResponse, + ErrorType = AppError, + >( + url: string, + body: Record, + options?: HttpOptionsType, + ): Promise> { + return apiCall(url, { + ...options, + method: 'PATCH', + body: JSON.stringify(body), + }); + }, + put: async function apiPut< + ResponseType = AppResponse, + ErrorType = AppError, + >( + url: string, + body: Record, + options?: HttpOptionsType, + ): Promise> { + return apiCall(url, { + ...options, + method: 'PUT', + body: JSON.stringify(body), + }); + }, + delete: async function apiDelete< + ResponseType = AppResponse, + ErrorType = AppError, + >( + url: string, + options?: HttpOptionsType, + ): Promise> { + return apiCall(url, { + ...options, + method: 'DELETE', + }); + }, + }; +} diff --git a/src/api/user.ts b/src/api/user.ts new file mode 100644 index 000000000000..b10458dbf971 --- /dev/null +++ b/src/api/user.ts @@ -0,0 +1,124 @@ +import { type APIContext } from 'astro'; +import { api } from './api.ts'; +import type { ResourceType } from '../lib/resource-progress.ts'; + +export const allowedRoadmapVisibility = ['all', 'none', 'selected'] as const; +export type AllowedRoadmapVisibility = + (typeof allowedRoadmapVisibility)[number]; + +export const allowedCustomRoadmapVisibility = [ + 'all', + 'none', + 'selected', +] as const; +export type AllowedCustomRoadmapVisibility = + (typeof allowedCustomRoadmapVisibility)[number]; + +export const allowedProfileVisibility = ['public', 'private'] as const; +export type AllowedProfileVisibility = + (typeof allowedProfileVisibility)[number]; + +export interface UserDocument { + _id?: string; + name: string; + email: string; + avatar?: string; + password: string; + isEnabled: boolean; + authProvider: 'github' | 'google' | 'email' | 'linkedin'; + metadata: Record; + calculatedStats: { + activityCount: number; + totalVisitCount: number; + longestVisitStreak: number; + currentVisitStreak: number; + updatedAt: Date; + }; + verificationCode: string; + resetPasswordCode: string; + isSyncedWithSendy: boolean; + links?: { + github?: string; + linkedin?: string; + twitter?: string; + website?: string; + }; + username?: string; + profileVisibility: AllowedProfileVisibility; + publicConfig?: { + isAvailableForHire: boolean; + isEmailVisible: boolean; + headline: string; + roadmaps: string[]; + customRoadmaps: string[]; + roadmapVisibility: AllowedRoadmapVisibility; + customRoadmapVisibility: AllowedCustomRoadmapVisibility; + }; + resetPasswordCodeAt: string; + verifiedAt: string; + createdAt: string; + updatedAt: string; +} + +export type UserActivityCount = { + activityCount: Record; + totalActivityCount: number; +}; + +type ProgressResponse = { + updatedAt: string; + title: string; + id: string; + learning: number; + skipped: number; + done: number; + total: number; + isCustomResource?: boolean; + roadmapSlug?: string; +}; + +export type GetPublicProfileResponse = Omit< + UserDocument, + 'password' | 'verificationCode' | 'resetPasswordCode' | 'resetPasswordCodeAt' +> & { + activity: UserActivityCount; + roadmaps: ProgressResponse[]; + isOwnProfile: boolean; +}; + +export type GetUserProfileRoadmapResponse = { + title: string; + topicCount: number; + roadmapSlug?: string; + isCustomResource?: boolean; + done: string[]; + learning: string[]; + skipped: string[]; + nodes: any[]; + edges: any[]; +}; + +export function userApi(context: APIContext) { + return { + getPublicProfile: async function (username: string) { + return api(context).get( + `${import.meta.env.PUBLIC_API_URL}/v1-get-public-profile/${username}`, + ); + }, + getUserProfileRoadmap: async function ( + username: string, + resourceId: string, + resourceType: ResourceType = 'roadmap', + ) { + return api(context).get( + `${ + import.meta.env.PUBLIC_API_URL + }/v1-get-user-profile-roadmap/${username}`, + { + resourceId, + resourceType, + }, + ); + }, + }; +} diff --git a/src/components/AccountSidebar.astro b/src/components/AccountSidebar.astro index 1f6b7507f3bf..9b609304e376 100644 --- a/src/components/AccountSidebar.astro +++ b/src/components/AccountSidebar.astro @@ -23,6 +23,16 @@ const sidebarLinks = [ classes: 'h-3 w-4', }, }, + { + href: '/account/update-profile', + title: 'Profile', + id: 'profile', + isNew: true, + icon: { + glyph: 'user', + classes: 'h-4 w-4', + }, + }, { href: '/account/friends', title: 'Friends', @@ -37,7 +47,7 @@ const sidebarLinks = [ href: '/account/roadmaps', title: 'Roadmaps', id: 'roadmaps', - isNew: true, + isNew: false, icon: { glyph: 'users', classes: 'h-4 w-4', @@ -54,16 +64,6 @@ const sidebarLinks = [ classes: 'h-4 w-4', }, }, - { - href: '/account/update-profile', - title: 'Profile', - id: 'profile', - isNew: false, - icon: { - glyph: 'user', - classes: 'h-4 w-4', - }, - }, { href: '/account/settings', title: 'Settings', diff --git a/src/components/Activity/ActivityPage.tsx b/src/components/Activity/ActivityPage.tsx index 354a0bdaedcd..482fd27e5cce 100644 --- a/src/components/Activity/ActivityPage.tsx +++ b/src/components/Activity/ActivityPage.tsx @@ -14,6 +14,7 @@ type ProgressResponse = { done: number; total: number; isCustomResource: boolean; + roadmapSlug?: string; }; export type ActivityResponse = { diff --git a/src/components/Activity/ResourceProgress.tsx b/src/components/Activity/ResourceProgress.tsx index 3b037d06ee68..536528f986bd 100644 --- a/src/components/Activity/ResourceProgress.tsx +++ b/src/components/Activity/ResourceProgress.tsx @@ -17,6 +17,7 @@ type ResourceProgressType = { onCleared?: () => void; showClearButton?: boolean; isCustomResource: boolean; + roadmapSlug?: string; }; export function ResourceProgress(props: ResourceProgressType) { @@ -37,6 +38,7 @@ export function ResourceProgress(props: ResourceProgressType) { doneCount, skippedCount, onCleared, + roadmapSlug, } = props; async function clearProgress() { @@ -46,7 +48,7 @@ export function ResourceProgress(props: ResourceProgressType) { { resourceId, resourceType, - } + }, ); if (error || !response) { @@ -72,7 +74,7 @@ export function ResourceProgress(props: ResourceProgressType) { : `/best-practices/${resourceId}`; if (isCustomResource) { - url = `/r?id=${resourceId}`; + url = `/r/${roadmapSlug}`; } const totalMarked = doneCount + skippedCount; diff --git a/src/components/CreateTeam/RoadmapSelector.tsx b/src/components/CreateTeam/RoadmapSelector.tsx index b3d160276ee3..c7462361f070 100644 --- a/src/components/CreateTeam/RoadmapSelector.tsx +++ b/src/components/CreateTeam/RoadmapSelector.tsx @@ -14,6 +14,7 @@ import { useToast } from '../../hooks/use-toast'; export type TeamResourceConfig = { isCustomResource: boolean; + roadmapSlug?: string; title: string; description?: string; visibility?: AllowedRoadmapVisibility; @@ -80,7 +81,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) { { resourceId: roadmapId, resourceType: 'roadmap', - } + }, ); if (error || !response) { @@ -114,7 +115,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) { resourceId: roadmapId, resourceType: 'roadmap', removed: [], - } + }, ); if (error || !response) { @@ -312,7 +313,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) { `${ import.meta.env.PUBLIC_EDITOR_APP_URL }/${resourceId}`, - '_blank' + '_blank', ); return; } @@ -335,7 +336,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) { )} ); - } + }, )} )} diff --git a/src/components/CreateVersion/CreateVersion.tsx b/src/components/CreateVersion/CreateVersion.tsx index 8fcf50554b09..075886182d7d 100644 --- a/src/components/CreateVersion/CreateVersion.tsx +++ b/src/components/CreateVersion/CreateVersion.tsx @@ -82,7 +82,7 @@ export function CreateVersion(props: CreateVersionProps) { return (
diff --git a/src/components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx b/src/components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx index b7cf3ea05da3..00be8ac5d00f 100644 --- a/src/components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx +++ b/src/components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx @@ -27,6 +27,7 @@ export interface RoadmapDocument { _id?: string; title: string; description?: string; + slug?: string; creatorId: string; teamId?: string; isDiscoverable: boolean; @@ -145,7 +146,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) { name="title" id="title" required - className="block text-black w-full rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm" + className="block w-full rounded-md border border-gray-300 px-2.5 py-2 text-black outline-none focus:border-black sm:text-sm" placeholder="Enter Title" value={title} onChange={(e) => setTitle(e.target.value)} @@ -165,8 +166,8 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) { name="description" required className={cn( - 'block text-black h-24 w-full resize-none rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm', - isInvalidDescription && 'border-red-300 bg-red-100' + 'block h-24 w-full resize-none rounded-md border border-gray-300 px-2.5 py-2 text-black outline-none focus:border-black sm:text-sm', + isInvalidDescription && 'border-red-300 bg-red-100', )} placeholder="Enter Description" value={description} diff --git a/src/components/CustomRoadmap/CustomRoadmap.tsx b/src/components/CustomRoadmap/CustomRoadmap.tsx index e8c16008c4d5..d04af93b605a 100644 --- a/src/components/CustomRoadmap/CustomRoadmap.tsx +++ b/src/components/CustomRoadmap/CustomRoadmap.tsx @@ -56,10 +56,11 @@ export function hideRoadmapLoader() { type CustomRoadmapProps = { isEmbed?: boolean; + slug?: string; }; export function CustomRoadmap(props: CustomRoadmapProps) { - const { isEmbed = false } = props; + const { isEmbed = false, slug } = props; const { id, secret } = getUrlParams() as { id: string; secret: string }; @@ -70,9 +71,11 @@ export function CustomRoadmap(props: CustomRoadmapProps) { async function getRoadmap() { setIsLoading(true); - const roadmapUrl = new URL( - `${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${id}`, - ); + const roadmapUrl = slug + ? new URL( + `${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap-by-slug/${slug}`, + ) + : new URL(`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${id}`); if (secret) { roadmapUrl.searchParams.set('secret', secret); diff --git a/src/components/CustomRoadmap/PersonalRoadmapList.tsx b/src/components/CustomRoadmap/PersonalRoadmapList.tsx index 7e863abbb126..c50b780de0a9 100644 --- a/src/components/CustomRoadmap/PersonalRoadmapList.tsx +++ b/src/components/CustomRoadmap/PersonalRoadmapList.tsx @@ -18,7 +18,7 @@ import { PersonalRoadmapActionDropdown } from './PersonalRoadmapActionDropdown'; import type { GetRoadmapListResponse } from './RoadmapListPage'; import { useState, type Dispatch, type SetStateAction } from 'react'; import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal'; -import {RoadmapIcon} from "../ReactIcons/RoadmapIcon.tsx"; +import { RoadmapIcon } from '../ReactIcons/RoadmapIcon.tsx'; type PersonalRoadmapListType = { roadmaps: GetRoadmapListResponse['personalRoadmaps']; @@ -37,7 +37,7 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) { async function deleteRoadmap(roadmapId: string) { const { response, error } = await httpDelete( - `${import.meta.env.PUBLIC_API_URL}/v1-delete-roadmap/${roadmapId}` + `${import.meta.env.PUBLIC_API_URL}/v1-delete-roadmap/${roadmapId}`, ); if (error || !response) { @@ -61,6 +61,7 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) { const shareSettingsModal = selectedRoadmap && ( Promise; setSelectedRoadmap: ( - roadmap: GetRoadmapListResponse['personalRoadmaps'][number] | null + roadmap: GetRoadmapListResponse['personalRoadmaps'][number] | null, ) => void; }; @@ -183,9 +184,9 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) { Edit diff --git a/src/components/CustomRoadmap/ResourceProgressStats.tsx b/src/components/CustomRoadmap/ResourceProgressStats.tsx index f9a4bfa43d22..9ebe21a6bb75 100644 --- a/src/components/CustomRoadmap/ResourceProgressStats.tsx +++ b/src/components/CustomRoadmap/ResourceProgressStats.tsx @@ -24,6 +24,7 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) { <> {isSharing && $canManageCurrentRoadmap && $currentRoadmap && (

setIsSharingWithOthers(false)} @@ -135,7 +137,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {

@@ -144,6 +146,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) { <> {isSharing && $currentRoadmap && ( diff --git a/src/components/FeaturedGuides.astro b/src/components/FeaturedGuides.astro index a9c5ec032d84..704850adf402 100644 --- a/src/components/FeaturedGuides.astro +++ b/src/components/FeaturedGuides.astro @@ -11,7 +11,7 @@ const { heading, guides } = Astro.props; ---
-

{heading}

+

{heading}

{guides.map((guide) => )} diff --git a/src/components/FeaturedVideos.astro b/src/components/FeaturedVideos.astro index 267e9b217865..dbd70b7923ee 100644 --- a/src/components/FeaturedVideos.astro +++ b/src/components/FeaturedVideos.astro @@ -11,7 +11,7 @@ const { heading, videos } = Astro.props; ---
-

{heading}

+

{heading}

{videos.map((video) => )} diff --git a/src/components/GenerateRoadmap/AITermSuggestionInput.tsx b/src/components/GenerateRoadmap/AITermSuggestionInput.tsx index f6670e264b0a..1d183534a007 100644 --- a/src/components/GenerateRoadmap/AITermSuggestionInput.tsx +++ b/src/components/GenerateRoadmap/AITermSuggestionInput.tsx @@ -69,6 +69,10 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { return []; } + if (trimmedValue.length < 3) { + return []; + } + if (termCache.has(trimmedValue)) { const cachedData = termCache.get(trimmedValue); return cachedData || []; diff --git a/src/components/HeroSection/FavoriteRoadmaps.tsx b/src/components/HeroSection/FavoriteRoadmaps.tsx index 73dced17b8e6..a639be6262ff 100644 --- a/src/components/HeroSection/FavoriteRoadmaps.tsx +++ b/src/components/HeroSection/FavoriteRoadmaps.tsx @@ -16,6 +16,7 @@ export type UserProgressResponse = { total: number; updatedAt: Date; isCustomResource: boolean; + roadmapSlug?: string; team?: { name: string; id: string; @@ -41,7 +42,7 @@ function renderProgress(progressList: UserProgressResponse) { resourceType: progress.resourceType, isFavorite: progress.isFavorite, }, - }) + }), ); const totalDone = progress.done + progress.skipped; @@ -89,7 +90,7 @@ export function FavoriteRoadmaps() { setIsLoading(true); const { response: progressList, error } = await httpGet( - `${import.meta.env.PUBLIC_API_URL}/v1-get-hero-roadmaps` + `${import.meta.env.PUBLIC_API_URL}/v1-get-hero-roadmaps`, ); if (error || !progressList) { @@ -121,7 +122,7 @@ export function FavoriteRoadmaps() { const hasProgress = progress?.length > 0; const customRoadmaps = progress?.filter( - (p) => p.isCustomResource && !p.team?.name + (p) => p.isCustomResource && !p.team?.name, ); const defaultRoadmaps = progress?.filter((p) => !p.isCustomResource); const teamRoadmaps: HeroTeamRoadmaps = progress diff --git a/src/components/HeroSection/HeroRoadmaps.tsx b/src/components/HeroSection/HeroRoadmaps.tsx index 8f92e4a76b7e..02662102f373 100644 --- a/src/components/HeroSection/HeroRoadmaps.tsx +++ b/src/components/HeroSection/HeroRoadmaps.tsx @@ -172,7 +172,7 @@ export function HeroRoadmaps(props: ProgressListProps) { customRoadmap.total) * 100 } - url={`/r?id=${customRoadmap.resourceId}`} + url={`/r/${customRoadmap?.roadmapSlug}`} allowFavorite={false} /> ); @@ -242,7 +242,7 @@ export function HeroRoadmaps(props: ProgressListProps) { customRoadmap.total) * 100 } - url={`/r?id=${customRoadmap.resourceId}`} + url={`/r/${customRoadmap?.roadmapSlug}`} allowFavorite={false} /> ); diff --git a/src/components/Navigation/AccountDropdownList.tsx b/src/components/Navigation/AccountDropdownList.tsx index 395b6733944e..562c096c95fb 100644 --- a/src/components/Navigation/AccountDropdownList.tsx +++ b/src/components/Navigation/AccountDropdownList.tsx @@ -1,4 +1,12 @@ -import { ChevronRight, LogOut, Map, Plus, User2, Users2 } from 'lucide-react'; +import { + ChevronRight, + LogOut, + Map, + Plus, + SquareUserRound, + User2, + Users2, +} from 'lucide-react'; import { logout } from './navigation'; import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx'; import { useState } from 'react'; @@ -10,7 +18,6 @@ type AccountDropdownListProps = { export function AccountDropdownList(props: AccountDropdownListProps) { const { setIsTeamsOpen, onCreateRoadmap } = props; - const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false); return (
diff --git a/src/components/UpdateProfile/UpdatePublicProfileForm.tsx b/src/components/UpdateProfile/UpdatePublicProfileForm.tsx new file mode 100644 index 000000000000..155c23775799 --- /dev/null +++ b/src/components/UpdateProfile/UpdatePublicProfileForm.tsx @@ -0,0 +1,578 @@ +import { type FormEvent, useEffect, useState } from 'react'; +import { httpGet, httpPatch } from '../../lib/http'; +import { pageProgressMessage } from '../../stores/page'; +import type { + AllowedCustomRoadmapVisibility, + AllowedProfileVisibility, + AllowedRoadmapVisibility, + UserDocument, +} from '../../api/user'; +import { SelectionButton } from '../RoadCard/SelectionButton'; +import { + ArrowUpRight, Check, + CheckCircle, + Copy, + Eye, + EyeOff, + FileBadge, + Trophy, +} from 'lucide-react'; +import { useToast } from '../../hooks/use-toast'; +import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx'; +import { VisibilityDropdown } from './VisibilityDropdown.tsx'; +import { ProfileUsername } from './ProfileUsername.tsx'; +import UploadProfilePicture from './UploadProfilePicture.tsx'; +import { SkillProfileAlert } from './SkillProfileAlert.tsx'; +import { useCopyText } from '../../hooks/use-copy-text.ts'; +import { cn } from '../../lib/classname.ts'; + +type RoadmapType = { + id: string; + title: string; + isCustomResource: boolean; +}; + +type GetProfileSettingsResponse = Pick< + UserDocument, + 'username' | 'profileVisibility' | 'publicConfig' | 'links' +>; + +export function UpdatePublicProfileForm() { + const [profileVisibility, setProfileVisibility] = + useState('public'); + + const toast = useToast(); + + const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false); + const [publicProfileUrl, setPublicProfileUrl] = useState(''); + const [isAvailableForHire, setIsAvailableForHire] = useState(false); + const [isEmailVisible, setIsEmailVisible] = useState(true); + const [headline, setHeadline] = useState(''); + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [roadmapVisibility, setRoadmapVisibility] = + useState('all'); + const [customRoadmapVisibility, setCustomRoadmapVisibility] = + useState('all'); + const [roadmaps, setRoadmaps] = useState([]); + const [customRoadmaps, setCustomRoadmaps] = useState([]); + + const [currentUsername, setCurrentUsername] = useState(''); + const [name, setName] = useState(''); + + const [avatar, setAvatar] = useState(''); + const [github, setGithub] = useState(''); + const [twitter, setTwitter] = useState(''); + const [linkedin, setLinkedin] = useState(''); + const [website, setWebsite] = useState(''); + + const [profileRoadmaps, setProfileRoadmaps] = useState([]); + + const [isLoading, setIsLoading] = useState(false); + + const { isCopied, copyText } = useCopyText(); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setIsLoading(true); + + const { response, error } = await httpPatch( + `${import.meta.env.PUBLIC_API_URL}/v1-update-public-profile-config`, + { + isAvailableForHire, + isEmailVisible, + profileVisibility, + headline, + username, + roadmapVisibility, + customRoadmapVisibility, + roadmaps, + customRoadmaps, + github, + twitter, + linkedin, + website, + name, + email, + }, + ); + + if (error || !response) { + setIsLoading(false); + toast.error(error?.message || 'Something went wrong'); + + return; + } + + await loadProfileSettings(); + toast.success('Profile updated successfully'); + }; + + const loadProfileSettings = async () => { + setIsLoading(true); + + const { error, response } = await httpGet( + `${import.meta.env.PUBLIC_API_URL}/v1-get-profile-settings`, + ); + + if (error || !response) { + setIsLoading(false); + toast.error(error?.message || 'Something went wrong'); + + return; + } + + const { + name, + email, + links, + username, + profileVisibility: defaultProfileVisibility, + publicConfig, + avatar, + } = response; + + setAvatar(avatar || ''); + setPublicProfileUrl(username ? `/u/${username}` : ''); + setUsername(username || ''); + setCurrentUsername(username || ''); + setName(name || ''); + setEmail(email || ''); + setGithub(links?.github || ''); + setTwitter(links?.twitter || ''); + setLinkedin(links?.linkedin || ''); + setWebsite(links?.website || ''); + setProfileVisibility(defaultProfileVisibility || 'public'); + setHeadline(publicConfig?.headline || ''); + setRoadmapVisibility(publicConfig?.roadmapVisibility || 'all'); + setCustomRoadmapVisibility(publicConfig?.customRoadmapVisibility || 'all'); + setCustomRoadmaps(publicConfig?.customRoadmaps || []); + setRoadmaps(publicConfig?.roadmaps || []); + setIsAvailableForHire(publicConfig?.isAvailableForHire || false); + setIsEmailVisible(publicConfig?.isEmailVisible ?? true); + + setIsLoading(false); + }; + + const loadProfileRoadmaps = async () => { + setIsLoading(true); + + const { error, response } = await httpGet<{ + roadmaps: RoadmapType[]; + }>(`${import.meta.env.PUBLIC_API_URL}/v1-get-profile-roadmaps`); + + if (error || !response) { + setIsLoading(false); + toast.error(error?.message || 'Something went wrong'); + + return; + } + + setProfileRoadmaps(response?.roadmaps || []); + setIsLoading(false); + }; + + // Make a request to the backend to fill in the form with the current values + useEffect(() => { + Promise.all([loadProfileSettings(), loadProfileRoadmaps()]).finally(() => { + pageProgressMessage.set(''); + }); + }, []); + + const publicCustomRoadmaps = profileRoadmaps.filter( + (r) => r.isCustomResource, + ); + const publicRoadmaps = profileRoadmaps.filter((r) => !r.isCustomResource); + + return ( +
+ {isCreatingRoadmap && ( + setIsCreatingRoadmap(false)} /> + )} + + + +
+
+

Skill Profile

+ {publicProfileUrl && ( + <> + + + Visit + + + + )} +
+ +
+

+ Create your skill profile to showcase your skills. +

+ + + +
+
+ + setName((e.target as HTMLInputElement).value)} + /> +
+ +
+ + +
+
+ setIsEmailVisible(e.target.checked)} + /> + +
+
+
+ +
+ + setHeadline((e.target as HTMLInputElement).value)} + required={profileVisibility === 'public'} + /> +
+ + + +
+

+ Which roadmap progresses do you want to show on your profile? +

+
+ { + setRoadmapVisibility('all'); + setRoadmaps([]); + }} + /> + { + setRoadmapVisibility('none'); + setRoadmaps([]); + }} + /> +
+ +

+ Or select the roadmaps you want to show +

+ {publicRoadmaps.length > 0 ? ( +
+ {publicRoadmaps.map((r) => ( + { + if (roadmapVisibility !== 'selected') { + setRoadmapVisibility('selected'); + } + + if (roadmaps.includes(r.id)) { + setRoadmaps(roadmaps.filter((id) => id !== r.id)); + } else { + setRoadmaps([...roadmaps, r.id]); + } + }} + /> + ))} +
+ ) : ( +

+ Update{' '} + + your progress on roadmaps + {' '} + to show your learning activity. +

+ )} +
+ +
+

+ Pick your custom roadmaps to show on your profile +

+
+ { + setCustomRoadmapVisibility('all'); + setCustomRoadmaps([]); + }} + /> + { + setCustomRoadmapVisibility('none'); + setCustomRoadmaps([]); + }} + /> +
+ +

+ Or select the custom roadmaps you want to show +

+ {publicCustomRoadmaps.length > 0 ? ( +
+ {publicCustomRoadmaps.map((r) => ( + { + if (customRoadmapVisibility !== 'selected') { + setCustomRoadmapVisibility('selected'); + } + + if (customRoadmaps.includes(r.id)) { + setCustomRoadmaps( + customRoadmaps.filter((id) => id !== r.id), + ); + } else { + setCustomRoadmaps([...customRoadmaps, r.id]); + } + }} + /> + ))} +
+ ) : ( +

+ You do not have any custom roadmaps.{' '} + + . +

+ )} +
+ +
+ + setGithub((e.target as HTMLInputElement).value)} + /> +
+
+ + setTwitter((e.target as HTMLInputElement).value)} + /> +
+ +
+ + setLinkedin((e.target as HTMLInputElement).value)} + /> +
+ +
+ + setWebsite((e.target as HTMLInputElement).value)} + /> +
+ +
+
+ setIsAvailableForHire(e.target.checked)} + /> + +
+
+ + + +
+ ); +} diff --git a/src/components/UpdateProfile/VisibilityDropdown.tsx b/src/components/UpdateProfile/VisibilityDropdown.tsx new file mode 100644 index 000000000000..6e3354b9fd9d --- /dev/null +++ b/src/components/UpdateProfile/VisibilityDropdown.tsx @@ -0,0 +1,99 @@ +import { ChevronDown, Globe, LockIcon } from 'lucide-react'; +import { type AllowedProfileVisibility } from '../../api/user.ts'; +import { pageProgressMessage } from '../../stores/page.ts'; +import { httpPatch } from '../../lib/http.ts'; +import { useToast } from '../../hooks/use-toast.ts'; +import { useRef, useState } from 'react'; +import { useOutsideClick } from '../../hooks/use-outside-click.ts'; +import { cn } from '../../lib/classname.ts'; + +type VisibilityDropdownProps = { + visibility: AllowedProfileVisibility; + setVisibility: (visibility: AllowedProfileVisibility) => void; +}; + +export function VisibilityDropdown(props: VisibilityDropdownProps) { + const { visibility, setVisibility } = props; + const toast = useToast(); + const dropdownRef = useRef(null); + + useOutsideClick(dropdownRef, () => { + setIsVisibilityDropdownOpen(false); + }); + + const [isVisibilityDropdownOpen, setIsVisibilityDropdownOpen] = + useState(false); + + async function updateProfileVisibility(visibility: AllowedProfileVisibility) { + pageProgressMessage.set('Updating profile visibility'); + setIsVisibilityDropdownOpen(false); + + const { error } = await httpPatch( + `${import.meta.env.PUBLIC_API_URL}/v1-update-public-profile-visibility`, + { + profileVisibility: visibility, + }, + ); + + if (error) { + toast.error(error.message || 'Something went wrong'); + + return; + } + + pageProgressMessage.set(''); + setVisibility(visibility); + } + + return ( +
+ + {isVisibilityDropdownOpen && ( +
+ + +
+ )} +
+ ); +} diff --git a/src/components/UserPublicProfile/PrivateProfileBanner.tsx b/src/components/UserPublicProfile/PrivateProfileBanner.tsx new file mode 100644 index 000000000000..f47cdcc81ee8 --- /dev/null +++ b/src/components/UserPublicProfile/PrivateProfileBanner.tsx @@ -0,0 +1,22 @@ +import type { GetPublicProfileResponse } from '../../api/user'; +import { Lock } from 'lucide-react'; + +type PrivateProfileBannerProps = Pick< + GetPublicProfileResponse, + 'isOwnProfile' | 'profileVisibility' +>; + +export function PrivateProfileBanner(props: PrivateProfileBannerProps) { + const { isOwnProfile, profileVisibility } = props; + + if (isOwnProfile && profileVisibility === 'private') { + return ( +
+ + Your profile is private. Only you can see this page. +
+ ); + } + + return null; +} diff --git a/src/components/UserPublicProfile/UserProfileRoadmap.tsx b/src/components/UserPublicProfile/UserProfileRoadmap.tsx new file mode 100644 index 000000000000..0d42ba79d78c --- /dev/null +++ b/src/components/UserPublicProfile/UserProfileRoadmap.tsx @@ -0,0 +1,109 @@ +import type { + GetUserProfileRoadmapResponse, + GetPublicProfileResponse, +} from '../../api/user'; +import { getPercentage } from '../../helper/number'; +import { PrivateProfileBanner } from './PrivateProfileBanner'; +import { UserProfileRoadmapRenderer } from './UserProfileRoadmapRenderer'; + +type UserProfileRoadmapProps = GetUserProfileRoadmapResponse & + Pick< + GetPublicProfileResponse, + 'username' | 'name' | 'isOwnProfile' | 'profileVisibility' + > & { + resourceId: string; + }; + +export function UserProfileRoadmap(props: UserProfileRoadmapProps) { + const { + username, + name, + title, + resourceId, + isCustomResource, + done = [], + skipped = [], + learning = [], + topicCount, + isOwnProfile, + profileVisibility, + } = props; + + const trackProgressRoadmapUrl = isCustomResource + ? `/r/${resourceId}` + : `/${resourceId}`; + + const totalMarked = done.length + skipped.length; + const progressPercentage = getPercentage(totalMarked, topicCount); + + return ( + <> + +
+ + +

{title}

+

+ Skills {name} has mastered on the {title?.toLowerCase()}. +

+
+ +
+

+ + {progressPercentage}% Done + + + + + {done.length} completed + + · + + {learning.length} in progress + + · + + {skipped.length} skipped + + · + + {topicCount} Total + + + + {totalMarked} of {topicCount} Done + +

+
+ + + + ); +} diff --git a/src/components/UserPublicProfile/UserProfileRoadmapRenderer.tsx b/src/components/UserPublicProfile/UserProfileRoadmapRenderer.tsx new file mode 100644 index 000000000000..aa6103f83289 --- /dev/null +++ b/src/components/UserPublicProfile/UserProfileRoadmapRenderer.tsx @@ -0,0 +1,146 @@ +import { useEffect, useRef, useState, type RefObject } from 'react'; +import '../FrameRenderer/FrameRenderer.css'; +import { Spinner } from '../ReactIcons/Spinner'; +import { + renderTopicProgress, + topicSelectorAll, +} from '../../lib/resource-progress'; +import { useToast } from '../../hooks/use-toast'; +import { replaceChildren } from '../../lib/dom.ts'; +import type { GetUserProfileRoadmapResponse } from '../../api/user.ts'; +import { ReadonlyEditor } from '../../../editor/readonly-editor.tsx'; +import { cn } from '../../lib/classname.ts'; + +export type UserProfileRoadmapRendererProps = GetUserProfileRoadmapResponse & { + resourceId: string; + resourceType: 'roadmap' | 'best-practice'; +}; + +export function UserProfileRoadmapRenderer( + props: UserProfileRoadmapRendererProps, +) { + const { + resourceId, + resourceType, + done, + skipped, + learning, + edges, + nodes, + isCustomResource, + } = props; + + const containerEl = useRef(null); + + const [isLoading, setIsLoading] = useState(!isCustomResource); + const toast = useToast(); + + let resourceJsonUrl = 'https://roadmap.sh'; + if (resourceType === 'roadmap') { + resourceJsonUrl += `/${resourceId}.json`; + } else { + resourceJsonUrl += `/best-practices/${resourceId}.json`; + } + + async function renderResource(jsonUrl: string) { + const res = await fetch(jsonUrl, {}); + const json = await res.json(); + const { wireframeJSONToSVG } = await import('roadmap-renderer'); + const svg: SVGElement | null = await wireframeJSONToSVG(json, { + fontURL: '/fonts/balsamiq.woff2', + }); + + replaceChildren(containerEl.current!, svg); + } + + useEffect(() => { + if ( + !containerEl.current || + !resourceJsonUrl || + !resourceId || + !resourceType || + isCustomResource + ) { + return; + } + + setIsLoading(true); + renderResource(resourceJsonUrl) + .then(() => { + done.forEach((id: string) => renderTopicProgress(id, 'done')); + learning.forEach((id: string) => renderTopicProgress(id, 'learning')); + skipped.forEach((id: string) => renderTopicProgress(id, 'skipped')); + setIsLoading(false); + }) + .catch((err) => { + console.error(err); + toast.error(err?.message || 'Something went wrong. Please try again!'); + }) + .finally(() => { + setIsLoading(false); + }); + }, []); + + return ( +
+
+ {isCustomResource ? ( + ) => { + done?.forEach((topicId: string) => { + topicSelectorAll(topicId, wrapperRef?.current!).forEach( + (el) => { + el.classList.add('done'); + }, + ); + }); + + learning?.forEach((topicId: string) => { + topicSelectorAll(topicId, wrapperRef?.current!).forEach( + (el) => { + el.classList.add('learning'); + }, + ); + }); + + skipped?.forEach((topicId: string) => { + topicSelectorAll(topicId, wrapperRef?.current!).forEach( + (el) => { + el.classList.add('skipped'); + }, + ); + }); + }} + fontFamily="Balsamiq Sans" + fontURL="/fonts/balsamiq.woff2" + /> + ) : ( +
+ )} + + {isLoading && ( +
+ +
+ )} +
+
+ ); +} diff --git a/src/components/UserPublicProfile/UserPublicActivityHeatmap.tsx b/src/components/UserPublicProfile/UserPublicActivityHeatmap.tsx new file mode 100644 index 000000000000..80be3cbaaeb5 --- /dev/null +++ b/src/components/UserPublicProfile/UserPublicActivityHeatmap.tsx @@ -0,0 +1,110 @@ +import CalendarHeatmap from 'react-calendar-heatmap'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; +import 'react-calendar-heatmap/dist/styles.css'; +import 'react-tooltip/dist/react-tooltip.css'; +import { formatActivityDate, formatMonthDate } from '../../lib/date'; +import type { UserActivityCount } from '../../api/user'; +import dayjs from 'dayjs'; + +type UserActivityHeatmapProps = { + activity: UserActivityCount; + joinedAt: string; +}; + +const legends = [ + { count: '1-2', color: 'bg-gray-200' }, + { count: '3-4', color: 'bg-gray-300' }, + { count: '5-9', color: 'bg-gray-500' }, + { count: '10-19', color: 'bg-gray-600' }, + { count: '20+', color: 'bg-gray-800' }, +]; + +export function UserActivityHeatmap(props: UserActivityHeatmapProps) { + const { activity } = props; + const data = Object.entries(activity.activityCount).map(([date, count]) => ({ + date, + count, + })); + + const startDate = dayjs().subtract(1, 'year').toDate(); + const endDate = dayjs().toDate(); + + return ( +
+
+
+

Activity

+

+ Progress updates over the past year +

+
+ + Member since: {formatMonthDate(props.joinedAt)} + +
+ { + if (!value) { + return 'fill-gray-100 rounded-md [rx:2px] focus:outline-none'; + } + + const { count } = value; + if (count >= 20) { + return 'fill-gray-800 rounded-md [rx:2px] focus:outline-none'; + } else if (count >= 10) { + return 'fill-gray-600 rounded-md [rx:2px] focus:outline-none'; + } else if (count >= 5) { + return 'fill-gray-500 rounded-md [rx:2px] focus:outline-none'; + } else if (count >= 3) { + return 'fill-gray-300 rounded-md [rx:2px] focus:outline-none'; + } else { + return 'fill-gray-200 rounded-md [rx:2px] focus:outline-none'; + } + }} + tooltipDataAttrs={(value: any) => { + if (!value || !value.date) { + return null; + } + + const formattedDate = formatActivityDate(value.date); + return { + 'data-tooltip-id': 'user-activity-tip', + 'data-tooltip-content': `${value.count} Updates - ${formattedDate}`, + }; + }} + /> + + + +
+ + Number of topics marked as learning, or completed by day + +
+ Less + {legends.map((legend) => ( +
+
+
+ ))} + More + +
+
+
+ ); +} diff --git a/src/components/UserPublicProfile/UserPublicProfileHeader.tsx b/src/components/UserPublicProfile/UserPublicProfileHeader.tsx new file mode 100644 index 000000000000..769e310af470 --- /dev/null +++ b/src/components/UserPublicProfile/UserPublicProfileHeader.tsx @@ -0,0 +1,65 @@ +import { Github, Globe, LinkedinIcon, Mail, Twitter } from 'lucide-react'; +import type { GetPublicProfileResponse } from '../../api/user'; + +type UserPublicProfileHeaderProps = { + userDetails: GetPublicProfileResponse; +}; + +export function UserPublicProfileHeader(props: UserPublicProfileHeaderProps) { + const { userDetails } = props; + + const { name, links, publicConfig, avatar, email } = userDetails; + const { headline, isAvailableForHire, isEmailVisible } = publicConfig!; + + return ( +
+ {name} + +
+ {isAvailableForHire && ( + + Available for hire + + )} +

{name}

+

{headline}

+
+ {links?.github && } + {links?.linkedin && ( + + )} + {links?.twitter && } + {links?.website && } + {isEmailVisible && } +
+
+
+ ); +} + +type UserLinkProps = { + href: string; + icon: typeof Github; +}; + +export function UserLink(props: UserLinkProps) { + const { href, icon: Icon } = props; + + return ( + + + + ); +} diff --git a/src/components/UserPublicProfile/UserPublicProfilePage.tsx b/src/components/UserPublicProfile/UserPublicProfilePage.tsx new file mode 100644 index 000000000000..d7c8342bbbbe --- /dev/null +++ b/src/components/UserPublicProfile/UserPublicProfilePage.tsx @@ -0,0 +1,39 @@ +import type { GetPublicProfileResponse } from '../../api/user'; +import { PrivateProfileBanner } from './PrivateProfileBanner'; +import { UserActivityHeatmap } from './UserPublicActivityHeatmap'; +import { UserPublicProfileHeader } from './UserPublicProfileHeader'; +import { UserPublicProgresses } from './UserPublicProgresses'; + +type UserPublicProfilePageProps = GetPublicProfileResponse; + +export function UserPublicProfilePage(props: UserPublicProfilePageProps) { + const { + activity, + username, + isOwnProfile, + profileVisibility, + _id: userId, + createdAt, + } = props; + + return ( +
+
+ + + + + + +
+
+ ); +} diff --git a/src/components/UserPublicProfile/UserPublicProgressStats.tsx b/src/components/UserPublicProfile/UserPublicProgressStats.tsx new file mode 100644 index 000000000000..9b8fc85f5a26 --- /dev/null +++ b/src/components/UserPublicProfile/UserPublicProgressStats.tsx @@ -0,0 +1,70 @@ +import { getPercentage } from '../../helper/number'; +import { getRelativeTimeString } from '../../lib/date'; + +type UserPublicProgressStats = { + resourceType: 'roadmap'; + resourceId: string; + title: string; + updatedAt: string; + totalCount: number; + doneCount: number; + learningCount: number; + skippedCount: number; + showClearButton?: boolean; + isCustomResource?: boolean; + roadmapSlug?: string; + username: string; + userId: string; +}; + +export function UserPublicProgressStats(props: UserPublicProgressStats) { + const { + updatedAt, + resourceId, + title, + totalCount, + learningCount, + doneCount, + skippedCount, + roadmapSlug, + isCustomResource = false, + username, + userId, + } = props; + + // Currently we only support roadmap not (best-practices) + const url = isCustomResource + ? `/r/${roadmapSlug}` + : `/${resourceId}?s=${userId}`; + const totalMarked = doneCount + skippedCount; + const progressPercentage = getPercentage(totalMarked, totalCount); + + return ( + +

+ {title} +

+
+
+
+ +
+ + {progressPercentage}% completed + + + Last updated {getRelativeTimeString(updatedAt)} + +
+
+ ); +} diff --git a/src/components/UserPublicProfile/UserPublicProgresses.tsx b/src/components/UserPublicProfile/UserPublicProgresses.tsx new file mode 100644 index 000000000000..ce575627071e --- /dev/null +++ b/src/components/UserPublicProfile/UserPublicProgresses.tsx @@ -0,0 +1,112 @@ +import type { GetPublicProfileResponse } from '../../api/user'; +import { UserPublicProgressStats } from './UserPublicProgressStats'; +import { getPercentage } from '../../helper/number.ts'; + +type UserPublicProgressesProps = { + userId: string; + username: string; + roadmaps: GetPublicProfileResponse['roadmaps']; + publicConfig: GetPublicProfileResponse['publicConfig']; +}; + +export function UserPublicProgresses(props: UserPublicProgressesProps) { + const { + roadmaps: roadmapProgresses = [], + username, + publicConfig, + userId, + } = props; + const { roadmapVisibility, customRoadmapVisibility } = publicConfig! || {}; + + const roadmaps = roadmapProgresses.filter( + (roadmap) => !roadmap.isCustomResource, + ); + const customRoadmaps = roadmapProgresses.filter( + (roadmap) => roadmap.isCustomResource, + ); + + // + + return ( +
+ {customRoadmapVisibility !== 'none' && customRoadmaps?.length > 0 && ( +
+

+ Roadmaps made by me +

+
+ {customRoadmaps.map((roadmap, counter) => { + const doneCount = roadmap.done; + const skippedCount = roadmap.skipped; + const totalCount = roadmap.total; + + const totalMarked = doneCount + skippedCount; + const progressPercentage = getPercentage(totalMarked, totalCount); + + return ( + + {roadmap.title} + + ); + })} +
+
+ )} + + {roadmapVisibility !== 'none' && roadmaps.length > 0 && ( + <> +

+ Skills I have mastered +

+
+ {roadmaps.map((roadmap, counter) => { + const percentageDone = getPercentage( + roadmap.done + roadmap.skipped, + roadmap.total, + ); + + return ( + + {roadmap.title} + + {parseInt(percentageDone, 10)}% + + + + + ); + })} +
+ + )} +
+ ); +} diff --git a/src/data/question-groups/react/react.md b/src/data/question-groups/react/react.md index 71d28e6d9cc6..4a3cb4e33962 100644 --- a/src/data/question-groups/react/react.md +++ b/src/data/question-groups/react/react.md @@ -210,7 +210,7 @@ questions: - 'Intermediate' - question: What are Server Components in React? answer: | - Server Components in allow developers to write components that render on the server instead of the client. Unlike traditional components, Server Components do not have a client-side runtime, meaning they result in a smaller bundle size and faster loads. They can seamlessly integrate with client components and can fetch data directly from the backend without the need for an API layer. This enables developers to build rich, interactive apps with less client-side code, improving performance and developer experience. + Server Components allow developers to write components that render on the server instead of the client. Unlike traditional components, Server Components do not have a client-side runtime, meaning they result in a smaller bundle size and faster loads. They can seamlessly integrate with client components and can fetch data directly from the backend without the need for an API layer. This enables developers to build rich, interactive apps with less client-side code, improving performance and developer experience. topics: - 'SSR' - 'Intermediate' diff --git a/src/data/roadmaps/devops/content/101-operating-systems/linux/100-ubuntu.md b/src/data/roadmaps/devops/content/101-operating-systems/linux/100-ubuntu.md index 2a8af73a218b..b1ef814b2612 100644 --- a/src/data/roadmaps/devops/content/101-operating-systems/linux/100-ubuntu.md +++ b/src/data/roadmaps/devops/content/101-operating-systems/linux/100-ubuntu.md @@ -11,6 +11,6 @@ Visit the following resources to learn more: - [Learn the ways of Linux-fu, for free](https://linuxjourney.com/) - [Linux Operating System - Crash Course for Beginners](https://www.youtube.com/watch?v=ROjZy1WbCIA) - [The Linux Command Line by William Shotts](https://linuxcommand.org/tlcl.php) -- [r/linuxupskillchallenge](https://www.reddit.com/r/linuxupskillchallenge/) +- [Linux Upskill Challenge](https://linuxupskillchallenge.org/) - [Introduction to Linux - Full Course for Beginners](https://www.youtube.com/watch?v=sWbUDq4S6Y8&pp=ygUTVWJ1bnR1IGNyYXNoIGNvdXJzZQ%3D%3D) -- [Linux Fundamentals](https://academy.hackthebox.com/course/preview/linux-fundamentals) \ No newline at end of file +- [Linux Fundamentals](https://academy.hackthebox.com/course/preview/linux-fundamentals) diff --git a/src/data/roadmaps/docker/content/104-data-persistence/100-ephemeral-container-fs.md b/src/data/roadmaps/docker/content/104-data-persistence/100-ephemeral-container-fs.md index 54aa8a2317ff..639e45e89537 100644 --- a/src/data/roadmaps/docker/content/104-data-persistence/100-ephemeral-container-fs.md +++ b/src/data/roadmaps/docker/content/104-data-persistence/100-ephemeral-container-fs.md @@ -6,7 +6,7 @@ This temporary or short-lived storage is called the "ephemeral container file sy ### Ephemeral FS and Data Persistence -As any data stored within the container's ephemeral FS is lost when the container is stopped or removed, it poses a challenge to data persistence in applications. This is especially problematic for applications like databases, which require data to be persisted across multiple container life cycles. +As any data stored within the container's ephemeral FS is lost when the container is stopped and removed, it poses a challenge to data persistence in applications. This is especially problematic for applications like databases, which require data to be persisted across multiple container life cycles. To overcome these challenges, Docker provides several methods for data persistence, such as: @@ -14,4 +14,4 @@ To overcome these challenges, Docker provides several methods for data persisten - **Bind mounts**: Mapping a host machine's directory or file into a container, effectively sharing host's storage with the container. - **tmpfs mounts**: In-memory storage, useful for cases where just the persistence of data within the life-cycle of the container is required. -By implementing these strategies, Docker ensures that application data can be preserved beyond the life-cycle of a single container, making it possible to work with stateful applications. \ No newline at end of file +By implementing these strategies, Docker ensures that application data can be preserved beyond the life-cycle of a single container, making it possible to work with stateful applications. diff --git a/src/data/roadmaps/frontend/content/117-progressive-web-apps/112-lighthouse.md b/src/data/roadmaps/frontend/content/117-progressive-web-apps/112-lighthouse.md index d797988b3244..94ea7fe97907 100644 --- a/src/data/roadmaps/frontend/content/117-progressive-web-apps/112-lighthouse.md +++ b/src/data/roadmaps/frontend/content/117-progressive-web-apps/112-lighthouse.md @@ -9,4 +9,4 @@ Lighthouse provides a comprehensive and easy-to-use tool for identifying and fix Visit the following resources to learn more: - [Lighthouse - Google Developers](https://developers.google.com/web/tools/lighthouse) -- [Improving Load Performance - Chrome DevTools 101](https://www.youtube.com/watch?v=5flw5q5odie) \ No newline at end of file +- [Improving Load Performance - Chrome DevTools 101](https://www.youtube.com/watch?v=5fLW5Q5ODiE) diff --git a/src/data/roadmaps/frontend/faqs.astro b/src/data/roadmaps/frontend/faqs.astro index 54a5163fa0d9..f8ddca8c1050 100644 --- a/src/data/roadmaps/frontend/faqs.astro +++ b/src/data/roadmaps/frontend/faqs.astro @@ -1,5 +1,5 @@ --- -import type { FAQType } from '../../components/FAQs/FAQs.astro'; +import type { FAQType } from '../../../components/FAQs/FAQs.astro'; export const faqs: FAQType[] = [ { diff --git a/src/data/roadmaps/system-design/content/116-performance-antipatterns/102-chatty-io.md b/src/data/roadmaps/system-design/content/116-performance-antipatterns/102-chatty-io.md index 9ad1eef27e5f..442fa0aeb941 100644 --- a/src/data/roadmaps/system-design/content/116-performance-antipatterns/102-chatty-io.md +++ b/src/data/roadmaps/system-design/content/116-performance-antipatterns/102-chatty-io.md @@ -1,4 +1,4 @@ -# Chat I/O +# Chatty I/O The cumulative effect of a large number of I/O requests can have a significant impact on performance and responsiveness. diff --git a/src/helper/number.ts b/src/helper/number.ts new file mode 100644 index 000000000000..fc67f117a989 --- /dev/null +++ b/src/helper/number.ts @@ -0,0 +1,9 @@ +export function getPercentage(portion: number, total: number): string { + if (total <= 0 || portion <= 0) { + return '0'; + } else if (portion > total) { + return '100'; + } + + return ((portion / total) * 100).toFixed(2); +} diff --git a/src/lib/best-practice-topic.ts b/src/lib/best-practice-topic.ts index dc53fdcdea35..40a0debee703 100644 --- a/src/lib/best-practice-topic.ts +++ b/src/lib/best-practice-topic.ts @@ -1,5 +1,5 @@ import type { MarkdownFileType } from './file'; -import type { BestPracticeFrontmatter } from './best-pratice'; +import type { BestPracticeFrontmatter } from './best-practice'; // Generates URL from the topic file path e.g. // -> /src/data/best-practices/frontend-performance/content/100-use-https-everywhere @@ -34,7 +34,7 @@ export async function getAllBestPracticeTopicFiles(): Promise< '/src/data/best-practices/*/content/**/*.md', { eager: true, - } + }, ); const mapping: Record = {}; @@ -63,4 +63,4 @@ export async function getAllBestPracticeTopicFiles(): Promise< } return mapping; -} \ No newline at end of file +} diff --git a/src/lib/best-pratice.ts b/src/lib/best-practice.ts similarity index 100% rename from src/lib/best-pratice.ts rename to src/lib/best-practice.ts diff --git a/src/lib/date.ts b/src/lib/date.ts index 4df386da3a02..ecb6bd726587 100644 --- a/src/lib/date.ts +++ b/src/lib/date.ts @@ -32,3 +32,17 @@ export function getRelativeTimeString(date: string): string { return relativeTime; } + +export function formatMonthDate(date: string): string { + return new Date(date).toLocaleDateString('en-US', { + month: 'long', + year: 'numeric', + }); +} + +export function formatActivityDate(date: string): string { + return new Date(date).toLocaleDateString('en-US', { + month: 'long', + day: 'numeric', + }); +} diff --git a/src/lib/jwt.ts b/src/lib/jwt.ts index fac2e8f5938f..080de1b4f93d 100644 --- a/src/lib/jwt.ts +++ b/src/lib/jwt.ts @@ -109,3 +109,19 @@ export function removeAIReferralCode() { domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', }); } + +export function setViewSponsorCookie(sponsorId: string) { + const key = `vsc-${sponsorId}`; + const alreadyExist = Cookies.get(key); + if (alreadyExist) { + return; + } + + Cookies.set(key, '1', { + path: '/', + expires: 1, + sameSite: 'lax', + secure: true, + domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh', + }); +} diff --git a/src/lib/link-group.ts b/src/lib/link-group.ts index 960d8d1ea07f..9ed56cf6e3e5 100644 --- a/src/lib/link-group.ts +++ b/src/lib/link-group.ts @@ -29,7 +29,7 @@ export async function getAllLinkGroups(): Promise { '/src/data/link-groups/*.md', { eager: true, - } + }, ); return Object.values(linkGroups).map((linkGroupFile) => ({ @@ -37,3 +37,14 @@ export async function getAllLinkGroups(): Promise { id: linkGroupPathToId(linkGroupFile.file), })); } + +export async function getLinkGroupById( + groupId: string, +): Promise { + const linkGroup = await import(`../data/link-groups/${groupId}.md`); + + return { + ...linkGroup, + id: linkGroupPathToId(linkGroup.file), + }; +} diff --git a/src/lib/question-group.ts b/src/lib/question-group.ts index e50eee1f6f50..a41e866e42a2 100644 --- a/src/lib/question-group.ts +++ b/src/lib/question-group.ts @@ -118,6 +118,12 @@ export async function getAllQuestionGroups(): Promise { .sort((a, b) => a.frontmatter.order - b.frontmatter.order); } +export async function getQuestionGroupById(id: string) { + const questionGroups = await getAllQuestionGroups(); + + return questionGroups.find((group) => group.id === id); +} + export async function getQuestionGroupsByIds( ids: string[], ): Promise<{ id: string; title: string; description: string }[]> { diff --git a/src/lib/roadmap.ts b/src/lib/roadmap.ts index cb4983405886..b3303c606f09 100644 --- a/src/lib/roadmap.ts +++ b/src/lib/roadmap.ts @@ -128,3 +128,11 @@ export async function getRoadmapsByIds( return Promise.all(ids.map((id) => getRoadmapById(id))); } + +export async function getRoadmapFaqsById(roadmapId: string): Promise { + const { faqs } = await import( + `../data/roadmaps/${roadmapId}/faqs.astro` + ).catch(() => ({})); + + return faqs || []; +} diff --git a/src/lib/video.ts b/src/lib/video.ts index 09629cf210ce..d34e27ab415d 100644 --- a/src/lib/video.ts +++ b/src/lib/video.ts @@ -1,8 +1,8 @@ import type { MarkdownFileType } from './file'; import type { AuthorFileType } from './author.ts'; import { getAllAuthors } from './author.ts'; -import type {GuideFileType} from "./guide.ts"; -import {getAllGuides} from "./guide.ts"; +import type { GuideFileType } from './guide.ts'; +import { getAllGuides } from './guide.ts'; export interface VideoFrontmatter { title: string; @@ -40,7 +40,7 @@ function videoPathToId(filePath: string): string { } export async function getVideosByAuthor( - authorId: string, + authorId: string, ): Promise { const allVideos = await getAllVideos(); @@ -73,3 +73,22 @@ export async function getAllVideos(): Promise { new Date(a.frontmatter.date).valueOf(), ); } + +export async function getVideoById(id: string): Promise { + const videoFilesMap: Record = + import.meta.glob('../data/videos/*.md', { + eager: true, + }); + + const videoFile = Object.values(videoFilesMap).find((videoFile) => { + return videoPathToId(videoFile.file) === id; + }); + if (!videoFile) { + throw new Error(`Video with ID ${id} not found`); + } + + return { + ...videoFile, + id: videoPathToId(videoFile.file), + }; +} diff --git a/src/pages/[roadmapId]/[...topicId].astro b/src/pages/[roadmapId]/[...topicId].astro index ce1b14977963..aec201c8533a 100644 --- a/src/pages/[roadmapId]/[...topicId].astro +++ b/src/pages/[roadmapId]/[...topicId].astro @@ -1,6 +1,4 @@ --- -import RoadmapBanner from '../../components/RoadmapBanner.astro'; -import BaseLayout from '../../layouts/BaseLayout.astro'; import { getRoadmapTopicFiles, type RoadmapTopicFileType, @@ -36,4 +34,4 @@ const gitHubUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/maste
- \ No newline at end of file + diff --git a/src/pages/[roadmapId]/index.astro b/src/pages/[roadmapId]/index.astro index 55cf232b8f04..898c6d1617cd 100644 --- a/src/pages/[roadmapId]/index.astro +++ b/src/pages/[roadmapId]/index.astro @@ -1,7 +1,6 @@ --- -import FAQs from '../../components/FAQs/FAQs.astro'; +import FAQs, { type FAQType } from '../../components/FAQs/FAQs.astro'; import FrameRenderer from '../../components/FrameRenderer/FrameRenderer.astro'; -import MarkdownFile from '../../components/MarkdownFile.astro'; import RelatedRoadmaps from '../../components/RelatedRoadmaps.astro'; import RoadmapHeader from '../../components/RoadmapHeader.astro'; import ShareIcons from '../../components/ShareIcons/ShareIcons.astro'; @@ -54,7 +53,7 @@ if (roadmapData.schema) { } if (roadmapFAQs.length) { - jsonLdSchema.push(generateFAQSchema(roadmapFAQs)); + jsonLdSchema.push(generateFAQSchema(roadmapFAQs as unknown as FAQType[])); } const ogImageUrl = @@ -125,7 +124,7 @@ const ogImageUrl = client:only='react' /> - +
diff --git a/src/pages/[roadmapId]/index.json.ts b/src/pages/[roadmapId]/index.json.ts index db772aacde29..7d4b6c0f397f 100644 --- a/src/pages/[roadmapId]/index.json.ts +++ b/src/pages/[roadmapId]/index.json.ts @@ -1,7 +1,7 @@ import type { APIRoute } from 'astro'; export async function getStaticPaths() { - const roadmapJsons = await import.meta.glob('/src/data/roadmaps/**/*.json', { + const roadmapJsons = import.meta.glob('/src/data/roadmaps/**/*.json', { eager: true, }); diff --git a/src/pages/account/index.astro b/src/pages/account/index.astro index 056751212d47..08fc68c2a022 100644 --- a/src/pages/account/index.astro +++ b/src/pages/account/index.astro @@ -4,8 +4,12 @@ import { ActivityPage } from '../../components/Activity/ActivityPage'; import AccountLayout from '../../layouts/AccountLayout.astro'; --- - + - + diff --git a/src/pages/account/update-profile.astro b/src/pages/account/update-profile.astro index 3ad19b0dd1fd..1f803861309f 100644 --- a/src/pages/account/update-profile.astro +++ b/src/pages/account/update-profile.astro @@ -1,6 +1,6 @@ --- import AccountSidebar from '../../components/AccountSidebar.astro'; -import { UpdateProfileForm } from '../../components/UpdateProfile/UpdateProfileForm'; +import { UpdatePublicProfileForm } from '../../components/UpdateProfile/UpdatePublicProfileForm'; import AccountLayout from '../../layouts/AccountLayout.astro'; --- @@ -10,6 +10,6 @@ import AccountLayout from '../../layouts/AccountLayout.astro'; initialLoadingMessage={'Loading profile'} > - + diff --git a/src/pages/authors/[authorId].astro b/src/pages/authors/[authorId].astro index 1731ec9da695..1d0cf6138881 100644 --- a/src/pages/authors/[authorId].astro +++ b/src/pages/authors/[authorId].astro @@ -2,7 +2,7 @@ import BaseLayout from '../../layouts/BaseLayout.astro'; import AstroIcon from '../../components/AstroIcon.astro'; import { getGuidesByAuthor } from '../../lib/guide'; -import { getAllVideos, getVideosByAuthor } from '../../lib/video'; +import { getVideosByAuthor } from '../../lib/video'; import GuideListItem from '../../components/GuideListItem.astro'; import { getAuthorById, getAuthorIds } from '../../lib/author'; import VideoListItem from '../../components/VideoListItem.astro'; diff --git a/src/pages/backend/languages.astro b/src/pages/backend/languages.astro index cfb5edc2c84f..11746690fae2 100644 --- a/src/pages/backend/languages.astro +++ b/src/pages/backend/languages.astro @@ -6,7 +6,10 @@ import { getGuideById } from '../../lib/guide'; import { getOpenGraphImageUrl } from '../../lib/open-graph'; const guideId = 'backend-languages'; -const guide = await getGuideById('backend-languages'); +const guide = await getGuideById(guideId).catch(() => null); +if (!guide) { + return Astro.redirect('/404'); +} const { frontmatter: guideData } = guide!; diff --git a/src/pages/best-practices/[bestPracticeId]/index.astro b/src/pages/best-practices/[bestPracticeId]/index.astro index fa7dce388f80..54a1fd74401e 100644 --- a/src/pages/best-practices/[bestPracticeId]/index.astro +++ b/src/pages/best-practices/[bestPracticeId]/index.astro @@ -7,13 +7,13 @@ import { TopicDetail } from '../../../components/TopicDetail/TopicDetail'; import UpcomingForm from '../../../components/UpcomingForm.astro'; import BaseLayout from '../../../layouts/BaseLayout.astro'; import { UserProgressModal } from '../../../components/UserProgress/UserProgressModal'; -import { - type BestPracticeFileType, - type BestPracticeFrontmatter, - getAllBestPractices, -} from '../../../lib/best-pratice'; import { generateArticleSchema } from '../../../lib/jsonld-schema'; import { getOpenGraphImageUrl } from '../../../lib/open-graph'; +import { + BestPracticeFileType, + BestPracticeFrontmatter, + getAllBestPractices, +} from '../../../lib/best-practice'; export async function getStaticPaths() { const bestPractices = await getAllBestPractices(); diff --git a/src/pages/best-practices/[bestPracticeId]/index.json.ts b/src/pages/best-practices/[bestPracticeId]/index.json.ts index 4a7c7b06c1c5..00400ce13259 100644 --- a/src/pages/best-practices/[bestPracticeId]/index.json.ts +++ b/src/pages/best-practices/[bestPracticeId]/index.json.ts @@ -5,7 +5,7 @@ export async function getStaticPaths() { '/src/data/best-practices/**/*.json', { eager: true, - } + }, ); return Object.keys(bestPracticeJsons).map((filePath) => { diff --git a/src/pages/best-practices/index.astro b/src/pages/best-practices/index.astro index 52b7433d4b5b..1e92f2fe285f 100644 --- a/src/pages/best-practices/index.astro +++ b/src/pages/best-practices/index.astro @@ -2,7 +2,7 @@ import GridItem from '../../components/GridItem.astro'; import SimplePageHeader from '../../components/SimplePageHeader.astro'; import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getAllBestPractices } from '../../lib/best-pratice'; +import { getAllBestPractices } from '../../lib/best-practice'; const bestPractices = await getAllBestPractices(); --- diff --git a/src/pages/index.astro b/src/pages/index.astro index 0013b5209d22..524822985bf7 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -4,7 +4,7 @@ import FeaturedGuides from '../components/FeaturedGuides.astro'; import FeaturedItems from '../components/FeaturedItems/FeaturedItems.astro'; import HeroSection from '../components/HeroSection/HeroSection.astro'; import BaseLayout from '../layouts/BaseLayout.astro'; -import { getAllBestPractices } from '../lib/best-pratice'; +import { getAllBestPractices } from '../lib/best-practice'; import { getAllGuides } from '../lib/guide'; import { getRoadmapsByTag } from '../lib/roadmap'; import { getAllVideos } from '../lib/video'; diff --git a/src/pages/pages.json.ts b/src/pages/pages.json.ts index 1fc6ebd7b002..90288d7e3f9c 100644 --- a/src/pages/pages.json.ts +++ b/src/pages/pages.json.ts @@ -1,4 +1,4 @@ -import { getAllBestPractices } from '../lib/best-pratice'; +import { getAllBestPractices } from '../lib/best-practice'; import { getAllGuides } from '../lib/guide'; import { getRoadmapsByTag } from '../lib/roadmap'; import { getAllVideos } from '../lib/video'; diff --git a/src/pages/questions/[questionGroupId].astro b/src/pages/questions/[questionGroupId].astro index 6091fceb8dd3..1f72d1a46b69 100644 --- a/src/pages/questions/[questionGroupId].astro +++ b/src/pages/questions/[questionGroupId].astro @@ -38,11 +38,11 @@ const { frontmatter } = questionGroup; >
-
+
diff --git a/src/pages/r/[customRoadmapSlug].astro b/src/pages/r/[customRoadmapSlug].astro new file mode 100644 index 000000000000..e72b34c9f4c5 --- /dev/null +++ b/src/pages/r/[customRoadmapSlug].astro @@ -0,0 +1,26 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import { CustomRoadmap } from '../../components/CustomRoadmap/CustomRoadmap'; +import { SkeletonRoadmapHeader } from '../../components/CustomRoadmap/SkeletonRoadmapHeader'; +import Loader from '../../components/Loader.astro'; +import ProgressHelpPopup from '../../components/ProgressHelpPopup.astro'; + +export const prerender = false; + +const { customRoadmapSlug } = Astro.params; +--- + + + +
+
+
+ +
+ +
+
+ +
+
+
diff --git a/src/pages/u/[username].astro b/src/pages/u/[username].astro new file mode 100644 index 000000000000..dece246d9e0b --- /dev/null +++ b/src/pages/u/[username].astro @@ -0,0 +1,59 @@ +--- +import { FrownIcon } from 'lucide-react'; +import { userApi } from '../../api/user'; +import AccountLayout from '../../layouts/AccountLayout.astro'; +import { UserPublicProfilePage } from '../../components/UserPublicProfile/UserPublicProfilePage'; +import OpenSourceBanner from '../../components/OpenSourceBanner.astro'; +import Footer from '../../components/Footer.astro'; +import BaseLayout from "../../layouts/BaseLayout.astro"; + +export const prerender = false; + +interface Params extends Record { + username: string; +} + +const { username } = Astro.params as Params; +if (!username) { + return Astro.redirect('/404'); +} + +const userClient = userApi(Astro as any); +const { response: userDetails, error } = + await userClient.getPublicProfile(username); + +let errorMessage = ''; +if (error || !userDetails) { + errorMessage = error?.message || 'User not found'; +} +--- + + + {!errorMessage && } + { + errorMessage && ( +
+ + + 😞 + +

+ Problem loading user! +

+

+ + {errorMessage} + +

+
+ ) + } +
diff --git a/src/pages/v1-health.ts b/src/pages/v1-health.ts new file mode 100644 index 000000000000..a4e591eaffe3 --- /dev/null +++ b/src/pages/v1-health.ts @@ -0,0 +1,7 @@ +import { execSync } from 'child_process'; + +export const prerender = false; + +export async function GET() { + return new Response(JSON.stringify({}), {}); +} diff --git a/src/pages/v1-stats.json.ts b/src/pages/v1-stats.json.ts new file mode 100644 index 000000000000..4986d75a4797 --- /dev/null +++ b/src/pages/v1-stats.json.ts @@ -0,0 +1,33 @@ +import { execSync } from 'child_process'; + +export const prerender = true; + +export async function GET() { + const commitHash = execSync('git rev-parse HEAD').toString().trim(); + const commitDate = execSync('git log -1 --format=%cd').toString().trim(); + const commitMessage = execSync('git log -1 --format=%B').toString().trim(); + + const prevCommitHash = execSync('git rev-parse HEAD~1').toString().trim(); + const prevCommitDate = execSync('git log -1 --format=%cd HEAD~1') + .toString() + .trim(); + const prevCommitMessage = execSync('git log -1 --format=%B HEAD~1') + .toString() + .trim(); + + return new Response( + JSON.stringify({ + current: { + hash: commitHash, + date: commitDate, + message: commitMessage, + }, + previous: { + hash: prevCommitHash, + date: prevCommitDate, + message: prevCommitMessage, + }, + }), + {}, + ); +} diff --git a/src/pages/videos/[videoId].astro b/src/pages/videos/[videoId].astro index 801712252e1b..096e230a0a6d 100644 --- a/src/pages/videos/[videoId].astro +++ b/src/pages/videos/[videoId].astro @@ -1,7 +1,7 @@ --- import VideoHeader from '../../components/VideoHeader.astro'; import BaseLayout from '../../layouts/BaseLayout.astro'; -import { getAllVideos,VideoFileType } from '../../lib/video'; +import { getAllVideos, VideoFileType } from '../../lib/video'; export interface Props { video: VideoFileType; @@ -29,7 +29,7 @@ const { video } = Astro.props;