Compare commits

...

279 Commits

Author SHA1 Message Date
Arik Chakma
e5420d2f56 Fix markdown link issue 2023-12-11 20:03:37 +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
Kamran Ahmed
980e243124 Fix issue with chrome v83 2023-10-29 16:54:54 +00:00
Arik Chakma
044046e044 Add forkable Backend Roadmap (#4635)
* Add forkable Backend roadmap

* Add `(Fork)` at title
2023-10-28 13:02:32 +01:00
Kamran Ahmed
793764c3a3 Fix URL for http caching 2023-10-27 14:41:19 +01:00
Kamran Ahmed
abc8a97676 Update twitter link 2023-10-27 01:57:53 +01:00
Kamran Ahmed
79355cd876 Update meta titles 2023-10-26 22:59:18 +01:00
Kamran Ahmed
2809b81920 Add game developer roadmap 2023-10-26 22:54:46 +01:00
Kamran Ahmed
204a9577cd Add content for game developer roadmap 2023-10-26 20:34:04 +01:00
Kamran Ahmed
577e724aa7 Add game developer roadmap 2023-10-26 19:53:45 +01:00
Abdelrhman Kamal
14a1544ed4 Feat auto-focused side panel (#4631)
* Fix gtx-trans close sidepanel

* reset the package-lock.json file

* Feat: Add auto focus to side panels

* resote changes
2023-10-25 19:10:59 +01:00
Kamran Ahmed
14ea7ba0ad Open roadmap editor in same window 2023-10-25 16:32:37 +01:00
Kamran Ahmed
5e7ec4f8d8 Add scalability article 2023-10-25 16:06:50 +01:00
Sherkhan Azimov
417badc6ea fix: broken link to scalability in system design (#4616) 2023-10-25 16:05:49 +01:00
Arik Chakma
0558957673 Allow creating personal version of frontend roadmap (#4627)
* Create Roadmap Version

* Change button position

* Update frontend JSON

* Remove `topicCount`

* Add fork at title

* Update UI for create your own version

* Add functionality to load your own version

* Load user version of roadmap

* Update forkable roadmap

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2023-10-25 12:51:05 +01:00
Abdelrhman Kamal
7f6a42a0c5 Clarify Usage of MongoDB's $currentDate operator (#4630)
* Fix gtx-trans close sidepanel

* reset the package-lock.json file

* Fix: mongoDB date type
2023-10-25 09:47:15 +01:00
Abdelrhman Kamal
cc258b7612 Fix mongodb optimization section (#4629)
* Fix gtx-trans close sidepanel

* reset the package-lock.json file

* Fix: Performance Optimization

* Restore src/components/TopicDetail/TopicProgressButton.tsx file
2023-10-25 09:42:35 +01:00
Kamran Ahmed
7da244fe10 Add related questions below roadmaps 2023-10-24 23:48:50 +01:00
Kamran Ahmed
cf78628c0c Add content for android 2023-10-24 21:01:55 +01:00
Kamran Ahmed
498e03720f Create files for android roadmap 2023-10-24 20:57:54 +01:00
Kamran Ahmed
5c69b05470 Update android roadmap 2023-10-24 20:49:59 +01:00
Abdelrhman Kamal
309cf3d6d9 Fix: google translate extenstion close side panel (#4625)
* Fix gtx-trans close sidepanel

* reset the package-lock.json file
2023-10-24 14:19:53 +01:00
Kamran Ahmed
4f3b891e45 Update dependencies 2023-10-24 14:16:26 +01:00
Kamran Ahmed
47f548a0e4 Update dependencies 2023-10-24 14:07:41 +01:00
Kamran Ahmed
a988ecc4ab Roadmap action button color 2023-10-24 14:03:36 +01:00
Kamran Ahmed
c723070057 Remove web-draw package 2023-10-23 16:57:58 +01:00
Kamran Ahmed
3a0e588530 Refactor to fix editor scaling issues (#4618)
* Ignore editor file

* Integrate Readonly Editor

* Remove logs

* Implement minimum height

* Implement Custom Roadmap Modal

* Implement Custom Roadmap progress modal

* Implement Readonly Editor

* Implement utils

* Update `gitignore`

* Fix generate renderer script

* Refactor UI

* Add Empty Roadmap state

* Upgrade dependencies and editor update

* Update deployment workflow

* Update roadmap header

* Update dependencies

* Refactor Readonly editor

* Add Readonly Dummy Editor

* Add editor to gitignore

* Add Assume Unchanged

* Add editor in the tailwind

* Fix tailwind issue

* Fix URL for add friends

* Add share with friends functionality

* Update workflow

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2023-10-21 19:42:55 +01:00
Arik Chakma
d46cf26812 Minor Improvement for Custom Roadmap (#4590)
* Add Edit button in the roadmap list

* Add share with others button

* Fix editor link
2023-10-21 19:40:26 +01:00
Kamran Ahmed
b06e82de5f Sponsor for nginx 2023-10-13 22:41:46 +01:00
Kamran Ahmed
d65ecac777 Account dropdown changes 2023-10-13 19:52:21 +01:00
Kamran Ahmed
c46d962803 Add links to questions 2023-10-12 21:22:01 +01:00
Kamran Ahmed
bd4e7ea3d0 Add links to questions 2023-10-12 21:20:21 +01:00
Kamran Ahmed
252b083a48 add roadmap editor image 2023-10-12 20:56:01 +01:00
Arik Chakma
abbeb717d1 Add JavaScript questions (#4505)
* Add Javascript questions

* wip: add more questions

* wip: add ternary operator

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* fix: set example

* wip: add more questions

* wip: add more question

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add another question

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions

* wip: add more questions
2023-10-12 15:03:19 +01:00
Kamran Ahmed
485ca9dd8f Spring testing link fix 2023-10-11 14:32:40 +01:00
Kamran Ahmed
c3315fb41e Fix typo on teams page 2023-10-11 12:56:56 +01:00
Kamran Ahmed
6ed436674f Discovery page option in sharing 2023-10-10 00:12:05 +01:00
Kamran Ahmed
76c6c4dc1f isDiscoverable not persisted 2023-10-10 00:06:24 +01:00
Kamran Ahmed
cb56e85651 Discoverable option selection 2023-10-09 23:27:49 +01:00
Kamran Ahmed
dcf740e275 Update share buttons text 2023-10-09 21:57:33 +01:00
Arik Chakma
16662ed699 Implement Social Share options (#4569)
* Implement social share options

* Minor fix
2023-10-09 21:49:21 +01:00
Kamran Ahmed
6f9fe361ae Change style of custom roadmap page 2023-10-09 09:07:59 +01:00
Arik Chakma
036b34c6f3 Implement Custom Roadmap minor features (#4565)
* Remove roadmap type

* Add Edit Roadmap button

* Add Edit Roadmap permission

* Add Edit and Share roadmap button

* Remove Margin

* Implement Discoverable Checkbox

* Add Loading State for buttons
2023-10-09 08:44:30 +01:00
Kamran Ahmed
93c2043f23 Fix warning in hero roadmap 2023-10-08 18:38:02 +01:00
Saleh Hashemi
d2da3c8621 update checkout version to v4 (#4559) 2023-10-07 22:15:01 +01:00
Kamran Ahmed
4aa8f15c07 Add email icon in footer 2023-10-07 15:31:24 +01:00
Arik Chakma
ceb4c3b95d Remove invited members from sharing settings (#4555)
* Fix team member list

* Minor change
2023-10-06 19:00:48 +01:00
Kamran Ahmed
7ec5e30b51 Hero roadmap section updates 2023-10-06 19:00:08 +01:00
Kamran Ahmed
e5e0a7c8c5 Add teams banner 2023-10-04 20:32:28 +01:00
Kamran Ahmed
90f3ffe270 Add banner for teams 2023-10-04 16:13:41 +01:00
Kamran Ahmed
ce47a7433e Teams button in navigation 2023-10-04 15:44:34 +01:00
Selva Muthu Kumaran
21b8358683 roadmap-aspnet-change-tracker-api.md (#4546)
aspnet-change-tracker-api URL fixed
fixes : #4544
2023-10-04 20:40:45 +06:00
Kamran Ahmed
e1751b105f Add team page 2023-10-04 15:28:46 +01:00
Kamran Ahmed
e43bea7c40 Setup redirects on the teams page 2023-10-04 15:22:20 +01:00
Kamran Ahmed
5fa669aec2 Update team page 2023-10-04 15:06:59 +01:00
Kamran Ahmed
4b8f868b2b Add roadmaps and friends to account dropdown 2023-10-04 10:34:29 +01:00
Kamran Ahmed
a0743a8272 Fix sharing options button 2023-10-04 10:30:28 +01:00
Arik Chakma
2cae13c090 Add Members while Transferring Roadmap (#4534)
* Add members while Transferring Roadmap

* Implement Responsive in Roadmaps page
2023-10-04 10:15:56 +01:00
Kamran Ahmed
0bf287f1d6 Add features to pricing 2023-10-04 10:12:08 +01:00
Kamran Ahmed
d7d819b4b3 Add teams introduction page 2023-10-03 21:07:53 +01:00
Kamran Ahmed
29cff6a6f8 Update badge 2023-10-02 17:34:51 +01:00
Kamran Ahmed
044df81b7a Creator details on roadmap page 2023-10-02 17:03:20 +01:00
Arik Chakma
3151ee5021 Add Creator Details (#4530)
* Add Creator details

* Add Skeleton Loading
2023-10-02 16:37:30 +01:00
Kamran Ahmed
e6ce9f40ee Update roadmap contribution template 2023-10-02 15:50:42 +01:00
Kamran Ahmed
3b5e3c44f9 Update label for roadmap creation 2023-10-02 15:10:48 +01:00
Kamran Ahmed
c286e0a6f8 Increase max team member count 2023-10-01 03:24:16 +01:00
linxiaowang
3bebe0c1de fix(typo): fix typo in 101-instanceof-operator.md (#4514) 2023-10-01 03:00:55 +06:00
Sherkhan Azimov
9845fe624a separate articles in 107-domain-name-system.md (#4517)
Transfer an article to a new line
2023-10-01 02:59:02 +06:00
Nicky Lim
4b2b2ebe8c Fix typo cpp 104 index (#4520) 2023-09-30 21:40:03 +06:00
Arik Chakma
82c2aaacc3 Fix Roadmap Share Link (#4522) 2023-09-30 14:48:04 +01:00
Kamran Ahmed
6d1edb76c7 Fix failing build 2023-09-30 14:28:16 +01:00
Kamran Ahmed
5d57d5baaf Update deployment workflow 2023-09-30 14:25:25 +01:00
Kamran Ahmed
d31d626c61 Update deployment workflow 2023-09-30 14:23:00 +01:00
Kamran Ahmed
71bf34e683 Add personal token 2023-09-30 14:17:47 +01:00
Kamran Ahmed
93a91b1d9b Fix failing build 2023-09-30 14:14:47 +01:00
Kamran Ahmed
18c8bd14b2 Fix failing build 2023-09-30 14:13:06 +01:00
Kamran Ahmed
e34695e334 Fix failing build 2023-09-30 14:03:49 +01:00
Arik Chakma
8310671123 Allow creating custom roadmaps (#4486)
* wip: custom roadmap renderer

* wip: custom roadmap events

* wip: roadmap content

* wip: svg styles

* wip: custom roadmap progress

* Render progress

* Shortcut progress

* Progress Tracking styles

* wip: edit and share button

* fix: disabled the share button

* wip: content links rendering

* Fix progress share

* Replace disabled with `canShare`

* wip: show custom roadmaps

* wip: users all roadmaps

* fix: create roadmap api

* chore: roadmap sidebar icon

* wip: content links

* Update links color

* Create roadmap home

* Create Roadmap button

* Roadmap type

* chore: share progress modal

* wip: share roadmap

* wip: change visibility

* chore: custom roadmap progress in activity

* wip: custom roadmap share progress

* chore: friend's roadmap

* wip: custom roadmap skeleton

* chore: roadmap title

* Restricted Page

* fix: skeleton loading width

* Fix create roadmap button

* chore: remove user id

* chore: pick roadmap and share

* chore: open new tab on create roadmap

* chore: change share title

* chore: use team id from params

* chore: team roadmap create modal

* chore: create team roadmap

* chore: custom roadmap modal

* chore: placeholde roadmaps

* chore: roadmap hint

* chore: visibility label

* chore: public roadmap

* chore: empty screen

* chore: team progress

* chore: create roadmap responsive

* chore: form error

* chore: multi user history

* wip: manage custom roadmap

* chore: empty roadmap list

* chore: custom roadmap visit

* chore: shared roadmaps

* chore: shared roadmaps

* chore: empty screen and topic title

* chore: show progress bar

* Implement Error in topic details

* Add Modal close button

* fix: link groups

* Refactor roadmap creation

* Refactor roadmap creation

* Refactor team creation

* Refactor team roadmaps

* Refactor team creation roadmap selection

* Refactor

* Refactor team roadmap loading

* Refactor team roadmaps

* Refactor team roadmaps listing

* Refactor Account dropdown

* Updates

* Refactor Account dropdown

* Fix Team name overflow

* Change Icon color

* Update team dropdown

* Minor UI fixes

* Fix minor UI

* Flicker fix in team dropdown

* Roadmap action dropdown with responsiveness

* Team roadmaps listing

* Update team settings

* Team roadmaps listing

* fix: remove visibility change

* Update roadmap options modal

* Add dummy renderer

* Add renderer script

* Add generate renderer script

* Add generate renderer

* wip: add share settings

* Update

* Update UI

* Update Minor UI

* Fix team issue

* Update Personal roadmaps UI

* Add Roadmap Secret

* Update teams type

* Rearrange sections

* Change Secret name

* Add action button on roadmap detail page

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2023-09-30 13:55:24 +01:00
Kamran Ahmed
d45c8f9cb2 Update coursera links 2023-09-29 17:26:54 +01:00
Kamran Ahmed
573263ed74 Fix back button not working 2023-09-26 21:18:35 +01:00
Kamran Ahmed
f27aa58ac3 Fix back button not working 2023-09-26 21:14:26 +01:00
Saleh Hashemi
518cf4ce73 Fix broken Git Tutorial for Dummies link 2023-09-26 19:18:42 +06:00
Akshay Jagiasi
7bde0b3f44 Add EVM link (#3727) 2023-09-22 20:46:09 +01:00
Lane Wagner
4b6dcb3a37 Add golang course (#3730) 2023-09-22 20:45:29 +01:00
Alyxson Marques
c50200bfe7 Added links to exception handling javascript roadmap (#3775)
Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2023-09-22 20:40:06 +01:00
Akshay Jagiasi
5ffb9fad9f Add solana whitepaper (#3781)
* Solana whitepaper added

* Update src/data/roadmaps/blockchain/content/101-blockchain-general-knowledge/109-blockchains/100-solana.md

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2023-09-22 20:39:06 +01:00
The New Stack
dd7d312aa1 Add TypeScript resources (#3789)
Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2023-09-22 20:34:58 +01:00
The New Stack
81447f6b43 Add TypeScript vs JavaScript (#3790) 2023-09-22 20:33:15 +01:00
Valentino Traverso
fe711f498d Fix typos (#3794) 2023-09-22 20:32:50 +01:00
rane gray
c65f12fcb8 Add zustand resource (#3803)
Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2023-09-22 20:27:37 +01:00
Tim Jonas Meinerzhagen
cab075bf5b Fix salting typo link (#3820) 2023-09-22 20:24:50 +01:00
Benjamin Norval
685021493c Fix typos and wording (#3902)
Improved grammar and wording choices for the file of `choose-image-format-approprietly.md` in Frontend Performance - Best Practices.
2023-09-22 20:24:29 +01:00
Rafael Bicalho
482cf64bf5 Update CORS Resources (#3908)
* Update 106-cors.md

* Update 102-cors.md

* Update 106-cors.md
2023-09-22 20:23:59 +01:00
keeplz
9051e22476 Add event loop resource (#3921)
add a video for event loop, it's js conf in asia 2018
2023-09-22 20:23:30 +01:00
Selva Muthu Kumaran
1b538b399f Fix backend link (#4484)
backend-roadmap-throttling- missed URL fixed
fixes : #4473
2023-09-22 20:22:40 +01:00
アドヴァイス
05673087c5 Remove dead link (#4489)
In the "Rate limiting" section, Blogs and tutorials on RxJS refers to a 404 page.

The dead link has been fixed with the correct and appropriate link.
2023-09-22 20:21:59 +01:00
Orca
5256df9c07 Fix typos (#4500)
- Fixed a typo
- Fixed the title of the freecodecamp link
2023-09-22 20:19:23 +01:00
Orca
ddf8884501 Update 100-builtin-modules.md (#4501)
- Removed a redundant `events` entry
2023-09-22 20:18:26 +01:00
steph
05492b60ee Update blockchain resources (#4502) 2023-09-22 20:18:04 +01:00
Kamran Ahmed
b92ae9b836 Increase line height of question answers 2023-09-22 05:27:06 +01:00
Kamran Ahmed
83df0da6b4 Enable indexing of question pages 2023-09-22 05:22:45 +01:00
Kamran Ahmed
a58b78bfe9 Hide account dropdown when user clicks anywhere 2023-09-22 05:20:28 +01:00
Kamran Ahmed
2fa41f583e Add react questions 2023-09-22 05:15:52 +01:00
Kamran Ahmed
80819f8914 UI fixes for questions 2023-09-22 05:08:24 +01:00
Arik Chakma
edcf0e683d Add react questions (#4492)
* Add more questions

* wip: add lazy, conditional questions

* wip: Add RSC questions

* wip: add component's lifecycle

* wip: add dependency array question

* wip: add comment and state

* chore: add more questions

* wip: add list question

* wip: add directive questions

* fix: conventions and examples

* wip: add custom hook question

* wip: add hydration question

* wip: add error boundary example

* wip: add strict mode question

* wip: investigating slow react app

* Update src/data/question-groups/react/react.md

* Update src/data/question-groups/react/react.md

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2023-09-22 05:02:12 +01:00
Arik Chakma
aa6d48b775 Add more questions and remove setState 2023-09-19 07:34:29 +06:00
Kamran Ahmed
3e622ecc2c UI Change for Title Question 2023-09-18 18:27:48 +01:00
Kamran Ahmed
ea5c3c2c01 UI Change for Title Question 2023-09-18 18:25:38 +01:00
Kamran Ahmed
8dc0424823 Update description meta for frontend, backend, devops 2023-09-18 17:39:54 +01:00
Kamran Ahmed
f3b16eb50f Fix headings 2023-09-18 16:23:44 +01:00
Kamran Ahmed
e07112a3a9 Remove duplicate questions 2023-09-18 16:19:39 +01:00
Kamran Ahmed
81983b6b06 Add more questions 2023-09-18 16:15:12 +01:00
Kamran Ahmed
bc6b100c26 Add introductory paragraph on roadmaps 2023-09-16 11:20:23 +01:00
Ihor
846bbc1533 fix(typescript): fix template lineral type definition (#4474) 2023-09-13 21:09:45 +01:00
roadmap bot
0b0168b40f chore: add resource under qa:qa-basics:project-management:atlassian 2023-09-12 17:05:41 +01:00
Matvey Volkov
4c9371ee74 Fix issue in typescript (#3922)
json_build_object is used to create json object and get it
2023-09-12 17:03:57 +01:00
Toshita Singh
bb9cc31e8a Fix typo in prototypal inheritance (#3930)
Completed missing property name used to set the prototype of an object.
2023-09-12 16:44:01 +01:00
Jakub Olszewski
8585857cc3 Add ChangeNotifier and ValueNotifier tutorials (#3997)
Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2023-09-12 16:41:36 +01:00
JasonMan34
8c2e812667 Fix recursive types example in typescript roadmap (#4022)
Co-authored-by: Itamar Zwi <itamarz@amplicy.io>
2023-09-12 16:39:19 +01:00
Olawuwo Abideen
bfbee6da0f Add a resource for REST (#4025) 2023-09-12 16:38:47 +01:00
Selva Muthu Kumaran
8057b218a0 Fix video link (#4398)
Computer network | Google IT Support certificate video fixed
fix : #4396
2023-09-12 16:38:10 +01:00
Selva Muthu Kumaran
c3d24a65d1 Fix appium link (#4402)
QA-roadmap-appium website - new link provided
fix: #4205
2023-09-12 16:37:45 +01:00
Tomasz Mikulski
67beb4e8c4 Fix broken http link to presentation - use https (#4405) 2023-09-12 16:37:15 +01:00
Selva Muthu Kumaran
35066d5b70 Fix video lini (#4408)
python-roadmap-oop-classes-python OOP tutorial - fixed video link
fixes : #4221

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2023-09-12 16:36:59 +01:00
Julien Alric
bb76ae411f Update system-design.json fix typo (#4414) 2023-09-12 16:36:22 +01:00
Mohit Rajput
98ea93da8c fix incorrect url change (#4415) 2023-09-12 16:36:06 +01:00
Selva Muthu Kumaran
a69f0cc1b1 Fix YARP in .net roadmap (#4416)
asp.net-core-YARP-description fixex
fixes : #4406
2023-09-12 16:35:36 +01:00
Michał Gałązka
e50e75479a Fixed PHP official website address in backend roadmap (#4417)
changed from php.org to php.net
2023-09-12 16:35:13 +01:00
FranMD
f4592b1e58 Update URL for "Enabling HTTPS on Your Servers" site (#4418) 2023-09-12 16:34:50 +01:00
Selva Muthu Kumaran
45c88da643 Add information about local scope (#4420)
javascript-roadmap-scope-variable-local scope description added
fixes : #4388
2023-09-12 16:34:32 +01:00
Mikhail Ostashchenko
a54fe0d1ba Fix broken links (#4421) 2023-09-12 16:33:51 +01:00
Leo Wang
e1f494776e Fix content link in contributing.md (#4431)
Co-authored-by: Leo Wang <ab0988956087@gamil.com>
2023-09-12 16:33:08 +01:00
Muhammad Afzal
11272da330 docs: add content for Google Cloud Functions (#4443) 2023-09-12 16:31:40 +01:00
Andret Carpizo
8903f11f02 Fix Template Specialization Index CodeBlock for const in printData (#4446) 2023-09-12 16:31:24 +01:00
Selva Muthu Kumaran
8ca9f976cd python-roadmap-decorators (#4448)
python-roadmap-modules-decorators - new link for python decorators in 1 minute
2023-09-12 16:30:57 +01:00
Blake
488521d2e3 Update URL for OpenID Link (#4459) 2023-09-12 16:30:40 +01:00
Kirill Bryntsev
072953c69a Add information about function pointer (#4460) 2023-09-12 16:30:27 +01:00
Akash Sharma
79a656e171 Fixing PRIMARY_KEY NULL constraint (#4465) 2023-09-12 16:27:19 +01:00
Aus Gomez
b565ce9bce issue-442 (#4470) 2023-09-12 16:26:30 +01:00
Kamran Ahmed
460ea8b95a Fix icon on the team creation page 2023-09-06 17:39:04 +01:00
Kamran Ahmed
26ab7b9098 Remove EKS from devops beginner 2023-09-05 11:25:39 +01:00
Kamran Ahmed
0eebcd03a4 Add questions on homepage 2023-09-03 23:18:00 +01:00
Kamran Ahmed
9c75404d0c feat: responsiveness of questions 2023-09-03 23:12:27 +01:00
Kamran Ahmed
61c3c88fb6 Integrate question backend 2023-09-03 19:57:51 +01:00
Kamran Ahmed
1ed54bad90 Change confetti to show on completion of quiz 2023-09-03 17:07:39 +01:00
Kamran Ahmed
437d879af3 feat: add finished screen for questions 2023-09-03 14:11:56 +01:00
Kamran Ahmed
58dd3f2f41 Fix flickering numbers 2023-09-03 12:17:30 +01:00
Kamran Ahmed
cbe758349c Add reset progress functionality 2023-09-03 12:14:20 +01:00
Kamran Ahmed
a847d0b08d Show user progress 2023-09-03 12:02:34 +01:00
Kamran Ahmed
548b7f31f9 Fix confetti does not show up properly 2023-09-03 11:49:00 +01:00
Kamran Ahmed
2e18d5a563 feat: question page with progress tracking 2023-09-03 03:20:59 +01:00
Kamran Ahmed
5bbcd85e6c Update question ui 2023-09-02 23:09:02 +01:00
Kamran Ahmed
1eb0e8869a fix: broken type on hero 2023-09-02 18:00:58 +01:00
Kamran Ahmed
1b74e86db7 Custom roadmaps listing on homepage 2023-09-02 17:49:07 +01:00
Kamran Ahmed
07b2cb0f9b fix: ui 2023-09-02 02:04:44 +01:00
Kamran Ahmed
fba926625d fix: scroll to top when user hides answer 2023-09-02 01:59:07 +01:00
Kamran Ahmed
e4c29b03ab feat: question page ui 2023-09-02 01:56:06 +01:00
Kamran Ahmed
2a7fd53c8b feat: question page confetti 2023-09-01 20:07:17 +01:00
Kamran Ahmed
4cb905b69a feat: design for question page 2023-09-01 18:58:00 +01:00
Kamran Ahmed
a123fc0828 fix: client:only=react 2023-09-01 17:25:10 +01:00
Kamran Ahmed
e15a36a2ce Fix accessibility issues 2023-09-01 00:04:25 +01:00
Kamran Ahmed
ca32c814da Fix accessibility issues 2023-08-31 23:54:27 +01:00
Kamran Ahmed
c4ef2bfcb4 fix: broken build 2023-08-31 23:23:08 +01:00
Kamran Ahmed
bb42c809fb fix: broken build 2023-08-31 23:21:18 +01:00
Kamran Ahmed
03d0a32fd6 chore: upgrade to astro v3 (#4437) 2023-08-31 23:17:51 +01:00
Kamran Ahmed
b8c90948f9 chore: trigger build 2023-08-31 19:05:54 +01:00
Kamran Ahmed
5c57a84e82 chore: migrate from preact to react (#4435) 2023-08-31 17:19:18 +01:00
Kamran Ahmed
c274feced1 Fix broken build 2023-08-30 18:56:36 +01:00
Kamran Ahmed
cdb9153029 Change in grid item design 2023-08-30 18:51:20 +01:00
Jamie Douglas
d3bebfeea6 Updated link to Gradle site (#4423) 2023-08-30 14:20:28 +01:00
Pawel Janicki
68f9e4576b Fix ArgoCD Beginner tutorial link (#4425)
https://github.com/kamranahmedse/developer-roadmap/issues/4424
2023-08-30 14:20:05 +01:00
Leo Wang
051bcce933 Fix video link in Python OOP classes (#4427)
Co-authored-by: Leo Wang <ab0988956087@gamil.com>
2023-08-30 14:19:40 +01:00
Kamran Ahmed
e3793b00c7 Change background color of grid 2023-08-29 18:35:53 +01:00
Kamran Ahmed
f256a5a9b0 Fix logout not working 2023-08-28 17:51:34 +01:00
Mohit Rajput
7e18c97e78 from inside compilerOptions to outside exclude (#4410) 2023-08-28 11:26:46 +06:00
Selva Muthu Kumaran
c95caccae5 golang\conditional-statement (#4400)
* golang\conditional-statement

golang\conditional-statement\golang programs switch case - new link provided

* Add both conditional statements

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2023-08-26 22:47:33 +06:00
Juan Pablo Partridge
102c57e925 Add mongodb roadmap in backend content (#4035) 2023-08-26 00:15:25 +01:00
Mikhail Ostashchenko
1ec6005fe1 Fix code style (#4356) 2023-08-25 19:40:58 +01:00
KT
ce41b3a955 Remove duplicate resource (#4375)
Remove resource hosting a duplicate of another listed resource.
2023-08-25 19:40:35 +01:00
Selva Muthu Kumaran
eea79968e2 Fix data type content (#4381)
data type (struct and class) clarification
2023-08-25 19:39:37 +01:00
Selva Muthu Kumaran
538e41307c Fix video link (#4385)
javascript\variable\scope\understanding global local function block scope
2023-08-25 19:39:01 +01:00
Mikhail Ostashchenko
79fcf2400f SFINAE fix (C++) (#4390)
When you try to use these function templates, there can be ambiguity issues. If you call foo with an arithmetic type, both template overloads will be considered valid, and the compiler won't know which one to choose.

You can use specialization or tag dispatching to properly handle different cases.
2023-08-25 19:38:41 +01:00
Tushar Daiya
0da1edaa55 Fix link in python roadmap (#4393) 2023-08-25 19:37:46 +01:00
Kamran Ahmed
b04b8c702f Cookie sharing across sub-domains 2023-08-25 02:19:10 +01:00
Bartłomiej Majowicz - Unlimitech
1a7a6db50c Fix typos (#4362)
* Fix misspell.

* Add missing link title.

* Fix add missing word.
2023-08-22 12:37:32 +01:00
Mikhail Ostashchenko
7072c4cf80 Fix code in c++ (#4373) 2023-08-22 12:19:08 +01:00
Mikhail Ostashchenko
f2b29f80f9 Add Reverse iterator (C++) (#4374) 2023-08-22 12:18:19 +01:00
roadmap bot
76c2686269 chore: add resource under cpp:language-concepts:type-casting 2023-08-20 19:14:11 +01:00
Kamran Ahmed
b7728fa6fd Change related roadmaps + faqs to h2 2023-08-18 18:52:50 +01:00
Kamran Ahmed
ca5bae687b Update AI and Data Scientist roadmap 2023-08-18 14:29:33 +01:00
Kamran Ahmed
face1eefbb Update data scientist roadmap 2023-08-17 19:39:08 +01:00
Kamran Ahmed
498ef2eb3b Change feedback popup design 2023-08-17 17:20:19 +01:00
Kamran Ahmed
80d53a9c5d Add AI and Data Scientist Roadmap 2023-08-17 13:57:42 +01:00
Arik Chakma
e0eccaa30e Add team feedback popup (#4341)
* wip: submit feedback popup

* wip: feedback popup state
2023-08-17 13:45:22 +01:00
Erasmo Hernández
c43ee13c94 Fix typo from bug 4210 (#4211) 2023-08-17 03:01:58 +01:00
Kyrylo Nehaturov
b57c4cb558 fix: removed dublicated link (#4217)
removed dublicated link in react roadmap
2023-08-17 03:01:29 +01:00
Kyrylo Nehaturov
c236bf9bf9 fix: removed duplicate link in react 103-rendering/104-events (#4218)
removed duplicate link in react roadmap section 103-rendering/104-events
2023-08-17 03:01:15 +01:00
Kyrylo Nehaturov
c92c67acc9 fix: added link + rewritten link text react 106-state-management (#4219)
I found more obvious such resources list, with the documentation as  the starting resource and then the link for the repo as the next one
2023-08-17 03:01:01 +01:00
Mirac Seref
bec59ed630 Update 101-functional-components.md (#4233)
Fix link issue

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2023-08-17 03:00:34 +01:00
Mark Puchala 2/
7f800f2717 Update history-of-javascript.md (#4287)
Fix reference to non-existent comment section.
2023-08-17 02:58:57 +01:00
carmen
e5579ef7d1 fix: removed unavailable youtube link (#4288) 2023-08-17 02:58:25 +01:00
Gary Y
9e5baad85f Update 103-reporting.md (#4298)
The current link leads to an outdated page with a 404.

https://www.ministryoftesting.com/dojo/series/the-testing-planet-2019/lessons/the-art-of-the-bug-report

This is the new updated link.
https://www.ministryoftesting.com/articles/11b82aee?s_id=15465627
2023-08-17 02:56:42 +01:00
Hugo Poças
146022d1ed Removed duplicate sentence (#4325)
removed the sentence:
"JOIN Queries
Absolutely, here’s a brief summary about SQL JOIN Queries:"

It's the same information as the sentence below and it doesn't give the impression it's answering anything.
2023-08-17 02:56:22 +01:00
obvTiger
6af8033764 Remove extra "s" on 102-razor-pages.md (#4329) 2023-08-17 02:56:08 +01:00
Nikola Hristov
e4d6cd9f41 Restores astro-compress (#4339)
* Restores astro-compress

* squash!
2023-08-17 02:55:49 +01:00
Mikhail Ostashchenko
5cff162a94 Change to the correct data type (#4343) 2023-08-17 02:54:56 +01:00
Kamran Ahmed
3b7e5d5ce2 Add AI and Data Scientist roadmap 2023-08-17 02:52:35 +01:00
1314 changed files with 102158 additions and 15084 deletions

View File

@@ -1,2 +1,3 @@
PUBLIC_API_URL=http://api.roadmap.sh
PUBLIC_API_URL=https://api.roadmap.sh
PUBLIC_AVATAR_BASE_URL=https://dodrc8eu8m09s.cloudfront.net/avatars
PUBLIC_EDITOR_APP_URL=https://draw.roadmap.sh

View File

@@ -14,24 +14,12 @@ body:
placeholder: e.g. Roadmap to learn Data Science
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: Is this roadmap prepared by you or someone else?
options:
- I prepared this roadmap
- I found this roadmap online (please provide a link below)
- type: textarea
id: roadmap-description
attributes:
label: Roadmap Items
description: Please submit a nested list of items which we can convert into the visual. Here is an [example of roadmap items list.](https://gist.github.com/kamranahmedse/98758d2c73799b3a6ce17385e4c548a5).
label: Roadmap Link
description: Please create the roadmap [using our roadmap editor](https://twitter.com/kamrify/status/1708293162693767426) and submit the roadmap link.
placeholder: |
- Item 1
- Subitem 1
- Subitem 2
- Item 2
- Subitem 1
- Subitem 2
https://roadmap.sh/xyz
validations:
required: true
required: true

View File

@@ -1,24 +1,26 @@
name: Deployment to GH Pages
name: App Deployment
on:
push:
branches: [ master ]
env:
PUBLIC_API_URL: "https://api.roadmap.sh"
PUBLIC_EDITOR_APP_URL: "https://draw.roadmap.sh"
PUBLIC_AVATAR_BASE_URL: "https://dodrc8eu8m09s.cloudfront.net/avatars"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PAT: ${{ secrets.PAT }}
CI: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v1
with:
node-version: 18
- run: git config --global url."https://${{ secrets.PAT }}@github.com/".insteadOf ssh://git@github.com/
- name: Prepare Draw Repository
run: |
git clone https://${{ secrets.GH_PAT }}@github.com/roadmapsh/web-draw.git .temp/web-draw --depth 1
- uses: pnpm/action-setup@v2.2.2
with:
version: 7.13.4
@@ -27,6 +29,7 @@ jobs:
pnpm install
- name: Generate meta and build
run: |
npm run generate-renderer
npm run build
touch ./dist/.nojekyll
echo 'roadmap.sh' > ./dist/CNAME

View File

@@ -9,7 +9,7 @@ jobs:
upgrade-deps:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: 18

4
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea
.temp
# build output
dist/
@@ -27,3 +28,6 @@ pnpm-debug.log*
/playwright/.cache/
tests-examples
*.csv
/editor/*
!/editor/readonly-editor.tsx

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
auto-install-peers=true
strict-peer-dependencies=false

View File

@@ -13,6 +13,6 @@ module.exports = {
],
plugins: [
require.resolve('prettier-plugin-astro'),
require('prettier-plugin-tailwindcss'),
'prettier-plugin-tailwindcss',
],
};

View File

@@ -1,13 +1,14 @@
// https://astro.build/config
import preact from '@astrojs/preact';
import sitemap from '@astrojs/sitemap';
import tailwind from '@astrojs/tailwind';
import compress from '@otterlord/astro-compress';
import compress from 'astro-compress';
import { defineConfig } from 'astro/config';
import rehypeExternalLinks from 'rehype-external-links';
import { fileURLToPath } from 'node:url';
import { serializeSitemap, shouldIndexPage } from './sitemap.mjs';
import react from '@astrojs/react';
// https://astro.build/config
export default defineConfig({
site: 'https://roadmap.sh/',
@@ -31,11 +32,9 @@ export default defineConfig({
'https://cs.fyi',
'https://roadmap.sh',
];
if (whiteListedStarts.some((start) => href.startsWith(start))) {
return [];
}
return 'noopener noreferrer nofollow';
},
},
@@ -46,22 +45,6 @@ export default defineConfig({
format: 'file',
},
integrations: [
{
name: 'client-authenticated',
hooks: {
'astro:config:setup'(options) {
options.addClientDirective({
name: 'authenticated',
entrypoint: fileURLToPath(
new URL(
'./src/directives/client-authenticated.mjs',
import.meta.url
)
),
});
},
},
},
tailwind({
config: {
applyBaseStyles: false,
@@ -72,9 +55,10 @@ export default defineConfig({
serialize: serializeSitemap,
}),
compress({
css: false,
js: false,
HTML: false,
CSS: false,
JavaScript: false,
}),
preact(),
react(),
],
});

View File

@@ -30,11 +30,12 @@ Find [the content directory inside the relevant roadmap](https://github.com/kamr
## Guidelines
- <p><strong>Adding everything available out there is not the goal!</strong><br />
- <p><strong>Adding everything available out there is not the goal!</strong><br />
The roadmaps represent the skillset most valuable today, i.e., if you were to enter any of the listed fields today, what would you learn?! There might be things that are of-course being used today but prioritize the things that are most in demand today, e.g., agreed that lots of people are using angular.js today but you wouldn't want to learn that instead of React, Angular, or Vue. Use your critical thinking to filter out non-essential stuff. Give honest arguments for why the resource should be included.</p>
- <p><strong>Do not add things you have not evaluated personally!</strong><br />
- <p><strong>Do not add things you have not evaluated personally!</strong><br />
Use your critical thinking to filter out non-essential stuff. Give honest arguments for why the resource should be included. Have you read this book? Can you give a short article?</p>
- <p><strong>Create a Single PR for Content Additions</strong></p>
If you are planning to contribute by adding content to the roadmaps, I recommend you to clone the repository, add content to the [content directory of the roadmap](./content/roadmaps/) and create a single PR to make it easier for me to review and merge the PR.
If you are planning to contribute by adding content to the roadmaps, I recommend you to clone the repository, add content to the [content directory of the roadmap](./src/data/roadmaps/) and create a single PR to make it easier for me to review and merge the PR.
- Write meaningful commit messages
- Look at the existing issues/pull requests before opening new ones

View File

@@ -0,0 +1,14 @@
export function ReadonlyEditor(props: any) {
return (
<div className="fixed bottom-0 left-0 right-0 top-0 z-[9999] border bg-white p-5 text-black">
<h2 className="mb-2 text-xl font-semibold">Private Component</h2>
<p className="mb-4">
Renderer is a private component. If you are a collaborator and have
access to it. Run the following command:
</p>
<code className="mt-5 rounded-md bg-gray-800 p-2 text-white">
npm run generate-renderer
</code>
</div>
);
}

View File

@@ -1,10 +1,10 @@
{
"name": "roadmap.sh",
"type": "module",
"version": "0.0.1",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
"dev": "astro dev --port 3000",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
@@ -16,40 +16,54 @@
"roadmap-links": "node scripts/roadmap-links.cjs",
"roadmap-dirs": "node scripts/roadmap-dirs.cjs",
"roadmap-content": "node scripts/roadmap-content.cjs",
"generate-renderer": "sh scripts/generate-renderer.sh",
"best-practice-dirs": "node scripts/best-practice-dirs.cjs",
"best-practice-content": "node scripts/best-practice-content.cjs",
"test:e2e": "playwright test"
},
"dependencies": {
"@astrojs/preact": "^2.2.1",
"@astrojs/sitemap": "^1.3.3",
"@astrojs/tailwind": "^3.1.3",
"@fingerprintjs/fingerprintjs": "^3.4.1",
"@nanostores/preact": "^0.5.0",
"astro": "^2.6.6",
"@otterlord/astro-compress": "^1.1.48",
"jose": "^4.14.4",
"@astrojs/react": "^3.0.7",
"@astrojs/sitemap": "^3.0.3",
"@astrojs/tailwind": "^5.0.3",
"@fingerprintjs/fingerprintjs": "^4.2.1",
"@nanostores/react": "^0.7.1",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"astro": "^4.0.3",
"astro-compress": "^2.2.3",
"clsx": "^2.0.0",
"dracula-prism": "^2.1.13",
"jose": "^5.1.3",
"js-cookie": "^3.0.5",
"nanostores": "^0.9.2",
"node-html-parser": "^6.1.5",
"npm-check-updates": "^16.10.12",
"preact": "^10.15.1",
"rehype-external-links": "^2.1.0",
"lucide-react": "^0.294.0",
"nanoid": "^5.0.4",
"nanostores": "^0.9.5",
"node-html-parser": "^6.1.11",
"npm-check-updates": "^16.14.11",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-confetti": "^6.1.0",
"react-dom": "^18.2.0",
"reactflow": "^11.10.1",
"rehype-external-links": "^3.0.0",
"roadmap-renderer": "^1.0.6",
"slugify": "^1.6.6",
"tailwindcss": "^3.3.2"
"tailwind-merge": "^2.1.0",
"tailwindcss": "^3.3.6",
"zustand": "^4.4.7"
},
"devDependencies": {
"@playwright/test": "^1.35.1",
"@tailwindcss/typography": "^0.5.9",
"@types/js-cookie": "^3.0.3",
"@playwright/test": "^1.40.1",
"@tailwindcss/typography": "^0.5.10",
"@types/js-cookie": "^3.0.6",
"@types/prismjs": "^1.26.3",
"csv-parser": "^3.0.0",
"gh-pages": "^5.0.0",
"gh-pages": "^6.1.0",
"js-yaml": "^4.1.0",
"markdown-it": "^13.0.1",
"openai": "^3.3.0",
"prettier": "^2.8.8",
"prettier-plugin-astro": "^0.10.0",
"prettier-plugin-tailwindcss": "^0.3.0"
"markdown-it": "^14.0.0",
"openai": "^4.20.1",
"prettier": "^3.1.0",
"prettier-plugin-astro": "^0.12.2",
"prettier-plugin-tailwindcss": "^0.5.9"
}
}

4189
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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: 1.1 MiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 561 KiB

BIN
public/roadmaps/aws.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 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

@@ -9,8 +9,8 @@
<a href="https://roadmap.sh/best-practices">
<img src="https://img.shields.io/badge/%E2%9C%A8-Best%20Practices-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="best practices" />
</a>
<a href="https://youtube.com/theroadmap?sub_confirmation=1">
<img src="https://img.shields.io/badge/%E2%9C%A8-Videos-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="videos" />
<a href="https://roadmap.sh/questions">
<img src="https://img.shields.io/badge/%E2%9C%A8-Questions-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="videos" />
</a>
<a href="https://www.youtube.com/channel/UCA0H2KIWgWTwpTFjSxp0now?sub_confirmation=1">
<img src="https://img.shields.io/badge/%E2%9C%A8-YouTube%20Channel-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="roadmaps" />
@@ -24,7 +24,7 @@
Roadmaps are now interactive, you can click the nodes to read more about the topics.
### [View all Roadmaps](https://roadmap.sh)
### [View all Roadmaps](https://roadmap.sh) &nbsp;&middot;&nbsp; [Best Practices](https://roadmap.sh/best-practices) &nbsp;&middot;&nbsp; [Questions](https://roadmap.sh/questions)
![](https://i.imgur.com/waxVImv.png)
@@ -35,8 +35,11 @@ Here is the list of available roadmaps with more being actively worked upon.
- [DevOps Roadmap](https://roadmap.sh/devops) / [DevOps Beginner Roadmap](https://roadmap.sh/devops?r=devops-beginner)
- [Full Stack Roadmap](https://roadmap.sh/full-stack)
- [Computer Science Roadmap](https://roadmap.sh/computer-science)
- [AI and Data Scientist Roadmap](https://roadmap.sh/ai-data-scientist)
- [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) / [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)
@@ -49,8 +52,8 @@ Here is the list of available roadmaps with more being actively worked upon.
- [GraphQL Roadmap](https://roadmap.sh/graphql)
- [Android Roadmap](https://roadmap.sh/android)
- [Flutter Roadmap](https://roadmap.sh/flutter)
- [Python Roadmap](https://roadmap.sh/python)
- [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)
@@ -65,14 +68,20 @@ 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)
We have also added a new form of visual content covering best practices:
There are also interactive best practices:
- [Code Review Best Practices](https://roadmap.sh/best-practices/code-review)
- [Frontend Performance Best Practices](https://roadmap.sh/best-practices/frontend-performance)
- [API Security Best Practices](https://roadmap.sh/best-practices/api-security)
- [AWS Best Practices](https://roadmap.sh/best-practices/aws)
..and questions to help you test, rate and improve your knowledge
- [JavaScript Questions](https://roadmap.sh/questions/javascript)
- [React Questions](https://roadmap.sh/questions/react)
![](https://i.imgur.com/waxVImv.png)
## Share with the community

14
renderer/index.tsx Normal file
View File

@@ -0,0 +1,14 @@
export function Renderer(props: any) {
return (
<div className="fixed bottom-0 left-0 right-0 top-0 z-[9999] border bg-white p-5 text-black">
<h2 className="mb-2 text-xl font-semibold">Private Component</h2>
<p className="mb-4">
Renderer is a private component. If you are a collaborator and have
access to it. Run the following command:
</p>
<code className="mt-5 rounded-md bg-gray-800 p-2 text-white">
npm run generate-renderer
</code>
</div>
);
}

5
renderer/renderer.ts Normal file
View File

@@ -0,0 +1,5 @@
export function renderFlowJSON(data: any, options?: any) {
console.warn("renderFlowJSON is not implemented");
console.warn("run the following command to generate the renderer:");
console.warn("> npm run generate-renderer");
}

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -e
# ignore cloning if .temp/web-draw already exists
if [ ! -d ".temp/web-draw" ]; then
mkdir -p .temp
git clone git@github.com:roadmapsh/web-draw.git .temp/web-draw
fi
rm -rf editor
mkdir editor
# copy the files at /src/editor/* to /editor
# while replacing any existing files
cp -rf .temp/web-draw/src/editor/* editor
# Add @ts-nocheck to the top of each ts and tsx file
# so that the typescript compiler doesn't complain
# about the missing types
find editor -type f \( -name "*.ts" -o -name "*.tsx" \) -print0 | while IFS= read -r -d '' file; do
if [ -f "$file" ]; then
echo "// @ts-nocheck" > temp
cat "$file" >> temp
mv temp "$file"
echo "Added @ts-nocheck to $file"
fi
done
# ignore the worktree changes for the editor directory
git update-index --assume-unchanged editor/readonly-editor.tsx

View File

@@ -19,13 +19,12 @@ if (!allowedRoadmapIds.includes(roadmapId)) {
}
const ROADMAP_CONTENT_DIR = path.join(ALL_ROADMAPS_DIR, roadmapId, 'content');
const { Configuration, OpenAIApi } = require('openai');
const configuration = new Configuration({
const OpenAI = require('openai');
const openai = new OpenAI({
apiKey: OPEN_AI_API_KEY,
});
const openai = new OpenAIApi(configuration);
function getFilesInFolder(folderPath, fileList = {}) {
const files = fs.readdirSync(folderPath);
@@ -60,16 +59,16 @@ 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 with a brief summary of that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output. Also include the code examples if applicable to this topic.`;
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 with a brief summary of that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output. Also include the code examples if applicable to this topic.`;
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}'...`);
return new Promise((resolve, reject) => {
openai
.createChatCompletion({
openai.chat.completions
.create({
model: 'gpt-4',
messages: [
{
@@ -79,7 +78,7 @@ function writeTopicContent(currTopicUrl) {
],
})
.then((response) => {
const article = response.data.choices[0].message.content;
const article = response.choices[0].message.content;
resolve(article);
})
@@ -92,7 +91,7 @@ function writeTopicContent(currTopicUrl) {
async function writeFileForGroup(group, topicUrlToPathMapping) {
const topicId = group?.properties?.controlName;
const topicTitle = group?.children?.controls?.control?.find(
(control) => control?.typeID === 'Label'
(control) => control?.typeID === 'Label',
)?.properties?.text;
const currTopicUrl = topicId?.replace(/^\d+-/g, '/')?.replace(/:/g, '/');
if (!currTopicUrl) {
@@ -138,15 +137,14 @@ async function writeFileForGroup(group, topicUrlToPathMapping) {
async function run() {
const topicUrlToPathMapping = getFilesInFolder(ROADMAP_CONTENT_DIR);
const roadmapJson = require(path.join(
ALL_ROADMAPS_DIR,
`${roadmapId}/${roadmapId}`
));
const roadmapJson = require(
path.join(ALL_ROADMAPS_DIR, `${roadmapId}/${roadmapId}`),
);
const groups = roadmapJson?.mockup?.controls?.control?.filter(
(control) =>
control.typeID === '__group__' &&
!control.properties?.controlName?.startsWith('ext_link')
!control.properties?.controlName?.startsWith('ext_link'),
);
if (!OPEN_AI_API_KEY) {

View File

@@ -53,12 +53,12 @@ function prepareDirTree(control, dirTree, dirSortOrders) {
const sortOrder = controlName.match(/^\d+/)?.[0];
// No directory for a group without control name
if (!controlName || !sortOrder) {
if (!controlName || (!sortOrder && !controlName.startsWith('check:'))) {
return;
}
// e.g. testing-your-apps:other-options
const controlNameWithoutSortOrder = controlName.replace(/^\d+-/, '');
const controlNameWithoutSortOrder = controlName.replace(/^\d+-/, '').replace(/^check:/, '');
// e.g. ['testing-your-apps', 'other-options']
const dirParts = controlNameWithoutSortOrder.split(':');

View File

@@ -2,6 +2,7 @@
import AstroIcon from './AstroIcon.astro';
import { TeamDropdown } from './TeamDropdown/TeamDropdown';
import { SidebarFriendsCounter } from './Friends/SidebarFriendsCounter';
import { Map } from 'lucide-react';
export interface Props {
activePageId: string;
@@ -26,10 +27,21 @@ const sidebarLinks = [
href: '/account/friends',
title: 'Friends',
id: 'friends',
isNew: false,
icon: {
glyph: 'users',
classes: 'h-4 w-4',
},
},
{
href: '/account/roadmaps',
title: 'Roadmaps',
id: 'roadmaps',
isNew: true,
icon: {
glyph: 'users',
classes: 'h-4 w-4',
component: Map,
},
},
{
@@ -85,7 +97,7 @@ const sidebarLinks = [
}`}
>
<AstroIcon icon={'users'} class={`h-4 w-4 mr-2`} />
Teams
Teams
</a>
</li>
{
@@ -100,10 +112,16 @@ const sidebarLinks = [
isActive ? 'bg-slate-100' : ''
}`}
>
<AstroIcon
icon={sidebarLink.icon.glyph}
class={`${sidebarLink.icon.classes} mr-2`}
/>
{sidebarLink.icon.component ? (
<sidebarLink.icon.component
className={`${sidebarLink.icon.classes} mr-2`}
/>
) : (
<AstroIcon
icon={sidebarLink.icon.glyph}
class={`${sidebarLink.icon.classes} mr-2`}
/>
)}
{sidebarLink.title}
</a>
</li>
@@ -136,21 +154,25 @@ const sidebarLinks = [
}`}
>
<span class='flex flex-grow items-center'>
<AstroIcon
icon={sidebarLink.icon.glyph}
class={`${sidebarLink.icon.classes} mr-2`}
/>
{sidebarLink.icon.component ? (
<sidebarLink.icon.component
className={`${sidebarLink.icon.classes} mr-2`}
/>
) : (
<AstroIcon
icon={sidebarLink.icon.glyph}
class={`${sidebarLink.icon.classes} mr-2`}
/>
)}
{sidebarLink.title}
</span>
{sidebarLink.isNew &&
sidebarLink.id !== 'friends' &&
!isActive && (
<span class='relative mr-1 flex items-center'>
<span class='relative rounded-full bg-gray-200 p-1 text-xs' />
<span class='absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs' />
</span>
)}
{sidebarLink.isNew && !isActive && (
<span class='relative mr-1 flex items-center'>
<span class='relative rounded-full bg-gray-200 p-1 text-xs' />
<span class='absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs' />
</span>
)}
{sidebarLink.id === 'friends' && (
<SidebarFriendsCounter client:load />

View File

@@ -21,11 +21,11 @@ function ActivityCounter(props: ActivityCounterType) {
const { text, count } = props;
return (
<div class="relative flex flex-1 flex-row-reverse sm:flex-col px-0 sm:px-4 py-2 sm:py-4 text-center sm:pt-10 items-center gap-2 sm:gap-0 justify-end">
<h2 class="text-base sm:text-5xl font-bold">
<div className="relative flex flex-1 flex-row-reverse sm:flex-col px-0 sm:px-4 py-2 sm:py-4 text-center sm:pt-10 items-center gap-2 sm:gap-0 justify-end">
<h2 className="text-base sm:text-5xl font-bold">
{count}
</h2>
<p class="mt-0 sm:mt-2 text-sm text-gray-400">{text}</p>
<p className="mt-0 sm:mt-2 text-sm text-gray-400">{text}</p>
</div>
);
}
@@ -34,8 +34,8 @@ export function ActivityCounters(props: ActivityCountersType) {
const { done, learning, streak } = props;
return (
<div class="mx-0 -mt-5 sm:-mx-10 md:-mt-10">
<div class="flex flex-col sm:flex-row gap-0 sm:gap-2 divide-y sm:divide-y-0 divide-x-0 sm:divide-x border-b">
<div className="mx-0 -mt-5 sm:-mx-10 md:-mt-10">
<div className="flex flex-col sm:flex-row gap-0 sm:gap-2 divide-y sm:divide-y-0 divide-x-0 sm:divide-x border-b">
<ActivityCounter
text={'Topics Completed'}
count={`${done?.total || 0}`}

View File

@@ -1,10 +1,21 @@
import { useEffect, useState } from 'preact/hooks';
import { useEffect, useState } from 'react';
import { httpGet } from '../../lib/http';
import { ActivityCounters } from './ActivityCounters';
import { ResourceProgress } from './ResourceProgress';
import { pageProgressMessage } from '../../stores/page';
import { EmptyActivity } from './EmptyActivity';
type ProgressResponse = {
updatedAt: string;
title: string;
id: string;
learning: number;
skipped: number;
done: number;
total: number;
isCustomResource: boolean;
};
export type ActivityResponse = {
done: {
today: number;
@@ -13,24 +24,9 @@ export type ActivityResponse = {
learning: {
today: number;
total: number;
roadmaps: {
title: string;
id: string;
learning: number;
done: number;
total: number;
skipped: number;
updatedAt: string;
}[];
bestPractices: {
title: string;
id: string;
learning: number;
done: number;
skipped: number;
total: number;
updatedAt: string;
}[];
roadmaps: ProgressResponse[];
bestPractices: ProgressResponse[];
customs: ProgressResponse[];
};
streak: {
count: number;
@@ -91,16 +87,16 @@ export function ActivityPage() {
streak={activity?.streak || { count: 0 }}
/>
<div class="mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8">
<div className="mx-0 px-0 py-5 md:-mx-10 md:px-8 md:py-8">
{learningRoadmaps.length === 0 &&
learningBestPractices.length === 0 && <EmptyActivity />}
{(learningRoadmaps.length > 0 || learningBestPractices.length > 0) && (
<>
<h2 class="mb-3 text-xs uppercase text-gray-400">
<h2 className="mb-3 text-xs uppercase text-gray-400">
Continue Following
</h2>
<div class="flex flex-col gap-3">
<div className="flex flex-col gap-3">
{learningRoadmaps
.sort((a, b) => {
const updatedAtA = new Date(a.updatedAt);
@@ -110,6 +106,8 @@ export function ActivityPage() {
})
.map((roadmap) => (
<ResourceProgress
key={roadmap.id}
isCustomResource={roadmap.isCustomResource}
doneCount={roadmap.done || 0}
learningCount={roadmap.learning || 0}
totalCount={roadmap.total || 0}
@@ -136,6 +134,8 @@ export function ActivityPage() {
})
.map((bestPractice) => (
<ResourceProgress
isCustomResource={bestPractice.isCustomResource}
key={bestPractice.id}
doneCount={bestPractice.done || 0}
totalCount={bestPractice.total || 0}
learningCount={bestPractice.learning || 0}

View File

@@ -1,22 +1,19 @@
import RoadmapIcon from '../../icons/roadmap.svg';
import { RoadmapIcon } from "../ReactIcons/RoadmapIcon";
export function EmptyActivity() {
return (
<div class="rounded-md">
<div class="flex flex-col items-center p-7 text-center">
<img
alt="no roadmaps"
src={RoadmapIcon}
class="mb-2 w-[60px] h-[60px] sm:h-[120px] sm:w-[120px] opacity-10"
/>
<h2 class="text-lg sm:text-xl font-bold">No Progress</h2>
<div className="rounded-md">
<div className="flex flex-col items-center p-7 text-center">
<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{' '}
<a href="/roadmaps" class="mt-4 text-blue-500 hover:underline">
<a href="/roadmaps" className="mt-4 text-blue-500 hover:underline">
Roadmaps
</a>{' '}
or{' '}
<a href="/best-practices" class="mt-4 text-blue-500 hover:underline">
<a href="/best-practices" className="mt-4 text-blue-500 hover:underline">
Best Practices
</a>{' '}
progress.

View File

@@ -1,8 +1,9 @@
import { useState } from 'preact/hooks';
import { httpPost } from '../../lib/http';
import { getRelativeTimeString } from '../../lib/date';
import { useToast } from '../../hooks/use-toast';
import { ProgressShareButton } from '../UserProgress/ProgressShareButton';
import { useState } from 'react';
import { getUser } from '../../lib/jwt';
type ResourceProgressType = {
resourceType: 'roadmap' | 'best-practice';
@@ -15,14 +16,17 @@ type ResourceProgressType = {
skippedCount: number;
onCleared?: () => void;
showClearButton?: boolean;
isCustomResource: boolean;
};
export function ResourceProgress(props: ResourceProgressType) {
const { showClearButton = true } = props;
const { showClearButton = true, isCustomResource } = props;
const toast = useToast();
const [isClearing, setIsClearing] = useState(false);
const [isConfirming, setIsConfirming] = useState(false);
const userId = getUser()?.id;
const {
updatedAt,
resourceType,
@@ -52,8 +56,8 @@ export function ResourceProgress(props: ResourceProgressType) {
return;
}
localStorage.removeItem(`${resourceType}-${resourceId}-favorite`);
localStorage.removeItem(`${resourceType}-${resourceId}-progress`);
localStorage.removeItem(`${resourceType}-${resourceId}-${userId}-favorite`);
localStorage.removeItem(`${resourceType}-${resourceId}-${userId}-progress`);
setIsClearing(false);
setIsConfirming(false);
@@ -62,11 +66,15 @@ export function ResourceProgress(props: ResourceProgressType) {
}
}
const url =
let url =
resourceType === 'roadmap'
? `/${resourceId}`
: `/best-practices/${resourceId}`;
if (isCustomResource) {
url = `/r?id=${resourceId}`;
}
const totalMarked = doneCount + skippedCount;
const progressPercentage = Math.round((totalMarked / totalCount) * 100);
@@ -112,6 +120,7 @@ export function ResourceProgress(props: ResourceProgressType) {
<ProgressShareButton
resourceType={resourceType}
resourceId={resourceId}
isCustomResource={isCustomResource}
className="text-xs font-normal"
shareIconClassName="w-2.5 h-2.5 stroke-2"
checkIconClassName="w-2.5 h-2.5"

View File

@@ -1,6 +1,6 @@
import { useRef, useState } from 'preact/hooks';
import { useRef, useState } from 'react';
import { useOutsideClick } from '../hooks/use-outside-click';
import { OptionType, SearchSelector } from './SearchSelector';
import { type OptionType, SearchSelector } from './SearchSelector';
import type { PageType } from './CommandMenu/CommandMenu';
import { CheckIcon } from './ReactIcons/CheckIcon';
import { httpPut } from '../lib/http';
@@ -65,15 +65,15 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
)?.title;
return (
<div class="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
<div class="relative h-full w-full max-w-md p-4 md:h-auto">
<div className="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
<div className="relative h-full w-full max-w-md p-4 md:h-auto">
<div
ref={popupBodyEl}
class="popup-body relative rounded-lg bg-white p-4 shadow"
className="popup-body relative rounded-lg bg-white p-4 shadow"
>
{isLoading && (
<>
<div class="flex items-center justify-center gap-2 py-8">
<div className="flex items-center justify-center gap-2 py-8">
<Spinner isDualRing={false} className="h-4 w-4" />
<h2 className="font-medium">Loading...</h2>
</div>
@@ -82,7 +82,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
{!isLoading && !error && selectedRoadmap && (
<div className={'text-center'}>
<CheckIcon additionalClasses="h-10 w-10 mx-auto opacity-20 mb-3 mt-4" />
<h3 class="mb-1.5 text-2xl font-medium">
<h3 className="mb-1.5 text-2xl font-medium">
{selectedRoadmapTitle} Added
</h3>
<p className="mb-4 text-sm leading-none text-gray-400">
@@ -95,11 +95,11 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
to make changes to the roadmap.
</p>
<div class="flex items-center gap-2">
<div className="flex items-center gap-2">
<button
onClick={onClose}
type="button"
class="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
>
Done
</button>
@@ -110,7 +110,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
setIsLoading(false);
}}
type="button"
class="flex-grow cursor-pointer rounded-lg bg-black py-2 text-center text-white"
className="flex-grow cursor-pointer rounded-lg bg-black py-2 text-center text-white"
>
+ Add More
</button>
@@ -119,14 +119,14 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
)}
{!isLoading && error && (
<>
<h3 class="mb-1.5 text-2xl font-medium">Error</h3>
<h3 className="mb-1.5 text-2xl font-medium">Error</h3>
<p className="mb-3 text-sm leading-none text-red-400">{error}</p>
<div class="flex items-center gap-2">
<div className="flex items-center gap-2">
<button
onClick={onClose}
type="button"
class="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
>
Cancel
</button>
@@ -135,7 +135,7 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
)}
{!isLoading && !error && !selectedRoadmap && (
<>
<h3 class="mb-1.5 text-2xl font-medium">Add Roadmap</h3>
<h3 className="mb-1.5 text-2xl font-medium">Add Roadmap</h3>
<p className="mb-3 text-sm leading-none text-gray-400">
Search and add a roadmap
</p>
@@ -156,11 +156,11 @@ export function AddTeamRoadmap(props: AddTeamRoadmapProps) {
placeholder={'Search for roadmap'}
/>
<div class="flex items-center gap-2">
<div className="flex items-center gap-2">
<button
onClick={onClose}
type="button"
class="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
className="flex-grow cursor-pointer rounded-lg bg-gray-200 py-2 text-center hover:bg-gray-300"
>
Cancel
</button>

View File

@@ -1,17 +1,17 @@
import Cookies from 'js-cookie';
import type { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import type { FormEvent } from 'react';
import { useState } from 'react';
import { httpPost } from '../../lib/http';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
const EmailLoginForm: FunctionComponent<{}> = () => {
export function EmailLoginForm() {
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const handleFormSubmit = async (e: Event) => {
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setError('');
@@ -21,7 +21,7 @@ const EmailLoginForm: FunctionComponent<{}> = () => {
{
email,
password,
}
},
);
// Log the user in and reload the page
@@ -29,6 +29,7 @@ const EmailLoginForm: FunctionComponent<{}> = () => {
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
path: '/',
expires: 30,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
window.location.reload();
@@ -38,7 +39,7 @@ const EmailLoginForm: FunctionComponent<{}> = () => {
// @todo use proper types
if ((error as any).type === 'user_not_verified') {
window.location.href = `/verification-pending?email=${encodeURIComponent(
email
email,
)}`;
return;
}
@@ -76,7 +77,7 @@ const EmailLoginForm: FunctionComponent<{}> = () => {
onInput={(e) => setPassword(String((e.target as any).value))}
/>
<p class="mb-3 mt-2 text-sm text-gray-500">
<p className="mb-3 mt-2 text-sm text-gray-500">
<a
href="/forgot-password"
className="text-blue-800 hover:text-blue-600"
@@ -98,6 +99,4 @@ const EmailLoginForm: FunctionComponent<{}> = () => {
</button>
</form>
);
};
export default EmailLoginForm;
}

View File

@@ -1,8 +1,7 @@
import type { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import { type FormEvent, useState } from 'react';
import { httpPost } from '../../lib/http';
const EmailSignupForm: FunctionComponent = () => {
export function EmailSignupForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
@@ -10,7 +9,7 @@ const EmailSignupForm: FunctionComponent = () => {
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const onSubmit = async (e: Event) => {
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
@@ -98,6 +97,4 @@ const EmailSignupForm: FunctionComponent = () => {
</button>
</form>
);
};
export default EmailSignupForm;
}

View File

@@ -1,4 +1,4 @@
import { useState } from 'preact/hooks';
import { type FormEvent, useState } from 'react';
import { httpPost } from '../../lib/http';
export function ForgotPasswordForm() {
@@ -7,7 +7,7 @@ export function ForgotPasswordForm() {
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const handleSubmit = async (e: Event) => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setError('');
@@ -29,7 +29,7 @@ export function ForgotPasswordForm() {
};
return (
<form onSubmit={handleSubmit} class="w-full">
<form onSubmit={handleSubmit} className="w-full">
<input
type="email"
name="email"

View File

@@ -1,10 +1,9 @@
import { useEffect, useState } from 'preact/hooks';
import GitHubIcon from '../../icons/github.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { useEffect, useState } from 'react';
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 = {};
@@ -14,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);
@@ -30,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) {
@@ -57,11 +55,18 @@ export function GitHubButton(props: GitHubButtonProps) {
}
}
const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) {
localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl;
}
localStorage.removeItem(GITHUB_REDIRECT_AT);
localStorage.removeItem(GITHUB_LAST_PAGE);
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
path: '/',
expires: 30,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
window.location.href = redirectUrl;
})
@@ -75,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);
@@ -90,10 +95,11 @@ export function GitHubButton(props: GitHubButtonProps) {
// For non authentication pages, we want to redirect back to the page
// 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.search
: window.location.pathname;
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname,
)
? window.location.pathname + window.location.search
: window.location.pathname;
localStorage.setItem(GITHUB_REDIRECT_AT, Date.now().toString());
localStorage.setItem(GITHUB_LAST_PAGE, pagePath);
@@ -105,15 +111,15 @@ export function GitHubButton(props: GitHubButtonProps) {
return (
<>
<button
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoading}
onClick={handleClick}
>
<img
src={icon}
alt="GitHub"
class={`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 'preact/hooks';
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) {
@@ -55,11 +54,18 @@ export function GoogleButton(props: GoogleButtonProps) {
}
}
const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) {
localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl;
}
localStorage.removeItem(GOOGLE_REDIRECT_AT);
localStorage.removeItem(GOOGLE_LAST_PAGE);
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
path: '/',
expires: 30,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
window.location.href = redirectUrl;
})
@@ -72,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) {
@@ -85,10 +91,11 @@ export function GoogleButton(props: GoogleButtonProps) {
// For non authentication pages, we want to redirect back to the page
// 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.search
: window.location.pathname;
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname,
)
? window.location.pathname + window.location.search
: window.location.pathname;
localStorage.setItem(GOOGLE_REDIRECT_AT, Date.now().toString());
localStorage.setItem(GOOGLE_LAST_PAGE, pagePath);
@@ -105,15 +112,15 @@ export function GoogleButton(props: GoogleButtonProps) {
return (
<>
<button
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoading}
onClick={handleClick}
>
<img
src={icon}
alt="Google"
class={`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 'preact/hooks';
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) {
@@ -55,11 +54,18 @@ export function LinkedInButton(props: LinkedInButtonProps) {
}
}
const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) {
localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl;
}
localStorage.removeItem(LINKEDIN_REDIRECT_AT);
localStorage.removeItem(LINKEDIN_LAST_PAGE);
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
path: '/',
expires: 30,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
window.location.href = redirectUrl;
})
@@ -72,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) {
@@ -85,10 +91,11 @@ export function LinkedInButton(props: LinkedInButtonProps) {
// For non authentication pages, we want to redirect back to the page
// 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.search
: window.location.pathname;
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname,
)
? window.location.pathname + window.location.search
: window.location.pathname;
localStorage.setItem(LINKEDIN_REDIRECT_AT, Date.now().toString());
localStorage.setItem(LINKEDIN_LAST_PAGE, pagePath);
@@ -105,15 +112,15 @@ export function LinkedInButton(props: LinkedInButtonProps) {
return (
<>
<button
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoading}
onClick={handleClick}
>
<img
src={icon}
alt="Google"
class={`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,6 +1,6 @@
---
import Popup from '../Popup/Popup.astro';
import EmailLoginForm from './EmailLoginForm';
import { EmailLoginForm } from './EmailLoginForm';
import Divider from './Divider.astro';
import { GitHubButton } from './GitHubButton';
import { GoogleButton } from './GoogleButton';
@@ -9,9 +9,9 @@ import { LinkedInButton } from './LinkedInButton';
<Popup id='login-popup' title='' subtitle=''>
<div class='text-center'>
<h2 class='mb-3 text-2xl font-semibold leading-5 text-slate-900'>
<p class='mb-3 text-2xl font-semibold leading-5 text-slate-900'>
Login to your account
</h2>
</p>
<p class='mt-2 text-sm leading-4 text-slate-600'>
You must be logged in to perform this action.
</p>

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from 'preact/hooks';
import { type FormEvent, useEffect, useState } from 'react';
import { httpPost } from '../../lib/http';
import Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
export default function ResetPasswordForm() {
export function ResetPasswordForm() {
const [code, setCode] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirm, setPasswordConfirm] = useState('');
@@ -21,7 +21,7 @@ export default function ResetPasswordForm() {
}
}, []);
const handleSubmit = async (e: Event) => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
@@ -56,6 +56,7 @@ export default function ResetPasswordForm() {
Cookies.set(TOKEN_COOKIE_NAME, token, {
path: '/',
expires: 30,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
window.location.href = '/';
};

View File

@@ -1,10 +1,9 @@
import SpinnerIcon from '../../icons/spinner.svg';
import ErrorIcon from '../../icons/error.svg';
import { useEffect, useState } from 'preact/hooks';
import { useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { httpPost } from '../../lib/http';
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);
@@ -17,7 +16,7 @@ export function TriggerVerifyAccount() {
`${import.meta.env.PUBLIC_API_URL}/v1-verify-account`,
{
code,
}
},
)
.then(({ response, error }) => {
if (!response?.token) {
@@ -30,6 +29,7 @@ export function TriggerVerifyAccount() {
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
path: '/',
expires: 30,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
window.location.href = '/';
})
@@ -55,26 +55,14 @@ 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}
class={'mx-auto h-16 w-16 animate-spin'}
/>
)}
{error && (
<img
alt={'Please wait.'}
src={ErrorIcon}
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>
<div className="text-sm sm:text-base">
{isLoading && <p>Please wait while we verify your account..</p>}
{error && <p class="text-red-700">{error}</p>}
{error && <p className="text-red-700">{error}</p>}
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
import VerifyLetterIcon from '../../icons/verify-letter.svg';
import { useEffect, useState } from 'preact/hooks';
import { useEffect, useState } from 'react';
import { httpPost } from '../../lib/http';
import { VerifyLetterIcon } from '../ReactIcons/VerifyLetterIcon';
export function VerificationEmailMessage() {
const [email, setEmail] = useState('..');
@@ -37,15 +37,11 @@ export function VerificationEmailMessage() {
return (
<div className="mx-auto max-w-md text-center">
<img
alt="Verify Email"
src={VerifyLetterIcon}
class="mx-auto mb-4 h-20 w-40 sm:h-40"
/>
<h2 class="my-2 text-center text-xl font-semibold sm:my-5 sm:text-2xl">
<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>
<div class="text-sm sm:text-base">
<div className="text-sm sm:text-base">
<p>
We have sent you an email at{' '}
<span className="font-bold">{email}</span>. Please click the link to
@@ -53,7 +49,7 @@ export function VerificationEmailMessage() {
soon!
</p>
<hr class="my-4" />
<hr className="my-4" />
{!isEmailResent && (
<>
@@ -72,12 +68,12 @@ export function VerificationEmailMessage() {
</p>
)}
{error && <p class="text-red-700">{error}</p>}
{error && <p className="text-red-700">{error}</p>}
</>
)}
{isEmailResent && (
<p class="text-green-700">Verification email has been sent!</p>
<p className="text-green-700">Verification email has been sent!</p>
)}
</div>
</div>

View File

@@ -36,6 +36,7 @@ function handleGuest() {
'/account/notification',
'/account/update-password',
'/account/settings',
'/account/roadmaps',
'/account/road-card',
'/account/friends',
'/account',
@@ -72,7 +73,10 @@ function handleAuthenticated() {
// If the user is on a guest route, redirect them to the home page
if (guestRoutes.includes(window.location.pathname)) {
window.location.href = '/';
const authRedirect = window.localStorage.getItem('authRedirect') || '/';
window.localStorage.removeItem('authRedirect');
window.location.href = authRedirect;
}
}

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'preact/hooks';
import { httpDelete, httpGet, httpPatch, httpPost } from '../lib/http';
import { useEffect, useState } from 'react';
import { httpDelete, httpGet, httpPost } from '../lib/http';
import { pageProgressMessage } from '../stores/page';
import { isLoggedIn } from '../lib/jwt';
import { showLoginPopup } from '../lib/popup';
@@ -118,7 +118,7 @@ export function Befriend() {
<ErrorIcon additionalClasses="mx-auto mb-4 mt-24 w-20 opacity-20" />
<h2 className={'mb-1 text-2xl font-bold'}>Error</h2>
<p class="mb-4 text-base leading-6 text-gray-600">
<p className="mb-4 text-base leading-6 text-gray-600">
{error || 'There was a problem, please try again.'}
</p>
@@ -141,7 +141,7 @@ export function Befriend() {
const isMe = currentUser?.id === user.id;
return (
<div className="container max-w-[400px] text-center">
<div className="container !max-w-[400px] text-center">
<img
alt={'join team'}
src={userAvatar}
@@ -149,13 +149,13 @@ export function Befriend() {
/>
<h2 className={'mb-1 text-3xl font-bold'}>{user.name}</h2>
<p class="mb-6 text-base leading-6 text-gray-600">
<p className="mb-6 text-base leading-6 text-gray-600">
After you add {user.name} as a friend, you will be able to view each
other's skills and progress.
</p>
<div class="mx-auto w-full duration-500 sm:max-w-md">
<div class="flex w-full flex-col items-center gap-2">
<div className="mx-auto w-full duration-500 sm:max-w-md">
<div className="flex w-full flex-col items-center gap-2">
{user.status === 'none' && (
<button
disabled={isMe}
@@ -169,7 +169,7 @@ export function Befriend() {
});
}}
type="button"
class="w-full flex-grow cursor-pointer rounded-lg bg-black px-3 py-2 text-center text-white disabled:cursor-not-allowed disabled:opacity-40"
className="w-full flex-grow cursor-pointer rounded-lg bg-black px-3 py-2 text-center text-white disabled:cursor-not-allowed disabled:opacity-40"
>
{isMe ? "You can't add yourself" : 'Add Friend'}
</button>
@@ -177,7 +177,7 @@ export function Befriend() {
{user.status === 'sent' && (
<>
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<CheckIcon additionalClasses="mr-2 h-4 w-4" />
Request Sent
</span>
@@ -188,7 +188,7 @@ export function Befriend() {
setIsConfirming(true);
}}
type="button"
class="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700"
className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700"
>
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Withdraw Request
@@ -196,7 +196,7 @@ export function Befriend() {
)}
{isConfirming && (
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '}
<button
className="ml-2 text-red-700 underline"
@@ -225,7 +225,7 @@ export function Befriend() {
{user.status === 'accepted' && (
<>
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<AddedUserIcon additionalClasses="mr-2 h-5 w-5" />
You are friends
</span>
@@ -236,7 +236,7 @@ export function Befriend() {
setIsConfirming(true);
}}
type="button"
class="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700"
className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700"
>
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Remove Friend
@@ -244,7 +244,7 @@ export function Befriend() {
)}
{isConfirming && (
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '}
<button
className="ml-2 text-red-700 underline"
@@ -271,12 +271,12 @@ export function Befriend() {
{user.status === 'rejected' && (
<>
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<DeleteUserIcon additionalClasses="mr-2 h-4 w-4" />
Request Rejected
</span>
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Changed your mind?{' '}
<button
className="ml-2 text-red-700 underline"
@@ -296,7 +296,7 @@ export function Befriend() {
{user.status === 'got_rejected' && (
<>
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-500 px-3 py-2 text-center text-red-500">
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-500 px-3 py-2 text-center text-red-500">
<StopIcon additionalClasses="mr-2 h-4 w-4" />
Request Rejected
</span>
@@ -311,7 +311,7 @@ export function Befriend() {
pageProgressMessage.set('');
});
}}
class="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-gray-800 bg-gray-800 px-3 py-2 text-center text-white hover:bg-black"
className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-gray-800 bg-gray-800 px-3 py-2 text-center text-white hover:bg-black"
>
<CheckIcon additionalClasses="mr-2 h-4 w-4" />
Accept Request
@@ -323,7 +323,7 @@ export function Befriend() {
setIsConfirming(true);
}}
type="button"
class="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-white px-3 py-2 text-center text-red-600 hover:bg-red-100"
className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-white px-3 py-2 text-center text-red-600 hover:bg-red-100"
>
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Reject Request
@@ -331,7 +331,7 @@ export function Befriend() {
)}
{isConfirming && (
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '}
<button
className="ml-2 text-red-700 underline"

View File

@@ -1,44 +0,0 @@
---
import type { BreadcrumbItem } from '../lib/roadmap-topic';
export interface Props {
breadcrumbs: BreadcrumbItem[];
roadmapId: string;
}
const { breadcrumbs, roadmapId } = Astro.props;
---
<div class='py-7 pb-6'>
<!-- Desktop breadcrumbs -->
<p class='text-gray-500 container hidden sm:block'>
{
breadcrumbs.map((breadcrumb, counter) => {
const isLast = counter === breadcrumbs.length - 1;
if (!isLast) {
return (
<>
<a class='hover:text-gray-800' href={`${breadcrumb.url}`}>
{breadcrumb.title}
</a>
<span>&nbsp;&middot;&nbsp;</span>
</>
);
}
return <span class='text-gray-400'>{breadcrumb.title}</span>;
})
}
</p>
<!-- Mobile breadcrums -->
<p class='container block sm:hidden'>
<a
class='bg-gray-500 py-1.5 px-3 rounded-md text-white text-xs sm:text-sm font-medium hover:bg-gray-600'
href={`/${roadmapId}`}
>
&larr; Back to Topics List
</a>
</p>
</div>

View File

@@ -1,34 +1,47 @@
import { useEffect, useRef, useState } from 'preact/hooks';
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 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 },
{
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,
icon: <UserIcon className="mr-2 h-4 w-4 stroke-2" />,
isProtected: true,
},
{
@@ -36,7 +49,15 @@ const defaultPages: PageType[] = [
url: '/team',
title: 'Teams',
group: 'Pages',
icon: GroupIcon,
icon: <GroupIcon className="mr-2 h-4 w-4 stroke-2" />,
isProtected: true,
},
{
id: 'friends',
url: '/account/friends',
title: 'Friends',
group: 'Pages',
icon: <GroupIcon className="mr-2 h-4 w-4 stroke-2" />,
isProtected: true,
},
{
@@ -44,28 +65,43 @@ const defaultPages: PageType[] = [
url: '/roadmaps',
title: 'Roadmaps',
group: 'Pages',
icon: RoadmapIcon,
icon: <RoadmapIcon className="mr-2 h-4 w-4 stroke-2" />,
},
{
id: 'account-roadmaps',
url: '/account/roadmaps',
title: 'Custom Roadmaps',
group: 'Pages',
icon: <RoadmapIcon className="mr-2 h-4 w-4 stroke-2" />,
isProtected: true,
},
{
id: 'best-practices',
url: '/best-practices',
title: 'Best Practices',
group: 'Pages',
icon: BestPracticesIcon,
icon: <BestPracticesIcon className="mr-2 h-4 w-4 stroke-2" />,
},
{
id: 'questions',
url: '/questions',
title: 'Questions',
group: 'Pages',
icon: <ClipboardIcon className="mr-2 h-4 w-4 stroke-2" />,
},
{
id: 'guides',
url: '/guides',
title: 'Guides',
group: 'Pages',
icon: GuideIcon,
icon: <GuideIcon className="mr-2 h-4 w-4 stroke-2" />,
},
{
id: 'videos',
url: '/videos',
title: 'Videos',
group: 'Pages',
icon: VideoIcon,
icon: <VideoIcon className="mr-2 h-4 w-4 stroke-2" />,
},
];
@@ -158,12 +194,12 @@ export function CommandMenu() {
<div className="relative rounded-lg bg-white shadow" ref={modalRef}>
<input
ref={inputRef}
autofocus={true}
autoFocus={true}
type="text"
value={searchedText}
className="w-full rounded-t-md border-b p-4 text-sm focus:bg-gray-50 focus:outline-none"
placeholder="Search roadmaps, guides or pages .."
autocomplete="off"
autoComplete="off"
onInput={(e) => {
const value = (e.target as HTMLInputElement).value.trim();
setSearchedText(value);
@@ -175,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();
@@ -191,39 +227,37 @@ export function CommandMenu() {
}}
/>
<div class="px-2 py-2">
<div className="px-2 py-2">
<div className="flex flex-col">
{searchResults.length === 0 && (
<div class="p-5 text-center text-sm text-gray-400">
<div className="p-5 text-center text-sm text-gray-400">
No results found
</div>
)}
{searchResults.map((page, counter) => {
{searchResults.map((page: PageType, counter: number) => {
const prevPage = searchResults[counter - 1];
const groupChanged = prevPage && prevPage.group !== page.group;
return (
<>
<Fragment key={page.id}>
{groupChanged && (
<div class="border-b border-gray-100"></div>
<div className="border-b border-gray-100"></div>
)}
<a
class={`flex w-full items-center rounded p-2 text-sm ${
className={`flex w-full items-center rounded p-2 text-sm ${
counter === activeCounter ? 'bg-gray-100' : ''
}`}
onMouseOver={() => setActiveCounter(counter)}
href={page.url}
>
{!page.icon && (
<span class="mr-2 text-gray-400">{page.group}</span>
)}
{page.icon && (
<img alt={page.title} src={page.icon} class="mr-2 h-4 w-4" />
<span className="mr-2 text-gray-400">{page.group}</span>
)}
{page.icon && page.icon}
{page.title}
</a>
</>
</Fragment>
);
})}
</div>

View File

@@ -0,0 +1,69 @@
import { useEffect, useState } from 'react';
import ReactConfetti from 'react-confetti';
type ConfettiPosition = {
x: number;
y: number;
w: number;
h: number;
};
type ConfettiProps = {
pieces?: number;
element?: HTMLElement | null;
onDone?: () => void;
};
export function Confetti(props: ConfettiProps) {
const { element = document.body, onDone = () => null, pieces = 40 } = props;
const [confettiPos, setConfettiPos] = useState<
undefined | ConfettiPosition
>();
function populateConfettiPosition(element: HTMLElement) {
const elRect = element.getBoundingClientRect();
// set confetti position, keeping in mind the scroll values
setConfettiPos({
x: elRect?.x || 0,
y: (elRect?.y || 0) + window.scrollY,
w: elRect?.width || 0,
h: elRect?.height || 0,
});
}
useEffect(() => {
if (!element) {
setConfettiPos(undefined);
return;
}
populateConfettiPosition(element);
}, [element]);
if (!confettiPos) {
return null;
}
return (
<ReactConfetti
height={document.body.scrollHeight}
numberOfPieces={pieces}
recycle={false}
onConfettiComplete={(confettiInstance) => {
setConfettiPos(undefined);
onDone();
}}
initialVelocityX={4}
initialVelocityY={8}
tweenDuration={10}
confettiSource={{
x: confettiPos.x,
y: confettiPos.y,
w: confettiPos.w,
h: confettiPos.h,
}}
/>
);
}

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from 'preact/hooks';
import { useEffect, useState } from 'react';
import { Stepper } from '../Stepper';
import { Step0, ValidTeamType } from './Step0';
import { Step1, ValidTeamSize } from './Step1';
import { Step0, type ValidTeamType } from './Step0';
import { Step1, type ValidTeamSize } from './Step1';
import { Step2 } from './Step2';
import { httpGet } from '../../lib/http';
import { getUrlParams, setUrlParams } from '../../lib/browser';

View File

@@ -21,7 +21,7 @@ export function NextButton(props: NextButtonProps) {
return (
<button
type={type}
type={type as any}
onClick={onClick}
disabled={isLoading}
className={

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}
className={'relative top-[1px] h-[17px] w-[17px] opacity-40'}
/>
<ChevronDownIcon className="relative top-[1px] h-[17px] w-[17px] opacity-40" />
</div>
);
}

View File

@@ -1,37 +1,53 @@
import { useEffect, useState } from 'preact/hooks';
import { useEffect, useState } from 'react';
import { httpGet, httpPut } from '../../lib/http';
import type { PageType } from '../CommandMenu/CommandMenu';
import ChevronDownIcon from '../../icons/chevron-down.svg';
import { pageProgressMessage } from '../../stores/page';
import type { TeamDocument } from './CreateTeamForm';
import { UpdateTeamResourceModal } from './UpdateTeamResourceModal';
import { SelectRoadmapModal } from './SelectRoadmapModal';
import { NotDropdown } from './NotDropdown';
import { Map, Shapes } from 'lucide-react';
import type {
AllowedRoadmapVisibility,
RoadmapDocument,
} from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { useToast } from '../../hooks/use-toast';
export type TeamResourceConfig = {
isCustomResource: boolean;
title: string;
description?: string;
visibility?: AllowedRoadmapVisibility;
resourceId: string;
resourceType: string;
removed: string[];
topics?: number;
sharedTeamMemberIds: string[];
sharedFriendIds: string[];
}[];
type RoadmapSelectorProps = {
teamId: string;
teamResourceConfig: TeamResourceConfig;
setTeamResourceConfig: (config: TeamResourceConfig) => void;
teamResources: TeamResourceConfig;
setTeamResources: (config: TeamResourceConfig) => void;
};
export function RoadmapSelector(props: RoadmapSelectorProps) {
const { teamId, teamResourceConfig = [], setTeamResourceConfig } = props;
const { teamId, teamResources = [], setTeamResources } = props;
const toast = useToast();
const [removingRoadmapId, setRemovingRoadmapId] = useState<string>('');
const [showSelectRoadmapModal, setShowSelectRoadmapModal] = useState(false);
const [allRoadmaps, setAllRoadmaps] = useState<PageType[]>([]);
const [changingRoadmapId, setChangingRoadmapId] = useState<string>('');
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState<boolean>(false);
const [error, setError] = useState<string>('');
async function loadAllRoadmaps() {
const { error, response } = await httpGet<PageType[]>(`/pages.json`);
if (error) {
toast.error(error.message || 'Something went wrong. Please try again!');
setError(error.message || 'Something went wrong. Please try again!');
return;
}
@@ -72,7 +88,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
return;
}
setTeamResourceConfig(response);
setTeamResources(response);
}
async function onRemove(resourceId: string) {
@@ -106,13 +122,25 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
return;
}
setTeamResourceConfig(response);
setTeamResources(response);
}
useEffect(() => {
loadAllRoadmaps().finally();
loadAllRoadmaps().finally(() => {});
}, []);
function handleCustomRoadmapCreated(roadmap: RoadmapDocument) {
const { _id: roadmapId } = roadmap;
if (!roadmapId) {
return;
}
loadAllRoadmaps().finally(() => {});
addTeamResource(roadmapId).finally(() => {
pageProgressMessage.set('');
});
}
return (
<div>
{changingRoadmapId && (
@@ -121,9 +149,9 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
resourceId={changingRoadmapId}
resourceType={'roadmap'}
teamId={teamId}
setTeamResourceConfig={setTeamResourceConfig}
setTeamResourceConfig={setTeamResources}
defaultRemovedItems={
teamResourceConfig.find((c) => c.resourceId === changingRoadmapId)
teamResources.find((c) => c.resourceId === changingRoadmapId)
?.removed || []
}
/>
@@ -131,7 +159,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
{showSelectRoadmapModal && (
<SelectRoadmapModal
onClose={() => setShowSelectRoadmapModal(false)}
teamResourceConfig={teamResourceConfig}
teamResourceConfig={teamResources}
allRoadmaps={allRoadmaps}
teamId={teamId}
onRoadmapAdd={(roadmapId) => {
@@ -145,72 +173,170 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
/>
)}
<div className="mt-3">
<NotDropdown
<div className="my-3 flex items-center gap-4">
{isCreatingRoadmap && (
<CreateRoadmapModal
teamId={teamId}
onClose={() => setIsCreatingRoadmap(false)}
onCreated={(roadmap: RoadmapDocument) => {
handleCustomRoadmapCreated(roadmap);
setIsCreatingRoadmap(false);
}}
/>
)}
<button
className="flex h-10 grow items-center justify-center gap-2 rounded-md border border-black bg-white text-black transition-colors hover:bg-black hover:text-white"
onClick={() => {
setShowSelectRoadmapModal(true);
}}
selectedCount={teamResourceConfig.length}
singularName={'roadmap'}
pluralName={'roadmaps'}
/>
>
<Map className="h-4 w-4 stroke-[2.5]" />
Pick from our roadmaps
</button>
<span className="text-base text-gray-400">or</span>
<button
className="flex h-10 grow items-center justify-center gap-2 rounded-md border border-black bg-white text-black transition-colors hover:bg-black hover:text-white"
onClick={() => {
setIsCreatingRoadmap(true);
}}
>
<Shapes className="h-4 w-4 stroke-[2.5]" />
Create Custom Roadmap
</button>
</div>
{!teamResourceConfig.length && (
<p className={'mb-3 mt-2 text-base text-gray-400'}>
No roadmaps selected.
</p>
{!teamResources.length && (
<div className="flex min-h-[240px] flex-col items-center justify-center rounded-lg border">
<Map className="mb-2 h-12 w-12 text-gray-300" />
<p className={'text-lg font-semibold'}>No roadmaps selected.</p>
<p className={'text-base text-gray-400'}>
Pick from{' '}
<span
onClick={() => setShowSelectRoadmapModal(true)}
className="cursor-pointer underline"
>
our roadmaps
</span>{' '}
or{' '}
<span
onClick={() => {
setIsCreatingRoadmap(true);
}}
className="cursor-pointer underline"
>
create a new one
</span>
.
</p>
</div>
)}
{teamResourceConfig.length > 0 && (
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 flex-wrap gap-2.5">
{teamResourceConfig.map(({ resourceId, removed: removedTopics }) => {
const roadmapTitle =
allRoadmaps.find((roadmap) => roadmap.id === resourceId)?.title ||
'...';
{teamResources.length > 0 && (
<div className="mb-3 grid grid-cols-1 flex-wrap gap-2.5 sm:grid-cols-3">
{teamResources.map(
({
isCustomResource,
title: roadmapTitle,
resourceId,
removed: removedTopics,
topics,
}) => {
return (
<div
className="relative flex flex-col items-start overflow-hidden rounded-md border border-gray-300"
key={resourceId}
>
<div className={'w-full flex-grow px-3 pb-2 pt-4'}>
<span className="mb-0.5 block text-base font-medium leading-snug text-black">
{roadmapTitle}
</span>
{removedTopics.length > 0 || (topics && topics > 0) ? (
<span className={'text-xs leading-none text-gray-400'}>
{isCustomResource ? (
<>
Custom &middot; {topics} topic
{topics && topics > 1 ? 's' : ''}
</>
) : (
<>
{removedTopics.length} topic
{removedTopics.length > 1 ? 's' : ''} removed
</>
)}
</span>
) : (
<span className="text-xs italic leading-none text-gray-400/60">
{isCustomResource
? 'Placeholder roadmap.'
: 'No changes made ..'}
</span>
)}
</div>
return (
<div className="flex flex-col items-start rounded-md border border-gray-300">
<div className={'w-full px-3 pb-2 pt-4'}>
<span className="mb-0.5 block text-base font-medium leading-none text-black">
{roadmapTitle}
</span>
{removedTopics.length > 0 ? (
<span className={'text-xs leading-none text-gray-900'}>
{removedTopics.length} topic
{removedTopics.length > 1 ? 's' : ''} removed
</span>
) : (
<span className="text-xs italic leading-none text-gray-400/60">
No changes made ..
</span>
{removingRoadmapId === resourceId && (
<div
className={
'flex w-full items-center justify-end p-3 text-sm'
}
>
<span className="text-xs text-gray-500">
Are you sure?{' '}
<button
onClick={() => onRemove(resourceId)}
className="mx-0.5 text-red-500 underline underline-offset-1"
>
Yes
</button>{' '}
<button
onClick={() => setRemovingRoadmapId('')}
className="text-red-500 underline underline-offset-1"
>
No
</button>
</span>
</div>
)}
{(!removingRoadmapId || removingRoadmapId !== resourceId) && (
<div className={'flex w-full justify-between p-3'}>
<button
type="button"
className={
'text-xs text-gray-500 underline hover:text-black focus:outline-none'
}
onClick={() => {
if (isCustomResource) {
window.open(
`${
import.meta.env.PUBLIC_EDITOR_APP_URL
}/${resourceId}`,
'_blank'
);
return;
}
setChangingRoadmapId(resourceId);
}}
>
Customize
</button>
<button
type="button"
className={
'text-xs text-red-500 underline hover:text-black'
}
onClick={() => setRemovingRoadmapId(resourceId)}
>
Remove
</button>
</div>
)}
</div>
<div className={'flex w-full justify-between p-3'}>
<button
type="button"
className={
'text-xs text-gray-500 underline hover:text-black focus:outline-none'
}
onClick={() => setChangingRoadmapId(resourceId)}
>
Customize
</button>
<button
type="button"
className={
'text-xs text-red-500 underline hover:text-black'
}
onClick={() => onRemove(resourceId)}
>
Remove
</button>
</div>
</div>
);
})}
);
}
)}
</div>
)}
</div>

View File

@@ -1,5 +1,5 @@
import { ChevronDownIcon } from '../ReactIcons/ChevronDownIcon';
import { useRef, useState } from 'preact/hooks';
import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
const allowedRoles = [

View File

@@ -1,10 +1,10 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { useEffect, useRef, useState } from 'react';
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,27 +60,27 @@ 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 (
<div class="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div class="relative mx-auto h-full w-full max-w-2xl p-4 md:h-auto">
<div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div className="relative mx-auto h-full w-full max-w-2xl p-4 md:h-auto">
<div
ref={popupBodyEl}
class="popup-body relative mt-4 overflow-hidden rounded-lg bg-white shadow"
className="popup-body relative mt-4 overflow-hidden rounded-lg bg-white shadow"
>
<button
type="button"
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} className="h-4 w-4" />
<span class="sr-only">Close modal</span>
<XIcon className="h-4 w-4" />
<span className="sr-only">Close modal</span>
</button>
<input
ref={searchInputEl}
@@ -100,12 +100,13 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
{roleBasedRoadmaps.length > 0 && (
<div className="mb-5 flex flex-wrap items-center gap-2">
{roleBasedRoadmaps.map((roadmap) => {
const isSelected = !!teamResourceConfig.find(
(r) => r.resourceId === roadmap.id
const isSelected = !!teamResourceConfig?.find(
(r) => r.resourceId === roadmap.id,
);
return (
<SelectRoadmapModalItem
key={roadmap.id}
title={roadmap.title}
isSelected={isSelected}
onClick={() => {
@@ -126,11 +127,12 @@ 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 (
<SelectRoadmapModalItem
key={roadmap.id}
title={roadmap.title}
isSelected={isSelected}
onClick={() => {

View File

@@ -1,22 +1,24 @@
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 'preact/hooks';
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,
description: 'Track the skills and learning progress of the tech team at your company',
description:
'Track the skills and learning progress of the tech team at your company',
},
{
value: 'study_group',
label: 'Study Group',
icon: UsersIcon,
description: 'Invite your friends or course-mates and track your learning progress together',
description:
'Invite your friends or course-mates and track your learning progress together',
},
] as const;
@@ -54,7 +56,7 @@ export function Step0(props: Step0Props) {
teamSize: team.teamSize,
linkedInUrl: team?.links?.linkedIn || undefined,
}),
}
},
);
if (error || !response) {
@@ -70,27 +72,28 @@ export function Step0(props: Step0Props) {
return (
<>
<div className={'flex flex-col sm:flex-row gap-3'}>
<div className={'flex flex-col gap-3 sm:flex-row'}>
{validTeamTypes.map((validTeamType) => (
<button
className={`flex flex-grow flex-col items-center rounded-lg border px-5 py-12 ${
key={validTeamType.value}
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
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>
<span className="text-sm text-gray-500 leading-[21px]">
<span className="text-sm leading-[21px] text-gray-500">
{validTeamType.description}
</span>
</button>
@@ -100,11 +103,11 @@ export function Step0(props: Step0Props) {
{/*Error message*/}
{error && <div className="mt-4 text-sm text-red-500">{error}</div>}
<div className="mt-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-2">
<div className="mt-4 flex flex-col items-stretch justify-between gap-2 md:flex-row md:items-center">
<a
href="/account"
className={
'rounded-md border border-red-400 bg-white px-8 py-2 text-red-500 text-center'
'rounded-md border border-red-400 bg-white px-8 py-2 text-center text-red-500'
}
>
Cancel

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { AppError, httpPost, httpPut } from '../../lib/http';
import { type FormEvent, useEffect, useRef, useState } from 'react';
import { type AppError, httpPost, httpPut } from '../../lib/http';
import type { ValidTeamType } from './Step0';
import type { TeamDocument } from './CreateTeamForm';
import { NextButton } from './NextButton';
@@ -49,7 +49,7 @@ export function Step1(props: Step1Props) {
team?.teamSize || ('' as any)
);
const handleSubmit = async (e: Event) => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
if (!name || !selectedTeamType) {
@@ -124,7 +124,7 @@ export function Step1(props: Step1Props) {
<form onSubmit={handleSubmit}>
<div className="flex w-full flex-col">
<label
for="name"
htmlFor="name"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
>
{selectedTeamType === 'company' ? 'Company Name' : 'Group Name'}
@@ -133,7 +133,7 @@ export function Step1(props: Step1Props) {
type="text"
name="name"
ref={nameRef as any}
autofocus={true}
autoFocus={true}
id="name"
className="mt-2 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
placeholder="Roadmap Inc."
@@ -147,7 +147,7 @@ export function Step1(props: Step1Props) {
{selectedTeamType === 'company' && (
<div className="mt-4 flex w-full flex-col">
<label
for="website"
htmlFor="website"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
>
Website
@@ -168,7 +168,7 @@ export function Step1(props: Step1Props) {
{selectedTeamType === 'company' && (
<div className="mt-4 flex w-full flex-col">
<label for="website" className="text-sm leading-none text-slate-500">
<label htmlFor="website" className="text-sm leading-none text-slate-500">
Company LinkedIn URL
</label>
<input
@@ -187,7 +187,7 @@ export function Step1(props: Step1Props) {
)}
<div className="mt-4 flex w-full flex-col">
<label for="website" className="text-sm leading-none text-slate-500">
<label htmlFor="website" className="text-sm leading-none text-slate-500">
GitHub Organization URL
</label>
<input
@@ -205,7 +205,7 @@ export function Step1(props: Step1Props) {
{selectedTeamType === 'company' && (
<div className="mt-4 flex w-full flex-col">
<label
for="team-size"
htmlFor="team-size"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
>
Tech Team Size
@@ -221,11 +221,11 @@ export function Step1(props: Step1Props) {
setTeamSize((e.target as HTMLSelectElement).value as any)
}
>
<option value="" selected>
<option value="">
Select team size
</option>
{validTeamSizes.map((size) => (
<option value={size}>{size} people</option>
<option key={size} value={size}>{size} people</option>
))}
</select>
</div>
@@ -237,7 +237,7 @@ export function Step1(props: Step1Props) {
</div>
)}
<div className="mt-4 flex flex-col md:flex-row items-center justify-between gap-2">
<div className="mt-4 flex flex-col items-center justify-between gap-2 md:flex-row">
<button
type="button"
onClick={onBack}

View File

@@ -1,4 +1,4 @@
import { RoadmapSelector, TeamResourceConfig } from './RoadmapSelector';
import { RoadmapSelector, type TeamResourceConfig } from './RoadmapSelector';
import type { TeamDocument } from './CreateTeamForm';
type Step2Props = {
@@ -17,7 +17,9 @@ export function Step2(props: Step2Props) {
<>
<div className="mt-4 flex w-full flex-col">
<div className="mb-1 mt-2">
<h2 className="mb-1 md:mb-1.5 text-lg md:text-2xl font-bold">Select Roadmaps</h2>
<h2 className="mb-1 text-lg font-bold md:mb-1.5 md:text-2xl">
Select Roadmaps
</h2>
<p className="text-sm text-gray-700">
You can always add and customize your roadmaps later.
</p>
@@ -25,12 +27,12 @@ export function Step2(props: Step2Props) {
<RoadmapSelector
teamId={team._id!}
teamResourceConfig={teamResourceConfig}
setTeamResourceConfig={setTeamResourceConfig}
teamResources={teamResourceConfig}
setTeamResources={setTeamResourceConfig}
/>
</div>
<div className="mt-4 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-2">
<div className="mt-4 flex flex-col items-stretch justify-between gap-2 md:flex-row md:items-center">
<button
type="button"
onClick={onBack}
@@ -46,8 +48,9 @@ export function Step2(props: Step2Props) {
<button
type="button"
onClick={onNext}
disabled={teamResourceConfig.length !== 0}
className={
'flex-grow rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-500 hover:border-gray-400 hover:text-black md:flex-auto'
'flex-grow rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-500 hover:border-gray-400 hover:text-black md:flex-auto disabled:opacity-50 disabled:pointer-events-none'
}
>
Skip for Now

View File

@@ -1,8 +1,8 @@
import type { TeamDocument } from './CreateTeamForm';
import { NextButton } from './NextButton';
import { TrashIcon } from '../ReactIcons/TrashIcon';
import { AllowedRoles, RoleDropdown } from './RoleDropdown';
import { useEffect, useRef, useState } from 'preact/hooks';
import { type AllowedRoles, RoleDropdown } from './RoleDropdown';
import { useEffect, useRef, useState } from 'react';
import { httpPost } from '../../lib/http';
type Step3Props = {
@@ -75,9 +75,9 @@ export function Step3(props: Step3Props) {
return (
<form className="mt-4 flex w-full flex-col" onSubmit={onSubmit}>
<div class="mb-1 mt-2">
<h2 class="mb-1 md:mb-2 text-lg md:text-2xl font-bold">Invite your Team</h2>
<p class="text-sm text-gray-700">
<div className="mb-1 mt-2">
<h2 className="mb-1 md:mb-2 text-lg md:text-2xl font-bold">Invite your Team</h2>
<p className="text-sm text-gray-700">
Use the form below to invite your team members to your team. You can
also invite them later.
</p>
@@ -88,7 +88,7 @@ export function Step3(props: Step3Props) {
<div className="flex flex-col sm:flex-row gap-2" key={user.id}>
<input
ref={userCounter === users.length - 1 ? emailInputRef : null}
autofocus={true}
autoFocus={true}
type="email"
name="email"
required
@@ -178,8 +178,9 @@ export function Step3(props: Step3Props) {
<button
type="button"
onClick={onNext}
disabled={users.filter((u) => u.email).length !== 0}
className={
'rounded-md flex-grow md:flex-auto border border-gray-300 bg-white px-4 py-2 text-gray-500 hover:border-gray-400 hover:text-black'
'rounded-md flex-grow md:flex-auto border border-gray-300 bg-white px-4 py-2 text-gray-500 hover:border-gray-400 hover:text-black disabled:opacity-50 disabled:pointer-events-none'
}
>
Skip for Now

View File

@@ -8,15 +8,15 @@ type Step4Props = {
export function Step4({ team }: Step4Props) {
return (
<div className="mt-4 flex flex-col rounded-xl border py-12 text-center">
<div class="mb-1 flex flex-col items-center">
<div className="mb-1 flex flex-col items-center">
<CheckIcon additionalClasses={'h-14 w-14 mb-4 opacity-100'} />
<h2 class="mb-2 text-2xl font-bold">Team Created</h2>
<p class="text-sm text-gray-700">
<h2 className="mb-2 text-2xl font-bold">Team Created</h2>
<p className="text-sm text-gray-700">
Your team has been created. Happy learning!
</p>
<a
href={`/team/progress?t=${team._id}`}
class="mt-4 rounded-md bg-black px-5 py-1.5 text-sm text-white"
className="mt-4 rounded-md bg-black px-5 py-1.5 text-sm text-white"
>
View Team
</a>

View File

@@ -1,15 +1,14 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { useEffect, useRef, useState } from 'react';
import { wireframeJSONToSVG } from 'roadmap-renderer';
import { Spinner } from '../ReactIcons/Spinner';
import { httpGet, httpPut } from '../../lib/http';
import { httpPut } from '../../lib/http';
import { renderTopicProgress } from '../../lib/resource-progress';
import '../FrameRenderer/FrameRenderer.css';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { useKeydown } from '../../hooks/use-keydown';
import type { TeamResourceConfig } from './RoadmapSelector';
import { useToast } from '../../hooks/use-toast';
import { useStore } from '@nanostores/preact';
import { $currentTeam } from '../../stores/team';
import {replaceChildren} from "../../lib/dom.ts";
export type ProgressMapProps = {
teamId: string;
@@ -40,8 +39,6 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
const [removedItems, setRemovedItems] =
useState<string[]>(defaultRemovedItems);
const currentTeam = useStore($currentTeam);
useEffect(() => {
function onTopicClick(e: any) {
const groupEl = e.target.closest('.clickable-group');
@@ -69,7 +66,9 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
};
}, [removedItems]);
let resourceJsonUrl = 'https://roadmap.sh';
let resourceJsonUrl = import.meta.env.DEV
? 'http://localhost:3000'
: 'https://roadmap.sh';
if (resourceType === 'roadmap') {
resourceJsonUrl += `/${resourceId}.json`;
} else {
@@ -83,7 +82,8 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
fontURL: '/fonts/balsamiq.woff2',
});
containerEl.current?.replaceChildren(svg);
replaceChildren(containerEl.current!, svg);
// containerEl.current?.replaceChildren(svg);
// Render team configuration
removedItems.forEach((topicId: string) => {
@@ -148,16 +148,12 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
}, []);
return (
<div class="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div class="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto">
<div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto">
<div
id={
currentTeam?.type === 'company'
? 'customized-roadmap'
: 'original-roadmap'
}
id={'customized-roadmap'}
ref={popupBodyEl}
class="popup-body relative rounded-lg bg-white shadow"
className="popup-body relative rounded-lg bg-white shadow"
>
<div
className={
@@ -201,7 +197,7 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
<div id="resource-svg-wrap" ref={containerEl} className="px-4"></div>
{isLoading && (
<div class="flex w-full justify-center">
<div className="flex w-full justify-center">
<Spinner
isDualRing={false}
className="mb-4 mt-2 h-4 w-4 animate-spin fill-blue-600 text-gray-200 sm:h-8 sm:w-8"

View File

@@ -0,0 +1,145 @@
import { useEffect, useState } from 'react';
import { httpGet, httpPost } from '../../lib/http';
import { useToast } from '../../hooks/use-toast';
import { isLoggedIn } from '../../lib/jwt';
import { GitFork, Loader2, Map } from 'lucide-react';
import { showLoginPopup } from '../../lib/popup';
import type { RoadmapDocument } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
type CreateVersionProps = {
roadmapId: string;
};
export function CreateVersion(props: CreateVersionProps) {
const { roadmapId } = props;
const toast = useToast();
const [isLoading, setIsLoading] = useState(true);
const [isCreating, setIsCreating] = useState(false);
const [isConfirming, setIsConfirming] = useState(false);
const [userVersion, setUserVersion] = useState<RoadmapDocument>();
async function loadMyVersion() {
if (!isLoggedIn()) {
return;
}
setIsLoading(true);
const { response, error } = await httpGet<RoadmapDocument>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-my-version/${roadmapId}`,
{},
);
if (error || !response) {
setIsLoading(false);
return;
}
setIsLoading(false);
setUserVersion(response);
}
useEffect(() => {
loadMyVersion().finally(() => {
setIsLoading(false);
});
}, []);
async function createVersion() {
if (isCreating || !roadmapId) {
return;
}
if (!isLoggedIn()) {
showLoginPopup();
return;
}
setIsCreating(true);
const { response, error } = await httpPost<{ roadmapId: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-create-version/${roadmapId}`,
{},
);
if (error || !response) {
setIsCreating(false);
toast.error(error?.message || 'Failed to create version');
return;
}
window.location.href = `${
import.meta.env.PUBLIC_EDITOR_APP_URL
}/${response?.roadmapId}`;
}
if (isLoading) {
return (
<div className="h-[30px] w-[312px] animate-pulse rounded-md bg-gray-300"></div>
);
}
if (!isLoading && userVersion?._id) {
return (
<div className={'flex items-center'}>
<a
href={`/r?id=${userVersion._id}`}
className="flex items-center rounded-md border border-blue-400 bg-gray-50 px-2.5 py-1 text-xs font-medium text-blue-600 hover:bg-blue-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:hover:bg-gray-100 max-sm:hidden sm:text-sm"
>
<Map size="15px" className="mr-1.5" />
Visit your own version of this Roadmap
</a>
</div>
);
}
if (isConfirming) {
return (
<p className="flex h-[30px] items-center text-sm text-red-500">
Create and edit a custom roadmap from this roadmap?
<button
onClick={() => {
setIsConfirming(false);
createVersion().finally(() => null);
}}
className="ml-2 font-semibold underline underline-offset-2"
>
Yes
</button>
<span className="text-xs">&nbsp;/&nbsp;</span>
<button
className="font-semibold underline underline-offset-2"
onClick={() => setIsConfirming(false)}
>
No
</button>
</p>
);
}
return (
<button
disabled={isCreating}
className="flex items-center justify-center rounded-md border border-gray-300 bg-gray-50 px-2.5 py-1 text-xs font-medium text-black hover:bg-gray-200 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:hover:bg-gray-100 max-sm:hidden sm:text-sm"
onClick={() => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
setIsConfirming(true);
}}
>
{isCreating ? (
<>
<Loader2 className="mr-2 h-3 w-3 animate-spin stroke-[2.5]" />
Please wait ..
</>
) : (
<>
<GitFork className="mr-1.5" size="16px" />
Create your own version of this roadmap
</>
)}
</button>
);
}

View File

@@ -0,0 +1,50 @@
import { Plus } from 'lucide-react';
import { isLoggedIn } from '../../../lib/jwt';
import { showLoginPopup } from '../../../lib/popup';
import { cn } from '../../../lib/classname';
import { CreateRoadmapModal } from './CreateRoadmapModal';
import { useState } from 'react';
type CreateRoadmapButtonProps = {
className?: string;
text?: string;
teamId?: string;
};
export function CreateRoadmapButton(props: CreateRoadmapButtonProps) {
const { teamId, className, text = 'Create your own Roadmap' } = props;
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
function toggleCreateRoadmapHandler() {
if (!isLoggedIn()) {
return showLoginPopup();
}
setIsCreatingRoadmap(true);
}
return (
<>
{isCreatingRoadmap && (
<CreateRoadmapModal
teamId={teamId}
onClose={() => {
setIsCreatingRoadmap(false);
}}
/>
)}
<button
className={cn(
'flex h-full w-full items-center justify-center gap-1 overflow-hidden rounded-md border border-dashed border-gray-800 p-3 text-sm text-gray-400 hover:border-gray-600 hover:bg-gray-900 hover:text-gray-300',
className,
)}
onClick={toggleCreateRoadmapHandler}
>
<Plus size={16} />
{text}
</button>
</>
);
}

View File

@@ -0,0 +1,244 @@
import {
type FormEvent,
type MouseEvent,
useEffect,
useRef,
useState,
} from 'react';
import { Loader2 } from 'lucide-react';
import { Modal } from '../../Modal';
import { useToast } from '../../../hooks/use-toast';
import { httpPost } from '../../../lib/http';
import { cn } from '../../../lib/classname';
export const allowedRoadmapVisibility = [
'me',
'friends',
'team',
'public',
] as const;
export type AllowedRoadmapVisibility =
(typeof allowedRoadmapVisibility)[number];
export const allowedCustomRoadmapType = ['role', 'skill'] as const;
export type AllowedCustomRoadmapType =
(typeof allowedCustomRoadmapType)[number];
export interface RoadmapDocument {
_id?: string;
title: string;
description?: string;
creatorId: string;
teamId?: string;
isDiscoverable: boolean;
type: AllowedCustomRoadmapType;
visibility: AllowedRoadmapVisibility;
sharedFriendIds?: string[];
sharedTeamMemberIds?: string[];
nodes: any[];
edges: any[];
createdAt: Date;
updatedAt: Date;
canManage: boolean;
isCustomResource: boolean;
}
interface CreateRoadmapModalProps {
onClose: () => void;
onCreated?: (roadmap: RoadmapDocument) => void;
teamId?: string;
visibility?: AllowedRoadmapVisibility;
}
export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
const { onClose, onCreated, teamId } = props;
const titleRef = useRef<HTMLInputElement>(null);
const toast = useToast();
const [isLoading, setIsLoading] = useState(false);
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const isInvalidDescription = description?.trim().length > 80;
async function handleSubmit(
e: FormEvent<HTMLFormElement> | MouseEvent<HTMLButtonElement>,
redirect: boolean = true,
) {
e.preventDefault();
if (isLoading) {
return;
}
if (title.trim() === '' || isInvalidDescription) {
toast.error('Please fill all the fields');
return;
}
setIsLoading(true);
const { response, error } = await httpPost<RoadmapDocument>(
`${import.meta.env.PUBLIC_API_URL}/v1-create-roadmap`,
{
title,
description,
...(teamId && {
teamId,
}),
nodes: [],
edges: [],
},
);
if (error) {
setIsLoading(false);
toast.error(error?.message || 'Something went wrong, please try again');
return;
}
toast.success('Roadmap created successfully');
if (redirect) {
window.location.href = `${
import.meta.env.PUBLIC_EDITOR_APP_URL
}/${response?._id}`;
return;
}
if (onCreated) {
onCreated(response as RoadmapDocument);
return;
}
onClose();
setTitle('');
setDescription('');
setIsLoading(false);
}
useEffect(() => {
titleRef.current?.focus();
}, []);
return (
<Modal
onClose={onClose}
bodyClassName="p-4"
wrapperClassName={cn(teamId && 'max-w-lg')}
>
<div className="mb-4">
<h2 className="text-lg font-medium text-gray-900">Create Roadmap</h2>
<p className="mt-1 text-sm text-gray-500">
Add a title and description to your roadmap.
</p>
</div>
<form onSubmit={handleSubmit}>
<div className="mt-4">
<label
htmlFor="title"
className="block text-xs uppercase text-gray-400"
>
Roadmap Title
</label>
<div className="mt-1">
<input
ref={titleRef}
type="text"
name="title"
id="title"
required
className="block text-black w-full rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm"
placeholder="Enter Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
</div>
<div className="mt-4">
<label
htmlFor="description"
className="block text-xs uppercase text-gray-400"
>
Description
</label>
<div className="relative mt-1">
<textarea
id="description"
name="description"
required
className={cn(
'block text-black h-24 w-full resize-none rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm',
isInvalidDescription && 'border-red-300 bg-red-100'
)}
placeholder="Enter Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<div className="absolute bottom-2 right-2 text-xs text-gray-400">
{description.length}/80
</div>
</div>
</div>
<div
className={cn('mt-4 flex justify-between gap-2', teamId && 'mt-8')}
>
<button
onClick={onClose}
type="button"
className={cn(
'block h-9 rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-black outline-none hover:border-gray-300 hover:bg-gray-50 focus:border-gray-300 focus:bg-gray-100',
!teamId && 'w-full',
)}
>
Cancel
</button>
<div className={cn('flex items-center gap-2', !teamId && 'w-full')}>
{teamId && !isLoading && (
<button
disabled={isLoading}
type="button"
onClick={(e) => handleSubmit(e, false)}
className="flex h-9 items-center justify-center rounded-md border border-black bg-white px-4 py-2 text-sm font-medium text-black outline-none hover:bg-black hover:text-white focus:bg-black focus:text-white"
>
{isLoading ? (
<Loader2 size={16} className="animate-spin" />
) : (
'Save as Placeholder'
)}
</button>
)}
<button
disabled={isLoading}
type="submit"
className={cn(
'flex h-9 items-center justify-center rounded-md border border-transparent bg-black px-4 py-2 text-sm font-medium text-white outline-none hover:bg-gray-800 focus:bg-gray-800',
teamId ? 'hidden sm:flex' : 'w-full',
)}
>
{isLoading ? (
<Loader2 size={16} className="animate-spin" />
) : teamId ? (
'Continue to Editor'
) : (
'Create'
)}
</button>
</div>
</div>
{teamId && (
<>
<p className="mt-4 hidden rounded-md border border-orange-200 bg-orange-50 p-2.5 text-sm text-orange-600 sm:block">
Preparing the roadmap might take some time, feel free to save it
as a placeholder and anyone with the role <strong>admin</strong>{' '}
or <strong>manager</strong> can prepare it later.
</p>
<p className="mt-4 rounded-md border border-orange-200 bg-orange-50 p-2.5 text-sm text-orange-600 sm:hidden">
Create a placeholder now and prepare it later.
</p>
</>
)}
</form>
</Modal>
);
}

View File

@@ -0,0 +1,137 @@
import { useEffect, useState } from 'react';
import { getUrlParams } from '../../lib/browser';
import {
type AppError,
type FetchError,
httpGet,
httpPost,
} from '../../lib/http';
import { RoadmapHeader } from './RoadmapHeader';
import { TopicDetail } from '../TopicDetail/TopicDetail';
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
import { currentRoadmap } from '../../stores/roadmap';
import { RestrictedPage } from './RestrictedPage';
import { isLoggedIn } from '../../lib/jwt';
import { FlowRoadmapRenderer } from './FlowRoadmapRenderer';
export const allowedLinkTypes = [
'video',
'article',
'opensource',
'course',
'website',
'podcast',
] as const;
export type AllowedLinkTypes = (typeof allowedLinkTypes)[number];
export interface RoadmapContentDocument {
_id?: string;
roadmapId: string;
nodeId: string;
title: string;
description: string;
links: {
id: string;
type: AllowedLinkTypes;
title: string;
url: string;
}[];
}
export type CreatorType = {
id: string;
name: string;
avatar: string;
};
export type GetRoadmapResponse = RoadmapDocument & {
canManage: boolean;
creator?: CreatorType;
team?: CreatorType;
};
export function hideRoadmapLoader() {
const loaderEl = document.querySelector(
'[data-roadmap-loader]',
) as HTMLElement;
if (loaderEl) {
loaderEl.remove();
}
}
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);
const [roadmap, setRoadmap] = useState<GetRoadmapResponse | null>(null);
const [error, setError] = useState<AppError | FetchError | undefined>();
async function getRoadmap() {
setIsLoading(true);
const roadmapUrl = new URL(
`${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(),
);
if (error || !response) {
setError(error);
setIsLoading(false);
return;
}
document.title = `${response.title} - roadmap.sh`;
setRoadmap(response);
currentRoadmap.set(response);
setIsLoading(false);
}
async function trackVisit() {
if (!isLoggedIn() || isEmbed) {
return;
}
await httpPost(`${import.meta.env.PUBLIC_API_URL}/v1-visit`, {
resourceId: id,
resourceType: 'roadmap',
});
}
useEffect(() => {
getRoadmap().finally(() => {
hideRoadmapLoader();
});
trackVisit().then();
}, []);
if (isLoading) {
return null;
}
if (error) {
return <RestrictedPage error={error} />;
}
return (
<>
{!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

@@ -0,0 +1,32 @@
import { CircleSlash, PenSquare, Shapes } from 'lucide-react';
import { cn } from '../../lib/classname';
type EmptyRoadmapProps = {
roadmapId: string;
canManage: boolean;
className?: string;
};
export function EmptyRoadmap(props: EmptyRoadmapProps) {
const { roadmapId, canManage, className } = props;
const editUrl = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${roadmapId}`;
return (
<div className={cn('flex h-full items-center justify-center', className)}>
<div className="flex flex-col items-center">
<CircleSlash className="mx-auto h-20 w-20 text-gray-400" />
<h3 className="mt-2">This roadmap is currently empty.</h3>
{canManage && (
<a
href={editUrl}
className="mt-4 flex items-center rounded-md bg-gray-500 px-4 py-2 font-medium text-white hover:bg-gray-600"
>
<Shapes className="mr-2 inline-block h-4 w-4" />
Edit Roadmap
</a>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,170 @@
import { ReadonlyEditor } from '../../../editor/readonly-editor';
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
import {
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 { 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, isEmbed = false } = props;
const roadmapId = String(roadmap._id!);
const [hideRenderer, setHideRenderer] = useState(false);
const editorWrapperRef = useRef<HTMLDivElement>(null);
const toast = useToast();
async function updateTopicStatus(
topicId: string,
newStatus: ResourceProgressType,
) {
if (isEmbed) {
return;
}
pageProgressMessage.set('Updating progress');
updateResourceProgress(
{
resourceId: roadmapId,
resourceType: 'roadmap',
topicId,
},
newStatus,
)
.then(() => {
renderTopicProgress(topicId, newStatus);
})
.catch((err) => {
toast.error('Something went wrong, please try again.');
console.error(err);
})
.finally(() => {
pageProgressMessage.set('');
refreshProgressCounters();
});
return;
}
const handleTopicRightClick = useCallback((e: MouseEvent, node: Node) => {
const target = e?.currentTarget as HTMLDivElement;
if (!target) {
return;
}
const isCurrentStatusDone = target?.classList.contains('done');
updateTopicStatus(node.id, isCurrentStatusDone ? 'pending' : 'done');
}, []);
const handleTopicShiftClick = useCallback((e: MouseEvent, node: Node) => {
const target = e?.currentTarget as HTMLDivElement;
if (!target) {
return;
}
const isCurrentStatusLearning = target?.classList.contains('learning');
updateTopicStatus(
node.id,
isCurrentStatusLearning ? 'pending' : 'learning',
);
}, []);
const handleTopicAltClick = useCallback((e: MouseEvent, node: Node) => {
const target = e?.currentTarget as HTMLDivElement;
if (!target) {
return;
}
const isCurrentStatusSkipped = target?.classList.contains('skipped');
updateTopicStatus(node.id, isCurrentStatusSkipped ? 'pending' : 'skipped');
}, []);
const handleTopicClick = useCallback((e: MouseEvent, node: Node) => {
const target = e?.currentTarget as HTMLDivElement;
if (!target) {
return;
}
window.dispatchEvent(
new CustomEvent('roadmap.node.click', {
detail: {
topicId: node.id,
resourceId: roadmapId,
resourceType: 'roadmap',
isCustomResource: true,
},
}),
);
}, []);
const handleLinkClick = useCallback((linkId: string, href: string) => {
if (!href) {
return;
}
const isExternalLink = href.startsWith('http');
if (isExternalLink) {
window.open(href, '_blank');
} else {
window.location.href = href;
}
}, []);
return (
<>
{hideRenderer && (
<EmptyRoadmap
roadmapId={roadmapId}
canManage={roadmap.canManage}
className="grow"
/>
)}
<ReadonlyEditor
ref={editorWrapperRef}
roadmap={roadmap}
className={cn(
roadmap?.nodes?.length === 0
? 'grow'
: 'min-h-0 max-md:min-h-[1000px]',
)}
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');
}
});
}}
onTopicClick={handleTopicClick}
onTopicRightClick={handleTopicRightClick}
onTopicShiftClick={handleTopicShiftClick}
onTopicAltClick={handleTopicAltClick}
onButtonNodeClick={handleLinkClick}
onLinkClick={handleLinkClick}
fontFamily="Balsamiq Sans"
fontURL="/fonts/balsamiq.woff2"
/>
</>
);
}

View File

@@ -0,0 +1,96 @@
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;
onCustomize?: () => void;
onUpdateSharing?: () => void;
};
export function PersonalRoadmapActionDropdown(
props: PersonalRoadmapActionDropdownProps,
) {
const { onDelete, onUpdateSharing, onCustomize } = props;
const menuRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
useOutsideClick(menuRef, () => {
setIsOpen(false);
});
return (
<div className="relative">
<button
disabled={false}
onClick={() => setIsOpen(!isOpen)}
className="hidden items-center opacity-60 transition-opacity hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-30 sm:flex"
>
<MoreVerticalIcon className={'h-4 w-4'} />
</button>
<button
disabled={false}
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-1 rounded-md border border-gray-300 bg-white px-2 py-1.5 text-xs hover:bg-gray-50 focus:outline-none sm:hidden"
>
<MoreVertical size={14} />
Options
</button>
{isOpen && (
<div
ref={menuRef}
className="align-right absolute right-auto top-full z-50 mt-1 w-[140px] rounded-md bg-slate-800 px-2 py-2 text-white shadow-md sm:right-0"
>
<ul>
{onUpdateSharing && (
<li>
<button
onClick={() => {
setIsOpen(false);
onUpdateSharing();
}}
className="flex w-full cursor-pointer items-center rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<Lock size={14} className="mr-2" />
Sharing
</button>
</li>
)}
{onCustomize && (
<li>
<button
onClick={() => {
setIsOpen(false);
onCustomize();
}}
className="flex w-full cursor-pointer items-center rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<Shapes size={14} className="mr-2" />
Customize
</button>
</li>
)}
{onDelete && (
<li>
<button
onClick={() => {
setIsOpen(false);
onDelete();
}}
className="flex w-full cursor-pointer items-center rounded p-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<Trash2 size={14} className="mr-2" />
Delete
</button>
</li>
)}
</ul>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,248 @@
import { httpDelete } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page';
import {
ExternalLink,
Shapes,
type LucideIcon,
Globe,
LockIcon,
Users,
PenSquare,
} from 'lucide-react';
import { useToast } from '../../hooks/use-toast';
import {
type AllowedRoadmapVisibility,
type RoadmapDocument,
} from './CreateRoadmap/CreateRoadmapModal';
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'];
onDelete: (roadmapId: string) => void;
setAllRoadmaps: Dispatch<SetStateAction<GetRoadmapListResponse>>;
};
export function PersonalRoadmapList(props: PersonalRoadmapListType) {
const { roadmaps: roadmapList, onDelete, setAllRoadmaps } = props;
const toast = useToast();
const [selectedRoadmap, setSelectedRoadmap] = useState<
GetRoadmapListResponse['personalRoadmaps'][number] | null
>(null);
async function deleteRoadmap(roadmapId: string) {
const { response, error } = await httpDelete<RoadmapDocument[]>(
`${import.meta.env.PUBLIC_API_URL}/v1-delete-roadmap/${roadmapId}`
);
if (error || !response) {
console.error(error);
toast.error(error?.message || 'Something went wrong, please try again');
return;
}
toast.success('Roadmap deleted');
onDelete(roadmapId);
}
async function onRemove(roadmapId: string) {
pageProgressMessage.set('Deleting roadmap');
deleteRoadmap(roadmapId).finally(() => {
pageProgressMessage.set('');
});
}
const shareSettingsModal = selectedRoadmap && (
<ShareOptionsModal
isDiscoverable={selectedRoadmap.isDiscoverable}
description={selectedRoadmap.description}
visibility={selectedRoadmap.visibility}
sharedFriendIds={selectedRoadmap.sharedFriendIds}
sharedTeamMemberIds={selectedRoadmap.sharedTeamMemberIds}
roadmapId={selectedRoadmap._id!}
onClose={() => setSelectedRoadmap(null)}
onShareSettingsUpdate={(settings) => {
setAllRoadmaps((prev) => {
return {
...prev,
personalRoadmaps: prev.personalRoadmaps.map((roadmap) => {
if (roadmap._id === selectedRoadmap._id) {
return {
...roadmap,
...settings,
};
}
return roadmap;
}),
};
});
}}
/>
);
if (roadmapList.length === 0) {
return (
<div className="flex flex-col items-center p-4 py-20">
<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
</p>
</div>
);
}
return (
<div>
{shareSettingsModal}
<div className="mb-3 flex items-center justify-between">
<span className={'text-sm text-gray-400'}>
{roadmapList.length} custom roadmap(s)
</span>
</div>
<ul className="flex flex-col divide-y rounded-md border">
{roadmapList.map((roadmap) => {
return (
<CustomRoadmapItem
key={roadmap._id!}
roadmap={roadmap}
onRemove={onRemove}
setSelectedRoadmap={setSelectedRoadmap}
/>
);
})}
</ul>
</div>
);
}
type CustomRoadmapItemProps = {
roadmap: GetRoadmapListResponse['personalRoadmaps'][number];
onRemove: (roadmapId: string) => Promise<void>;
setSelectedRoadmap: (
roadmap: GetRoadmapListResponse['personalRoadmaps'][number] | null
) => void;
};
function CustomRoadmapItem(props: CustomRoadmapItemProps) {
const { roadmap, onRemove, setSelectedRoadmap } = props;
const editorLink = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${roadmap._id}`;
return (
<li
className="grid grid-cols-1 p-2.5 sm:grid-cols-[auto_172px]"
key={roadmap._id!}
>
<div className="mb-3 grid grid-cols-1 sm:mb-0">
<p className="mb-1.5 truncate text-base font-medium leading-tight text-black">
{roadmap.title}
</p>
<span className="flex items-center text-xs leading-none text-gray-400">
<VisibilityBadge
visibility={roadmap.visibility!}
sharedFriendIds={roadmap.sharedFriendIds}
/>
<span className="mx-2 font-semibold">&middot;</span>
<Shapes size={16} className="mr-1 inline-block h-4 w-4" />
{roadmap.topics} topic
</span>
</div>
<div className="mr-1 flex items-center justify-start sm:justify-end">
<PersonalRoadmapActionDropdown
onUpdateSharing={() => {
setSelectedRoadmap(roadmap);
}}
onCustomize={() => {
window.open(editorLink, '_blank');
}}
onDelete={() => {
if (confirm('Are you sure you want to remove this roadmap?')) {
onRemove(roadmap._id!).finally(() => {});
}
}}
/>
<a
href={editorLink}
className={
'ml-2 flex items-center gap-2 rounded-md border border-gray-300 bg-white px-2.5 py-1.5 text-xs text-black hover:bg-gray-50 focus:outline-none'
}
target={'_blank'}
>
<PenSquare className="inline-block h-4 w-4" />
Edit
</a>
<a
href={`/r?id=${roadmap._id}`}
className={
'ml-2 flex items-center gap-2 rounded-md border border-blue-400 bg-white px-2 py-1.5 text-xs hover:bg-blue-50 focus:outline-none text-blue-600'
}
target={'_blank'}
>
<ExternalLink className="inline-block h-4 w-4" />
Visit
</a>
</div>
</li>
);
}
type VisibilityLabelProps = {
visibility: AllowedRoadmapVisibility;
sharedFriendIds?: string[];
};
const visibilityDetails: Record<
AllowedRoadmapVisibility,
{
icon: LucideIcon;
label: string;
}
> = {
public: {
icon: Globe,
label: 'Public',
},
me: {
icon: LockIcon,
label: 'Only me',
},
team: {
icon: Users,
label: 'Team Member(s)',
},
friends: {
icon: Users,
label: 'Friend(s)',
},
} as const;
function VisibilityBadge(props: VisibilityLabelProps) {
const { visibility, sharedFriendIds = [] } = props;
const { label, icon: Icon } = visibilityDetails[visibility];
return (
<span
className={`inline-flex items-center gap-1.5 whitespace-nowrap text-xs font-normal`}
>
<Icon className="inline-block h-3 w-3" />
<div className="flex items-center">
{visibility === 'friends' && sharedFriendIds?.length > 0 && (
<span className="mr-1">{sharedFriendIds.length}</span>
)}
{label}
</div>
</span>
);
}

View File

@@ -0,0 +1,113 @@
import { HelpCircle } from 'lucide-react';
import { cn } from '../../lib/classname';
import type { ResourceType } from '../../lib/resource-progress';
import { useState } from 'react';
import { useStore } from '@nanostores/react';
import { canManageCurrentRoadmap, currentRoadmap } from '../../stores/roadmap';
import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal';
type ResourceProgressStatsProps = {
resourceId: string;
resourceType: ResourceType;
isSecondaryBanner?: boolean;
};
export function ResourceProgressStats(props: ResourceProgressStatsProps) {
const { isSecondaryBanner = false } = props;
const [isSharing, setIsSharing] = useState(false);
const $canManageCurrentRoadmap = useStore(canManageCurrentRoadmap);
const $currentRoadmap = useStore(currentRoadmap);
return (
<>
{isSharing && $canManageCurrentRoadmap && $currentRoadmap && (
<ShareOptionsModal
isDiscoverable={$currentRoadmap.isDiscoverable}
description={$currentRoadmap?.description}
visibility={$currentRoadmap?.visibility}
teamId={$currentRoadmap?.teamId}
roadmapId={$currentRoadmap?._id!}
sharedFriendIds={$currentRoadmap?.sharedFriendIds || []}
sharedTeamMemberIds={$currentRoadmap?.sharedTeamMemberIds || []}
onClose={() => setIsSharing(false)}
onShareSettingsUpdate={(settings) => {
currentRoadmap.set({
...$currentRoadmap,
...settings,
});
}}
/>
)}
<div
data-progress-nums-container=""
className={cn(
'striped-loader relative z-50 hidden items-center justify-between bg-white px-2 py-1.5 sm:flex',
{
'rounded-bl-md rounded-br-md': isSecondaryBanner,
'rounded-md': !isSecondaryBanner,
}
)}
>
<p
className="flex text-sm opacity-0 transition-opacity duration-300"
data-progress-nums=""
>
<span className="mr-2.5 rounded-sm bg-yellow-200 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900">
<span data-progress-percentage="">0</span>% Done
</span>
<span className="itesm-center hidden md:flex">
<span>
<span data-progress-done="">0</span> completed
</span>
<span className="mx-1.5 text-gray-400">&middot;</span>
<span>
<span data-progress-learning="">0</span> in progress
</span>
<span className="mx-1.5 text-gray-400">&middot;</span>
<span>
<span data-progress-skipped="">0</span> skipped
</span>
<span className="mx-1.5 text-gray-400">&middot;</span>
<span>
<span data-progress-total="">0</span> Total
</span>
</span>
<span className="md:hidden">
<span data-progress-done="">0</span> of{' '}
<span data-progress-total="">0</span> Done
</span>
</p>
<div
className="flex items-center gap-3 opacity-0 transition-opacity duration-300"
data-progress-nums=""
>
<button
data-popup="progress-help"
className="flex items-center gap-1 text-sm font-medium text-gray-500 opacity-0 transition-opacity hover:text-black"
data-progress-nums=""
>
<HelpCircle className="h-3.5 w-3.5 stroke-[2.5px]" />
Track Progress
</button>
</div>
</div>
<div
data-progress-nums-container=""
className="striped-loader relative -mb-2 flex items-center justify-between rounded-md border bg-white px-2 py-1.5 text-sm text-gray-700 sm:hidden"
>
<span
data-progress-nums=""
className="text-gray-500 opacity-0 transition-opacity duration-300"
>
<span data-progress-done="">0</span> of{' '}
<span data-progress-total="">0</span> Done
</span>
</div>
</>
);
}

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