Compare commits

..

127 Commits

Author SHA1 Message Date
Arik Chakma
96d8f7385a fix: duplicate hero roadmap keys 2024-01-23 23:20:01 +06:00
Arik Chakma
cff26c495e feat: upgrade packages 2024-01-23 09:12:43 +06:00
Kamran Ahmed
ce0f2a4ee4 Add beginner backend roadmap 2024-01-22 21:51:30 +05:00
Kamran Ahmed
bf89b013d1 Add spring tile 2024-01-22 16:01:55 +05:00
Kamran Ahmed
5bcb3e282d Update new tags on roadmaps 2024-01-22 15:59:06 +05:00
Arik Chakma
747652c0f3 feat: add Node.js questions (#5056)
* feat: add nodejs questions

* feat: add more questions
2024-01-22 15:44:48 +05:00
Jafar Zakariya
ed0e376d46 feat: add Gradle resources (#5051)
* Update 104-using-gradle.md

Added a link to a tutorial for Gradle for complete beginners

* Update 104-using-gradle.md

Updated the resources with proper formatting and added one more video.

* Update 104-using-gradle.md

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2024-01-19 19:09:11 +06:00
Kamran Ahmed
ef3e4fc3f3 Add author information for fernando 2024-01-19 12:20:17 +05:00
Alvin
932896c3af fix: replace broken link to complete java course (#5049) 2024-01-18 23:08:14 +06:00
Kamran Ahmed
1539c6ccaf Update markdown 2024-01-18 21:15:50 +05:00
Kamran Ahmed
84aa35cdec Merge branch 'master' of github.com:kamranahmedse/developer-roadmap 2024-01-18 21:05:38 +05:00
Kamran Ahmed
b6a852b29b Add a guide for backend languages 2024-01-18 21:00:33 +05:00
Marco Malvicini
2d2f670153 typo-fix: typescript roadmap update 108-enum.md (#5030) 2024-01-18 11:57:33 +05:00
Komal Shehzadi
5cf7aa340f Correction in as type operator example (#5017) 2024-01-11 11:11:38 +05:00
Selva Muthu Kumaran Boopalan
601d21ca9d doc: add new AWS resources (#4954)
* devops-roadmap-aws

devops-roadmap-aws-newlink-provided
fixes: #4838

* Update src/data/roadmaps/devops/content/107-cloud-providers/100-aws.md

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2024-01-09 18:16:21 +06:00
Shreyash
a5527dd872 fix: react-native pdf (#4645)
The previous pdf contained 2 pages - 1st page for c++ and 2nd for react-native. Fixed this by deleting the first page of the pdf.
2024-01-09 18:00:02 +06:00
Ahmed Abdul Saad
4d6d943b4e doc: fix typo (#4889)
typo fix
2024-01-09 17:39:38 +06:00
Randil Tharusha
85214da400 doc: add javascript testing youtube video tutorial (#4926) 2024-01-09 17:15:07 +06:00
Tobiáš Smolný
46eb27a810 doc: add new resources (#5002) 2024-01-09 17:00:57 +06:00
Umut
e47bd63cc9 fix: typo (#4900)
There was a duplicate "the" word in the Sentiment Analysis entry. I fixed it.
2024-01-09 14:51:08 +06:00
Agustin Velez
d314f3d8c1 doc: add resources for variable and data types (#4948)
Add links for variables and data types from The Book (Official docs)
2024-01-09 14:49:51 +06:00
Dominik Galiev
52fdd8f07d fix: missing syntax (#5005) 2024-01-09 14:44:24 +06:00
dev-aly3n
22f59c66f0 fix: replace broken link with a valid one in frontend performance (#5001) 2024-01-09 14:15:17 +06:00
dev-aly3n
4a862241d3 fix: markdown missing closing parenthesis for link (#4999) 2024-01-09 14:13:38 +06:00
Sonvir
b1fdc7ff49 fix: update enable http reference url (#4922)
fix #4862

Co-authored-by: sonvir249 <39142-sonvir249@users.noreply.drupalcode.org>
2024-01-09 14:09:06 +06:00
dev-aly3n
445bdabde5 fix: change the broken link to a valid resource (#4970) 2024-01-09 00:03:55 +06:00
ArianHamdi
c46b4220a7 fix: change 'decentralised' to 'decentralized' (#4967) 2024-01-09 00:02:45 +06:00
Khalil Habib Shariff
cdcdfc4973 doc: clarity about flask (#4973)
added clarity about flask features
2024-01-08 23:59:44 +06:00
John Moore
d4b4b3c55c fix: grammars (#4989)
Proper and consistent styling
2024-01-08 23:49:58 +06:00
Samuel Mensah Boafo
2c0ebe4209 fix: javascript versions (#4992)
* Update 102-javascript-versions.md

* Update 102-javascript-versions.md

* fix: add link text

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2024-01-08 23:35:46 +06:00
Dhruv Kumar
c51438142c fix : year from 2023 to 2024 (#4979) 2024-01-08 23:29:49 +06:00
P J Sahrudh
d5a47b79db fix: try, catch, throw broken video link (#4961)
* Update index.md

Removed 'try, catch, throw" video as it is broken.

* Update src/data/roadmaps/javascript/content/107-javascript-control-flow/100-exception-handling/index.md

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2024-01-03 17:24:04 +06:00
Young
ca2a75537e fix: npm broken links (#4968)
The old link has expired 404

Co-authored-by: Young <yunyg@qq.com>
2024-01-03 17:19:02 +06:00
mshafiqyajid
f62faf214c fix: broken superagent official website link (#4942) 2023-12-31 07:28:42 +06:00
Muhammad Khalid
00b9630669 typo: followinw to following (#4943) 2023-12-31 07:24:07 +06:00
Rishiraj S
49ba524c15 fix: remove deprecated remove method (#4924) 2023-12-28 19:40:26 +06:00
ArianHamdi
d4436e8a8f fix: introduction to blockchain links (#4935)
* fix: introduction to blockchain links

* fix: Smart Contracts Introduction link
2023-12-28 19:37:46 +06:00
Arik Chakma
e0b3209dc4 Fix markdown link issue (#4849) 2023-12-25 21:21:52 +05:00
Olivier Tassinari
cf5dd19652 Update Material UI guide to v5 (#4588)
* Update Material UI guide to v5

* keep in sycn
2023-12-24 19:02:16 +06:00
Kamran Ahmed
16680e2629 Update dependencies 2023-12-24 16:20:12 +05:00
Selva Muthu Kumaran
b9b12333cb Fix broken Youtube link (#4888)
devops-roadmap-networking-protocols-sshfullgu :ide-new-videolink-provided
2023-12-19 20:33:47 +06:00
lucasffa
8a9bb60211 Fix link text typo (#4873)
There was a typo
2023-12-18 23:17:28 +06:00
Yusuf Seward
2c6bef62b2 Fix Content Typo (#3525) 2023-12-13 05:51:14 +06:00
steph
efb7e13f7d Fix Resource Typo (#4535) 2023-12-13 05:50:14 +06:00
Sarah Mak
b34c7eff65 Fix "discuss" typo (#4524) 2023-12-13 05:47:41 +06:00
bivashy
15c43fda5d Fix Broken Link and Typo (#4763)
* Fix typo in angular/rxjs-basics/marble-diagrams

* Remove Marble Diagrams broken link
2023-12-13 05:46:42 +06:00
rasalagean
b38f34a722 Fix Angular Roadmap Type Guard video URL (#4800) 2023-12-13 05:38:29 +06:00
Abderrahmane Larchi
f1780fabda Update Duplicate Link (#4819)
The first link now 'Control Flow Statements' just sends to the the third link for 'Branches', so it's just a duplicate now.
2023-12-13 05:36:03 +06:00
Simon
5362a64c29 Fix Minor Punctuations (#4826) 2023-12-13 05:34:55 +06:00
Sherkhan Azimov
720809f139 Fix Service Discovery Link (#4827) 2023-12-13 05:32:58 +06:00
Mahyar
5b03601aa2 Fix Polynomial Time Complexity (#4836) 2023-12-13 05:31:22 +06:00
Wasif
90df308751 Fix Content Heading (#4850)
Fixing Typos: Heading was wrong.
2023-12-13 05:27:33 +06:00
Sepehr Safari
3c0545e54f Fix Heap Typo (#4851) 2023-12-13 05:26:19 +06:00
Kamran Habib
4eb145dff4 Fix Content Grammar Typo (#4854)
I made a slight modification to improve the clarity of the sentence.
Specifically, I changed:

"Make sure to follow the instructions provided by the editor's documentation to set up C++ correctly."

to:

"Make sure to follow the instructions provided in the editor's documentation to set up C++ correctly."

This change maintains the same meaning but improves the flow of the sentence.
2023-12-13 05:25:38 +06:00
HlexNC
966d5fedb5 Update database section to focus on scaling databases (#4843) 2023-12-11 00:39:45 +00:00
Drew Rodrigues
243778cf11 Update session-based-authentication.md (#4844) 2023-12-11 00:39:10 +00:00
Kamran Ahmed
9c9c59911b Update backend roadmap JSON 2023-12-09 20:15:38 +00:00
Kamran Ahmed
7a93301b5b Upgrade dependencies 2023-12-09 20:08:12 +00:00
Kamran Ahmed
aa056c1da8 Update backend roadmap 2023-12-09 20:02:05 +00:00
Kamran Ahmed
13d1879977 Add embed roadmap functionalityg 2023-12-05 22:58:46 +00:00
Hardik
aca3163ba9 Flutter : update 100-material-widgets.md (#4824)
'RaisedButton' is deprecated and shouldn't be used. It is replaced by ElevatedButton
2023-12-05 21:36:19 +06:00
Kamran Ahmed
5e80d9d4d8 Add embed functionality 2023-12-05 15:32:17 +00:00
Kamran Ahmed
0fc28c482a Add content for AWS roadmap 2023-11-29 14:53:08 +00:00
Kamran Ahmed
837d2ac782 Add roadmap dirs for AWS 2023-11-29 14:09:49 +00:00
Kamran Ahmed
68c62bacc2 Add AWS roadmap 2023-11-29 05:31:41 +00:00
Jakub Kaźmierczak
720438e619 Add DDD resources (#4765) 2023-11-27 23:11:14 +00:00
Selva Muthu Kumaran
3afab1aa70 Add resource for DDOS (#4776)
cyber-security-ddos-link-updated
2023-11-27 23:10:14 +00:00
Selva Muthu Kumaran
f40585f992 Remove redundant link (#4777)
roadmap-javascript-built-in-object-redundant-link
fixes: #4758
2023-11-27 23:09:47 +00:00
Selva Muthu Kumaran
9232d03e24 Add resource for np completness (#4778)
roadmap-computer-science-np-complete-new-video-link-provided
fixes: #4731
2023-11-27 23:09:19 +00:00
Jakub Kaźmierczak
01cb4b5131 Add backpressure resource (#4779) 2023-11-27 23:08:46 +00:00
Yoandre Saavedra Gonzalez
50f02b504a Update Top Python Asynchronous Web Frameworks (#4766)
The number changed from 5 to 9 in the original article.
2023-11-27 01:42:51 +06:00
Jakub Kaźmierczak
2d12bffe46 Update backend service mesh description (#4752) 2023-11-25 01:13:29 +00:00
Rishi
3b1762cd91 Update Next.js resource (#4750)
Updated to latest video of NextJS from freecodecamp in Forntend RoadMap
2023-11-23 20:20:58 +00:00
Rachit Agrawal
d9be6e3c8b Fix invalid URL (#4747) 2023-11-23 20:20:24 +00:00
Kamran Ahmed
b65328ebc9 Add zilliz logo 2023-11-23 20:19:29 +00:00
Kamran Ahmed
5da5924b6c Update embed logo 2023-11-22 14:06:58 +00:00
Kamran Ahmed
b35a169315 Allow embedding of roadmaps 2023-11-22 13:53:30 +00:00
omkarl08
9d05c64f50 Add course for linear algebra (#4703)
* Update 100-linear-algebra.md

Kimberly Brehm's Linear Algebra Course

> Matrices: Properties, operations, and applications.
> Determinants: Role in system solvability and transformations.
> Vectors: Geometric interpretation and relevance in 
    transformations.

Eigenvalues & Eigenvectors: Stability and system analysis.
This course lays a solid foundation for game development. Matrices and vectors are core elements in creating graphics, handling transformations, and optimizing game performance. Understanding determinants aids in solving complex problems, while eigenvalues/eigenvectors are crucial for stability in game mechanics. Focus on matrices, vectors, and transformations for practical game design applications.

* Update src/data/roadmaps/game-developer/content/101-game-mathematics/100-linear-algebra.md

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2023-11-16 19:33:47 +00:00
Kaya-Sem
e94296cdd4 Update 101-app-shortcuts.md (#4705)
Removed unnecessary chatgpt line.
2023-11-16 19:32:37 +00:00
Max Mynter
7a4796508d Add Fullstack Deep Learning to MLOps (#4698) 2023-11-15 23:06:09 +00:00
Kamran Ahmed
e0f5d6f436 Update contribution functionality 2023-11-15 02:58:56 +00:00
Kamran Ahmed
d103bc629c Topic links contribution functionality 2023-11-15 02:46:45 +00:00
Kamran Ahmed
cb9943191e Add axum 2023-11-14 20:20:38 +00:00
Kamran Ahmed
eaa567dfe0 Add link to rust roadmap 2023-11-14 18:53:35 +00:00
Kamran Ahmed
277713e16b Add rust roadmap 2023-11-14 18:35:14 +00:00
Kamran Ahmed
5ed49b965c Add rust roadmap 2023-11-14 18:19:07 +00:00
Ebenezer Adeoye
a27aaf6e2d Fixed the links not working (#4677) 2023-11-12 18:50:59 +00:00
Kamran Ahmed
be02cc59ea Add favorite functionality 2023-11-12 18:50:05 +00:00
Abdelrhman Kamal
068847af08 Abdelrhman: Fix best practices articles (#4680)
* Fix gtx-trans close sidepanel

* reset the package-lock.json file

* Install the CodeSee workflow. Learn more at https://docs.codesee.io

* Fix Best Practcies roadmaps articels

* Restore files

---------

Co-authored-by: codesee-maps[bot] <86324825+codesee-maps[bot]@users.noreply.github.com>
2023-11-12 18:37:31 +00:00
Kamran Ahmed
c6c91ef8fe UI fix for friends page 2023-11-09 21:40:51 +00:00
Kamran Ahmed
8fb3e7983b Partial usage in the topics 2023-11-09 21:36:58 +00:00
Kamran Ahmed
80ec1a1c4b Fix images not working in latest astro (#4676)
* Fix respond invite

* Team roadmap icon fix

* Personal roadmap list and empty friends

* Fix invite friend

* Fix user progress modal

* Friends and notification pages

* Friends and notification pages

* Update

* Fix progress modal

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2023-11-09 21:32:46 +00: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
723 changed files with 59764 additions and 32506 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,48 +22,48 @@
"test:e2e": "playwright test"
},
"dependencies": {
"@astrojs/react": "^3.0.3",
"@astrojs/sitemap": "^3.0.2",
"@astrojs/tailwind": "^5.0.2",
"@fingerprintjs/fingerprintjs": "^4.1.0",
"@astrojs/react": "^3.0.9",
"@astrojs/sitemap": "^3.0.5",
"@astrojs/tailwind": "^5.1.0",
"@fingerprintjs/fingerprintjs": "^4.2.1",
"@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",
"clsx": "^2.0.0",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"astro": "^4.2.1",
"astro-compress": "^2.2.8",
"clsx": "^2.1.0",
"dracula-prism": "^2.1.13",
"jose": "^4.15.4",
"jose": "^5.2.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.288.0",
"nanoid": "^5.0.2",
"nanostores": "^0.9.4",
"node-html-parser": "^6.1.10",
"npm-check-updates": "^16.14.6",
"lucide-react": "^0.300.0",
"nanoid": "^5.0.4",
"nanostores": "^0.9.5",
"node-html-parser": "^6.1.12",
"npm-check-updates": "^16.14.12",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-confetti": "^6.1.0",
"react-dom": "^18.2.0",
"reactflow": "^11.9.4",
"reactflow": "^11.10.2",
"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.2.1",
"tailwindcss": "^3.4.1",
"zustand": "^4.5.0"
},
"devDependencies": {
"@playwright/test": "^1.39.0",
"@playwright/test": "^1.41.1",
"@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",
"gh-pages": "^6.1.1",
"js-yaml": "^4.1.0",
"markdown-it": "^13.0.2",
"openai": "^4.13.0",
"prettier": "^3.0.3",
"prettier-plugin-astro": "^0.12.0",
"prettier-plugin-tailwindcss": "^0.5.6"
"markdown-it": "^14.0.0",
"openai": "^4.25.0",
"prettier": "^3.2.4",
"prettier-plugin-astro": "^0.12.3",
"prettier-plugin-tailwindcss": "^0.5.11"
}
}

3782
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

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

0
public/manifest/apple-touch-icon.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

0
public/manifest/favicon.ico Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

0
public/manifest/icon152.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

0
public/manifest/icon16.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 123 B

After

Width:  |  Height:  |  Size: 123 B

0
public/manifest/icon196.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

0
public/manifest/icon32.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 267 B

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/roadmaps/aws.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

BIN
public/roadmaps/rust.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

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)
@@ -53,6 +53,7 @@ Here is the list of available roadmaps with more being actively worked upon.
- [Android Roadmap](https://roadmap.sh/android)
- [Flutter Roadmap](https://roadmap.sh/flutter)
- [Go Roadmap](https://roadmap.sh/golang)
- [Rust Roadmap](https://roadmap.sh/rust)
- [Java Roadmap](https://roadmap.sh/java)
- [Spring Boot Roadmap](https://roadmap.sh/spring-boot)
- [Design System Roadmap](https://roadmap.sh/design-system)
@@ -67,6 +68,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

@@ -59,9 +59,9 @@ function writeTopicContent(currTopicUrl) {
const roadmapTitle = roadmapId.replace(/-/g, ' ');
let prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${childTopic}". Write me a brief paragraph for that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output.`;
let prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${childTopic}". Write me a brief paragraph for that. Your output should be strictly markdown. Do not include anything other than the description in your output. I already know the benefits of each so do not add benefits in the output.`;
if (!childTopic) {
prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${parentTopic}". Write me a brief paragraph for that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output.`;
prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${parentTopic}". Write me a brief paragraph for that. Your output should be strictly markdown. Do not include anything other than the description in your output. I already know the benefits of each so do not add benefits in the output.`;
}
console.log(`Generating '${childTopic || parentTopic}'...`);

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

@@ -53,14 +53,20 @@ export type GetRoadmapResponse = RoadmapDocument & {
export function hideRoadmapLoader() {
const loaderEl = document.querySelector(
'[data-roadmap-loader]'
'[data-roadmap-loader]',
) as HTMLElement;
if (loaderEl) {
loaderEl.remove();
}
}
export function CustomRoadmap() {
type CustomRoadmapProps = {
isEmbed?: boolean;
};
export function CustomRoadmap(props: CustomRoadmapProps) {
const { isEmbed = false } = props;
const { id, secret } = getUrlParams() as { id: string; secret: string };
const [isLoading, setIsLoading] = useState(true);
@@ -71,14 +77,15 @@ export function CustomRoadmap() {
setIsLoading(true);
const roadmapUrl = new URL(
`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${id}`
`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${id}`,
);
if (secret) {
roadmapUrl.searchParams.set('secret', secret);
}
const { response, error } = await httpGet<GetRoadmapResponse>(
roadmapUrl.toString()
roadmapUrl.toString(),
);
if (error || !response) {
@@ -95,7 +102,10 @@ export function CustomRoadmap() {
}
async function trackVisit() {
if (!isLoggedIn()) return;
if (!isLoggedIn() || isEmbed) {
return;
}
await httpPost(`${import.meta.env.PUBLIC_API_URL}/v1-visit`, {
resourceId: id,
resourceType: 'roadmap',
@@ -119,9 +129,9 @@ export function CustomRoadmap() {
return (
<>
<RoadmapHeader />
<FlowRoadmapRenderer roadmap={roadmap!} />
<TopicDetail canSubmitContribution={false} />
{!isEmbed && <RoadmapHeader />}
<FlowRoadmapRenderer isEmbed={isEmbed} roadmap={roadmap!} />
<TopicDetail isEmbed={isEmbed} canSubmitContribution={false} />
</>
);
}

View File

@@ -0,0 +1,81 @@
import { useStore } from '@nanostores/react';
import { Check, Copy } from 'lucide-react';
import { Modal } from '../Modal';
import { useToast } from '../../hooks/use-toast';
import { useCopyText } from '../../hooks/use-copy-text';
import { currentRoadmap, isCurrentRoadmapPersonal } from '../../stores/roadmap';
import { cn } from '../../lib/classname.ts';
type ShareRoadmapModalProps = {
onClose: () => void;
};
export function EmbedRoadmapModal(props: ShareRoadmapModalProps) {
const { onClose } = props;
const toast = useToast();
const $currentRoadmap = useStore(currentRoadmap);
const $isCurrentRoadmapPersonal = useStore(isCurrentRoadmapPersonal);
const roadmapId = $currentRoadmap?._id!;
const { copyText, isCopied } = useCopyText();
const isDev = import.meta.env.DEV;
const baseUrl = isDev ? 'http://localhost:3000' : 'https://roadmap.sh';
const embedHtml = `<iframe src="${baseUrl}/r/embed?id=${roadmapId}" width="100%" height="500px" frameBorder="0"\n></iframe>`;
return (
<Modal onClose={onClose} wrapperClassName={'max-w-[500px]'}>
<div className="p-4 pb-0">
<h1 className="text-xl font-semibold leading-5 text-gray-900">
Embed Roadmap
</h1>
</div>
<div className="px-4 pt-3">
<p className={'mb-2 text-sm text-gray-500'}>
Copy the following HTML code and paste it into your website.
</p>
<input
type="text"
value={embedHtml}
readOnly={true}
onClick={(e) => {
e.currentTarget.select();
copyText(embedHtml);
}}
className="w-full resize-none rounded-md border bg-gray-50 p-2 text-sm"
/>
</div>
<div className="flex items-center justify-between px-4 pb-4 pt-2">
<button
className={cn(
'flex h-9 w-full items-center justify-center rounded-md border border-transparent px-4 py-2 text-sm font-medium text-white outline-none',
{
'bg-green-500 hover:bg-green-600 focus:bg-green-600': isCopied,
'bg-gray-900 hover:bg-gray-800 focus:bg-gray-800': !isCopied,
},
)}
onClick={() => {
copyText(embedHtml);
}}
>
{isCopied ? (
<>
<Check size={14} className="mr-2 stroke-[2.5]" />
Copied
</>
) : (
<>
<Copy size={14} className="mr-2 stroke-[2.5]" />
Copy Link
</>
)}
</button>
</div>
</Modal>
);
}

View File

@@ -1,25 +1,27 @@
import { ReadonlyEditor } from '../../../editor/readonly-editor';
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
import {
renderResourceProgress,
updateResourceProgress,
type ResourceProgressType,
renderTopicProgress,
refreshProgressCounters,
renderResourceProgress,
renderTopicProgress,
type ResourceProgressType,
updateResourceProgress,
} from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page';
import { useToast } from '../../hooks/use-toast';
import type { Node } from 'reactflow';
import { useCallback, type MouseEvent, useMemo, useState, useRef } from 'react';
import { type MouseEvent, useCallback, useRef, useState } from 'react';
import { EmptyRoadmap } from './EmptyRoadmap';
import { cn } from '../../lib/classname';
import { totalRoadmapNodes } from '../../stores/roadmap.ts';
type FlowRoadmapRendererProps = {
isEmbed?: boolean;
roadmap: RoadmapDocument;
};
export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
const { roadmap } = props;
const { roadmap, isEmbed = false } = props;
const roadmapId = String(roadmap._id!);
const [hideRenderer, setHideRenderer] = useState(false);
@@ -31,6 +33,10 @@ export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
topicId: string,
newStatus: ResourceProgressType,
) {
if (isEmbed) {
return;
}
pageProgressMessage.set('Updating progress');
updateResourceProgress(
{
@@ -138,6 +144,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

@@ -11,6 +11,7 @@ import { RoadmapActionButton } from './RoadmapActionButton';
import { Lock, Shapes } from 'lucide-react';
import { Modal } from '../Modal';
import { ShareSuccess } from '../ShareOptions/ShareSuccess';
import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx';
type RoadmapHeaderProps = {};
@@ -44,11 +45,11 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
{
resourceId: roadmapId,
resourceType: 'roadmap',
}
},
));
} else {
({ error, response } = await httpDelete<TeamResourceConfig>(
`${baseApiUrl}/v1-delete-roadmap/${roadmapId}`
`${baseApiUrl}/v1-delete-roadmap/${roadmapId}`,
));
}
@@ -119,7 +120,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
</div>
<div className="flex justify-between gap-2 sm:gap-0">
<div className="flex gap-1 sm:gap-2">
<div className="flex justify-stretch gap-1 sm:gap-2">
<a
href="/roadmaps"
className="rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm"
@@ -128,14 +129,12 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
&larr;<span className="hidden sm:inline">&nbsp;All Roadmaps</span>
</a>
<button
data-guest-required
data-popup="login-popup"
className="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"
>
<span className="ml-2">Subscribe</span>
</button>
<ShareRoadmapButton
roadmapId={roadmapId!}
description={description!}
pageUrl={`https://roadmap.sh/r?id=${roadmapId}`}
allowEmbed={true}
/>
</div>
<div className="flex items-center gap-2">
{$canManageCurrentRoadmap && (
@@ -162,9 +161,9 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
)}
<a
href={`${import.meta.env.PUBLIC_EDITOR_APP_URL}/${
$currentRoadmap?._id
}`}
href={`${
import.meta.env.PUBLIC_EDITOR_APP_URL
}/${$currentRoadmap?._id}`}
target="_blank"
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
>
@@ -183,7 +182,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
<RoadmapActionButton
onDelete={() => {
const confirmation = window.confirm(
'Are you sure you want to delete this roadmap?'
'Are you sure you want to delete this roadmap?',
);
if (!confirmation) {

View File

@@ -12,7 +12,10 @@ export function SkeletonRoadmapHeader() {
</div>
<div className="flex justify-between gap-2 sm:gap-0">
<div className="h-7 w-[35.04px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-32" />
<div className='flex gap-1 sm:gap-2'>
<div className="h-7 w-[35.04px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-32" />
<div className="h-7 w-[35.04px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[85px]" />
</div>
<div className="flex items-center gap-2">
<div className="h-7 w-[60.52px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[139.71px]" />
<div className="h-7 w-[71.48px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[100.34px]" />

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 'lucide-react';
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 size={'60px'} 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

@@ -13,7 +13,7 @@ const { frontmatter, id } = guide;
class:list={[
"block no-underline py-2 group text-md items-center text-gray-600 hover:text-blue-600 flex justify-between border-b",
]}
href={`/guides/${id}`}
href={frontmatter.excludedBySlug ? frontmatter.excludedBySlug : `/guides/${id}`}
>
<span class="group-hover:translate-x-2 transition-transform">
{frontmatter.title}

View File

@@ -121,7 +121,7 @@ export function HeroRoadmaps(props: ProgressListProps) {
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
{progress.map((resource) => (
<HeroRoadmap
key={resource.resourceId}
key={`${resource.resourceType}-${resource.resourceId}`}
resourceId={resource.resourceId}
resourceType={resource.resourceType}
resourceTitle={resource.resourceTitle}

View File

@@ -1,5 +1,5 @@
<div
class='prose-xl prose-blockquote:font-normal prose container prose-code:bg-transparent prose-h2:text-3xl prose-h2:mt-10 prose-h2:mb-3 prose-h3:mt-2 prose-img:mt-1'
class='prose-xl prose-blockquote:font-normal prose container prose-code:bg-transparent prose-h2:text-3xl prose-h2:mt-10 prose-h2:mb-3 prose-h5:font-medium prose-h3:mt-2 prose-img:mt-1'
>
<slot />
</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

@@ -15,7 +15,7 @@ const { id, title, subtitle } = Astro.props;
<div
id={id}
tabindex='-1'
class='hidden bg-black/50 overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 h-full items-center justify-center popup'
class='hidden bg-black/50 overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-[999] h-full items-center justify-center popup'
>
<div class='relative p-4 w-full max-w-md h-full md:h-auto'>
<div class='relative bg-white rounded-lg shadow popup-body'>

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={ className?.indexOf('text-') !== -1 ? 'currentColor' : '#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

@@ -45,6 +45,8 @@ export function ShareSuccess(props: ShareSuccessProps) {
},
];
const embedHtml = `<iframe src="${baseUrl}/r/embed?id=${roadmapId}" width="100%" height="500px" frameBorder="0"\n></iframe>`;
return (
<div className="flex grow flex-col justify-center">
<div className="mt-5 flex grow flex-col items-center justify-center gap-1.5">
@@ -76,6 +78,23 @@ export function ShareSuccess(props: ShareSuccessProps) {
</p>
)}
<div className="mt-2 border-t pt-2">
<p className="text-sm text-gray-400">
You can also embed this roadmap on your website.
</p>
<div className="mt-2">
<input
onClick={(e) => {
e.currentTarget.select();
copyText(embedHtml);
}}
readOnly={true}
className="w-full resize-none rounded-md border bg-gray-50 p-2 text-sm"
value={embedHtml}
/>
</div>
</div>
{visibility === 'public' && (
<>
<div className="-mx-4 mt-4 flex items-center gap-1.5">

View File

@@ -0,0 +1,164 @@
import { Check, Code2, Copy, Facebook, Linkedin, Share2 } 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';
import { EmbedRoadmapModal } from './CustomRoadmap/EmbedRoadmapModal.tsx';
type ShareRoadmapButtonProps = {
roadmapId?: string;
description: string;
pageUrl: string;
allowEmbed?: boolean;
};
export function ShareRoadmapButton(props: ShareRoadmapButtonProps) {
const { description, pageUrl, allowEmbed = false, roadmapId } = props;
const { isCopied, copyText } = useCopyText();
const [isEmbedModalOpen, setIsEmbedModalOpen] = useState(false);
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);
});
const embedHtml = `<iframe src="https://roadmap.sh/r/embed?id=${roadmapId}" width="100%" height="500px" frameBorder="0"\n></iframe>`;
return (
<div className="relative" ref={containerRef}>
{isEmbedModalOpen && (
<EmbedRoadmapModal
onClose={() => {
setIsEmbedModalOpen(false);
}}
/>
)}
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className={cn(
'inline-flex h-full 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-[999] mt-1 w-48 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>
{allowEmbed && roadmapId && (
<button
onClick={() => {
setIsDropdownOpen(false);
setIsEmbedModalOpen(true);
}}
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">
<Code2 size="15px" className="text-slate-400" />
</div>
Embed Roadmap
</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';
@@ -17,19 +15,21 @@ import {
} from '../../lib/resource-progress';
import { pageProgressMessage, sponsorHidden } from '../../stores/page';
import { TopicProgressButton } from './TopicProgressButton';
import { ContributionForm } from './ContributionForm';
import { showLoginPopup } from '../../lib/popup';
import { useToast } from '../../hooks/use-toast';
import type {
AllowedLinkTypes,
RoadmapContentDocument,
} from '../CustomRoadmap/CustomRoadmap';
import { markdownToHtml } from '../../lib/markdown';
import { markdownToHtml, sanitizeMarkdown } 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';
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
type TopicDetailProps = {
isEmbed?: boolean;
canSubmitContribution: boolean;
};
@@ -43,9 +43,10 @@ const linkTypes: Record<AllowedLinkTypes, string> = {
};
export function TopicDetail(props: TopicDetailProps) {
const { canSubmitContribution } = props;
const { canSubmitContribution, isEmbed = false } = props;
const [contributionAlertMessage, setContributionAlertMessage] = useState('');
const [hasEnoughLinks, setHasEnoughLinks] = useState(false);
const [contributionUrl, setContributionUrl] = useState('');
const [isActive, setIsActive] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isContributing, setIsContributing] = useState(false);
@@ -67,12 +68,10 @@ export function TopicDetail(props: TopicDetailProps) {
// Close the topic detail when user clicks outside the topic detail
useOutsideClick(topicRef, () => {
setIsActive(false);
setIsContributing(false);
});
useKeydown('Escape', () => {
setIsActive(false);
setIsContributing(false);
});
// Toggle topic is available even if the component UI is not active
@@ -121,7 +120,6 @@ export function TopicDetail(props: TopicDetailProps) {
setIsActive(true);
sponsorHidden.set(true);
setContributionAlertMessage('');
setTopicId(topicId);
setResourceType(resourceType);
setResourceId(resourceId);
@@ -159,20 +157,27 @@ export function TopicDetail(props: TopicDetailProps) {
}
let topicHtml = '';
if (!isCustomResource) {
// It's full HTML with page body, head etc.
// We only need the inner HTML of the #main-content
const node = new DOMParser().parseFromString(
response as string,
topicHtml = response as string;
const topicDom = new DOMParser().parseFromString(
topicHtml,
'text/html',
);
topicHtml = node?.getElementById('main-content')?.outerHTML || '';
const links = topicDom.querySelectorAll('a');
const urlElem: HTMLElement =
topicDom.querySelector('[data-github-url]')!;
const contributionUrl = urlElem?.dataset?.githubUrl || '';
setContributionUrl(contributionUrl);
setHasEnoughLinks(links.length >= 3);
} else {
setLinks((response as RoadmapContentDocument)?.links || []);
setTopicTitle((response as RoadmapContentDocument)?.title || '');
topicHtml = markdownToHtml(
(response as RoadmapContentDocument)?.description || '',
false,
const sanitizedMarkdown = sanitizeMarkdown(
(response as RoadmapContentDocument).description || '',
);
topicHtml = markdownToHtml(sanitizedMarkdown, false);
}
setIsLoading(false);
@@ -203,42 +208,28 @@ 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>
)}
{!isLoading && isContributing && (
<ContributionForm
resourceType={resourceType}
resourceId={resourceId}
topicId={topicId}
onClose={(message?: string) => {
if (message) {
setContributionAlertMessage(message);
}
setIsContributing(false);
}}
/>
)}
{!isContributing && !isLoading && !error && (
<>
{/* Actions for the topic */}
<div className="mb-2">
<TopicProgressButton
topicId={topicId}
resourceId={resourceId}
resourceType={resourceType}
onClose={() => {
setIsActive(false);
setIsContributing(false);
}}
/>
{!isEmbed && (
<TopicProgressButton
topicId={topicId}
resourceId={resourceId}
resourceType={resourceType}
onClose={() => {
setIsActive(false);
}}
/>
)}
<button
type="button"
@@ -246,10 +237,9 @@ export function TopicDetail(props: TopicDetailProps) {
className="absolute right-2.5 top-2.5 inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900"
onClick={() => {
setIsActive(false);
setIsContributing(false);
}}
>
<img alt="Close" className="h-5 w-5" src={CloseIcon.src} />
<X className="h-5 w-5" />
</button>
</div>
@@ -299,29 +289,21 @@ export function TopicDetail(props: TopicDetailProps) {
)}
{/* Contribution */}
{canSubmitContribution && (
{canSubmitContribution && !hasEnoughLinks && contributionUrl && (
<div className="mt-8 flex-1 border-t">
<p className="mb-2 mt-2 text-sm leading-relaxed text-gray-400">
Help others learn by submitting links to learn more about this
topic{' '}
Help us improve this introduction and submit a link to a good
article, podcast, video, or any other resource that helped you
understand this topic better.
</p>
<button
onClick={() => {
if (isGuest) {
setIsActive(false);
showLoginPopup();
return;
}
setIsContributing(true);
}}
disabled={!!contributionAlertMessage}
className="block w-full rounded-md bg-gray-800 p-2 text-sm text-white transition-colors hover:bg-black hover:text-white disabled:bg-green-200 disabled:text-black"
<a
href={contributionUrl}
target={'_blank'}
className="flex w-full items-center justify-center rounded-md bg-gray-800 p-2 text-sm text-white transition-colors hover:bg-black hover:text-white disabled:bg-green-200 disabled:text-black"
>
{contributionAlertMessage
? contributionAlertMessage
: 'Submit a Link'}
</button>
<GitHubIcon className="mr-2 inline-block h-4 w-4 text-white" />
Edit this Content
</a>
</div>
)}
</>
@@ -339,7 +321,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 && (

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