Compare commits

...

46 Commits

Author SHA1 Message Date
Arik Chakma
b3b43e3068 Fix progress modal 2023-11-10 03:31:23 +06:00
Kamran Ahmed
c95272e213 Update 2023-11-09 21:30:58 +00:00
Kamran Ahmed
689792665b Friends and notification pages 2023-11-09 21:30:58 +00:00
Arik Chakma
ae94ddfc04 Merge branch 'images-fix' of https://github.com/kamranahmedse/developer-roadmap into images-fix 2023-11-10 03:29:53 +06:00
Kamran Ahmed
b47f712dec Friends and notification pages 2023-11-09 21:29:21 +00:00
Arik Chakma
48b0059ade Fix user progress modal 2023-11-10 03:29:19 +06:00
Arik Chakma
2f07d5edf0 Fix invite friend 2023-11-10 03:27:48 +06:00
Kamran Ahmed
1dd9a53a32 Personal roadmap list and empty friends 2023-11-09 21:26:01 +00:00
Kamran Ahmed
540e13271e Team roadmap icon fix 2023-11-09 21:23:41 +00:00
Arik Chakma
12f617bd0e Merge branch 'images-fix' of https://github.com/kamranahmedse/developer-roadmap into images-fix 2023-11-10 03:23:08 +06:00
Arik Chakma
82a7c205eb Fix respond invite 2023-11-10 03:22:47 +06:00
Kamran Ahmed
76d1ca1333 Update images 2023-11-09 21:21:28 +00:00
Kamran Ahmed
40357f7956 Roadmap action dropdown fix 2023-11-09 21:17:29 +00:00
Arik Chakma
581f4a76a4 Merge branch 'images-fix' of https://github.com/kamranahmedse/developer-roadmap into images-fix 2023-11-10 03:15:46 +06:00
Arik Chakma
ef1a3031c4 Fix verify letter 2023-11-10 03:15:27 +06:00
Kamran Ahmed
3774f3c5ec Team sidebar fix 2023-11-09 21:15:15 +00:00
Kamran Ahmed
b11da48f41 Select roadmap modal fix 2023-11-09 21:15:15 +00:00
Kamran Ahmed
5edda5654c Team icons fix 2023-11-09 21:15:15 +00:00
Arik Chakma
505077a545 Fix trigger verify page 2023-11-10 03:10:58 +06:00
Arik Chakma
9f4967929f Fix page progress 2023-11-10 03:02:12 +06:00
Arik Chakma
27cb89494f Merge branch 'images-fix' of https://github.com/kamranahmedse/developer-roadmap into images-fix 2023-11-10 02:59:19 +06:00
Arik Chakma
ec556915e4 Fix roadmap page 2023-11-10 02:59:00 +06:00
Kamran Ahmed
c61e44119d Command menu icons 2023-11-09 20:58:02 +00:00
Kamran Ahmed
6f46d723bc Fix login buttons 2023-11-09 20:45:30 +00:00
Kamran Ahmed
ee6e3e4029 Github icon fix 2023-11-09 20:40:57 +00:00
Kamran Ahmed
6e9fe97e5c Update dependencies 2023-11-09 20:36:50 +00:00
Kamran Ahmed
13af03c930 Update renderer 2023-11-09 20:35:36 +00:00
Kamran Ahmed
78692ff13f Rename deployment 2023-11-09 20:19:30 +00:00
Kamran Ahmed
54d7388b09 Downgrade dependencies 2023-11-09 20:11:58 +00:00
Kamran Ahmed
b609c43055 Add link to technical writer roadmap 2023-11-09 20:03:16 +00:00
Kamran Ahmed
d83fe1279b Update twitter icon in sharing button 2023-11-09 04:07:49 +00:00
Kamran Ahmed
fb3cb85c14 Update twitter icon and progress nudge 2023-11-09 02:22:35 +00:00
Kamran Ahmed
82dbca95fb Add progress nudge on roadmap 2023-11-09 00:21:53 +00:00
Kamran Ahmed
7e702ee385 Update technical writer reference link 2023-11-07 17:49:28 +00:00
Kamran Ahmed
08fbb730ab Add content for technical writer roadmap 2023-11-07 17:48:28 +00:00
Kamran Ahmed
cd80338fa6 Add technical writer roadmap 2023-11-07 17:41:17 +00:00
Selva Muthu Kumaran
fa33d0c339 Fix broken Electron tutorial link (#4589)
frontend-roadmap-javascript-electron URL fixed
fixes : #4587
2023-11-05 23:14:06 +06:00
Hanzalah Waheed
8ec9a6e675 updated new twitter logo (#4659) 2023-11-05 23:02:31 +06:00
Kamran Ahmed
16853df928 UI change for sharing button 2023-11-04 22:56:10 +00:00
Arik Chakma
c15d139d54 Add DevOps forkable (#4638) 2023-11-03 19:11:31 +00:00
Kamran Ahmed
4e5cc5bd35 Change twitter share button text 2023-11-03 19:09:32 +00:00
Kamran Ahmed
a36bca2f42 Add sharing buttons in header 2023-11-03 19:08:04 +00:00
Kamran Ahmed
10b688049d Fix ui issue 2023-11-03 15:51:02 +00:00
Kamran Ahmed
0db92f6418 Add link to server side game developer roadmap 2023-10-31 12:17:26 +00:00
Kamran Ahmed
dccaa66ed4 Add server side game developer roadmap 2023-10-31 01:11:16 +00:00
Kamran Ahmed
3deee4dfc3 Add server side game developer roadmap 2023-10-30 23:32:21 +00:00
335 changed files with 33139 additions and 19880 deletions

View File

@@ -1,4 +1,4 @@
name: Deployment to GH Pages
name: App Deployment
on:
push:
branches: [ master ]

11028
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "roadmap.sh",
"type": "module",
"version": "0.0.1",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "astro dev --port 3000",
@@ -22,23 +22,23 @@
"test:e2e": "playwright test"
},
"dependencies": {
"@astrojs/react": "^3.0.3",
"@astrojs/sitemap": "^3.0.2",
"@astrojs/react": "^3.0.4",
"@astrojs/sitemap": "^3.0.3",
"@astrojs/tailwind": "^5.0.2",
"@fingerprintjs/fingerprintjs": "^4.1.0",
"@nanostores/react": "^0.7.1",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"astro": "^3.3.3",
"astro-compress": "^2.1.5",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"astro": "^3.5.0",
"astro-compress": "^2.2.3",
"clsx": "^2.0.0",
"dracula-prism": "^2.1.13",
"jose": "^4.15.4",
"jose": "^5.1.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.288.0",
"nanoid": "^5.0.2",
"lucide-react": "^0.292.0",
"nanoid": "^5.0.3",
"nanostores": "^0.9.4",
"node-html-parser": "^6.1.10",
"node-html-parser": "^6.1.11",
"npm-check-updates": "^16.14.6",
"prismjs": "^1.29.0",
"react": "^18.2.0",
@@ -48,22 +48,22 @@
"rehype-external-links": "^3.0.0",
"roadmap-renderer": "^1.0.6",
"slugify": "^1.6.6",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3",
"zustand": "^4.4.4"
"tailwind-merge": "^2.0.0",
"tailwindcss": "^3.3.5",
"zustand": "^4.4.6"
},
"devDependencies": {
"@playwright/test": "^1.39.0",
"@tailwindcss/typography": "^0.5.10",
"@types/js-cookie": "^3.0.5",
"@types/prismjs": "^1.26.2",
"@types/js-cookie": "^3.0.6",
"@types/prismjs": "^1.26.3",
"csv-parser": "^3.0.0",
"gh-pages": "^6.0.0",
"js-yaml": "^4.1.0",
"markdown-it": "^13.0.2",
"openai": "^4.13.0",
"openai": "^4.17.1",
"prettier": "^3.0.3",
"prettier-plugin-astro": "^0.12.0",
"prettier-plugin-tailwindcss": "^0.5.6"
"prettier-plugin-astro": "^0.12.1",
"prettier-plugin-tailwindcss": "^0.5.7"
}
}

455
pnpm-lock.yaml generated
View File

@@ -6,14 +6,14 @@ settings:
dependencies:
'@astrojs/react':
specifier: ^3.0.3
version: 3.0.3(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)(vite@4.5.0)
specifier: ^3.0.4
version: 3.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)(vite@4.5.0)
'@astrojs/sitemap':
specifier: ^3.0.2
version: 3.0.2
specifier: ^3.0.3
version: 3.0.3
'@astrojs/tailwind':
specifier: ^5.0.2
version: 5.0.2(astro@3.3.3)(tailwindcss@3.3.3)
version: 5.0.2(astro@3.5.0)(tailwindcss@3.3.5)
'@fingerprintjs/fingerprintjs':
specifier: ^4.1.0
version: 4.1.0
@@ -21,17 +21,17 @@ dependencies:
specifier: ^0.7.1
version: 0.7.1(nanostores@0.9.4)(react@18.2.0)
'@types/react':
specifier: ^18.2.31
version: 18.2.31
specifier: ^18.2.37
version: 18.2.37
'@types/react-dom':
specifier: ^18.2.14
version: 18.2.14
specifier: ^18.2.15
version: 18.2.15
astro:
specifier: ^3.3.3
version: 3.3.3(typescript@5.3.0-dev.20231021)
specifier: ^3.5.0
version: 3.5.0(typescript@5.2.2)
astro-compress:
specifier: ^2.1.5
version: 2.1.5
specifier: ^2.2.3
version: 2.2.3
clsx:
specifier: ^2.0.0
version: 2.0.0
@@ -39,23 +39,23 @@ dependencies:
specifier: ^2.1.13
version: 2.1.13
jose:
specifier: ^4.15.4
version: 4.15.4
specifier: ^5.1.0
version: 5.1.0
js-cookie:
specifier: ^3.0.5
version: 3.0.5
lucide-react:
specifier: ^0.288.0
version: 0.288.0(react@18.2.0)
specifier: ^0.292.0
version: 0.292.0(react@18.2.0)
nanoid:
specifier: ^5.0.2
version: 5.0.2
specifier: ^5.0.3
version: 5.0.3
nanostores:
specifier: ^0.9.4
version: 0.9.4
node-html-parser:
specifier: ^6.1.10
version: 6.1.10
specifier: ^6.1.11
version: 6.1.11
npm-check-updates:
specifier: ^16.14.6
version: 16.14.6
@@ -73,7 +73,7 @@ dependencies:
version: 18.2.0(react@18.2.0)
reactflow:
specifier: ^11.9.4
version: 11.9.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
version: 11.9.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
rehype-external-links:
specifier: ^3.0.0
version: 3.0.0
@@ -84,14 +84,14 @@ dependencies:
specifier: ^1.6.6
version: 1.6.6
tailwind-merge:
specifier: ^1.14.0
version: 1.14.0
specifier: ^2.0.0
version: 2.0.0
tailwindcss:
specifier: ^3.3.3
version: 3.3.3
specifier: ^3.3.5
version: 3.3.5
zustand:
specifier: ^4.4.4
version: 4.4.4(@types/react@18.2.31)(react@18.2.0)
specifier: ^4.4.6
version: 4.4.6(@types/react@18.2.37)(react@18.2.0)
devDependencies:
'@playwright/test':
@@ -99,13 +99,13 @@ devDependencies:
version: 1.39.0
'@tailwindcss/typography':
specifier: ^0.5.10
version: 0.5.10(tailwindcss@3.3.3)
version: 0.5.10(tailwindcss@3.3.5)
'@types/js-cookie':
specifier: ^3.0.5
version: 3.0.5
specifier: ^3.0.6
version: 3.0.6
'@types/prismjs':
specifier: ^1.26.2
version: 1.26.2
specifier: ^1.26.3
version: 1.26.3
csv-parser:
specifier: ^3.0.0
version: 3.0.0
@@ -119,17 +119,17 @@ devDependencies:
specifier: ^13.0.2
version: 13.0.2
openai:
specifier: ^4.13.0
version: 4.13.0
specifier: ^4.17.1
version: 4.17.1
prettier:
specifier: ^3.0.3
version: 3.0.3
prettier-plugin-astro:
specifier: ^0.12.0
version: 0.12.0
specifier: ^0.12.1
version: 0.12.1
prettier-plugin-tailwindcss:
specifier: ^0.5.6
version: 0.5.6(prettier-plugin-astro@0.12.0)(prettier@3.0.3)
specifier: ^0.5.7
version: 0.5.7(prettier-plugin-astro@0.12.1)(prettier@3.0.3)
packages:
@@ -149,21 +149,21 @@ packages:
resolution: {integrity: sha512-o/ObKgtMzl8SlpIdzaxFnt7SATKPxu4oIP/1NL+HDJRzxfJcAkOTAb/ZKMRyULbz4q+1t2/DAebs2Z1QairkZw==}
dev: true
/@astrojs/compiler@2.2.1:
resolution: {integrity: sha512-NJ1lWKzMkyEjE3W5NpPNAVot4/PLF5om/P6ekxNu3iLS05CaYFTcp7WpYMjdCC252b7wkNVAs45FNkVQ+RHW/g==}
/@astrojs/compiler@2.3.0:
resolution: {integrity: sha512-pxYRAaRdMS6XUll8lbFM+Lr0DI1HKIDT+VpiC+S+9di5H/nmm3znZOgdMlLiMxADot+56eps+M1BvtKfQremXA==}
dev: false
/@astrojs/internal-helpers@0.2.1:
resolution: {integrity: sha512-06DD2ZnItMwUnH81LBLco3tWjcZ1lGU9rLCCBaeUCGYe9cI0wKyY2W3kDyoW1I6GmcWgt1fu+D1CTvz+FIKf8A==}
dev: false
/@astrojs/markdown-remark@3.3.0(astro@3.3.3):
resolution: {integrity: sha512-ezFzEiZygc/ASe2Eul9v1yrTbNGqSbR348UGNXQ4Dtkx8MYRwfiBfmPm6VnEdfIGkW+bi5qIUReKfc7mPVUkIg==}
/@astrojs/markdown-remark@3.4.0(astro@3.5.0):
resolution: {integrity: sha512-uzLSKBQ4e70aH8gEbBHZ2pnv/KOJKB3WrXFBOF0U5Uwjcr2LNWeIBLjPRQjA4tbtteELh84YPBHny21mhvBGVA==}
peerDependencies:
astro: ^3.3.0
astro: ^3.0.0
dependencies:
'@astrojs/prism': 3.0.0
astro: 3.3.3(typescript@5.3.0-dev.20231021)
astro: 3.5.0(typescript@5.2.2)
github-slugger: 2.0.0
import-meta-resolve: 3.0.0
mdast-util-definitions: 6.0.0
@@ -188,8 +188,8 @@ packages:
prismjs: 1.29.0
dev: false
/@astrojs/react@3.0.3(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)(vite@4.5.0):
resolution: {integrity: sha512-foliIy1whJobo+ZpsvOMS4WCiR0z4/2Seyxth5xMlweVVM+gA1Lqk0GdzE6F0ISUW9CuXrCRS7ZyTNW8SM6vog==}
/@astrojs/react@3.0.4(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)(vite@4.5.0):
resolution: {integrity: sha512-v+qltrm5nfqqm8C5ymYDBxrGlvNouRr+iCLcUWvNX65abVz8GzUqquhPQjmCTDXphn1Qc558Uxzc4s79l02skw==}
engines: {node: '>=18.14.1'}
peerDependencies:
'@types/react': ^17.0.50 || ^18.0.21
@@ -197,8 +197,8 @@ packages:
react: ^17.0.2 || ^18.0.0
react-dom: ^17.0.2 || ^18.0.0
dependencies:
'@types/react': 18.2.31
'@types/react-dom': 18.2.14
'@types/react': 18.2.37
'@types/react-dom': 18.2.15
'@vitejs/plugin-react': 4.1.0(vite@4.5.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -208,30 +208,30 @@ packages:
- vite
dev: false
/@astrojs/sitemap@3.0.2:
resolution: {integrity: sha512-ldxCDc0+QHcq2jH4WJFcz5iBc5/SqotEqpT91dhvoLqoRUT5U21rQ6u6qA4FyRVjpd/0Nt1kFjEsRbwoB8IKiQ==}
/@astrojs/sitemap@3.0.3:
resolution: {integrity: sha512-+GRKp1yho9dpHBcMcU6JpbL41k0yYZghOkNsMRb8QIRflbGHvd787tdv9oIZ5NJj0SqAuOlqp2UpqLkJXuAe2A==}
dependencies:
sitemap: 7.1.1
zod: 3.21.1
zod: 3.22.4
dev: false
/@astrojs/tailwind@5.0.2(astro@3.3.3)(tailwindcss@3.3.3):
/@astrojs/tailwind@5.0.2(astro@3.5.0)(tailwindcss@3.3.5):
resolution: {integrity: sha512-oXqeqmBlkQmsltrsU9nEWeXOtrZIAHW8dcmX7BCdrjzPnU6dPwWzAwhddNQ9ihKiWwsLnlbwQZIo2CDigcZlIA==}
peerDependencies:
astro: ^3.2.4
tailwindcss: ^3.0.24
dependencies:
astro: 3.3.3(typescript@5.3.0-dev.20231021)
astro: 3.5.0(typescript@5.2.2)
autoprefixer: 10.4.16(postcss@8.4.31)
postcss: 8.4.31
postcss-load-config: 4.0.1(postcss@8.4.31)
tailwindcss: 3.3.3
tailwindcss: 3.3.5
transitivePeerDependencies:
- ts-node
dev: false
/@astrojs/telemetry@3.0.3:
resolution: {integrity: sha512-j19Cf5mfyLt9hxgJ9W/FMdAA5Lovfp7/CINNB/7V71GqvygnL7KXhRC3TzfB+PsVQcBtgWZzCXhUWRbmJ64Raw==}
/@astrojs/telemetry@3.0.4:
resolution: {integrity: sha512-A+0c7k/Xy293xx6odsYZuXiaHO0PL+bnDoXOc47sGDF5ffIKdKQGRPFl2NMlCF4L0NqN4Ynbgnaip+pPF0s7pQ==}
engines: {node: '>=18.14.1'}
dependencies:
ci-info: 3.9.0
@@ -456,6 +456,13 @@ packages:
'@babel/types': 7.23.0
dev: false
/@babel/runtime@7.23.2:
resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.0
dev: false
/@babel/template@7.22.15:
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
engines: {node: '>=6.9.0'}
@@ -1067,39 +1074,39 @@ packages:
config-chain: 1.1.13
dev: false
/@reactflow/background@11.3.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0):
/@reactflow/background@11.3.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-bgwvqWxF09chwmdkyClpYEMaewBspdwjgLbbFlLf4SpWPFMYyuvCBQrcISsvy/EDEWO9i3Uj9ktgGAhvtSQsmA==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.9.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.9.4(@types/react@18.2.37)(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.4.4(@types/react@18.2.31)(react@18.2.0)
zustand: 4.4.6(@types/react@18.2.37)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/controls@11.2.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0):
/@reactflow/controls@11.2.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-x6e5p9iHjC6gd+4SoZ3DOOp0F1MefGKQ8hT6yPVdqxfo1+rV2WhrWvrX/MCoEu12Dp7457LdLfa0giy3aho8tQ==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.9.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.9.4(@types/react@18.2.37)(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.4.4(@types/react@18.2.31)(react@18.2.0)
zustand: 4.4.6(@types/react@18.2.37)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/core@11.9.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0):
/@reactflow/core@11.9.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Ko7nKPOYalwDTTbRHi2+QXDiidSAcpUzGN3G+0B+QysLZkcaPCkpkMjjHiDC4c/Z1BJBzs1FRJg/T6BXaBnYkg==}
peerDependencies:
react: '>=17'
@@ -1115,19 +1122,19 @@ packages:
d3-zoom: 3.0.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
zustand: 4.4.4(@types/react@18.2.31)(react@18.2.0)
zustand: 4.4.6(@types/react@18.2.37)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/minimap@11.7.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0):
/@reactflow/minimap@11.7.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Jo1R+uDey9IV7O2s3m0gK2+cZpg9M8hq2EZJb3NGfOSzMAPhj3mby0fNJIgTzycreuht0TpA51c2YfjGI3YIOw==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.9.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.9.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
'@types/d3-selection': 3.0.8
'@types/d3-zoom': 3.0.6
classcat: 5.0.4
@@ -1135,41 +1142,41 @@ packages:
d3-zoom: 3.0.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
zustand: 4.4.4(@types/react@18.2.31)(react@18.2.0)
zustand: 4.4.6(@types/react@18.2.37)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/node-resizer@2.2.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0):
/@reactflow/node-resizer@2.2.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-+p271/hAsM5M1+RQTWW/02pbNkCHeGXwxGimIlL1tMIagyuko0NX2vOz2B8jxJnPKlF09Wj18BcXBNUm3nDcSg==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.9.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.9.4(@types/react@18.2.37)(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.4.4(@types/react@18.2.31)(react@18.2.0)
zustand: 4.4.6(@types/react@18.2.37)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- immer
dev: false
/@reactflow/node-toolbar@1.3.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0):
/@reactflow/node-toolbar@1.3.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-TfcmpXHRBb2mUfzKGjburiU6FWqRME9pPFs1OwIC1z5e9BjupQhNDEKEk8XHi7PKL/mAiDfwuGXaM1BVVFuPqw==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/core': 11.9.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.9.4(@types/react@18.2.37)(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.4.4(@types/react@18.2.31)(react@18.2.0)
zustand: 4.4.6(@types/react@18.2.37)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- immer
@@ -1220,7 +1227,7 @@ packages:
defer-to-connect: 2.0.1
dev: false
/@tailwindcss/typography@0.5.10(tailwindcss@3.3.3):
/@tailwindcss/typography@0.5.10(tailwindcss@3.3.5):
resolution: {integrity: sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==}
peerDependencies:
tailwindcss: '>=3.0.0 || insiders'
@@ -1229,7 +1236,7 @@ packages:
lodash.isplainobject: 4.0.6
lodash.merge: 4.6.2
postcss-selector-parser: 6.0.10
tailwindcss: 3.3.3
tailwindcss: 3.3.5
dev: true
/@tootallnate/once@2.0.0:
@@ -1507,8 +1514,8 @@ packages:
resolution: {integrity: sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA==}
dev: false
/@types/js-cookie@3.0.5:
resolution: {integrity: sha512-dtLshqoiGRDHbHueIT9sjkd2F4tW1qPSX2xKAQK8p1e6pM+Z913GM1shv7dOqqasEMYbC5zEaClJomQe8OtQLA==}
/@types/js-cookie@3.0.6:
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
dev: true
/@types/mdast@3.0.14:
@@ -1536,7 +1543,7 @@ packages:
/@types/node-fetch@2.6.7:
resolution: {integrity: sha512-lX17GZVpJ/fuCjguZ5b3TjEbSENxmEk1B2z02yoXSK9WMEWRivhdSY73wWMn6bpcCDAOh6qAdktpKHIlkDk2lg==}
dependencies:
'@types/node': 18.18.6
'@types/node': 20.8.9
form-data: 4.0.0
dev: true
@@ -1548,32 +1555,31 @@ packages:
resolution: {integrity: sha512-wf3Vz+jCmOQ2HV1YUJuCWdL64adYxumkrxtc+H1VUQlnQI04+5HtH+qZCOE21lBE7gIrt+CwX2Wv8Acrw5Ak6w==}
dev: true
/@types/node@20.8.7:
resolution: {integrity: sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==}
/@types/node@20.8.9:
resolution: {integrity: sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==}
dependencies:
undici-types: 5.25.3
dev: false
undici-types: 5.26.5
/@types/parse5@6.0.3:
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
dev: false
/@types/prismjs@1.26.2:
resolution: {integrity: sha512-/r7Cp7iUIk7gts26mHXD66geUC+2Fo26TZYjQK6Nr4LDfi6lmdRmMqM0oPwfiMhUwoBAOFe8GstKi2pf6hZvwA==}
/@types/prismjs@1.26.3:
resolution: {integrity: sha512-A0D0aTXvjlqJ5ZILMz3rNfDBOx9hHxLZYv2by47Sm/pqW35zzjusrZTryatjN/Rf8Us2gZrJD+KeHbUSTux1Cw==}
dev: true
/@types/prop-types@15.7.9:
resolution: {integrity: sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==}
dev: false
/@types/react-dom@18.2.14:
resolution: {integrity: sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==}
/@types/react-dom@18.2.15:
resolution: {integrity: sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==}
dependencies:
'@types/react': 18.2.31
'@types/react': 18.2.37
dev: false
/@types/react@18.2.31:
resolution: {integrity: sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==}
/@types/react@18.2.37:
resolution: {integrity: sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==}
dependencies:
'@types/prop-types': 15.7.9
'@types/scheduler': 0.16.5
@@ -1583,7 +1589,7 @@ packages:
/@types/sax@1.2.6:
resolution: {integrity: sha512-A1mpYCYu1aHFayy8XKN57ebXeAbh9oQIZ1wXcno6b1ESUAfMBDMx7mf/QGlYwcMRaFryh9YBuH03i/3FlPGDkQ==}
dependencies:
'@types/node': 17.0.45
'@types/node': 20.8.9
dev: false
/@types/scheduler@0.16.5:
@@ -1752,29 +1758,30 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/astro-compress@2.1.5:
resolution: {integrity: sha512-aoqGm2zbv8LMTl8tktiyV/BfTs4GrKb7SFYQ6Fuv7BqpYkn33yrxs0Qz+CFpXL72YUGmQuWw3vuUi5IA3S4BzA==}
/astro-compress@2.2.3:
resolution: {integrity: sha512-aU4yiTOsnrFdMQnfzxL3bxTCaa/4vIvGXR8e+Ngxr6jpZvzV1pOy7hIthzmt4vjHe6m4VfRSZwtc3/x16/fJcQ==}
dependencies:
'@types/csso': 5.0.2
'@types/html-minifier-terser': 7.0.1
csso: 5.0.5
files-pipe: 2.1.1
files-pipe: 2.1.2
html-minifier-terser: 7.2.0
lightningcss: 1.22.0
sharp: 0.32.6
svgo: 3.0.2
terser: 5.22.0
typescript-esbuild: 0.2.25
terser: 5.24.0
typescript-esbuild: 0.3.1
dev: false
/astro@3.3.3(typescript@5.3.0-dev.20231021):
resolution: {integrity: sha512-FZkv5nJfa2KADzwo8m6fytWzzhO3Uw/EOvxmBT2E1OW/dWUgIKbZd59TY3816gZl3le5Ct5amSAkaxcQghbUZA==}
/astro@3.5.0(typescript@5.2.2):
resolution: {integrity: sha512-Wwu9gXIlxqCUEY6Bj9e08TeBm7I/CupyzHrWTNSPL+iwrTo/3/pL+itCeYz2u84jRygBgd2oPEN0FbK/sjj+uQ==}
engines: {node: '>=18.14.1', npm: '>=6.14.0'}
hasBin: true
dependencies:
'@astrojs/compiler': 2.2.1
'@astrojs/compiler': 2.3.0
'@astrojs/internal-helpers': 0.2.1
'@astrojs/markdown-remark': 3.3.0(astro@3.3.3)
'@astrojs/telemetry': 3.0.3
'@astrojs/markdown-remark': 3.4.0(astro@3.5.0)
'@astrojs/telemetry': 3.0.4
'@babel/core': 7.23.2
'@babel/generator': 7.23.0
'@babel/parser': 7.23.0
@@ -1805,9 +1812,11 @@ packages:
js-yaml: 4.1.0
kleur: 4.1.5
magic-string: 0.30.5
mdast-util-to-hast: 12.3.0
mime: 3.0.0
ora: 7.0.1
p-limit: 4.0.0
p-queue: 7.4.1
path-to-regexp: 6.2.1
preferred-pm: 3.1.2
probe-image-size: 7.2.3
@@ -1819,14 +1828,14 @@ packages:
shikiji: 0.6.10
string-width: 6.1.0
strip-ansi: 7.1.0
tsconfck: 3.0.0(typescript@5.3.0-dev.20231021)
tsconfck: 3.0.0(typescript@5.2.2)
unist-util-visit: 4.1.2
vfile: 5.3.7
vite: 4.5.0
vitefu: 0.2.5(vite@4.5.0)
which-pm: 2.1.1
yargs-parser: 21.1.1
zod: 3.21.1
zod: 3.22.4
optionalDependencies:
sharp: 0.32.6
transitivePeerDependencies:
@@ -2480,6 +2489,12 @@ packages:
engines: {node: '>=6'}
dev: false
/detect-libc@1.0.3:
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
engines: {node: '>=0.10'}
hasBin: true
dev: false
/detect-libc@2.0.2:
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
engines: {node: '>=8'}
@@ -2743,6 +2758,10 @@ packages:
engines: {node: '>=6'}
dev: true
/eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
dev: false
/execa@8.0.1:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
@@ -2815,10 +2834,10 @@ packages:
trim-repeated: 1.0.0
dev: true
/files-pipe@2.1.1:
resolution: {integrity: sha512-grvkOxmaPpu7EHI4BeFpVAuZiNfbaftmKhy8ly5/miUCNYkIfrqiQB+ZG9jGrdlefYqM3MiFfE9GUw7g7sGd/A==}
/files-pipe@2.1.2:
resolution: {integrity: sha512-QWmVUJ4swy7Jan9x3VPUVjoGdUw+YZRY4j5AaqArOdRF5lX8vgIPdD+OzUqWzJ/pvjHY2zU43b5s2lb1hiSNlA==}
dependencies:
'@types/node': 20.8.7
'@types/node': 20.8.9
deepmerge-ts: 5.1.0
fast-glob: 3.3.1
dev: false
@@ -3351,7 +3370,7 @@ packages:
entities: 4.5.0
param-case: 3.0.4
relateurl: 0.2.7
terser: 5.22.0
terser: 5.24.0
dev: false
/html-void-elements@2.0.1:
@@ -3640,8 +3659,8 @@ packages:
resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==}
dev: false
/jose@4.15.4:
resolution: {integrity: sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==}
/jose@5.1.0:
resolution: {integrity: sha512-H+RVqxA6apaJ0rcQYupKYhos7uosAiF42gUcWZiwhICWMphDULFj/CRr1R0tV/JCv9DEeJaSyYYpc9luHHNT4g==}
dev: false
/js-cookie@3.0.5:
@@ -3742,6 +3761,104 @@ packages:
package-json: 8.1.1
dev: false
/lightningcss-darwin-arm64@1.22.0:
resolution: {integrity: sha512-aH2be3nNny+It5YEVm8tBSSdRlBVWQV8m2oJ7dESiYRzyY/E/bQUe2xlw5caaMuhlM9aoTMtOH25yzMhir0qPg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/lightningcss-darwin-x64@1.22.0:
resolution: {integrity: sha512-9KHRFA0Y6mNxRHeoQMp0YaI0R0O2kOgUlYPRjuasU4d+pI8NRhVn9bt0yX9VPs5ibWX1RbDViSPtGJvYYrfVAQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/lightningcss-freebsd-x64@1.22.0:
resolution: {integrity: sha512-xaYL3xperGwD85rQioDb52ozF3NAJb+9wrge3jD9lxGffplu0Mn35rXMptB8Uc2N9Mw1i3Bvl7+z1evlqVl7ww==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: false
optional: true
/lightningcss-linux-arm-gnueabihf@1.22.0:
resolution: {integrity: sha512-epQGvXIjOuxrZpMpMnRjK54ZqzhiHhCPLtHvw2fb6NeK2kK9YtF0wqmeTBiQ1AkbWfnnXGTstYaFNiadNK+StQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
/lightningcss-linux-arm64-gnu@1.22.0:
resolution: {integrity: sha512-AArGtKSY4DGTA8xP8SDyNyKtpsUl1Rzq6FW4JomeyUQ4nBrR71uPChksTpj3gmWuGhZeRKLeCUI1DBid/zhChg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/lightningcss-linux-arm64-musl@1.22.0:
resolution: {integrity: sha512-RRraNgP8hnBPhInTTUdlFm+z16C/ghbxBG51Sw00hd7HUyKmEUKRozyc5od+/N6pOrX/bIh5vIbtMXIxsos0lg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/lightningcss-linux-x64-gnu@1.22.0:
resolution: {integrity: sha512-grdrhYGRi2KrR+bsXJVI0myRADqyA7ekprGxiuK5QRNkv7kj3Yq1fERDNyzZvjisHwKUi29sYMClscbtl+/Zpw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/lightningcss-linux-x64-musl@1.22.0:
resolution: {integrity: sha512-t5f90X+iQUtIyR56oXIHMBUyQFX/zwmPt72E6Dane3P8KNGlkijTg2I75XVQS860gNoEFzV7Mm5ArRRA7u5CAQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/lightningcss-win32-x64-msvc@1.22.0:
resolution: {integrity: sha512-64HTDtOOZE9PUCZJiZZQpyqXBbdby1lnztBccnqh+NtbKxjnGzP92R2ngcgeuqMPecMNqNWxgoWgTGpC+yN5Sw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/lightningcss@1.22.0:
resolution: {integrity: sha512-+z0qvwRVzs4XGRXelnWRNwqsXUx8k3bSkbP8vD42kYKSk3z9OM2P3e/gagT7ei/gwh8DTS80LZOFZV6lm8Z8Fg==}
engines: {node: '>= 12.0.0'}
dependencies:
detect-libc: 1.0.3
optionalDependencies:
lightningcss-darwin-arm64: 1.22.0
lightningcss-darwin-x64: 1.22.0
lightningcss-freebsd-x64: 1.22.0
lightningcss-linux-arm-gnueabihf: 1.22.0
lightningcss-linux-arm64-gnu: 1.22.0
lightningcss-linux-arm64-musl: 1.22.0
lightningcss-linux-x64-gnu: 1.22.0
lightningcss-linux-x64-musl: 1.22.0
lightningcss-win32-x64-msvc: 1.22.0
dev: false
/lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@@ -3846,8 +3963,8 @@ packages:
engines: {node: '>=12'}
dev: false
/lucide-react@0.288.0(react@18.2.0):
resolution: {integrity: sha512-ikhb/9LOkq9orPoLV9lLC4UYyoXQycBhIgH7H59ahOkk0mkcAqkD52m84RXedE/qVqZHW8rEJquInT4xGmsNqw==}
/lucide-react@0.292.0(react@18.2.0):
resolution: {integrity: sha512-rRgUkpEHWpa5VCT66YscInCQmQuPCB1RFRzkkxMxg4b+jaL0V12E3riWWR2Sh5OIiUhCwGW/ZExuEO4Az32E6Q==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0
dependencies:
@@ -4539,8 +4656,8 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
/nanoid@5.0.2:
resolution: {integrity: sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==}
/nanoid@5.0.3:
resolution: {integrity: sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==}
engines: {node: ^18 || >=20}
hasBin: true
dev: false
@@ -4632,8 +4749,8 @@ packages:
- supports-color
dev: false
/node-html-parser@6.1.10:
resolution: {integrity: sha512-6/uWdWxjQWQ7tMcFK2wWlrflsQUzh1HsEzlIf2j5+TtzfhT2yUvg3DwZYAmjEHeR3uX74ko7exjHW69J0tOzIg==}
/node-html-parser@6.1.11:
resolution: {integrity: sha512-FAgwwZ6h0DSDWxfD0Iq1tsDcBCxdJB1nXpLPPxX8YyVWzbfCjKWEzaynF4gZZ/8hziUmp7ZSaKylcn0iKhufUQ==}
dependencies:
css-select: 5.1.0
he: 1.2.0
@@ -4828,8 +4945,8 @@ packages:
mimic-fn: 4.0.0
dev: false
/openai@4.13.0:
resolution: {integrity: sha512-EPqHcB0got9cXDZmQae1KytgA4YWtTnUc7tV8hlahZtcO70DMa4kiaXoxnutj9lwmeKQO7ntG+6pmXtrCMejuQ==}
/openai@4.17.1:
resolution: {integrity: sha512-8IrjeT9B63/6rKA2NI/0LFnjRA37G+NvNwbRvcEH9+AjAahReSMtT8xs4J+ClhzdUL9d91GJymgbo9RJt+HQuQ==}
hasBin: true
dependencies:
'@types/node': 18.18.6
@@ -4905,6 +5022,19 @@ packages:
aggregate-error: 3.1.0
dev: false
/p-queue@7.4.1:
resolution: {integrity: sha512-vRpMXmIkYF2/1hLBKisKeVYJZ8S2tZ0zEAmIJgdVKP2nq0nh4qCdf8bgw+ZgKrkh71AOCaqzwbJJk1WtdcF3VA==}
engines: {node: '>=12'}
dependencies:
eventemitter3: 5.0.1
p-timeout: 5.1.0
dev: false
/p-timeout@5.1.0:
resolution: {integrity: sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==}
engines: {node: '>=12'}
dev: false
/p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
@@ -5177,8 +5307,8 @@ packages:
which-pm: 2.0.0
dev: false
/prettier-plugin-astro@0.12.0:
resolution: {integrity: sha512-8E+9YQR6/5CPZJs8XsfBw579zrwZkc0Wb7x0fRVm/51JC8Iys4lBw4ecV8fHwpbQnzve86TUa4fJ08BJzqfWnA==}
/prettier-plugin-astro@0.12.1:
resolution: {integrity: sha512-1mlNIU/cV+25oB4z5wXzOz2fSDcawG3MsVUwgw2i8VSy7voLSENMSpR1juu3U5MAVUo3owuyax11QuylbpuqOQ==}
engines: {node: ^14.15.0 || >=16.0.0}
dependencies:
'@astrojs/compiler': 1.8.2
@@ -5186,8 +5316,8 @@ packages:
sass-formatter: 0.7.8
dev: true
/prettier-plugin-tailwindcss@0.5.6(prettier-plugin-astro@0.12.0)(prettier@3.0.3):
resolution: {integrity: sha512-2Xgb+GQlkPAUCFi3sV+NOYcSI5XgduvDBL2Zt/hwJudeKXkyvRS65c38SB0yb9UB40+1rL83I6m0RtlOQ8eHdg==}
/prettier-plugin-tailwindcss@0.5.7(prettier-plugin-astro@0.12.1)(prettier@3.0.3):
resolution: {integrity: sha512-4v6uESAgwCni6YF6DwJlRaDjg9Z+al5zM4JfngcazMy4WEf/XkPS5TEQjbD+DZ5iNuG6RrKQLa/HuX2SYzC3kQ==}
engines: {node: '>=14.21.3'}
peerDependencies:
'@ianvs/prettier-plugin-sort-imports': '*'
@@ -5239,7 +5369,7 @@ packages:
optional: true
dependencies:
prettier: 3.0.3
prettier-plugin-astro: 0.12.0
prettier-plugin-astro: 0.12.1
dev: true
/prettier@3.0.3:
@@ -5393,18 +5523,18 @@ packages:
loose-envify: 1.4.0
dev: false
/reactflow@11.9.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0):
/reactflow@11.9.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-IHAKBkJngNvU9y1vZ5Nw9rvA3Z+zc9geTgQQIi9qq9Y9knGLlDDr9KfsjbFMew9AycAAgVg8TvBEakF4IT5lqg==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
dependencies:
'@reactflow/background': 11.3.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/controls': 11.2.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.9.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/minimap': 11.7.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/node-resizer': 2.2.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/node-toolbar': 1.3.4(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/background': 11.3.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/controls': 11.2.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/core': 11.9.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/minimap': 11.7.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/node-resizer': 2.2.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
'@reactflow/node-toolbar': 1.3.4(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
@@ -5450,6 +5580,10 @@ packages:
dependencies:
picomatch: 2.3.1
/regenerator-runtime@0.14.0:
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
dev: false
/registry-auth-token@5.0.2:
resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==}
engines: {node: '>=14'}
@@ -6090,12 +6224,14 @@ packages:
picocolors: 1.0.0
dev: false
/tailwind-merge@1.14.0:
resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==}
/tailwind-merge@2.0.0:
resolution: {integrity: sha512-WO8qghn9yhsldLSg80au+3/gY9E4hFxIvQ3qOmlpXnqpDKoMruKfi/56BbbMg6fHTQJ9QD3cc79PoWqlaQE4rw==}
dependencies:
'@babel/runtime': 7.23.2
dev: false
/tailwindcss@3.3.3:
resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==}
/tailwindcss@3.3.5:
resolution: {integrity: sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==}
engines: {node: '>=14.0.0'}
hasBin: true
dependencies:
@@ -6172,8 +6308,8 @@ packages:
yallist: 4.0.0
dev: false
/terser@5.22.0:
resolution: {integrity: sha512-hHZVLgRA2z4NWcN6aS5rQDc+7Dcy58HOf2zbYwmFcQ+ua3h6eEFf5lIDKTzbWwlazPyOZsFQO8V80/IjVNExEw==}
/terser@5.24.0:
resolution: {integrity: sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==}
engines: {node: '>=10'}
hasBin: true
dependencies:
@@ -6227,7 +6363,7 @@ packages:
/ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
/tsconfck@3.0.0(typescript@5.3.0-dev.20231021):
/tsconfck@3.0.0(typescript@5.2.2):
resolution: {integrity: sha512-w3wnsIrJNi7avf4Zb0VjOoodoO0woEqGgZGQm+LHH9przdUI+XDKsWAXwxHA1DaRTjeuZNcregSzr7RaA8zG9A==}
engines: {node: ^18 || >=20}
hasBin: true
@@ -6237,7 +6373,7 @@ packages:
typescript:
optional: true
dependencies:
typescript: 5.3.0-dev.20231021
typescript: 5.2.2
dev: false
/tslib@2.6.2:
@@ -6286,7 +6422,7 @@ packages:
peerDependencies:
typedoc: ^0.25.0
dependencies:
typedoc: 0.25.2(typescript@5.3.0-dev.20231021)
typedoc: 0.25.2(typescript@5.2.2)
typescript: 5.2.2
dev: false
@@ -6295,7 +6431,7 @@ packages:
peerDependencies:
typedoc: '>= 0.23.14 || 0.24.x || 0.25.x'
dependencies:
typedoc: 0.25.2(typescript@5.3.0-dev.20231021)
typedoc: 0.25.2(typescript@5.2.2)
dev: false
/typedoc-plugin-merge-modules@5.1.0(typedoc@0.25.2):
@@ -6303,7 +6439,7 @@ packages:
peerDependencies:
typedoc: 0.24.x || 0.25.x
dependencies:
typedoc: 0.25.2(typescript@5.3.0-dev.20231021)
typedoc: 0.25.2(typescript@5.2.2)
dev: false
/typedoc-plugin-remove-references@0.0.6:
@@ -6315,7 +6451,7 @@ packages:
peerDependencies:
typedoc: 0.22.x || 0.23.x || 0.24.x || 0.25.x
dependencies:
typedoc: 0.25.2(typescript@5.3.0-dev.20231021)
typedoc: 0.25.2(typescript@5.2.2)
dev: false
/typedoc-plugin-zod@1.1.0(typedoc@0.25.2):
@@ -6323,10 +6459,10 @@ packages:
peerDependencies:
typedoc: 0.23.x || 0.24.x || 0.25.x
dependencies:
typedoc: 0.25.2(typescript@5.3.0-dev.20231021)
typedoc: 0.25.2(typescript@5.2.2)
dev: false
/typedoc@0.25.2(typescript@5.3.0-dev.20231021):
/typedoc@0.25.2(typescript@5.2.2):
resolution: {integrity: sha512-286F7BeATBiWe/qC4PCOCKlSTwfnsLbC/4cZ68oGBbvAqb9vV33quEOXx7q176OXotD+JdEerdQ1OZGJ818lnA==}
engines: {node: '>= 16'}
hasBin: true
@@ -6337,27 +6473,27 @@ packages:
marked: 4.3.0
minimatch: 9.0.3
shiki: 0.14.5
typescript: 5.3.0-dev.20231021
typescript: 5.2.2
dev: false
/typescript-esbuild@0.2.25:
resolution: {integrity: sha512-sOpKewLkTFG7ebqSTZNsHdnvoZ7X8jy7F1NjBYAdwiVfKFOR7fLuweYd17hRUTqcAf2wp28BJNqJ6c3WLgrMfw==}
/typescript-esbuild@0.3.1:
resolution: {integrity: sha512-ZJwbrzS8Jv2OTAPbFEvD85CbjRLVYbjUV51JPn3THe/bbh2pY+P9UVVZ8QUvHag8kpqEwgrdhVhYgrdgUkC/lw==}
hasBin: true
dependencies:
'@types/node': 20.8.7
'@types/node': 20.8.9
commander: 11.1.0
deepmerge-ts: 5.1.0
esbuild: 0.19.5
esbuild-plugin-copy: 2.1.1(esbuild@0.19.5)
fast-glob: 3.3.1
typedoc: 0.25.2(typescript@5.3.0-dev.20231021)
typedoc: 0.25.2(typescript@5.2.2)
typedoc-plugin-keywords: 1.5.0(typedoc@0.25.2)
typedoc-plugin-mdn-links: 3.1.0(typedoc@0.25.2)
typedoc-plugin-merge-modules: 5.1.0(typedoc@0.25.2)
typedoc-plugin-remove-references: 0.0.6
typedoc-plugin-rename-defaults: 0.6.7(typedoc@0.25.2)
typedoc-plugin-zod: 1.1.0(typedoc@0.25.2)
typescript: 5.3.0-dev.20231021
typescript: 5.2.2
dev: false
/typescript@5.2.2:
@@ -6366,12 +6502,6 @@ packages:
hasBin: true
dev: false
/typescript@5.3.0-dev.20231021:
resolution: {integrity: sha512-Q9le8GTluHWR1d6lo8anl5+ImJrZ9KApSpM4yEkDIMVcKueCC6q+Wz/kDL6FXPcO8odWHhDQD4vImed7KDp9Fw==}
engines: {node: '>=14.17'}
hasBin: true
dev: false
/uc.micro@1.0.6:
resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
dev: true
@@ -6380,9 +6510,8 @@ packages:
resolution: {integrity: sha512-qh4mBffhlkiXwDAOxvSGxhL0QEQsTbnP9BozOK3OYPEGvPvdWzvAUaXNtUSMdNsKDtuyjEbyVUPFZ52SSLhLqw==}
dev: false
/undici-types@5.25.3:
resolution: {integrity: sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==}
dev: false
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
/unherit@3.0.1:
resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==}
@@ -6816,12 +6945,12 @@ packages:
engines: {node: '>=12.20'}
dev: false
/zod@3.21.1:
resolution: {integrity: sha512-+dTu2m6gmCbO9Ahm4ZBDapx2O6ZY9QSPXst2WXjcznPMwf2YNpn3RevLx4KkZp1OPW/ouFcoBtBzFz/LeY69oA==}
/zod@3.22.4:
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
dev: false
/zustand@4.4.4(@types/react@18.2.31)(react@18.2.0):
resolution: {integrity: sha512-5UTUIAiHMNf5+mFp7/AnzJXS7+XxktULFN0+D1sCiZWyX7ZG+AQpqs2qpYrynRij4QvoDdCD+U+bmg/cG3Ucxw==}
/zustand@4.4.6(@types/react@18.2.37)(react@18.2.0):
resolution: {integrity: sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'
@@ -6835,7 +6964,7 @@ packages:
react:
optional: true
dependencies:
'@types/react': 18.2.31
'@types/react': 18.2.37
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32">
<path fill="#94a3b8" d="M5 5v22h22V5zm2 2h18v18H7zm4.5 4l3.5 6v5h2v-5l3.5-6h-2L16 15.281L13.5 11z"></path>
</svg>

After

Width:  |  Height:  |  Size: 203 B

1
public/images/reddit.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1024 1024"><path fill="#94a3b8" d="M288 568a56 56 0 1 0 112 0a56 56 0 1 0-112 0zm338.7 119.7c-23.1 18.2-68.9 37.8-114.7 37.8s-91.6-19.6-114.7-37.8c-14.4-11.3-35.3-8.9-46.7 5.5s-8.9 35.3 5.5 46.7C396.3 771.6 457.5 792 512 792s115.7-20.4 155.9-52.1a33.25 33.25 0 1 0-41.2-52.2zM960 456c0-61.9-50.1-112-112-112c-42.1 0-78.7 23.2-97.9 57.6c-57.6-31.5-127.7-51.8-204.1-56.5L612.9 195l127.9 36.9c11.5 32.6 42.6 56.1 79.2 56.1c46.4 0 84-37.6 84-84s-37.6-84-84-84c-32 0-59.8 17.9-74 44.2L603.5 123a33.2 33.2 0 0 0-39.6 18.4l-90.8 203.9c-74.5 5.2-142.9 25.4-199.2 56.2A111.94 111.94 0 0 0 176 344c-61.9 0-112 50.1-112 112c0 45.8 27.5 85.1 66.8 102.5c-7.1 21-10.8 43-10.8 65.5c0 154.6 175.5 280 392 280s392-125.4 392-280c0-22.6-3.8-44.5-10.8-65.5C932.5 541.1 960 501.8 960 456zM820 172.5a31.5 31.5 0 1 1 0 63a31.5 31.5 0 0 1 0-63zM120 456c0-30.9 25.1-56 56-56a56 56 0 0 1 50.6 32.1c-29.3 22.2-53.5 47.8-71.5 75.9a56.23 56.23 0 0 1-35.1-52zm392 381.5c-179.8 0-325.5-95.6-325.5-213.5S332.2 410.5 512 410.5S837.5 506.1 837.5 624S691.8 837.5 512 837.5zM868.8 508c-17.9-28.1-42.2-53.7-71.5-75.9c9-18.9 28.3-32.1 50.6-32.1c30.9 0 56 25.1 56 56c.1 23.5-14.5 43.7-35.1 52zM624 568a56 56 0 1 0 112 0a56 56 0 1 0-112 0z"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

View File

@@ -39,7 +39,7 @@ Here is the list of available roadmaps with more being actively worked upon.
- [QA Roadmap](https://roadmap.sh/qa)
- [Python Roadmap](https://roadmap.sh/python)
- [Software Architect Roadmap](https://roadmap.sh/software-architect)
- [Game Developer Roadmap](https://roadmap.sh/game-developer)
- [Game Developer Roadmap](https://roadmap.sh/game-developer) / [Server Side Game Developer](https://roadmap.sh/server-side-game-developer)
- [Software Design and Architecture Roadmap](https://roadmap.sh/software-design-architecture)
- [JavaScript Roadmap](https://roadmap.sh/javascript)
- [TypeScript Roadmap](https://roadmap.sh/typescript)
@@ -67,6 +67,7 @@ Here is the list of available roadmaps with more being actively worked upon.
- [UX Design Roadmap](https://roadmap.sh/ux-design)
- [Docker Roadmap](https://roadmap.sh/docker)
- [Prompt Engineering Roadmap](https://roadmap.sh/prompt-engineering)
- [Technical Writer Roadmap](https://roadmap.sh/technical-writer)
There are also interactive best practices:

View File

@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
#!/usr/bin/env bash
set -e

View File

@@ -1,14 +1,11 @@
import RoadmapIcon from '../../icons/roadmap.svg';
import { RoadmapIcon } from "../ReactIcons/RoadmapIcon";
export function EmptyActivity() {
return (
<div className="rounded-md">
<div className="flex flex-col items-center p-7 text-center">
<img
alt="no roadmaps"
src={RoadmapIcon.src}
className="mb-2 w-[60px] h-[60px] sm:h-[120px] sm:w-[120px] opacity-10"
/>
<RoadmapIcon className="mb-2 w-[60px] h-[60px] sm:h-[120px] sm:w-[120px] opacity-10" />
<h2 className="text-lg sm:text-xl font-bold">No Progress</h2>
<p className="my-1 sm:my-2 max-w-[400px] text-gray-500 text-sm sm:text-base">
Progress will appear here as you start tracking your{' '}

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import GitHubIcon from '../../icons/github.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
import Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { httpGet } from '../../lib/http';
import { Spinner } from '../ReactIcons/Spinner.tsx';
type GitHubButtonProps = {};
@@ -13,7 +13,6 @@ const GITHUB_LAST_PAGE = 'githubLastPage';
export function GitHubButton(props: GitHubButtonProps) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const icon = isLoading ? SpinnerIcon : GitHubIcon;
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
@@ -29,7 +28,7 @@ export function GitHubButton(props: GitHubButtonProps) {
httpGet<{ token: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-github-callback${
window.location.search
}`
}`,
)
.then(({ response, error }) => {
if (!response?.token) {
@@ -81,12 +80,12 @@ export function GitHubButton(props: GitHubButtonProps) {
setIsLoading(true);
const { response, error } = await httpGet<{ loginUrl: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-github-login`
`${import.meta.env.PUBLIC_API_URL}/v1-github-login`,
);
if (error || !response?.loginUrl) {
setError(
error?.message || 'Something went wrong. Please try again later.'
error?.message || 'Something went wrong. Please try again later.',
);
setIsLoading(false);
@@ -97,7 +96,7 @@ export function GitHubButton(props: GitHubButtonProps) {
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname
window.location.pathname,
)
? window.location.pathname + window.location.search
: window.location.pathname;
@@ -116,11 +115,11 @@ export function GitHubButton(props: GitHubButtonProps) {
disabled={isLoading}
onClick={handleClick}
>
<img
src={icon.src}
alt="GitHub"
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>
{isLoading ? (
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
) : (
<GitHubIcon className={'h-[18px] w-[18px]'} />
)}
Continue with GitHub
</button>
{error && (

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import GoogleIcon from '../../icons/google.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { httpGet } from '../../lib/http';
import { Spinner } from '../ReactIcons/Spinner.tsx';
import { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx';
type GoogleButtonProps = {};
@@ -13,7 +13,6 @@ const GOOGLE_LAST_PAGE = 'googleLastPage';
export function GoogleButton(props: GoogleButtonProps) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const icon = isLoading ? SpinnerIcon : GoogleIcon;
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
@@ -29,7 +28,7 @@ export function GoogleButton(props: GoogleButtonProps) {
httpGet<{ token: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-google-callback${
window.location.search
}`
}`,
)
.then(({ response, error }) => {
if (!response?.token) {
@@ -79,7 +78,7 @@ export function GoogleButton(props: GoogleButtonProps) {
const handleClick = () => {
setIsLoading(true);
httpGet<{ loginUrl: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-google-login`
`${import.meta.env.PUBLIC_API_URL}/v1-google-login`,
)
.then(({ response, error }) => {
if (!response?.loginUrl) {
@@ -93,7 +92,7 @@ export function GoogleButton(props: GoogleButtonProps) {
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname
window.location.pathname,
)
? window.location.pathname + window.location.search
: window.location.pathname;
@@ -117,11 +116,11 @@ export function GoogleButton(props: GoogleButtonProps) {
disabled={isLoading}
onClick={handleClick}
>
<img
src={icon.src}
alt="Google"
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>
{isLoading ? (
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
) : (
<GoogleIcon className={'h-[18px] w-[18px]'} />
)}
Continue with Google
</button>
{error && (

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import LinkedIn from '../../icons/linkedin.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { httpGet } from '../../lib/http';
import { Spinner } from '../ReactIcons/Spinner.tsx';
import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx';
type LinkedInButtonProps = {};
@@ -13,7 +13,6 @@ const LINKEDIN_LAST_PAGE = 'linkedInLastPage';
export function LinkedInButton(props: LinkedInButtonProps) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const icon = isLoading ? SpinnerIcon : LinkedIn;
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
@@ -29,7 +28,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
httpGet<{ token: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-callback${
window.location.search
}`
}`,
)
.then(({ response, error }) => {
if (!response?.token) {
@@ -79,7 +78,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
const handleClick = () => {
setIsLoading(true);
httpGet<{ loginUrl: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-login`
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-login`,
)
.then(({ response, error }) => {
if (!response?.loginUrl) {
@@ -93,7 +92,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname
window.location.pathname,
)
? window.location.pathname + window.location.search
: window.location.pathname;
@@ -117,11 +116,11 @@ export function LinkedInButton(props: LinkedInButtonProps) {
disabled={isLoading}
onClick={handleClick}
>
<img
src={icon.src}
alt="Google"
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>
{isLoading ? (
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
) : (
<LinkedInIcon className={'h-[18px] w-[18px]'} />
)}
Continue with LinkedIn
</button>
{error && (

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import { httpPost } from '../../lib/http';
import ErrorIcon from '../../icons/error.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { Spinner } from '../ReactIcons/Spinner';
import { ErrorIcon2 } from '../ReactIcons/ErrorIcon2';
export function TriggerVerifyAccount() {
const [isLoading, setIsLoading] = useState(true);
@@ -16,7 +16,7 @@ export function TriggerVerifyAccount() {
`${import.meta.env.PUBLIC_API_URL}/v1-verify-account`,
{
code,
}
},
)
.then(({ response, error }) => {
if (!response?.token) {
@@ -55,20 +55,8 @@ export function TriggerVerifyAccount() {
return (
<div className="mx-auto flex max-w-md flex-col items-center pt-0 sm:pt-12">
<div className="mx-auto max-w-md text-center">
{isLoading && (
<img
alt={'Please wait.'}
src={SpinnerIcon.src}
className={'mx-auto h-16 w-16 animate-spin'}
/>
)}
{error && (
<img
alt={'Please wait.'}
src={ErrorIcon.src}
className={'mx-auto h-16 w-16'}
/>
)}
{isLoading && <Spinner className="mx-auto h-16 w-16" />}
{error && <ErrorIcon2 className="mx-auto h-16 w-16" />}
<h2 className="mb-1 mt-4 text-center text-xl font-semibold sm:mb-3 sm:mt-4 sm:text-2xl">
Verifying your account
</h2>

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import VerifyLetterIcon from '../../icons/verify-letter.svg';
import { httpPost } from '../../lib/http';
import { VerifyLetterIcon } from '../ReactIcons/VerifyLetterIcon';
export function VerificationEmailMessage() {
const [email, setEmail] = useState('..');
@@ -37,11 +37,7 @@ export function VerificationEmailMessage() {
return (
<div className="mx-auto max-w-md text-center">
<img
alt="Verify Email"
src={VerifyLetterIcon.src}
className="mx-auto mb-4 h-20 w-40 sm:h-40"
/>
<VerifyLetterIcon className="mx-auto mb-4 h-20 w-40 sm:h-40" />
<h2 className="my-2 text-center text-xl font-semibold sm:my-5 sm:text-2xl">
Verify your email address
</h2>

View File

@@ -1,35 +1,47 @@
import { Fragment, useEffect, useRef, useState } from 'react';
import {
Fragment,
type ReactElement,
useEffect,
useRef,
useState,
} from 'react';
import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click';
import BestPracticesIcon from '../../icons/best-practices.svg';
import ClipboardIcon from '../../icons/clipboard.svg';
import GuideIcon from '../../icons/guide.svg';
import HomeIcon from '../../icons/home.svg';
import RoadmapIcon from '../../icons/roadmap.svg';
import UserIcon from '../../icons/user.svg';
import GroupIcon from '../../icons/group.svg';
import VideoIcon from '../../icons/video.svg';
import { httpGet } from '../../lib/http';
import { isLoggedIn } from '../../lib/jwt';
import { BestPracticesIcon } from '../ReactIcons/BestPracticesIcon.tsx';
import { UserIcon } from '../ReactIcons/UserIcon.tsx';
import { GroupIcon } from '../ReactIcons/GroupIcon.tsx';
import { RoadmapIcon } from '../ReactIcons/RoadmapIcon.tsx';
import { ClipboardIcon } from '../ReactIcons/ClipboardIcon.tsx';
import { GuideIcon } from '../ReactIcons/GuideIcon.tsx';
import { HomeIcon } from '../ReactIcons/HomeIcon.tsx';
import { VideoIcon } from '../ReactIcons/VideoIcon.tsx';
export type PageType = {
id: string;
url: string;
title: string;
group: string;
icon?: string;
icon?: ReactElement;
isProtected?: boolean;
metadata?: Record<string, any>;
};
const defaultPages: PageType[] = [
{ id: 'home', url: '/', title: 'Home', group: 'Pages', icon: HomeIcon.src },
{
id: 'home',
url: '/',
title: 'Home',
group: 'Pages',
icon: <HomeIcon className="mr-2 h-4 w-4 stroke-2" />,
},
{
id: 'account',
url: '/account',
title: 'Account',
group: 'Pages',
icon: UserIcon.src,
icon: <UserIcon className="mr-2 h-4 w-4 stroke-2" />,
isProtected: true,
},
{
@@ -37,7 +49,7 @@ const defaultPages: PageType[] = [
url: '/team',
title: 'Teams',
group: 'Pages',
icon: GroupIcon.src,
icon: <GroupIcon className="mr-2 h-4 w-4 stroke-2" />,
isProtected: true,
},
{
@@ -45,7 +57,7 @@ const defaultPages: PageType[] = [
url: '/account/friends',
title: 'Friends',
group: 'Pages',
icon: GroupIcon.src,
icon: <GroupIcon className="mr-2 h-4 w-4 stroke-2" />,
isProtected: true,
},
{
@@ -53,14 +65,14 @@ const defaultPages: PageType[] = [
url: '/roadmaps',
title: 'Roadmaps',
group: 'Pages',
icon: RoadmapIcon.src,
icon: <RoadmapIcon className="mr-2 h-4 w-4 stroke-2" />,
},
{
id: 'account-roadmaps',
url: '/account/roadmaps',
title: 'Custom Roadmaps',
group: 'Pages',
icon: RoadmapIcon.src,
icon: <RoadmapIcon className="mr-2 h-4 w-4 stroke-2" />,
isProtected: true,
},
{
@@ -68,28 +80,28 @@ const defaultPages: PageType[] = [
url: '/best-practices',
title: 'Best Practices',
group: 'Pages',
icon: BestPracticesIcon.src,
icon: <BestPracticesIcon className="mr-2 h-4 w-4 stroke-2" />,
},
{
id: 'questions',
url: '/questions',
title: 'Questions',
group: 'Pages',
icon: ClipboardIcon.src,
icon: <ClipboardIcon className="mr-2 h-4 w-4 stroke-2" />,
},
{
id: 'guides',
url: '/guides',
title: 'Guides',
group: 'Pages',
icon: GuideIcon.src,
icon: <GuideIcon className="mr-2 h-4 w-4 stroke-2" />,
},
{
id: 'videos',
url: '/videos',
title: 'Videos',
group: 'Pages',
icon: VideoIcon.src,
icon: <VideoIcon className="mr-2 h-4 w-4 stroke-2" />,
},
];
@@ -199,7 +211,7 @@ export function CommandMenu() {
} else if (e.key === 'ArrowUp') {
const canGoPrev = activeCounter > 0;
setActiveCounter(
canGoPrev ? activeCounter - 1 : searchResults.length - 1
canGoPrev ? activeCounter - 1 : searchResults.length - 1,
);
} else if (e.key === 'Tab') {
e.preventDefault();
@@ -242,13 +254,7 @@ export function CommandMenu() {
{!page.icon && (
<span className="mr-2 text-gray-400">{page.group}</span>
)}
{page.icon && (
<img
alt={page.title}
src={page.icon}
className="mr-2 h-4 w-4"
/>
)}
{page.icon && page.icon}
{page.title}
</a>
</Fragment>

View File

@@ -1,4 +1,4 @@
import ChevronDownIcon from '../../icons/chevron-down.svg';
import { ChevronDownIcon } from '../ReactIcons/ChevronDownIcon';
type NotDropdownProps = {
onClick: () => void;
@@ -37,11 +37,7 @@ export function NotDropdown(props: NotDropdownProps) {
</div>
)}
<img
alt={singularName}
src={ChevronDownIcon.src}
className={'relative top-[1px] h-[17px] w-[17px] opacity-40'}
/>
<ChevronDownIcon className="relative top-[1px] h-[17px] w-[17px] opacity-40" />
</div>
);
}

View File

@@ -3,8 +3,8 @@ import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click';
import type { PageType } from '../CommandMenu/CommandMenu';
import type { TeamResourceConfig } from './RoadmapSelector';
import CloseIcon from '../../icons/close.svg';
import { SelectRoadmapModalItem } from './SelectRoadmapModalItem';
import { XIcon } from 'lucide-react';
export type SelectRoadmapModalProps = {
teamId: string;
@@ -60,11 +60,11 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
setSearchResults(searchResults);
}, [searchText, allRoadmaps]);
const roleBasedRoadmaps = searchResults.filter((roadmap) =>
roadmap?.metadata?.tags?.includes('role-roadmap')
const roleBasedRoadmaps = searchResults.filter(
(roadmap) => roadmap?.metadata?.tags?.includes('role-roadmap'),
);
const skillBasedRoadmaps = searchResults.filter((roadmap) =>
roadmap?.metadata?.tags?.includes('skill-roadmap')
const skillBasedRoadmaps = searchResults.filter(
(roadmap) => roadmap?.metadata?.tags?.includes('skill-roadmap'),
);
return (
@@ -79,7 +79,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
className="popup-close absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-100 hover:text-gray-900"
onClick={onClose}
>
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
<XIcon className="h-4 w-4" />
<span className="sr-only">Close modal</span>
</button>
<input
@@ -101,7 +101,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
<div className="mb-5 flex flex-wrap items-center gap-2">
{roleBasedRoadmaps.map((roadmap) => {
const isSelected = !!teamResourceConfig?.find(
(r) => r.resourceId === roadmap.id
(r) => r.resourceId === roadmap.id,
);
return (
@@ -127,7 +127,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
<div className="flex flex-wrap items-center gap-2">
{skillBasedRoadmaps.map((roadmap) => {
const isSelected = !!teamResourceConfig.find(
(r) => r.resourceId === roadmap.id
(r) => r.resourceId === roadmap.id,
);
return (

View File

@@ -1,22 +1,22 @@
import BuildingIcon from '../../icons/building.svg';
import UsersIcon from '../../icons/users.svg';
import type { TeamDocument } from './CreateTeamForm';
import { httpPut } from '../../lib/http';
import { useState } from 'react';
import { NextButton } from './NextButton';
import { BuildingIcon } from '../ReactIcons/BuildingIcon.tsx';
import { UsersIcon } from '../ReactIcons/UsersIcon.tsx';
export const validTeamTypes = [
{
value: 'company',
label: 'Company',
icon: BuildingIcon.src,
icon: BuildingIcon,
description:
'Track the skills and learning progress of the tech team at your company',
},
{
value: 'study_group',
label: 'Study Group',
icon: UsersIcon.src,
icon: UsersIcon,
description:
'Invite your friends or course-mates and track your learning progress together',
},
@@ -56,7 +56,7 @@ export function Step0(props: Step0Props) {
teamSize: team.teamSize,
linkedInUrl: team?.links?.linkedIn || undefined,
}),
}
},
);
if (error || !response) {
@@ -76,21 +76,20 @@ export function Step0(props: Step0Props) {
{validTeamTypes.map((validTeamType) => (
<button
key={validTeamType.value}
className={`flex flex-grow flex-col items-center rounded-lg border px-5 pt-12 pb-10 ${
className={`flex flex-grow flex-col items-center rounded-lg border px-5 pb-10 pt-12 ${
validTeamType.value == selectedTeamType
? 'border-gray-400 bg-gray-100'
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-50'
}`}
onClick={() => setSelectedTeamType(validTeamType.value)}
>
<img
key={validTeamType.value}
alt={validTeamType.label}
src={validTeamType.icon}
className={`mb-3 h-12 w-12 opacity-10 ${
validTeamType.value === selectedTeamType ? 'opacity-100' : ''
}`}
/>
{
<validTeamType.icon
className={`mb-3 h-12 w-12 opacity-10 ${
validTeamType.value === selectedTeamType ? 'opacity-100' : ''
}`}
/>
}
<span className="mb-2 block text-2xl font-bold">
{validTeamType.label}
</span>

View File

@@ -13,6 +13,7 @@ import type { Node } from 'reactflow';
import { useCallback, type MouseEvent, useMemo, useState, useRef } from 'react';
import { EmptyRoadmap } from './EmptyRoadmap';
import { cn } from '../../lib/classname';
import { totalRoadmapNodes } from '../../stores/roadmap.ts';
type FlowRoadmapRendererProps = {
roadmap: RoadmapDocument;
@@ -138,6 +139,12 @@ export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
)}
onRendered={() => {
renderResourceProgress('roadmap', roadmapId).then(() => {
totalRoadmapNodes.set(
roadmap?.nodes?.filter((node) => {
return ['topic', 'subtopic'].includes(node.type);
}).length || 0,
);
if (roadmap?.nodes?.length === 0) {
setHideRenderer(true);
editorWrapperRef?.current?.classList.add('hidden');

View File

@@ -1,7 +1,7 @@
import MoreIcon from '../../icons/more-vertical.svg';
import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { Lock, MoreVertical, Shapes, Trash2 } from 'lucide-react';
import { MoreVerticalIcon } from '../ReactIcons/MoreVerticalIcon.tsx';
type PersonalRoadmapActionDropdownProps = {
onDelete?: () => void;
@@ -9,7 +9,9 @@ type PersonalRoadmapActionDropdownProps = {
onUpdateSharing?: () => void;
};
export function PersonalRoadmapActionDropdown(props: PersonalRoadmapActionDropdownProps) {
export function PersonalRoadmapActionDropdown(
props: PersonalRoadmapActionDropdownProps,
) {
const { onDelete, onUpdateSharing, onCustomize } = props;
const menuRef = useRef<HTMLDivElement>(null);
@@ -26,7 +28,7 @@ export function PersonalRoadmapActionDropdown(props: PersonalRoadmapActionDropdo
onClick={() => setIsOpen(!isOpen)}
className="hidden items-center opacity-60 transition-opacity hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-30 sm:flex"
>
<img alt="menu" src={MoreIcon.src} className="h-4 w-4" />
<MoreVerticalIcon className={'h-4 w-4'} />
</button>
<button

View File

@@ -14,11 +14,11 @@ import {
type AllowedRoadmapVisibility,
type RoadmapDocument,
} from './CreateRoadmap/CreateRoadmapModal';
import RoadmapIcon from '../../icons/roadmap.svg';
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";
type PersonalRoadmapListType = {
roadmaps: GetRoadmapListResponse['personalRoadmaps'];
@@ -91,11 +91,8 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) {
if (roadmapList.length === 0) {
return (
<div className="flex flex-col items-center p-4 py-20">
<img
alt="roadmap"
src={RoadmapIcon.src}
className="mb-4 h-24 w-24 opacity-10"
/>
<RoadmapIcon className="mb-4 h-24 w-24 opacity-10" />
<h3 className="mb-1 text-2xl font-bold text-gray-900">No roadmaps</h3>
<p className="text-base text-gray-500">
Create a roadmap to get started

View File

@@ -1,6 +1,7 @@
---
import Loader from '../Loader.astro';
import './FrameRenderer.css';
import { ProgressNudge } from "./ProgressNudge";
export interface Props {
resourceType: 'roadmap' | 'best-practice';
@@ -27,4 +28,6 @@ const { resourceId, resourceType, dimensions = null } = Astro.props;
</div>
</div>
<ProgressNudge resourceId={resourceId} resourceType={resourceType} client:only="react" />
<script src='./renderer.ts'></script>

View File

@@ -0,0 +1,65 @@
import { cn } from '../../lib/classname.ts';
import { roadmapProgress, totalRoadmapNodes } from '../../stores/roadmap.ts';
import { useStore } from '@nanostores/react';
type ProgressNudgeProps = {
resourceType: 'roadmap' | 'best-practice';
resourceId: string;
};
export function ProgressNudge(props: ProgressNudgeProps) {
const $totalRoadmapNodes = useStore(totalRoadmapNodes);
const $roadmapProgress = useStore(roadmapProgress);
const done = $roadmapProgress?.done?.length || 0;
const hasProgress = done > 0;
if (!$totalRoadmapNodes) {
return null;
}
return (
<div
className={
'fixed bottom-5 left-1/2 z-30 hidden -translate-x-1/2 transform animate-fade-slide-up overflow-hidden rounded-full bg-stone-900 px-4 py-2 text-center text-white shadow-2xl transition-all duration-300 sm:block'
}
>
<span
className={cn('block', {
hidden: hasProgress,
})}
>
<span className="mr-2 text-sm font-semibold uppercase text-yellow-400">
Tip
</span>
<span className="text-sm text-gray-200">
Right-click on a topic to mark it as done.{' '}
<button
data-popup="progress-help"
className="cursor-pointer font-semibold text-yellow-500 underline"
>
Learn more.
</button>
</span>
</span>
<span
className={cn('relative z-20 block text-sm', {
hidden: !hasProgress,
})}
>
<span className="relative -top-[0.45px] mr-2 text-xs font-medium uppercase text-yellow-400">
Progress
</span>
<span>{done}</span> of <span>{$totalRoadmapNodes}</span> Done
</span>
<span
className="absolute bottom-0 left-0 top-0 z-10 bg-stone-700"
style={{
width: `${(done / $totalRoadmapNodes) * 100}%`,
}}
></span>
</div>
);
}

View File

@@ -7,10 +7,13 @@ import {
renderTopicProgress,
updateResourceProgress,
} from '../../lib/resource-progress';
import type { ResourceProgressType, ResourceType } from '../../lib/resource-progress';
import type {
ResourceProgressType,
ResourceType,
} from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page';
import { showLoginPopup } from '../../lib/popup';
import {replaceChildren} from "../../lib/dom.ts";
import { replaceChildren } from '../../lib/dom.ts';
export class Renderer {
resourceId: string;
@@ -95,7 +98,7 @@ export class Renderer {
.then(() => {
return renderResourceProgress(
this.resourceType as ResourceType,
this.resourceId
this.resourceId,
);
})
.catch((error) => {
@@ -143,7 +146,7 @@ export class Renderer {
this.jsonToSvg(
this.resourceType === 'roadmap'
? `/${this.resourceId}.json`
: `/best-practices/${this.resourceId}.json`
: `/best-practices/${this.resourceId}.json`,
);
}
}
@@ -183,7 +186,7 @@ export class Renderer {
resourceType: this.resourceType as ResourceType,
topicId,
},
newStatus
newStatus,
)
.then(() => {
renderTopicProgress(topicId, newStatus);
@@ -215,9 +218,14 @@ export class Renderer {
const isCurrentStatusDone = targetGroup.classList.contains('done');
const normalizedGroupId = groupId.replace(/^\d+-/, '');
if (normalizedGroupId.startsWith('ext_link:')) {
return;
}
this.updateTopicStatus(
normalizedGroupId,
!isCurrentStatusDone ? 'done' : 'pending'
!isCurrentStatusDone ? 'done' : 'pending',
);
}
@@ -243,9 +251,12 @@ export class Renderer {
action: `${this.resourceType} / ${this.resourceId}`,
label: externalLink,
});
window.open(`https://${externalLink}`);
} else {
window.location.href = `https://${externalLink}`;
}
window.open(`https://${externalLink}`);
return;
}
@@ -265,7 +276,7 @@ export class Renderer {
resourceType: this.resourceType,
resourceId: this.resourceId,
},
})
}),
);
return;
}
@@ -280,7 +291,7 @@ export class Renderer {
e.preventDefault();
this.updateTopicStatus(
normalizedGroupId,
!isCurrentStatusLearning ? 'learning' : 'pending'
!isCurrentStatusLearning ? 'learning' : 'pending',
);
return;
}
@@ -289,7 +300,7 @@ export class Renderer {
e.preventDefault();
this.updateTopicStatus(
normalizedGroupId,
!isCurrentStatusSkipped ? 'skipped' : 'pending'
!isCurrentStatusSkipped ? 'skipped' : 'pending',
);
return;
@@ -302,7 +313,7 @@ export class Renderer {
resourceId: this.resourceId,
resourceType: this.resourceType,
},
})
}),
);
}

View File

@@ -1,6 +1,5 @@
import UserPlusIcon from '../../icons/user-plus.svg';
import CopyIcon from '../../icons/copy.svg';
import { useCopyText } from '../../hooks/use-copy-text';
import { CopyIcon, UserPlus2 } from 'lucide-react';
type EmptyFriendsProps = {
befriendUrl: string;
@@ -13,14 +12,12 @@ export function EmptyFriends(props: EmptyFriendsProps) {
return (
<div className="rounded-md">
<div className="mx-auto flex flex-col items-center p-7 text-center">
<img
alt="no friends"
src={UserPlusIcon.src}
className="mb-2 h-[60px] w-[60px] opacity-10 sm:h-[120px] sm:w-[120px]"
/>
<UserPlus2 className="mb-2 h-[60px] w-[60px] opacity-10 sm:h-[120px] sm:w-[120px]" />
<h2 className="text-lg font-bold sm:text-xl">Invite your Friends</h2>
<p className="mb-4 mt-1 max-w-[400px] text-sm leading-relaxed text-gray-500">
Share the unique link below with your friends to track their skills and progress.
Share the unique link below with your friends to track their skills
and progress.
</p>
<div className="flex w-full max-w-[352px] items-center justify-center gap-2 rounded-lg border-2 p-1 text-sm">
@@ -44,7 +41,8 @@ export function EmptyFriends(props: EmptyFriendsProps) {
copyText(befriendUrl);
}}
>
<img src={CopyIcon.src} className="h-4 w-4" alt="Invite Friends" />
<CopyIcon className="mr-1 h-4 w-4" />
{isCopied ? 'Copied' : 'Copy'}
</button>
</div>

View File

@@ -7,10 +7,10 @@ import type { FriendshipStatus } from '../Befriend';
import { useToast } from '../../hooks/use-toast';
import { EmptyFriends } from './EmptyFriends';
import { FriendProgressItem } from './FriendProgressItem';
import UserIcon from '../../icons/user.svg';
import { UserProgressModal } from '../UserProgress/UserProgressModal';
import { InviteFriendPopup } from './InviteFriendPopup';
import { UserCustomProgressModal } from '../UserProgress/UserCustomProgressModal';
import { UserIcon } from '../ReactIcons/UserIcon.tsx';
type FriendResourceProgress = {
updatedAt: string;
@@ -64,7 +64,7 @@ export function FriendsPage() {
async function loadFriends() {
const { response, error } = await httpGet<ListFriendsResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-list-friends`
`${import.meta.env.PUBLIC_API_URL}/v1-list-friends`,
);
if (error || !response) {
@@ -89,15 +89,15 @@ export function FriendsPage() {
const befriendUrl = `${baseUrl}/befriend?u=${user?.id}`;
const selectedGroupingType = groupingTypes.find(
(grouping) => grouping.value === selectedGrouping
(grouping) => grouping.value === selectedGrouping,
);
const filteredFriends = friends.filter((friend) =>
selectedGroupingType?.statuses.includes(friend.status)
const filteredFriends = friends.filter(
(friend) => selectedGroupingType?.statuses.includes(friend.status),
);
const receivedRequests = friends.filter(
(friend) => friend.status === 'received'
(friend) => friend.status === 'received',
);
if (isLoading) {
@@ -203,11 +203,8 @@ export function FriendsPage() {
{filteredFriends.length === 0 && (
<div className="flex flex-col items-center justify-center py-12">
<img
src={UserIcon.src}
alt="Empty Friends"
className="mb-3 w-12 opacity-20"
/>
<UserIcon className="mb-3 w-12 opacity-20" />
<h2 className="text-lg font-semibold">
{selectedGrouping === 'active' && 'No friends yet'}
{selectedGrouping === 'sent' && 'No requests sent'}

View File

@@ -1,8 +1,8 @@
import type { MouseEvent } from 'react';
import { useRef } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import CopyIcon from '../../icons/copy.svg';
import { useCopyText } from '../../hooks/use-copy-text';
import { CopyIcon } from 'lucide-react';
type InviteFriendPopupProps = {
befriendUrl: string;
@@ -54,11 +54,7 @@ export function InviteFriendPopup(props: InviteFriendPopupProps) {
copyText(befriendUrl);
}}
>
<img
src={CopyIcon.src}
className="h-4 w-4"
alt="Invite Friends"
/>
<CopyIcon className="mr-1 h-4 w-4" />
{isCopied ? 'Copied' : 'Copy URL'}
</button>
</div>

View File

@@ -22,7 +22,7 @@ import { AccountDropdown } from './AccountDropdown';
>Best Practices</a
>
</li>
<li class='hidden lg:inline'>
<li class='hidden xl:inline'>
<a href='/questions' class='text-gray-400 hover:text-white'>Questions</a
>
</li>

View File

@@ -1,10 +1,10 @@
import { useEffect, useState } from 'react';
import { httpGet, httpPatch, httpPost } from '../../lib/http';
import { httpGet, httpPatch } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page';
import type { TeamMemberDocument } from '../TeamMembers/TeamMembersPage';
import XIcon from '../../icons/close-dark.svg';
import AcceptIcon from '../../icons/accept.svg';
import { useToast } from '../../hooks/use-toast';
import { AcceptIcon } from '../ReactIcons/AcceptIcon.tsx';
import { XIcon } from 'lucide-react';
interface NotificationList extends TeamMemberDocument {
name: string;
@@ -18,7 +18,7 @@ export function NotificationPage() {
const lostNotifications = async () => {
const { error, response } = await httpGet<NotificationList[]>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-invitation-list`
`${import.meta.env.PUBLIC_API_URL}/v1-get-invitation-list`,
);
if (error || !response) {
toast.error(error?.message || 'Something went wrong');
@@ -28,28 +28,37 @@ export function NotificationPage() {
setNotifications(response);
};
async function respondInvitation(status: 'accept' | 'reject', inviteId: string) {
async function respondInvitation(
status: 'accept' | 'reject',
inviteId: string,
) {
setIsLoading(true);
setError('');
const { response, error } = await httpPatch<{ teamId: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-respond-invite/${inviteId}`, {
status
});
`${import.meta.env.PUBLIC_API_URL}/v1-respond-invite/${inviteId}`,
{
status,
},
);
if (error || !response) {
setError(error?.message || 'Something went wrong')
setIsLoading(false)
setError(error?.message || 'Something went wrong');
setIsLoading(false);
return;
}
if (status === 'accept') {
window.location.href = `/team/progress?t=${response.teamId}`;
} else {
window.dispatchEvent(new CustomEvent('refresh-notification', {
detail: {
count: notifications.length - 1
}
}));
setNotifications(notifications.filter((notification) => notification._id !== inviteId));
window.dispatchEvent(
new CustomEvent('refresh-notification', {
detail: {
count: notifications.length - 1,
},
}),
);
setNotifications(
notifications.filter((notification) => notification._id !== inviteId),
);
setIsLoading(false);
}
}
@@ -66,15 +75,20 @@ export function NotificationPage() {
<h2 className="text-3xl font-bold sm:text-4xl">Notification</h2>
<p className="mt-2 text-gray-400">Manage your notifications</p>
</div>
{
notifications.length === 0 && (
<div className="flex items-center justify-center mt-6">
<p className="text-gray-400">
No notifications, you can <a href="/team/new" className="text-blue-500 underline hover:no-underline">create a team</a> and invite your friends to join.
</p>
</div>
)
}
{notifications.length === 0 && (
<div className="mt-6 flex items-center justify-center">
<p className="text-gray-400">
No notifications, you can{' '}
<a
href="/team/new"
className="text-blue-500 underline hover:no-underline"
>
create a team
</a>{' '}
and invite your friends to join.
</p>
</div>
)}
<div className="space-y-4">
{notifications.map((notification) => (
<div className="flex items-center justify-between rounded-md border p-2">
@@ -86,19 +100,21 @@ export function NotificationPage() {
</div>
</div>
<div className="flex items-center space-x-2">
<button type="button"
<button
type="button"
disabled={isLoading}
className="inline-flex border p-1 rounded hover:bg-gray-50 disabled:opacity-75"
className="inline-flex rounded border p-1 hover:bg-gray-50 disabled:opacity-75"
onClick={() => respondInvitation('accept', notification?._id!)}
>
<img src={AcceptIcon.src} className="h-4 w-4" />
<AcceptIcon className="h-4 w-4" />
</button>
<button type="button"
<button
type="button"
disabled={isLoading}
className="inline-flex border p-1 rounded hover:bg-gray-50 disabled:opacity-75"
className="inline-flex rounded border p-1 hover:bg-gray-50 disabled:opacity-75"
onClick={() => respondInvitation('reject', notification?._id!)}
>
<img alt={'Close'} src={XIcon.src} className="h-4 w-4" />
<XIcon className="h-4 w-4" />
</button>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { useStore } from '@nanostores/react';
import SpinnerIcon from '../icons/spinner.svg';
import { pageProgressMessage } from '../stores/page';
import { useEffect, useState } from 'react';
import { Spinner } from './ReactIcons/Spinner';
export interface Props {
initialMessage: string;
@@ -30,10 +30,10 @@ export function PageProgress(props: Props) {
{/* Tailwind based spinner for full page */}
<div className="fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-white bg-opacity-75">
<div className="flex items-center justify-center rounded-md border bg-white px-4 py-2 ">
<img
src={SpinnerIcon.src}
alt="Loading"
className="h-4 w-4 animate-spin fill-blue-600 text-gray-200 sm:h-4 sm:w-4"
<Spinner
className="h-4 w-4 sm:h-4 sm:w-4"
outerFill="#e5e7eb"
innerFill="#2563eb"
/>
<h1 className="ml-2">
{message}

View File

@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import CloseIcon from '../icons/close.svg';
import { httpGet } from '../lib/http';
import { sponsorHidden } from '../stores/page';
import { useStore } from '@nanostores/react';
import { X } from 'lucide-react';
export type PageSponsorType = {
company: string;
@@ -46,7 +46,7 @@ export function PageSponsor(props: PageSponsorProps) {
`${import.meta.env.PUBLIC_API_URL}/v1-get-sponsor`,
{
href: window.location.pathname,
}
},
);
if (error) {
@@ -101,7 +101,7 @@ export function PageSponsor(props: PageSponsorProps) {
sponsorHidden.set(true);
}}
>
<img alt="Close" className="h-4 w-4" src={CloseIcon.src} />
<X className="h-4 w-4" />
</span>
<img
src={imageUrl}

View File

@@ -0,0 +1,24 @@
type AcceptIconProps = {
className?: string;
};
export function AcceptIcon(props: AcceptIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="#000"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
);
}

View File

@@ -0,0 +1,28 @@
type BestPracticesIconProps = {
className?: string;
};
export function BestPracticesIcon(props: BestPracticesIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<line x1="10" x2="21" y1="6" y2="6"></line>
<line x1="10" x2="21" y1="12" y2="12"></line>
<line x1="10" x2="21" y1="18" y2="18"></line>
<polyline points="3 6 4 7 6 5"></polyline>
<polyline points="3 12 4 13 6 11"></polyline>
<polyline points="3 18 4 19 6 17"></polyline>
</svg>
);
}

View File

@@ -0,0 +1,29 @@
type BuildingIconProps = {
className?: string;
};
export function BuildingIcon(props: BuildingIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"></path>
<path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"></path>
<path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"></path>
<path d="M10 6h4"></path>
<path d="M10 10h4"></path>
<path d="M10 14h4"></path>
<path d="M10 18h4"></path>
</svg>
);
}

View File

@@ -0,0 +1,28 @@
type ClipboardIconProps = {
className?: string;
};
export function ClipboardIcon(props: ClipboardIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<rect width="8" height="4" x="8" y="2" rx="1" ry="1" />
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
<path d="M12 11h4" />
<path d="M12 16h4" />
<path d="M8 11h.01" />
<path d="M8 16h.01" />
</svg>
);
}

View File

@@ -0,0 +1,28 @@
type CogIconProps = {
className?: string;
};
export function CogIcon(props: CogIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
);
}

View File

@@ -0,0 +1,26 @@
import { cn } from '../../lib/classname';
type DropdownIconProps = {
className?: string;
};
export function DropdownIcon(props: DropdownIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className={cn('h-5 w-5', className)}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
/>
</svg>
);
}

View File

@@ -0,0 +1,48 @@
type ErrorIcon2Props = {
className?: string;
};
export function ErrorIcon2(props: ErrorIcon2Props) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
className={className}
>
<linearGradient
id="wRKXFJsqHCxLE9yyOYHkza"
x1="9.858"
x2="38.142"
y1="9.858"
y2="38.142"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#f44f5a" />
<stop offset=".443" stopColor="#ee3d4a" />
<stop offset="1" stopColor="#e52030" />
</linearGradient>
<path
fill="url(#wRKXFJsqHCxLE9yyOYHkza)"
d="M44,24c0,11.045-8.955,20-20,20S4,35.045,4,24S12.955,4,24,4S44,12.955,44,24z"
/>
<path
d="M33.192,28.95L28.243,24l4.95-4.95c0.781-0.781,0.781-2.047,0-2.828l-1.414-1.414 c-0.781-0.781-2.047-0.781-2.828,0L24,19.757l-4.95-4.95c-0.781-0.781-2.047-0.781-2.828,0l-1.414,1.414 c-0.781,0.781-0.781,2.047,0,2.828l4.95,4.95l-4.95,4.95c-0.781,0.781-0.781,2.047,0,2.828l1.414,1.414 c0.781,0.781,2.047,0.781,2.828,0l4.95-4.95l4.95,4.95c0.781,0.781,2.047,0.781,2.828,0l1.414-1.414 C33.973,30.997,33.973,29.731,33.192,28.95z"
opacity=".05"
/>
<path
d="M32.839,29.303L27.536,24l5.303-5.303c0.586-0.586,0.586-1.536,0-2.121l-1.414-1.414 c-0.586-0.586-1.536-0.586-2.121,0L24,20.464l-5.303-5.303c-0.586-0.586-1.536-0.586-2.121,0l-1.414,1.414 c-0.586,0.586-0.586,1.536,0,2.121L20.464,24l-5.303,5.303c-0.586,0.586-0.586,1.536,0,2.121l1.414,1.414 c0.586,0.586,1.536,0.586,2.121,0L24,27.536l5.303,5.303c0.586,0.586,1.536,0.586,2.121,0l1.414-1.414 C33.425,30.839,33.425,29.889,32.839,29.303z"
opacity=".07"
/>
<path
fill="#fff"
d="M31.071,15.515l1.414,1.414c0.391,0.391,0.391,1.024,0,1.414L18.343,32.485 c-0.391,0.391-1.024,0.391-1.414,0l-1.414-1.414c-0.391-0.391-0.391-1.024,0-1.414l14.142-14.142 C30.047,15.124,30.681,15.124,31.071,15.515z"
/>
<path
fill="#fff"
d="M32.485,31.071l-1.414,1.414c-0.391,0.391-1.024,0.391-1.414,0L15.515,18.343 c-0.391-0.391-0.391-1.024,0-1.414l1.414-1.414c0.391-0.391,1.024-0.391,1.414,0l14.142,14.142 C32.876,30.047,32.876,30.681,32.485,31.071z"
/>
</svg>
);
}

View File

@@ -0,0 +1,20 @@
type GitHubIconProps = {
className?: string;
};
export function GitHubIcon(props: GitHubIconProps) {
const { className } = props;
return (
<svg
className={className || ''}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 98 96"
>
<path
fillRule="evenodd"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362l-.08-9.127c-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126l-.08 13.526c0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
fill="#24292f"
/>
</svg>
);
}

View File

@@ -0,0 +1,32 @@
type GoogleIconProps = {
className?: string;
};
export function GoogleIcon(props: GoogleIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 90 92"
fill="none"
className={className}
>
<path
d="M90 47.1c0-3.1-.3-6.3-.8-9.3H45.9v17.7h24.8c-1 5.7-4.3 10.7-9.2 13.9l14.8 11.5C85 72.8 90 61 90 47.1z"
fill="#4280ef"
/>
<path
d="M45.9 91.9c12.4 0 22.8-4.1 30.4-11.1L61.5 69.4c-4.1 2.8-9.4 4.4-15.6 4.4-12 0-22.1-8.1-25.8-18.9L4.9 66.6c7.8 15.5 23.6 25.3 41 25.3z"
fill="#34a353"
/>
<path
d="M20.1 54.8c-1.9-5.7-1.9-11.9 0-17.6L4.9 25.4c-6.5 13-6.5 28.3 0 41.2l15.2-11.8z"
fill="#f6b704"
/>
<path
d="M45.9 18.3c6.5-.1 12.9 2.4 17.6 6.9L76.6 12C68.3 4.2 57.3 0 45.9.1c-17.4 0-33.2 9.8-41 25.3l15.2 11.8c3.7-10.9 13.8-18.9 25.8-18.9z"
fill="#e54335"
/>
</svg>
);
}

View File

@@ -0,0 +1,26 @@
type GroupIconProps = {
className?: string;
};
export function GroupIcon(props: GroupIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
);
}

View File

@@ -0,0 +1,23 @@
type GuideIconProps = {
className?: string;
};
export function GuideIcon(props: GuideIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 6.75V15m6-6v8.25m.503 3.498l4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 00-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0z"
/>
</svg>
);
}

View File

@@ -0,0 +1,23 @@
type HomeIconProps = {
className?: string;
};
export function HomeIcon(props: HomeIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
/>
</svg>
);
}

View File

@@ -0,0 +1,49 @@
type LinkedInIconProps = {
className?: string;
};
export function LinkedInIcon(props: LinkedInIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={className}
x="0px"
y="0px"
width="100"
height="100"
viewBox="0,0,256,256"
>
<g transform="translate(-26.66667,-26.66667) scale(1.20833,1.20833)">
<g
fill="none"
fillRule="nonzero"
stroke="none"
strokeWidth="1"
strokeLinecap="butt"
strokeLinejoin="miter"
strokeMiterlimit="10"
strokeDasharray=""
strokeDashoffset="0"
fontFamily="none"
fontWeight="none"
fontSize="none"
textAnchor="none"
style={{ mixBlendMode: 'normal' }}
>
<g transform="scale(5.33333,5.33333)">
<path
d="M42,37c0,2.762 -2.238,5 -5,5h-26c-2.761,0 -5,-2.238 -5,-5v-26c0,-2.762 2.239,-5 5,-5h26c2.762,0 5,2.238 5,5z"
fill="#0288d1"
></path>
<path
d="M12,19h5v17h-5zM14.485,17h-0.028c-1.492,0 -2.457,-1.112 -2.457,-2.501c0,-1.419 0.995,-2.499 2.514,-2.499c1.521,0 2.458,1.08 2.486,2.499c0,1.388 -0.965,2.501 -2.515,2.501zM36,36h-5v-9.099c0,-2.198 -1.225,-3.698 -3.192,-3.698c-1.501,0 -2.313,1.012 -2.707,1.99c-0.144,0.35 -0.101,1.318 -0.101,1.807v9h-5v-17h5v2.616c0.721,-1.116 1.85,-2.616 4.738,-2.616c3.578,0 6.261,2.25 6.261,7.274l0.001,9.726z"
fill="#ffffff"
></path>
</g>
</g>
</g>
</svg>
);
}

View File

@@ -0,0 +1,21 @@
type MoreVerticalIconProps = {
className?: string;
};
export function MoreVerticalIcon(props: MoreVerticalIconProps) {
const { className } = props;
return (
<svg
clipRule="evenodd"
fillRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path d="m12 16.495c1.242 0 2.25 1.008 2.25 2.25s-1.008 2.25-2.25 2.25-2.25-1.008-2.25-2.25 1.008-2.25 2.25-2.25zm0-6.75c1.242 0 2.25 1.008 2.25 2.25s-1.008 2.25-2.25 2.25-2.25-1.008-2.25-2.25 1.008-2.25 2.25-2.25zm0-6.75c1.242 0 2.25 1.008 2.25 2.25s-1.008 2.25-2.25 2.25-2.25-1.008-2.25-2.25 1.008-2.25 2.25-2.25z" />
</svg>
);
}

View File

@@ -0,0 +1,25 @@
type RoadmapIconProps = {
className?: string;
};
export function RoadmapIcon(props: RoadmapIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M18 6H5a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h13l4-3.5L18 6Z"></path>
<path d="M12 13v8"></path>
<path d="M12 3v3"></path>
</svg>
);
}

View File

@@ -0,0 +1,24 @@
type TeamProgressIconProps = {
className?: string;
};
export function TeamProgressIcon(props: TeamProgressIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M3 3v18h18" />
<path d="m19 9-5 5-4-4-3 3" />
</svg>
);
}

View File

@@ -0,0 +1,23 @@
type TwitterIconProps = {
className?: string;
};
export function TwitterIcon(props: TwitterIconProps) {
const { className } = props;
return (
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M8.9285 6.35221L14.5135 0H13.1905L8.339 5.5144L4.467 0H0L5.8565 8.33955L0 15H1.323L6.443 9.17535L10.533 15H15M1.8005 0.976187H3.833L13.1895 14.0718H11.1565"
fill="currentColor"
/>
</svg>
);
}

View File

@@ -0,0 +1,21 @@
type UserIconProps = {
className?: string;
};
export function UserIcon(props: UserIconProps) {
const { className } = props;
return (
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
viewBox="0 0 1024 1024"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path d="M858.5 763.6a374 374 0 0 0-80.6-119.5 375.63 375.63 0 0 0-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 0 0-80.6 119.5A371.7 371.7 0 0 0 136 901.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 0 0 8-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"></path>
</svg>
);
}

View File

@@ -0,0 +1,26 @@
type UsersIconProps = {
className?: string;
};
export function UsersIcon(props: UsersIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
);
}

View File

@@ -0,0 +1,78 @@
type VerifyLetterIconProps = {
className?: string;
};
export function VerifyLetterIcon(props: VerifyLetterIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
className={className}
>
<path
fill="#f79219"
d="M222.58,114.782c0-8.69-3.979-16.901-10.8-22.286l-69.526-54.889c-8.357-6.598-20.15-6.598-28.508,0 L44.22,92.496c-6.82,5.385-10.8,13.596-10.8,22.286v12.732H222.58V114.782z"
/>
<path
fill="#ffa91a"
d="M213.336,223.341H42.664c-5.105,0-9.244-4.138-9.244-9.244V113.116c0-5.105,4.138-9.244,9.244-9.244 h170.672c5.105,0,9.244,4.139,9.244,9.244v100.981C222.58,219.203,218.441,223.341,213.336,223.341z"
/>
<path
fill="#f79219"
d="M213.336,103.872h-0.756v100.225c0,5.105-4.138,9.244-9.244,9.244H33.42v0.756 c0,5.105,4.138,9.244,9.244,9.244h170.672c5.105,0,9.244-4.138,9.244-9.244V113.116 C222.58,108.011,218.441,103.872,213.336,103.872z"
/>
<path
fill="#ef7816"
d="M213.336,103.872H42.664c-4.488,0-8.229,3.199-9.067,7.441l79.417,62.697 c8.787,6.937,21.186,6.937,29.973,0l79.417-62.698C221.564,107.071,217.824,103.872,213.336,103.872z"
/>
<path
fill="#f1f2f2"
d="M203.33,73.49v52.88l-60.34,47.64c-8.789,6.939-21.191,6.939-29.98,0l-60.34-47.64V73.49 c0-4.418,3.582-8,8-8h134.66C199.748,65.49,203.33,69.072,203.33,73.49z"
/>
<g>
<path
fill="#fff"
d="M58.67,125.46c-1.101,0-2-0.9-2-2V73.49c0-2.2,1.8-4,4-4h106.89c1.101,0,1.99,0.9,1.99,2s-0.89,2-1.99,2 H60.67v49.97C60.67,124.56,59.77,125.46,58.67,125.46z M175.55,73.49c-1.1,0-2-0.9-2-2s0.9-2,2-2c1.11,0,2,0.9,2,2 S176.66,73.49,175.55,73.49z"
/>
</g>
<g>
<path
fill="#e6e7e8"
d="M195.33,65.49h-2v50.88l-60.34,47.64c-8.789,6.939-21.191,6.939-29.98,0l-50.34-39.745v2.105l60.34,47.64 c8.789,6.939,21.191,6.939,29.98,0l60.34-47.64V73.49C203.33,69.072,199.748,65.49,195.33,65.49z"
/>
</g>
<g>
<path
fill="#d1d3d4"
d="M197.9,65.92c0.274,0.808,0.43,1.67,0.43,2.57v52.88l-60.34,47.64c-8.789,6.939-21.191,6.939-29.98,0 l-55.34-43.692v1.052l60.34,47.64c8.789,6.939,21.191,6.939,29.98,0l60.34-47.64V73.49 C203.33,69.972,201.056,66.991,197.9,65.92z"
/>
</g>
<g>
<path
fill="#d1d3d4"
d="M109.036,99.997H80.422c-1.431,0-2.591-1.16-2.591-2.591v0c0-1.431,1.16-2.591,2.591-2.591h28.614 c1.431,0,2.591,1.16,2.591,2.591v0C111.627,98.836,110.467,99.997,109.036,99.997z"
/>
<path
fill="#d1d3d4"
d="M175.578,124.03H80.422c-1.431,0-2.591-1.16-2.591-2.591v0c0-1.431,1.16-2.591,2.591-2.591h95.156 c1.431,0,2.591,1.16,2.591,2.591v0C178.169,122.87,177.009,124.03,175.578,124.03z"
/>
<path
fill="#d1d3d4"
d="M175.578,138.881H80.422c-1.431,0-2.591-1.16-2.591-2.591l0,0c0-1.431,1.16-2.591,2.591-2.591h95.156 c1.431,0,2.591,1.16,2.591,2.591l0,0C178.169,137.721,177.009,138.881,175.578,138.881z"
/>
<polygon
fill="#d1d3d4"
points="156.425,163.403 99.575,163.403 106.139,168.585 149.861,168.585"
/>
</g>
<g>
<polygon
fill="#d1d3d4"
points="175.236,148.551 80.764,148.551 87.328,153.733 168.672,153.733"
/>
</g>
</svg>
);
}

View File

@@ -0,0 +1,23 @@
type VideoIconProps = {
className: string;
};
export function VideoIcon(props: VideoIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className={className}
>
<path
strokeLinecap="round"
d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z"
/>
</svg>
);
}

View File

@@ -1,7 +1,5 @@
import { useEffect, useState } from 'react';
import { httpGet, httpPatch } from '../lib/http';
import BuildingIcon from '../icons/building.svg';
import ErrorIcon from '../icons/error.svg';
import { pageProgressMessage } from '../stores/page';
import type { TeamDocument } from './CreateTeam/CreateTeamForm';
import type { AllowedRoles } from './CreateTeam/RoleDropdown';
@@ -9,6 +7,8 @@ import type { AllowedMemberStatus } from './TeamDropdown/TeamDropdown';
import { isLoggedIn } from '../lib/jwt';
import { showLoginPopup } from '../lib/popup';
import { getUrlParams } from '../lib/browser';
import { ErrorIcon2 } from './ReactIcons/ErrorIcon2';
import { BuildingIcon } from './ReactIcons/BuildingIcon';
type InvitationResponse = {
team: TeamDocument;
@@ -85,11 +85,7 @@ export function RespondInviteForm() {
if (!invite) {
return (
<div className="container text-center">
<img
alt={'error'}
src={ErrorIcon.src}
className="mx-auto mb-4 mt-24 w-20 opacity-20"
/>
<ErrorIcon2 className="mx-auto mb-4 mt-24 w-20 opacity-20" />
<h2 className={'mb-1 text-2xl font-bold'}>Error</h2>
<p className="mb-4 text-base leading-6 text-gray-600">
@@ -110,11 +106,7 @@ export function RespondInviteForm() {
return (
<div className="container text-center">
<img
alt={'join team'}
src={BuildingIcon.src}
className="mx-auto mb-4 mt-24 w-20 opacity-20"
/>
<BuildingIcon className="mx-auto mb-4 mt-24 w-20 opacity-20" />
<h2 className={'mb-1 text-2xl font-bold'}>Join Team</h2>
<p className="mb-3 text-base leading-6 text-gray-600">

View File

@@ -1,5 +1,5 @@
import { useCopyText } from '../../hooks/use-copy-text';
import CopyIcon from '../../icons/copy.svg';
import { CopyIcon } from 'lucide-react';
type EditorProps = {
title: string;
@@ -20,11 +20,11 @@ export function Editor(props: EditorProps) {
<button className="flex items-center" onClick={() => copyText(text)}>
{isCopied && (
<span className="mr-1 text-xs leading-none text-gray-700">
Copied!
Copied!&nbsp;
</span>
)}
<img src={CopyIcon.src} alt="Copy" className="inline-block h-4 w-4" />
<CopyIcon className="inline-block h-4 w-4" />
</button>
</div>
<textarea

View File

@@ -2,13 +2,13 @@ import { useState } from 'react';
import { useCopyText } from '../../hooks/use-copy-text';
import { useAuth } from '../../hooks/use-auth';
import CopyIcon from '../../icons/copy.svg';
import { RoadmapSelect } from './RoadmapSelect';
import { GitHubReadmeBanner } from './GitHubReadmeBanner';
import { downloadImage } from '../../helper/download-image';
import { SelectionButton } from './SelectionButton';
import { StepCounter } from './StepCounter';
import { Editor } from './Editor';
import { CopyIcon } from 'lucide-react';
type StepLabelProps = {
label: string;
@@ -34,7 +34,7 @@ export function RoadCardPage() {
}
const badgeUrl = new URL(
`${import.meta.env.PUBLIC_API_URL}/v1-badge/${version}/${user?.id}`
`${import.meta.env.PUBLIC_API_URL}/v1-badge/${version}/${user?.id}`,
);
badgeUrl.searchParams.set('variant', variant);
@@ -44,7 +44,7 @@ export function RoadCardPage() {
return (
<>
<div className="flex items-start gap-4 mx-0 sm:-mx-10 px-0 sm:px-10 border-b pt-2 pb-4">
<div className="mx-0 flex items-start gap-4 border-b px-0 pb-4 pt-2 sm:-mx-10 sm:px-10">
<StepCounter step={1} />
<div>
<StepLabel label="Pick progress to show (Max. 4)" />
@@ -58,7 +58,7 @@ export function RoadCardPage() {
</div>
</div>
<div className="flex items-start gap-4 mx-0 sm:-mx-10 px-0 sm:px-10 border-b py-4">
<div className="mx-0 flex items-start gap-4 border-b px-0 py-4 sm:-mx-10 sm:px-10">
<StepCounter step={2} />
<div>
<StepLabel label="Select Mode (Dark vs Light)" />
@@ -85,7 +85,7 @@ export function RoadCardPage() {
</div>
</div>
<div className="flex items-start gap-4 mx-0 sm:-mx-10 px-0 sm:px-10 border-b py-4">
<div className="mx-0 flex items-start gap-4 border-b px-0 py-4 sm:-mx-10 sm:px-10">
<StepCounter step={3} />
<div>
<StepLabel label="Select Version" />
@@ -111,7 +111,7 @@ export function RoadCardPage() {
</div>
</div>
<div className="flex items-start gap-4 mx-0 sm:-mx-10 px-0 sm:px-10 border-b py-4">
<div className="mx-0 flex items-start gap-4 border-b px-0 py-4 sm:-mx-10 sm:px-10">
<StepCounter step={4} />
<div className="flex-grow">
<StepLabel label="Share your #RoadCard with others" />
@@ -146,7 +146,7 @@ export function RoadCardPage() {
className="flex cursor-pointer items-center justify-center rounded border border-gray-300 p-1.5 px-2 text-sm font-medium disabled:bg-blue-50"
onClick={() => copyText(badgeUrl.toString())}
>
<img alt="Copy" src={CopyIcon.src} className="mr-1" />
<CopyIcon size={16} className="inline-block h-4 w-4 mr-1" />
{isCopied ? 'Copied!' : 'Copy Link'}
</button>

View File

@@ -10,6 +10,9 @@ import { MarkFavorite } from './FeaturedItems/MarkFavorite';
import { TeamVersions } from './TeamVersions/TeamVersions';
import { CreateVersion } from './CreateVersion/CreateVersion';
import { type RoadmapFrontmatter } from '../lib/roadmap';
import { ShareRoadmapButton } from './ShareRoadmapButton';
import { Share2 } from 'lucide-react';
import ShareIcons from './ShareIcons/ShareIcons.astro';
export interface Props {
title: string;
@@ -96,6 +99,12 @@ const hasTnsBanner = !!tnsBannerLink;
&larr;<span class='hidden sm:inline'>&nbsp;All Roadmaps</span>
</a>
<ShareRoadmapButton
description={description}
pageUrl={`https://roadmap.sh/${roadmapId}`}
client:idle
/>
{isRoadmapReady && (
<>
<button
@@ -120,16 +129,6 @@ const hasTnsBanner = !!tnsBannerLink;
</a>
</>
)}
<button
data-guest-required
data-popup='login-popup'
class='inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm'
aria-label='Subscribe for Updates'
>
<Icon icon='email' />
<span class='ml-2'>Subscribe</span>
</button>
</>
)
}

View File

@@ -15,8 +15,8 @@ const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${page
---
<div class='absolute left-[-18px] top-[110px] h-full hidden' id='page-share-icons'>
<div class='flex sticky top-[100px] flex-col gap-1.5'>
<a href={twitterUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
<div class='flex sticky top-[100px] flex-col gap-1.5 items-center'>
<a href={twitterUrl} target='_blank' class='text-gray-500 hover:text-gray-700 mb-0.5'>
<Icon icon='twitter' />
</a>
<a href={fbUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>

View File

@@ -0,0 +1,136 @@
import { Check, Copy, Facebook, Linkedin, Share2, Twitter } from 'lucide-react';
import { useRef, useState } from 'react';
import { useOutsideClick } from '../hooks/use-outside-click.ts';
import { useCopyText } from '../hooks/use-copy-text.ts';
import { cn } from '../lib/classname.ts';
import { TwitterIcon } from './ReactIcons/TwitterIcon.tsx';
type ShareRoadmapButtonProps = {
description: string;
pageUrl: string;
};
export function ShareRoadmapButton(props: ShareRoadmapButtonProps) {
const { description, pageUrl } = props;
const { isCopied, copyText } = useCopyText();
const containerRef = useRef<HTMLDivElement>(null);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const twitterUrl = `https://twitter.com/intent/tweet?text=${description}&url=${pageUrl}`;
const fbUrl = `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${pageUrl}`;
const hnUrl = `https://news.ycombinator.com/submitlink?t=${description}&u=${pageUrl}`;
const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${pageUrl}`;
const linkedinUrl = `https://www.linkedin.com/shareArticle?mini=true&url=${pageUrl}&title=${description}`;
useOutsideClick(containerRef, () => {
setIsDropdownOpen(false);
});
return (
<div className="relative" ref={containerRef}>
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className={cn(
'inline-flex items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm',
{
'bg-yellow-500': isDropdownOpen,
'bg-green-400': isCopied,
},
)}
aria-label="Share Roadmap"
>
{!isCopied && (
<>
<Share2 size="15px" />
<span className="ml-2 hidden sm:inline">Share</span>
</>
)}
{isCopied && (
<>
<Check size="15px" />
<span className="ml-2 hidden sm:inline">Copied</span>
</>
)}
</button>
{isDropdownOpen && (
<div className="absolute left-0 z-50 mt-1 w-44 rounded-md bg-slate-800 text-sm text-white shadow-lg ring-1 ring-black ring-opacity-5">
<div className="flex flex-col px-1 py-1">
<button
onClick={() => {
copyText(pageUrl);
setIsDropdownOpen(false);
}}
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
>
<div className="flex w-[20px] items-center justify-center">
<Copy size="15px" className="text-slate-400" />
</div>
Copy Link
</button>
<a
href={twitterUrl}
target={'_blank'}
className="mt-1 flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
>
<div className="flex w-[20px] items-center justify-center">
<TwitterIcon className="h-[16px] text-slate-400" />
</div>
Twitter
</a>
<a
href={fbUrl}
target={'_blank'}
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
>
<div className="flex w-[20px] items-center justify-center">
<Facebook size="16px" className="text-slate-400" />
</div>
Facebook
</a>
<a
href={hnUrl}
target={'_blank'}
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
>
<div className="flex w-[20px] items-center justify-center">
<img
alt={'hackernews logo'}
src={'/images/hackernews.svg'}
className="h-5 w-5"
/>
</div>
Hacker News
</a>
<a
href={redditUrl}
target={'_blank'}
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
>
<div className="flex w-[20px] items-center justify-center">
<img
alt={'reddit logo'}
src={'/images/reddit.svg'}
className="h-5 w-5"
/>
</div>
Reddit
</a>
<a
href={linkedinUrl}
target={'_blank'}
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
>
<div className="flex w-[20px] items-center justify-center">
<Linkedin size="16px" className="text-slate-400" />
</div>
LinkedIn
</a>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,5 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import ChevronDown from '../../icons/dropdown.svg';
import { httpGet } from '../../lib/http';
import { useAuth } from '../../hooks/use-auth';
import { useOutsideClick } from '../../hooks/use-outside-click';
@@ -10,6 +9,7 @@ import { useStore } from '@nanostores/react';
import { useTeamId } from '../../hooks/use-team-id';
import { useToast } from '../../hooks/use-toast';
import type { ValidTeamType } from '../CreateTeam/Step0';
import { DropdownIcon } from '../ReactIcons/DropdownIcon.tsx';
const allowedStatus = ['invited', 'joined', 'rejected'] as const;
export type AllowedMemberStatus = (typeof allowedStatus)[number];
@@ -44,7 +44,7 @@ export function TeamDropdown() {
if (shouldShowTeamIndicator) {
localStorage.setItem(
'viewedTeamsCount',
(viewedTeamsCountNumber + 1).toString()
(viewedTeamsCountNumber + 1).toString(),
);
}
}, []);
@@ -67,7 +67,7 @@ export function TeamDropdown() {
async function getAllTeams() {
const { response, error } = await httpGet<TeamListResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-teams`
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-teams`,
);
if (error || !response) {
toast.error(error?.message || 'Something went wrong');
@@ -140,7 +140,7 @@ export function TeamDropdown() {
{isLoading && 'Loading ..'}
</span>
</div>
<img alt={'show dropdown'} src={ChevronDown.src} className="h-4 w-4" />
<DropdownIcon className={'h-4 w-4'} />
</button>
{showDropdown && (

View File

@@ -1,7 +1,7 @@
import { useRef, useState } from 'react';
import type { TeamMemberDocument } from './TeamMembersPage';
import MoreIcon from '../../icons/more-vertical.svg';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { MoreVerticalIcon } from '../ReactIcons/MoreVerticalIcon.tsx';
export function MemberActionDropdown({
member,
@@ -79,7 +79,7 @@ export function MemberActionDropdown({
onClick={() => setIsOpen(!isOpen)}
className="ml-2 flex items-center opacity-60 transition-opacity hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-30"
>
<img alt="menu" src={MoreIcon.src} className="h-4 w-4" />
<MoreVerticalIcon className="h-4 w-4" />
</button>
{isOpen && (

View File

@@ -1,8 +1,8 @@
import { useState } from 'react';
import type { GroupByRoadmap, TeamMember } from './TeamProgressPage';
import { getUrlParams } from '../../lib/browser';
import ExternalLinkIcon from '../../icons/external-link.svg';
import { useAuth } from '../../hooks/use-auth';
import { LucideExternalLink } from 'lucide-react';
type GroupRoadmapItemProps = {
roadmap: GroupByRoadmap;
@@ -33,11 +33,7 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
className="group mb-0.5 flex shrink-0 items-center justify-between text-base font-medium leading-none text-black"
target={'_blank'}
>
<img
alt={'link'}
src={ExternalLinkIcon.src}
className="ml-2 h-4 w-4 opacity-20 transition-opacity group-hover:opacity-100"
/>
<LucideExternalLink className="h-4 w-4 opacity-20 transition-opacity group-hover:opacity-100" />
</a>
</div>
</div>
@@ -58,7 +54,7 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
onClick={() => {
onShowResourceProgress(
member.member,
member.progress?.resourceId!
member.progress?.resourceId!,
);
}}
>

View File

@@ -25,6 +25,7 @@ import type { Node } from 'reactflow';
import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { MemberProgressModalHeader } from './MemberProgressModalHeader';
import { X } from 'lucide-react';
export type ProgressMapProps = {
member: TeamMember;
@@ -284,7 +285,7 @@ export function MemberCustomProgressModal(props: ProgressMapProps) {
}`}
onClick={onClose}
>
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
<X className="h-4 w-4" />
<span className="sr-only">Close modal</span>
</button>
</div>

View File

@@ -12,12 +12,12 @@ import {
type ResourceType,
updateResourceProgress,
} from '../../lib/resource-progress';
import CloseIcon from '../../icons/close.svg';
import { useToast } from '../../hooks/use-toast';
import { useAuth } from '../../hooks/use-auth';
import { pageProgressMessage } from '../../stores/page';
import { MemberProgressModalHeader } from './MemberProgressModalHeader';
import {replaceChildren} from "../../lib/dom.ts";
import { replaceChildren } from '../../lib/dom.ts';
import { XIcon } from 'lucide-react';
export type ProgressMapProps = {
member: TeamMember;
@@ -68,12 +68,12 @@ export function MemberProgressModal(props: ProgressMapProps) {
teamId: string,
memberId: string,
resourceType: string,
resourceId: string
resourceId: string,
) {
const { error, response } = await httpGet<MemberProgressResponse>(
`${
import.meta.env.PUBLIC_API_URL
}/v1-get-member-resource-progress/${teamId}/${memberId}?resourceType=${resourceType}&resourceId=${resourceId}`
}/v1-get-member-resource-progress/${teamId}/${memberId}?resourceType=${resourceType}&resourceId=${resourceId}`,
);
if (error || !response) {
toast.error(error?.message || 'Failed to get member progress');
@@ -160,14 +160,14 @@ export function MemberProgressModal(props: ProgressMapProps) {
resourceType: resourceType as ResourceType,
topicId,
},
newStatus
newStatus,
)
.then(() => {
renderTopicProgress(topicId, newStatus);
getMemberProgress(teamId, member._id, resourceType, resourceId).then(
(data) => {
setMemberProgress(data);
}
},
);
})
.catch((err) => {
@@ -227,7 +227,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
e.preventDefault();
updateTopicStatus(
topicId,
!isCurrentStatusLearning ? 'learning' : 'pending'
!isCurrentStatusLearning ? 'learning' : 'pending',
);
return;
}
@@ -236,7 +236,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
e.preventDefault();
updateTopicStatus(
topicId,
!isCurrentStatusSkipped ? 'skipped' : 'pending'
!isCurrentStatusSkipped ? 'skipped' : 'pending',
);
return;
@@ -298,7 +298,8 @@ export function MemberProgressModal(props: ProgressMapProps) {
}`}
onClick={onClose}
>
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
<XIcon className="h-4 w-4" />
<span className="sr-only">Close modal</span>
</button>
</div>

View File

@@ -1,7 +1,7 @@
import MoreIcon from '../../icons/more-vertical.svg';
import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { Lock, MoreVertical, Shapes, Trash2 } from 'lucide-react';
import { MoreVerticalIcon } from '../ReactIcons/MoreVerticalIcon.tsx';
type RoadmapActionDropdownProps = {
onDelete?: () => void;
@@ -26,7 +26,7 @@ export function RoadmapActionDropdown(props: RoadmapActionDropdownProps) {
onClick={() => setIsOpen(!isOpen)}
className="hidden items-center opacity-60 transition-opacity hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-30 sm:flex"
>
<img alt="menu" src={MoreIcon.src} className="h-4 w-4" />
<MoreVerticalIcon className={'h-4 w-4'} />
</button>
<button
disabled={false}

View File

@@ -4,7 +4,6 @@ import type { TeamDocument } from '../CreateTeam/CreateTeamForm';
import type { TeamResourceConfig } from '../CreateTeam/RoadmapSelector';
import { httpGet, httpPut } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page';
import RoadmapIcon from '../../icons/roadmap.svg';
import type { PageType } from '../CommandMenu/CommandMenu';
import { useStore } from '@nanostores/react';
import { $canManageCurrentTeam } from '../../stores/team';
@@ -28,6 +27,7 @@ import { RoadmapActionDropdown } from './RoadmapActionDropdown';
import { UpdateTeamResourceModal } from '../CreateTeam/UpdateTeamResourceModal';
import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal';
import { cn } from '../../lib/classname';
import { RoadmapIcon } from '../ReactIcons/RoadmapIcon.tsx';
export function TeamRoadmaps() {
const { t: teamId } = getUrlParams();
@@ -73,7 +73,7 @@ export function TeamRoadmaps() {
async function loadTeam(teamIdToFetch: string) {
const { response, error } = await httpGet<TeamDocument>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamIdToFetch}`
`${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamIdToFetch}`,
);
if (error || !response) {
@@ -87,7 +87,7 @@ export function TeamRoadmaps() {
async function loadTeamResourceConfig(teamId: string) {
const { error, response } = await httpGet<TeamResourceConfig>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-resource-config/${teamId}`
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-resource-config/${teamId}`,
);
if (error || !Array.isArray(response)) {
console.error(error);
@@ -127,7 +127,7 @@ export function TeamRoadmaps() {
{
resourceId: roadmapId,
resourceType: 'roadmap',
}
},
);
if (error || !response) {
@@ -156,7 +156,7 @@ export function TeamRoadmaps() {
resourceId: roadmapId,
resourceType: 'roadmap',
removed: [],
}
},
);
if (error || !response) {
@@ -190,13 +190,13 @@ export function TeamRoadmaps() {
}
window.addEventListener(
'custom-roadmap-created',
handleCustomRoadmapCreated
handleCustomRoadmapCreated,
);
return () => {
window.removeEventListener(
'custom-roadmap-created',
handleCustomRoadmapCreated
handleCustomRoadmapCreated,
);
};
}, []);
@@ -252,13 +252,13 @@ export function TeamRoadmaps() {
);
const placeholderRoadmaps = teamResources.filter(
(c: TeamResourceConfig[0]) => c.isCustomResource && !c.topics
(c: TeamResourceConfig[0]) => c.isCustomResource && !c.topics,
);
const customRoadmaps = teamResources.filter(
(c: TeamResourceConfig[0]) => c.isCustomResource && c.topics
(c: TeamResourceConfig[0]) => c.isCustomResource && c.topics,
);
const defaultRoadmaps = teamResources.filter(
(c: TeamResourceConfig[0]) => !c.isCustomResource
(c: TeamResourceConfig[0]) => !c.isCustomResource,
);
const hasRoadmaps =
@@ -272,11 +272,8 @@ export function TeamRoadmaps() {
{addRoadmapModal}
{createRoadmapModal}
<img
alt="roadmap"
src={RoadmapIcon.src}
className="mb-4 h-24 w-24 opacity-10"
/>
<RoadmapIcon className="mb-4 h-24 w-24 opacity-10" />
<h3 className="mb-1 text-2xl font-bold text-gray-900">No roadmaps</h3>
<p className="text-base text-gray-500">
{canManageCurrentTeam
@@ -380,11 +377,11 @@ export function TeamRoadmaps() {
onDelete={() => {
if (
confirm(
'Are you sure you want to remove this roadmap?'
'Are you sure you want to remove this roadmap?',
)
) {
onRemove(resourceConfig.resourceId).finally(
() => {}
() => {},
);
}
}}
@@ -405,7 +402,7 @@ export function TeamRoadmaps() {
)}
</div>
);
}
},
)}
</div>
</div>
@@ -433,7 +430,7 @@ export function TeamRoadmaps() {
'grid grid-cols-1 p-2.5',
canManageCurrentTeam
? 'sm:grid-cols-[auto_172px]'
: 'sm:grid-cols-[auto_110px]'
: 'sm:grid-cols-[auto_110px]',
)}
key={resourceConfig.resourceId}
>
@@ -464,11 +461,11 @@ export function TeamRoadmaps() {
onDelete={() => {
if (
confirm(
'Are you sure you want to remove this roadmap?'
'Are you sure you want to remove this roadmap?',
)
) {
onRemove(resourceConfig.resourceId).finally(
() => {}
() => {},
);
}
}}
@@ -557,11 +554,11 @@ export function TeamRoadmaps() {
onDelete={() => {
if (
confirm(
'Are you sure you want to remove this roadmap?'
'Are you sure you want to remove this roadmap?',
)
) {
onRemove(resourceConfig.resourceId).finally(
() => {}
() => {},
);
}
}}

View File

@@ -1,16 +1,16 @@
import { TeamDropdown } from './TeamDropdown/TeamDropdown';
import ChevronDown from '../icons/dropdown.svg';
import { useTeamId } from '../hooks/use-team-id';
import TeamProgress from '../icons/team-progress.svg';
import SettingsIcon from '../icons/cog.svg';
import ChatIcon from '../icons/chat.svg';
import MapIcon from '../icons/map.svg';
import GroupIcon from '../icons/group.svg';
import { useState } from 'react';
import type { ReactNode } from 'react';
import { useState } from 'react';
import { useStore } from '@nanostores/react';
import { $currentTeam } from '../stores/team';
import { SubmitFeedbackPopup } from './Feedback/SubmitFeedbackPopup';
import { ChevronDownIcon } from './ReactIcons/ChevronDownIcon.tsx';
import { GroupIcon } from './ReactIcons/GroupIcon.tsx';
import { TeamProgressIcon } from './ReactIcons/TeamProgressIcon.tsx';
import { MapIcon, MessageCircle } from 'lucide-react';
import { CogIcon } from './ReactIcons/CogIcon.tsx';
type TeamSidebarProps = {
activePageId: string;
@@ -29,26 +29,26 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
title: 'Progress',
href: `/team/progress?t=${teamId}`,
id: 'progress',
icon: TeamProgress.src,
icon: TeamProgressIcon,
},
{
title: 'Roadmaps',
href: `/team/roadmaps?t=${teamId}`,
id: 'roadmaps',
icon: MapIcon.src,
icon: MapIcon,
hasWarning: currentTeam?.roadmaps?.length === 0,
},
{
title: 'Members',
href: `/team/members?t=${teamId}`,
id: 'members',
icon: GroupIcon.src,
icon: GroupIcon,
},
{
title: 'Settings',
href: `/team/settings?t=${teamId}`,
id: 'settings',
icon: SettingsIcon.src,
icon: CogIcon,
},
];
@@ -66,7 +66,7 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
sidebarLinks.find((sidebarLink) => sidebarLink.id === activePageId)
?.title
}
<img alt="menu" src={ChevronDown.src} className="h-4 w-4" />
<ChevronDownIcon className="h-4 w-4" />
</button>
{menuShown && (
<ul
@@ -80,7 +80,7 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
activePageId === 'team' ? 'bg-slate-100' : ''
}`}
>
<img alt={'teams'} src={GroupIcon.src} className={`mr-2 h-4 w-4`} />
<GroupIcon className="mr-2 h-4 w-4" />
Personal Account / Teams
</a>
</li>
@@ -95,11 +95,8 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
isActive ? 'bg-slate-100' : ''
}`}
>
<img
alt={'menu icon'}
src={sidebarLink.icon}
className="mr-2 h-4 w-4"
/>
{<sidebarLink.icon className="mr-2 h-4 w-4" />}
{sidebarLink.title}
</a>
</li>
@@ -111,11 +108,7 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
className={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200`}
onClick={() => setShowFeedbackPopup(true)}
>
<img
alt={'menu icon'}
src={ChatIcon.src}
className="mr-2 h-4 w-4"
/>
<MessageCircle className="mr-2 h-4 w-4" />
Send Feedback
</button>
</li>
@@ -150,11 +143,8 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
>
<span className="flex flex-grow items-center justify-between">
<span className="flex">
<img
alt="menu icon"
src={sidebarLink.icon}
className="relative top-[2px] mr-2 h-4 w-4"
/>
{<sidebarLink.icon className="mr-2 h-4 w-4" />}
{sidebarLink.title}
</span>
{sidebarLink.hasWarning && (
@@ -174,7 +164,7 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
className="mr-3 mt-4 flex items-center justify-center rounded-md border p-2 text-sm text-gray-500 transition-colors hover:border-gray-300 hover:bg-gray-50 hover:text-black"
onClick={() => setShowFeedbackPopup(true)}
>
<img alt={'feedback'} src={ChatIcon.src} className="mr-2 h-4 w-4" />
<MessageCircle className="mr-2 h-4 w-4" />
Send Feedback
</button>
</nav>

View File

@@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react';
import type { TeamDocument } from '../CreateTeam/CreateTeamForm';
import type { TeamResourceConfig } from '../CreateTeam/RoadmapSelector';
import { httpGet } from '../../lib/http';
import DropdownIcon from '../../icons/dropdown.svg';
// import DropdownIcon from '../../icons/dropdown.svg';
import {
clearResourceProgress,
refreshProgressCounters,
@@ -15,6 +15,7 @@ import { useKeydown } from '../../hooks/use-keydown';
import { isLoggedIn } from '../../lib/jwt';
import { useAuth } from '../../hooks/use-auth';
import { useToast } from '../../hooks/use-toast';
import { DropdownIcon } from '../ReactIcons/DropdownIcon';
type TeamVersionsProps = {
resourceId: string;
@@ -75,7 +76,7 @@ export function TeamVersions(props: TeamVersionsProps) {
}/v1-get-team-versions?${new URLSearchParams({
resourceId,
resourceType,
})}`
})}`,
);
if (error || !response) {
@@ -142,11 +143,7 @@ export function TeamVersions(props: TeamVersionsProps) {
<span className="truncate">
{selectedTeamVersion?.team.name || 'Team Versions'}
</span>
<img
alt="Dropdown"
src={DropdownIcon.src}
className="h-3 w-3 sm:h-4 sm:w-4"
/>
<DropdownIcon className="h-3 w-3 sm:h-4 sm:w-4" />
</div>
<div className="sm:hidden">
{shouldShowAvatar ? (

View File

@@ -1,6 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import CloseIcon from '../../icons/close.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { useKeydown } from '../../hooks/use-keydown';
import { useLoadTopic } from '../../hooks/use-load-topic';
@@ -26,8 +24,9 @@ import type {
} from '../CustomRoadmap/CustomRoadmap';
import { markdownToHtml } from '../../lib/markdown';
import { cn } from '../../lib/classname';
import { Ban, FileText } from 'lucide-react';
import { Ban, FileText, X } from 'lucide-react';
import { getUrlParams } from '../../lib/browser';
import { Spinner } from '../ReactIcons/Spinner';
type TopicDetailProps = {
canSubmitContribution: boolean;
@@ -203,10 +202,10 @@ export function TopicDetail(props: TopicDetailProps) {
>
{isLoading && (
<div className="flex w-full justify-center">
<img
src={SpinnerIcon.src}
alt="Loading"
className="h-6 w-6 animate-spin fill-blue-600 text-gray-200 sm:h-12 sm:w-12"
<Spinner
outerFill="#d1d5db"
className="h-6 w-6 sm:h-12 sm:w-12"
innerFill="#2563eb"
/>
</div>
)}
@@ -249,7 +248,7 @@ export function TopicDetail(props: TopicDetailProps) {
setIsContributing(false);
}}
>
<img alt="Close" className="h-5 w-5" src={CloseIcon.src} />
<X className="h-5 w-5" />
</button>
</div>
@@ -339,7 +338,7 @@ export function TopicDetail(props: TopicDetailProps) {
setIsContributing(false);
}}
>
<img alt="Close" className="h-5 w-5" src={CloseIcon.src} />
<X className="h-5 w-5" />
</button>
<div className="flex h-full flex-col items-center justify-center">
<Ban className="h-16 w-16 text-red-500" />

View File

@@ -1,8 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click';
import DownIcon from '../../icons/down.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { isLoggedIn } from '../../lib/jwt';
import {
getTopicStatus,
@@ -10,9 +8,14 @@ import {
renderTopicProgress,
updateResourceProgress,
} from '../../lib/resource-progress';
import type { ResourceProgressType, ResourceType } from '../../lib/resource-progress';
import type {
ResourceProgressType,
ResourceType,
} from '../../lib/resource-progress';
import { showLoginPopup } from '../../lib/popup';
import { useToast } from '../../hooks/use-toast';
import { Spinner } from '../ReactIcons/Spinner';
import { ChevronDown } from 'lucide-react';
type TopicProgressButtonProps = {
topicId: string;
@@ -27,7 +30,7 @@ const statusColors: Record<ResourceProgressType, string> = {
learning: 'bg-yellow-500',
pending: 'bg-gray-300',
skipped: 'bg-black',
removed: ''
removed: '',
};
export function TopicProgressButton(props: TopicProgressButtonProps) {
@@ -71,7 +74,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
handleUpdateResourceProgress('done');
},
[progress]
[progress],
);
// Mark as learning
@@ -85,7 +88,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
handleUpdateResourceProgress('learning');
},
[progress]
[progress],
);
// Mark as learning
@@ -99,7 +102,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
handleUpdateResourceProgress('skipped');
},
[progress]
[progress],
);
// Mark as pending
@@ -114,7 +117,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
handleUpdateResourceProgress('pending');
},
[progress]
[progress],
);
const handleUpdateResourceProgress = (progress: ResourceProgressType) => {
@@ -131,7 +134,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
resourceId,
resourceType,
},
progress
progress,
)
.then(() => {
setProgress(progress);
@@ -149,22 +152,22 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
};
const allowMarkingSkipped = ['pending', 'learning', 'done'].includes(
progress
progress,
);
const allowMarkingDone = ['skipped', 'pending', 'learning'].includes(
progress
progress,
);
const allowMarkingLearning = ['done', 'skipped', 'pending'].includes(
progress
progress,
);
const allowMarkingPending = ['skipped', 'done', 'learning'].includes(
progress
progress,
);
if (isUpdatingProgress) {
return (
<button className="inline-flex cursor-default items-center rounded-md border border-gray-300 bg-white p-1 px-2 text-sm text-black">
<img alt="Check" className="h-4 w-4 animate-spin" src={SpinnerIcon.src} />
<Spinner className="h-4 w-4" />
<span className="ml-2">Updating Status..</span>
</button>
);
@@ -188,7 +191,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
onClick={() => setShowChangeStatus(true)}
>
<span className="mr-0.5">Update Status</span>
<img alt="Check" className="h-4 w-4" src={DownIcon.src} />
<ChevronDown className="h-4 w-4" />
</button>
{showChangeStatus && (

View File

@@ -4,13 +4,13 @@ import { useKeydown } from '../../hooks/use-keydown';
import { httpGet } from '../../lib/http';
import type { ResourceType } from '../../lib/resource-progress';
import { topicSelectorAll } from '../../lib/resource-progress';
import CloseIcon from '../../icons/close.svg';
import { deleteUrlParam, getUrlParams } from '../../lib/browser';
import { useAuth } from '../../hooks/use-auth';
import type { GetRoadmapResponse } from '../CustomRoadmap/CustomRoadmap';
import { ReadonlyEditor } from '../../../editor/readonly-editor';
import { ProgressLoadingError } from './ProgressLoadingError';
import { UserProgressModalHeader } from './UserProgressModalHeader';
import { X } from 'lucide-react';
export type ProgressMapProps = {
userId?: string;
@@ -208,7 +208,7 @@ export function UserCustomProgressModal(props: ProgressMapProps) {
className={`absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-gray-100 bg-transparent p-1.5 text-sm text-gray-400 hover:text-gray-900 lg:hidden`}
onClick={onClose}
>
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
<X className="h-4 w-4" />
<span className="sr-only">Close modal</span>
</button>
</div>

View File

@@ -6,11 +6,11 @@ import { useKeydown } from '../../hooks/use-keydown';
import { httpGet } from '../../lib/http';
import type { ResourceType } from '../../lib/resource-progress';
import { topicSelectorAll } from '../../lib/resource-progress';
import CloseIcon from '../../icons/close.svg';
import { deleteUrlParam, getUrlParams } from '../../lib/browser';
import { useAuth } from '../../hooks/use-auth';
import { ProgressLoadingError } from './ProgressLoadingError';
import { UserProgressModalHeader } from './UserProgressModalHeader';
import { X } from 'lucide-react';
export type ProgressMapProps = {
userId?: string;
@@ -70,12 +70,12 @@ export function UserProgressModal(props: ProgressMapProps) {
async function getUserProgress(
userId: string,
resourceType: string,
resourceId: string
resourceId: string,
): Promise<UserProgressResponse | undefined> {
const { error, response } = await httpGet<UserProgressResponse>(
`${
import.meta.env.PUBLIC_API_URL
}/v1-get-user-progress/${userId}?resourceType=${resourceType}&resourceId=${resourceId}`
}/v1-get-user-progress/${userId}?resourceType=${resourceType}&resourceId=${resourceId}`,
);
if (error || !response) {
@@ -86,7 +86,7 @@ export function UserProgressModal(props: ProgressMapProps) {
}
async function getRoadmapSVG(
jsonUrl: string
jsonUrl: string,
): Promise<SVGElement | undefined> {
const { error, response: roadmapJson } = await httpGet(jsonUrl);
if (error || !roadmapJson) {
@@ -216,7 +216,7 @@ export function UserProgressModal(props: ProgressMapProps) {
className={`absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-gray-100 bg-transparent p-1.5 text-sm text-gray-400 hover:text-gray-900 lg:hidden`}
onClick={onClose}
>
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
<X className="h-4 w-4" />
<span className="sr-only">Close modal</span>
</button>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ briefDescription: 'Step by step guide for DevOps or operations role in 2023'
title: 'DevOps Roadmap'
description: 'Step by step guide for DevOps, SRE or any other Operations Role in 2023'
hasTopics: true
isForkable: true
tnsBannerLink: 'https://thenewstack.io?utm_source=roadmap.sh&utm_medium=Referral&utm_campaign=Alert'
question:
title: 'What is DevOps?'

View File

@@ -6,4 +6,4 @@ Visit the following resources to learn more:
- [Electron Website](https://www.electronjs.org/)
- [Electron Docs](https://www.electronjs.org/docs/latest/)
- [Create a Desktop App With JavaScript & Electron](https://youtu.be/ml743nrkmhw)
- [Create a Desktop App With JavaScript & Electron](https://www.youtube.com/watch?v=ML743nrkMHw)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
# Note
These roadmaps cover everything that is there to learn for the paths listed below. Don't feel overwhelmed, you don't need to learn it all in the beginning if you are just getting started.

View File

@@ -0,0 +1,3 @@
# Server Side
The term "server-side" refers to operations that are performed by the server in a client-server relationship in a computer network. In games, the server-side is responsible for the logic and rules of the game, data persistence, security, and synchronization of the game state among all connected clients. It essentially handles all processing that isn't done on the client-side. Code run on the server-side can be written in various programming languages, but it's often in PHP, Ruby, Python, .NET, Java, or JavaScript (Node.js). Knowing server-side programming is critical for any game developer to create maintainable, scalable, and secure online games.

View File

@@ -0,0 +1,3 @@
# Checksum
A **checksum** in TCP (Transmission Control Protocol) is a 16-bit field in the TCP header used to perform error checks on the segments. TCP stack computes the checksum value for the data transmitted and sends it along with the data to the receiving system. The receiving system re-computes the checksum and compares it with the value sent along with the data. If both the computed values match, the data is assumed to be free from transmission errors. However, if the computed values don't match, TCP will detect a possible change in received data, and the receiver will request for the re-transmission of the lost or corrupted data packets. Please note that while the checksum operation helps to ensure data integrity, it is not entirely foolproof as it might not detect all possible errors, particularly those that involve multiple bit changes.

View File

@@ -0,0 +1,3 @@
# Segment Structure
TCP (Transmission Control Protocol) uses a method called "segmentation" to manage data transmission. In this approach, TCP divides a stream of data into smaller parts, known as "segments". Each of these segments is then independently sent over the internet. A basic TCP segment consists of a header and the data section. The header contains various fields such as source port, destination port, sequence number, acknowledgment number, data offset, reserved section, control bit, window, checksum, urgent pointer, and options. The rest of the segment is occupied by the data transferred by TCP. Understanding the structure of these segments is crucial for understanding how TCP, and thereby much of the Internet, operates.

View File

@@ -0,0 +1,3 @@
# Connection
`Connection` in server-side game development refers to the establishment of a link between the game server and the game client (usually the player's device). This connection can either be persistent or non-persistent, depending on the games requirements. Persistent connections remain active as long as the user is logged in, facilitating real-time communication between the server and the client. Non-persistent connections, on the other hand, are established and discontinued as needed. This element is crucial in multiplayer games where the server handles the synchronization of data among multiple clients, enabling players to interact with each other in the same virtual environment.

View File

@@ -0,0 +1,3 @@
# Resource Usage
In server-side game development, **resource usage** broadly refers to how a game uses the server's computational power, memory, storage, and networking capabilities. These resources are crucial in assuring overall game performance and responsiveness. When monitoring resource usage, you typically pay attention to CPU usage, memory utilization, storage capacity, network bandwidth, and database performance. Server-side code must be developed and optimized with these resources in mind, as inefficient code can lead to increased resource usage, potentially causing lag, disconnections, or crashes. Key elements like scalable architecture and serverless technologies are primarily employed to efficiently manage and balance resource usage. Tools and automated solutions are commonly utilized to monitor and manage these resources in real-time, allowing developers to identify potential issues and optimize accordingly.

View File

@@ -0,0 +1,3 @@
# Congestion Control
`Congestion Control` is a critical aspect of any network, particularly in server-side game development where real-time data transfer is a must. It refers to techniques and mechanisms that can prevent too much data from filling the network. This excessive data, also known as network congestion, can lead to packet loss, delay in data transfer, and ultimately a poor gaming experience. Congestion control algorithms, such as TCP congestion control algorithms, are implemented to avoid such situations. They adjust the data packet transmission rate based on the perceived network congestion, reducing the rate when congestion is detected and increasing it when the network is less congested.

View File

@@ -0,0 +1,3 @@
# Error Detection
Error detection is a mechanism that checks the integrity of data that is being delivered or stored. In the context of server-side game development, errors can occur due to reasons like network inconsistencies, corrupted data, or server overload. To ensure smooth operation, these errors need to be detected and handled. Some common types of error detection methods include parity check, checksum, and cyclic redundancy check (CRC). Parity check adds an extra bit to the data to make the number of 1s either even or odd. Checksum involves the sender adding up all the bits in the data and sending the sum along with data. CRC is a more complex and robust method involving binary division. These methods help detect errors but can't fix them. Once detected, errors are typically dealt with through retransmission or by using forward error correction codes.

View File

@@ -0,0 +1,3 @@
# Flow Control
Flow control, often referred to as stream control transmission protocol (SCTP), is essential in server-side game development, particularly in data transfer. Flow control is basically a technique used to manage the rate of data transmission between two nodes to prevent a fast sender from overwhelming a slow receiver. It provides an effective way to manage the amount of data that can be sent before having to receive an acknowledgment. There are two main types of flow control: window-based and rate-based. Window-based flow control allows the receiver to process a fixed set of packets before sending the acknowledgement, whereas rate-based control allows for data transfer at a specified rate. These methods make flow control indispensable in network communications.

View File

@@ -0,0 +1,3 @@
# Data Transfer
Data transfer in server-side game development refers to the movement of data between the server and client or among various components of the server itself. Game data, such as player scores, game states, and updates, are frequently transferred to ensure a consistent and updated gaming environment. Various methods are used for data transfer, including RESTful APIs, websockets, and protocol buffers. The choice of method often depends on factors such as the size and type of data, the target platform, and the specific needs of the game. Remember, efficient and secure data transfer is essential for providing a smooth and engaging gaming experience.

View File

@@ -0,0 +1,3 @@
# Reliable Transmission
Reliable Transmission is a service that ensures data is delivered from the source to the destination without any errors. This includes mechanisms that provide accurate, on-time delivery of all packets while managing packet loss, duplication, or out-of-order delivery. In server-side game development, protocols like TCP (Transmission Control Protocol) are often used to accomplish reliable transmission. Reliable transmission is particularly necessary for critical data where all transmitted information must arrive at the destination correctly and in the right order. TCP accomplishes this by establishing a connection before data transfer begins and using sequence numbers to track delivery and reassemble pieces in the correct order. TCP also sends acknowledgements back to the source confirming packet delivery, and implements re-transmission of lost or corrupted data. Despite its overhead and slower speed compared to other methods, such as the User Datagram Protocol (UDP), reliable transmission is vital when data integrity is a priority over speed.

View File

@@ -0,0 +1,3 @@
# Max Segment Scaling
`Max Segment Scaling (MSS)` is a TCP feature that defines the maximum amount of data that can be received in a single TCP segment. It is specified during the TCP connection establishment phase. The MSS is calculated as the data link layer Maximum Transmission Unit (MTU) minus the size of the TCP and IP headers. The mechanism helps to avoid fragmentation at the IP layer, ensuring the data packets sent are optimal for the network path taken, preventing potential transmission inefficiencies or packet loss issues.

View File

@@ -0,0 +1,3 @@
# Window Scaling
Window Scaling is a mechanism in the Transmission Control Protocol (TCP) that provides support for larger receiver window sizes beyond the maximum limit of 65,535 bytes. This TCP feature is essential when dealing with high latency or high bandwidth networks (common in server-side game development), where frames might be significantly delayed or rapidly transmitted. The window size initially specified in the TCP header is augmented via a scale factor (defined during the connection setup), allowing the receiver window size to be as large as 1 gigabyte. However, keep in mind that Window Scaling can only be employed at the connection setup stage; once the connection is established, the scaling factor cannot be changed.

View File

@@ -0,0 +1,3 @@
# Timestamp
A `timestamp` in server side game development is essentially a sequence of characters, encoding the date and time at which a certain event occurred. This data is particularly significant when tracking the sequence of certain events, debugging issues, logging, and when performing any sort of data analysis. The timestamp is typically generated by the server and it reflects the server's current date and time. For instance, you might find it in a server log file, indicating when each event occurred, or in a database row, showing when each row was created or last updated. Timestamps are often included in the HTTP headers to let the client know when the resource was last modified.

View File

@@ -0,0 +1,3 @@
# Out-of-Band Data
"Out of band" data, in the context of server-side game development, refers to data that is transmitted separately from the main data stream. This data is used for managing control information rather than actual game data, for instance, data regarding the status of the server, notifications about issues, or urgent commands. Given its importance, it's often designed to bypass any queueing or buffering systems to be delivered straight to the application, hence its name — it is "out of band" compared to the normal data transmissions in the game. Please note, out of band data needs proper handling to prevent potential vulnerabilities including security issues.

View File

@@ -0,0 +1,3 @@
# Selective Acknowledgement
Selective Acknowledgement (SACK) is a mechanism introduced in TCP to improve its efficiency in handling packet loss in the network. When TCP detects packet loss, it normally retransmits all packets sent after the lost packet, regardless of whether they were received successfully or not. SACK, however, allows the receiver to acknowledge non-consecutive packets, effectively informing the sender exactly which packets were received successfully and which weren't. By using this mechanism, TCP can selectively retransmit only those packets that were lost, saving bandwidth and improving overall performance.

View File

@@ -0,0 +1,3 @@
# Operations
TCP (Transmission Control Protocol) is a connection-oriented protocol that ensures reliable, ordered, and error-checked delivery of data between systems over an IP network. Some of the primary operations include "Connection Establishment" that initializes a connection using a 3-way handshake, "Data Transfer" where packets of data are sent from sender to receiver, "Connection Termination" that closes the connection when transmission is complete, and "Error Control" which handles retransmission of lost data or corrupted packets. Additionally, TCP handles "Flow Control" to prevent a sender from overwhelming a receiver with data, and "Congestion Control" to manage network congestion and avoid packet loss.

View File

@@ -0,0 +1,3 @@
# Denial of Service
Denial of Service (DoS) is a malicious attempt to disrupt the regular functioning of a network, service, or server by overwhelming the network or server with a flood of internet traffic. The most common type of DoS attack involves flooding the target with unnecessary requests in an attempt to overload the system. In a Distributed Denial of Service (DDoS) attack, multiple computers are used to carry out the cyber attack. For servers, these attacks can significantly affect the availability and performance of games, causing a poor experience for the users.

View File

@@ -0,0 +1,3 @@
# Connection Hijacking
`Connection hijacking`, also known as session hijacking, is a serious security threat in the realm of server side game development. It refers to the exploitation of a valid computer session, or more precisely, the intrusion of an unauthorized user into a valid connection between two nodes or endpoints. The attacker intercepts the traffic between these two nodes, thereby 'hijacking' the connection. In game development, this could involve intercepting information between a game server and a client's system. Connection hijacking could expose sensitive data, tamper with the data in transit, or even redirect clients to rogue servers. Hence, implementing necessary security protocols to mitigate such vulnerability is crucial.

View File

@@ -0,0 +1,3 @@
# Veto
`Veto` is often used in multiplayer games to prevent or allow certain actions during the game. For instance, players can issue commands to block specific actions from their opponents. As a server-side game developer, you must ensure security measures are in place to validate the authenticity of these commands to safeguard against potential vulnerability. A veto vulnerability can occur when malicious players manipulate veto commands to their advantage or disrupt the game, which can lead to an unfair gaming environment or even crash the server. Therefore, your code should always verify who is sending veto commands and check the validity of these commands.

View File

@@ -0,0 +1,3 @@
# Vulnerability
`TCP` (Transmission Control Protocol) is an important protocol known for its reliability in delivering data packets, but it also has several vulnerabilities. The most significant weakness is TCPs susceptibility to a variety of **Denial-of-Service (DoS)** attacks, such as TCP SYN flood attacks, where an attacker sends a succession of SYN requests to a target's system in an attempt to consume server resources to make the system unresponsive. Additionally, sequence prediction can be exploited where an attacker can predict the sequence number of TCP packets to hijack the TCP session. There are also risks of IP spoofing, where an attacker can forge the IP address in the TCP packet header and pretend to be another user in the network. These vulnerabilities may expose sensitive information, disrupt normal functions or allow unauthorized access, especially in the context of a server-side game developer dealing with potentially large volumes of traffic.

View File

@@ -0,0 +1,3 @@
# TCP
TCP, an acronym for Transmission Control Protocol, is a fundamental protocol in the suite of internet protocols. It is a connection-oriented, reliable, byte stream service that sits at the transport layer of the OSI Model. In simpler terms, TCP enables two hosts to establish a connection and exchange data. During this communication, TCP ensures that data is not lost or received out of order by providing error checking mechanisms and a system for retransmission of lost data. Furthermore, to ensure efficient use of network resources, TCP implements flow control, congestion control and provides a means for hosts to dynamically adjust the throughput of the connection.

Some files were not shown because too many files have changed in this diff Show More