Compare commits

..

236 Commits

Author SHA1 Message Date
Arik Chakma
d3e1324b31 Add DevOps forkable 2023-10-30 09:14:42 +06: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
Kamran Ahmed
6bc7c2f48c Accept friend request to redirect to friends page 2023-08-15 00:18:49 +01:00
Kamran Ahmed
458396f782 Fix cookie not removed 2023-08-13 20:06:19 +01:00
Kamran Ahmed
bb7f1f4d67 Update frontend roadmap 2023-08-12 14:25:23 +01:00
Jesús
430350fe88 Fix typo in Method Overriding in Typescript (#4306) 2023-08-10 14:30:09 +01:00
Shawn Gestupa
c1d37dead3 update link of "Functional Components and Props" (#4317) 2023-08-10 14:29:38 +01:00
Abdul Wahab
eafd36f6aa Fixed: Function Expression Syntax (#4324)
The syntax for Function Expression was incorrect. It was an example of a function declared using Function Declaration.
2023-08-10 14:28:44 +01:00
Abdul Wahab
ea70632de1 Fix instanceOf mistake (#4322)
instanceof is a runtime check and interface and types don't exist during runtime.

Also TypeScript has a structural type system, which means that they are matched according to the structure of the object and types - not according to instances.

For example:

interface Person {
    name: string;
    age: number
}

const person = {
    name: "Ken",
    age: 25
}

if (person instanceof Person) // Error
2023-08-10 13:14:40 +01:00
roadmap bot
08e29c2c14 chore: add resource under kubernetes:kubernetes-introduction:key-concepts-terminologies 2023-08-09 00:25:05 +01:00
roadmap bot
00b27eabd6 chore: add resource under cyber-security:networking-knowledge:basics-of-nas-and-san 2023-08-09 00:24:07 +01:00
roadmap bot
667e7f4c7f chore: add resource under devops:serverless:cloudflare 2023-08-09 00:23:45 +01:00
stokey
19edadcc18 fix: broken link to CompTIA A+ 220-1101 - (#4309)
Added a link to channel with a query searching for CompTIA A+ 220-1101 in Professor Messer channel
2023-08-08 22:13:06 +01:00
Kamran Ahmed
c5cb2e1877 Fix broken build 2023-08-08 22:10:55 +01:00
Kamran Ahmed
3a09982ff6 Show received friend request count in sidebar 2023-08-08 22:00:04 +01:00
Kamran Ahmed
1d716a9438 Add confirmation on withdraw request 2023-08-08 21:30:33 +01:00
Kamran Ahmed
b69889cc29 Add friends listing 2023-08-08 21:04:44 +01:00
Kamran Ahmed
92295a7906 Friend progress tracking 2023-08-08 19:50:12 +01:00
Kamran Ahmed
2c1ab6b19d Accept, reject friends 2023-08-08 19:29:50 +01:00
Kamran Ahmed
fb3fe8be42 Friends listing page 2023-08-07 19:48:22 +01:00
Kamran Ahmed
c3b34cde3f Add rejected user status 2023-08-07 18:19:24 +01:00
Kamran Ahmed
a30cb170d6 Remove friend 2023-08-07 18:14:48 +01:00
Kamran Ahmed
0a5eeae68c Add friend page 2023-08-07 18:02:25 +01:00
Kamran Ahmed
9ed60d836a Add friends invite page 2023-08-04 18:23:43 +01:00
Arik Chakma
c720888f2b Add functionality to share progress (#4279)
* wip: user progress modal

* wip: modal loading state

* wip: share progress

* chore: best practices share

* chore: prettier

* fix: classname

* Progress button design

* Progress modal

* Update

* Update

* Progress modal refactoring

* Remove event binding for progress

* Update

* UI changes on progress

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2023-08-02 20:49:55 +01:00
Barış Tanrıverdi
2018b9bf38 Fix typos in history of JavaScript (#4240)
Minor text formatting changes and punctuation fixes were made for a consistency of the article structure and clear understanding.
2023-07-31 17:00:02 +01:00
yandif
1ca36e8bfa Correct 'CDNss' to 'CDNs' (#4259) 2023-07-31 16:35:57 +01:00
Maitrayee Khalasi
c0e2e541ca Update the link of youtube channel (#4261)
Updated the link of youtube channel in c++ roadmap, what is c++
2023-07-31 16:35:44 +01:00
Maitrayee Khalasi
04478272c2 Update the link of youtube channel (#4262)
Updated the link of youtube channel in c++ roadmap, bitwise operators
2023-07-31 16:35:19 +01:00
Bartłomiej Majowicz - Unlimitech
9c2e9c1be6 Fix spelling mistake (#4265)
* Fix misspell.

* Add missing link title.
2023-07-31 16:35:01 +01:00
Tomasz Mikulski
77310d24d8 Update 102-sharding-strategies.md (#4269)
Typo in dat(a)base word
2023-07-31 16:34:16 +01:00
Kamran Ahmed
6524da9a9a Add feedback link 2023-07-31 16:04:04 +01:00
778 changed files with 80046 additions and 16056 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

@@ -4,21 +4,23 @@ on:
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,5 +1,4 @@
// https://astro.build/config
import preact from '@astrojs/preact';
import sitemap from '@astrojs/sitemap';
import tailwind from '@astrojs/tailwind';
import compress from 'astro-compress';
@@ -8,6 +7,8 @@ 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>
);
}

11028
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"version": "0.0.1",
"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",
"astro-compress": "^1.1.47",
"jose": "^4.14.4",
"@astrojs/react": "^3.0.3",
"@astrojs/sitemap": "^3.0.2",
"@astrojs/tailwind": "^5.0.2",
"@fingerprintjs/fingerprintjs": "^4.1.0",
"@nanostores/react": "^0.7.1",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"astro": "^3.3.3",
"astro-compress": "^2.1.5",
"clsx": "^2.0.0",
"dracula-prism": "^2.1.13",
"jose": "^4.15.4",
"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.288.0",
"nanoid": "^5.0.2",
"nanostores": "^0.9.4",
"node-html-parser": "^6.1.10",
"npm-check-updates": "^16.14.6",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-confetti": "^6.1.0",
"react-dom": "^18.2.0",
"reactflow": "^11.9.4",
"rehype-external-links": "^3.0.0",
"roadmap-renderer": "^1.0.6",
"slugify": "^1.6.6",
"tailwindcss": "^3.3.2"
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3",
"zustand": "^4.4.4"
},
"devDependencies": {
"@playwright/test": "^1.35.1",
"@tailwindcss/typography": "^0.5.9",
"@types/js-cookie": "^3.0.3",
"@playwright/test": "^1.39.0",
"@tailwindcss/typography": "^0.5.10",
"@types/js-cookie": "^3.0.5",
"@types/prismjs": "^1.26.2",
"csv-parser": "^3.0.0",
"gh-pages": "^5.0.0",
"gh-pages": "^6.0.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": "^13.0.2",
"openai": "^4.13.0",
"prettier": "^3.0.3",
"prettier-plugin-astro": "^0.12.0",
"prettier-plugin-tailwindcss": "^0.5.6"
}
}

3587
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 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)
- [Software Design and Architecture Roadmap](https://roadmap.sh/software-design-architecture)
- [JavaScript Roadmap](https://roadmap.sh/javascript)
- [TypeScript Roadmap](https://roadmap.sh/typescript)
@@ -49,7 +52,6 @@ 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)
- [Java Roadmap](https://roadmap.sh/java)
- [Spring Boot Roadmap](https://roadmap.sh/spring-boot)
@@ -66,13 +68,18 @@ Here is the list of available roadmaps with more being actively worked upon.
- [Docker Roadmap](https://roadmap.sh/docker)
- [Prompt Engineering Roadmap](https://roadmap.sh/prompt-engineering)
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. Content should be in markdown. 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. Content should be in markdown. 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

@@ -1,6 +1,8 @@
---
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;
@@ -21,6 +23,27 @@ const sidebarLinks = [
classes: 'h-3 w-4',
},
},
{
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,
},
},
{
href: '/account/road-card',
title: 'Card',
@@ -74,7 +97,7 @@ const sidebarLinks = [
}`}
>
<AstroIcon icon={'users'} class={`h-4 w-4 mr-2`} />
Teams
Teams
</a>
</li>
{
@@ -89,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>
@@ -125,10 +154,16 @@ 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>
@@ -138,6 +173,10 @@ const sidebarLinks = [
<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 />
)}
</a>
</li>
);
@@ -149,7 +188,12 @@ const sidebarLinks = [
}
<!-- /End Desktop Sidebar -->
<div class:list={['grow px-0 py-0 md:py-10', { 'md:px-10': hasDesktopSidebar, 'md:px-5': !hasDesktopSidebar }]}>
<div
class:list={[
'grow px-0 py-0 md:py-10',
{ 'md:px-10': hasDesktopSidebar, 'md:px-5': !hasDesktopSidebar },
]}
>
<slot />
</div>
</div>

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

@@ -2,21 +2,21 @@ import RoadmapIcon from '../../icons/roadmap.svg';
export function EmptyActivity() {
return (
<div class="rounded-md">
<div class="flex flex-col items-center p-7 text-center">
<div className="rounded-md">
<div className="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"
src={RoadmapIcon.src}
className="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>
<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,5 +1,4 @@
import { useEffect, useState } from 'preact/hooks';
import { useEffect, useState } from 'react';
import GitHubIcon from '../../icons/github.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import Cookies from 'js-cookie';
@@ -57,11 +56,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;
})
@@ -90,10 +96,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 =
window.location.pathname === '/respond-invite'
? 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,14 +112,14 @@ 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}
src={icon.src}
alt="GitHub"
class={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>
Continue with GitHub
</button>

View File

@@ -1,4 +1,4 @@
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';
@@ -55,11 +55,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;
})
@@ -85,10 +92,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 =
window.location.pathname === '/respond-invite'
? 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,14 +113,14 @@ 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}
src={icon.src}
alt="Google"
class={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>
Continue with Google
</button>

View File

@@ -1,4 +1,4 @@
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';
@@ -55,11 +55,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;
})
@@ -85,10 +92,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 =
window.location.pathname === '/respond-invite'
? 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,14 +113,14 @@ 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}
src={icon.src}
alt="Google"
class={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>
Continue with LinkedIn
</button>

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 ErrorIcon from '../../icons/error.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
export function TriggerVerifyAccount() {
const [isLoading, setIsLoading] = useState(true);
@@ -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 = '/';
})
@@ -58,14 +58,14 @@ export function TriggerVerifyAccount() {
{isLoading && (
<img
alt={'Please wait.'}
src={SpinnerIcon}
class={'mx-auto h-16 w-16 animate-spin'}
src={SpinnerIcon.src}
className={'mx-auto h-16 w-16 animate-spin'}
/>
)}
{error && (
<img
alt={'Please wait.'}
src={ErrorIcon}
src={ErrorIcon.src}
className={'mx-auto h-16 w-16'}
/>
)}
@@ -74,7 +74,7 @@ export function TriggerVerifyAccount() {
</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,5 +1,5 @@
import { useEffect, useState } from 'react';
import VerifyLetterIcon from '../../icons/verify-letter.svg';
import { useEffect, useState } from 'preact/hooks';
import { httpPost } from '../../lib/http';
export function VerificationEmailMessage() {
@@ -39,13 +39,13 @@ export function VerificationEmailMessage() {
<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"
src={VerifyLetterIcon.src}
className="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">
<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 +53,7 @@ export function VerificationEmailMessage() {
soon!
</p>
<hr class="my-4" />
<hr className="my-4" />
{!isEmailResent && (
<>
@@ -72,12 +72,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,14 +36,16 @@ function handleGuest() {
'/account/notification',
'/account/update-password',
'/account/settings',
'/account/roadmaps',
'/account/road-card',
'/account/friends',
'/account',
'/team',
'/team/progress',
'/team/roadmaps',
'/team/new',
'/team/members',
'/team/settings'
'/team/settings',
];
showHideAuthElements('hide');
@@ -71,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;
}
}

368
src/components/Befriend.tsx Normal file
View File

@@ -0,0 +1,368 @@
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';
import { getUrlParams } from '../lib/browser';
import { CheckIcon } from './ReactIcons/CheckIcon';
import { DeleteUserIcon } from './ReactIcons/DeleteUserIcon';
import { useToast } from '../hooks/use-toast';
import { useAuth } from '../hooks/use-auth';
import { AddedUserIcon } from './ReactIcons/AddedUserIcon';
import { StopIcon } from './ReactIcons/StopIcon';
import { ErrorIcon } from './ReactIcons/ErrorIcon';
export type FriendshipStatus =
| 'none'
| 'sent'
| 'received'
| 'accepted'
| 'rejected'
| 'got_rejected';
type UserResponse = {
id: string;
links: Record<string, string>;
avatar: string;
name: string;
status: FriendshipStatus;
};
export function Befriend() {
const { u: inviteId } = getUrlParams();
const toast = useToast();
const currentUser = useAuth();
const [isConfirming, setIsConfirming] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
const [user, setUser] = useState<UserResponse>();
const isAuthenticated = isLoggedIn();
async function loadUser(userId: string) {
const { response, error } = await httpGet<UserResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-friend/${userId}`
);
if (error || !response) {
setError(error?.message || 'Something went wrong');
return;
}
if (response.status === 'accepted') {
window.location.href = '/account/friends?c=fa';
return;
}
setUser(response);
}
useEffect(() => {
if (inviteId) {
loadUser(inviteId).finally(() => {
pageProgressMessage.set('');
setIsLoading(false);
});
} else {
setIsLoading(false);
setError('Missing invite ID in URL');
pageProgressMessage.set('');
}
}, [inviteId]);
async function addFriend(userId: string, successMessage: string) {
pageProgressMessage.set('Please wait...');
setError('');
const { response, error } = await httpPost<UserResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-add-friend/${userId}`,
{}
);
if (error || !response) {
setError(error?.message || 'Something went wrong');
return;
}
if (response.status === 'accepted') {
window.location.href = '/account/friends?c=fa';
return;
}
setUser(response);
}
async function deleteFriend(userId: string, successMessage: string) {
pageProgressMessage.set('Please wait...');
setError('');
const { response, error } = await httpDelete<UserResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-delete-friend/${userId}`,
{}
);
if (error || !response) {
setError(error?.message || 'Something went wrong');
return;
}
setUser(response);
toast.success(successMessage);
}
if (isLoading) {
return null;
}
if (!user) {
return (
<div className="container text-center">
<ErrorIcon additionalClasses="mx-auto mb-4 mt-24 w-20 opacity-20" />
<h2 className={'mb-1 text-2xl font-bold'}>Error</h2>
<p className="mb-4 text-base leading-6 text-gray-600">
{error || 'There was a problem, please try again.'}
</p>
<div>
<a
href="/"
className="flex-grow cursor-pointer rounded-lg bg-gray-200 px-3 py-2 text-center"
>
Back to home
</a>
</div>
</div>
);
}
const userAvatar = user.avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${user.avatar}`
: '/images/default-avatar.png';
const isMe = currentUser?.id === user.id;
return (
<div className="container !max-w-[400px] text-center">
<img
alt={'join team'}
src={userAvatar}
className="mx-auto mb-4 mt-24 w-28 rounded-full"
/>
<h2 className={'mb-1 text-3xl font-bold'}>{user.name}</h2>
<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 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}
onClick={() => {
if (!isAuthenticated) {
return showLoginPopup();
}
addFriend(user.id, 'Friend request sent').finally(() => {
pageProgressMessage.set('');
});
}}
type="button"
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>
)}
{user.status === 'sent' && (
<>
<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>
{!isConfirming && (
<button
onClick={() => {
setIsConfirming(true);
}}
type="button"
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
</button>
)}
{isConfirming && (
<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"
onClick={() => {
deleteFriend(user.id, 'Friend request withdrawn').finally(
() => {
pageProgressMessage.set('');
}
);
}}
>
Yes
</button>{' '}
<button
onClick={() => {
setIsConfirming(false);
}}
className="ml-2 text-red-600 underline"
>
No
</button>
</span>
)}
</>
)}
{user.status === 'accepted' && (
<>
<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>
{!isConfirming && (
<button
onClick={() => {
setIsConfirming(true);
}}
type="button"
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
</button>
)}
{isConfirming && (
<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"
onClick={() => {
deleteFriend(user.id, 'Friend removed').finally(() => {
pageProgressMessage.set('');
});
}}
>
Yes
</button>{' '}
<button
onClick={() => {
setIsConfirming(false);
}}
className="ml-2 text-red-600 underline"
>
No
</button>
</span>
)}
</>
)}
{user.status === 'rejected' && (
<>
<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 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"
onClick={() => {
addFriend(user.id, 'Friend request accepted').finally(
() => {
pageProgressMessage.set('');
}
);
}}
>
Click here to Accept
</button>
</span>
</>
)}
{user.status === 'got_rejected' && (
<>
<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>
</>
)}
{user.status === 'received' && (
<>
<button
onClick={() => {
addFriend(user.id, 'Friend request accepted').finally(() => {
pageProgressMessage.set('');
});
}}
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
</button>
{!isConfirming && (
<button
onClick={() => {
setIsConfirming(true);
}}
type="button"
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
</button>
)}
{isConfirming && (
<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"
onClick={() => {
deleteFriend(user.id, 'Friend request rejected').finally(
() => {
pageProgressMessage.set('');
}
);
}}
>
Yes
</button>{' '}
<button
onClick={() => {
setIsConfirming(false);
}}
className="ml-2 text-red-600 underline"
>
No
</button>
</span>
)}
</>
)}
</div>
</div>
{error && (
<p className="mt-2 rounded-lg bg-red-100 p-2 text-red-700">{error}</p>
)}
</div>
);
}

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,7 +1,8 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { Fragment, useEffect, useRef, useState } from 'react';
import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click';
import BestPracticesIcon from '../../icons/best-practices.svg';
import ClipboardIcon from '../../icons/clipboard.svg';
import GuideIcon from '../../icons/guide.svg';
import HomeIcon from '../../icons/home.svg';
import RoadmapIcon from '../../icons/roadmap.svg';
@@ -22,13 +23,13 @@ export type PageType = {
};
const defaultPages: PageType[] = [
{ id: 'home', url: '/', title: 'Home', group: 'Pages', icon: HomeIcon },
{ id: 'home', url: '/', title: 'Home', group: 'Pages', icon: HomeIcon.src },
{
id: 'account',
url: '/account',
title: 'Account',
group: 'Pages',
icon: UserIcon,
icon: UserIcon.src,
isProtected: true,
},
{
@@ -36,7 +37,15 @@ const defaultPages: PageType[] = [
url: '/team',
title: 'Teams',
group: 'Pages',
icon: GroupIcon,
icon: GroupIcon.src,
isProtected: true,
},
{
id: 'friends',
url: '/account/friends',
title: 'Friends',
group: 'Pages',
icon: GroupIcon.src,
isProtected: true,
},
{
@@ -44,28 +53,43 @@ const defaultPages: PageType[] = [
url: '/roadmaps',
title: 'Roadmaps',
group: 'Pages',
icon: RoadmapIcon,
icon: RoadmapIcon.src,
},
{
id: 'account-roadmaps',
url: '/account/roadmaps',
title: 'Custom Roadmaps',
group: 'Pages',
icon: RoadmapIcon.src,
isProtected: true,
},
{
id: 'best-practices',
url: '/best-practices',
title: 'Best Practices',
group: 'Pages',
icon: BestPracticesIcon,
icon: BestPracticesIcon.src,
},
{
id: 'questions',
url: '/questions',
title: 'Questions',
group: 'Pages',
icon: ClipboardIcon.src,
},
{
id: 'guides',
url: '/guides',
title: 'Guides',
group: 'Pages',
icon: GuideIcon,
icon: GuideIcon.src,
},
{
id: 'videos',
url: '/videos',
title: 'Videos',
group: 'Pages',
icon: VideoIcon,
icon: VideoIcon.src,
},
];
@@ -158,12 +182,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);
@@ -191,39 +215,43 @@ 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>
<span className="mr-2 text-gray-400">{page.group}</span>
)}
{page.icon && (
<img alt={page.title} src={page.icon} class="mr-2 h-4 w-4" />
<img
alt={page.title}
src={page.icon}
className="mr-2 h-4 w-4"
/>
)}
{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

@@ -39,7 +39,7 @@ export function NotDropdown(props: NotDropdownProps) {
<img
alt={singularName}
src={ChevronDownIcon}
src={ChevronDownIcon.src}
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,4 +1,4 @@
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';
@@ -68,19 +68,19 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
);
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>
<img alt={'close'} src={CloseIcon.src} 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(
const isSelected = !!teamResourceConfig?.find(
(r) => r.resourceId === roadmap.id
);
return (
<SelectRoadmapModalItem
key={roadmap.id}
title={roadmap.title}
isSelected={isSelected}
onClick={() => {
@@ -131,6 +132,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
return (
<SelectRoadmapModalItem
key={roadmap.id}
title={roadmap.title}
isSelected={isSelected}
onClick={() => {

View File

@@ -2,21 +2,23 @@ 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';
export const validTeamTypes = [
{
value: 'company',
label: 'Company',
icon: BuildingIcon,
description: 'Track the skills and learning progress of the tech team at your company',
icon: BuildingIcon.src,
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',
icon: UsersIcon.src,
description:
'Invite your friends or course-mates and track your learning progress together',
},
] as const;
@@ -70,10 +72,11 @@ 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 pt-12 pb-10 ${
validTeamType.value == selectedTeamType
? 'border-gray-400 bg-gray-100'
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-50'
@@ -81,6 +84,7 @@ export function Step0(props: Step0Props) {
onClick={() => setSelectedTeamType(validTeamType.value)}
>
<img
key={validTeamType.value}
alt={validTeamType.label}
src={validTeamType.icon}
className={`mb-3 h-12 w-12 opacity-10 ${
@@ -90,7 +94,7 @@ export function Step0(props: Step0Props) {
<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 +104,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,127 @@
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();
}
}
export function CustomRoadmap() {
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()) 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 (
<>
<RoadmapHeader />
<FlowRoadmapRenderer roadmap={roadmap!} />
<TopicDetail canSubmitContribution={false} />
</>
);
}

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,158 @@
import { ReadonlyEditor } from '../../../editor/readonly-editor';
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
import {
renderResourceProgress,
updateResourceProgress,
type ResourceProgressType,
renderTopicProgress,
refreshProgressCounters,
} from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page';
import { useToast } from '../../hooks/use-toast';
import type { Node } from 'reactflow';
import { useCallback, type MouseEvent, useMemo, useState, useRef } from 'react';
import { EmptyRoadmap } from './EmptyRoadmap';
import { cn } from '../../lib/classname';
type FlowRoadmapRendererProps = {
roadmap: RoadmapDocument;
};
export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
const { roadmap } = 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,
) {
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(() => {
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,94 @@
import MoreIcon from '../../icons/more-vertical.svg';
import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { Lock, MoreVertical, Shapes, Trash2 } from 'lucide-react';
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"
>
<img alt="menu" src={MoreIcon.src} 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,251 @@
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 RoadmapIcon from '../../icons/roadmap.svg';
import { PersonalRoadmapActionDropdown } from './PersonalRoadmapActionDropdown';
import type { GetRoadmapListResponse } from './RoadmapListPage';
import { useState, type Dispatch, type SetStateAction } from 'react';
import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal';
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">
<img
alt="roadmap"
src={RoadmapIcon.src}
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>
</>
);
}

View File

@@ -0,0 +1,52 @@
import { ShieldBan } from 'lucide-react';
import type { FetchError } from '../../lib/http';
type RestrictedPageProps = {
error: FetchError;
};
export function RestrictedPage(props: RestrictedPageProps) {
const { error } = props;
if (error.status === 404) {
return (
<ErrorMessage
icon={<ShieldBan className="h-16 w-16" />}
title="Roadmap not found"
message="The roadmap you are looking for does not exist or has been deleted."
/>
);
}
return (
<ErrorMessage
icon={<ShieldBan className="h-16 w-16" />}
title="Restricted Access"
message={error?.message}
/>
);
}
type ErrorMessageProps = {
title: string;
message: string;
icon: React.ReactNode;
};
function ErrorMessage(props: ErrorMessageProps) {
const { title, message, icon } = props;
return (
<div className="flex grow flex-col items-center justify-center">
{icon}
<h2 className="mt-4 text-2xl font-semibold">{title}</h2>
<p>{message || 'This roadmap is not available for public access.'}</p>
<a
href="/"
className="mt-4 font-medium underline underline-offset-2 hover:no-underline"
>
&larr; Go back to home
</a>
</div>
);
}

View File

@@ -0,0 +1,85 @@
import { useRef, useState } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { Lock, MoreVertical, Shapes, Trash2 } from 'lucide-react';
type RoadmapActionButtonProps = {
onDelete?: () => void;
onCustomize?: () => void;
onUpdateSharing?: () => void;
};
export function RoadmapActionButton(props: RoadmapActionButtonProps) {
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="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:pl-1.5 sm:pr-3 sm:text-sm"
>
<MoreVertical className="mr-0 h-4 w-4 stroke-[2.5] sm:mr-1.5" />
<span className="hidden sm:inline">Actions</span>
</button>
{isOpen && (
<div
ref={menuRef}
className="align-right absolute right-0 top-full mt-1 w-[140px] rounded-md bg-slate-800 px-2 py-2 text-white shadow-md z-[9999]"
>
<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,222 @@
import { RoadmapHint } from './RoadmapHint';
import { useStore } from '@nanostores/react';
import { canManageCurrentRoadmap, currentRoadmap } from '../../stores/roadmap';
import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal';
import { useState } from 'react';
import { pageProgressMessage } from '../../stores/page';
import { httpDelete, httpPut } from '../../lib/http';
import { type TeamResourceConfig } from '../CreateTeam/RoadmapSelector';
import { useToast } from '../../hooks/use-toast';
import { RoadmapActionButton } from './RoadmapActionButton';
import { Lock, Shapes } from 'lucide-react';
import { Modal } from '../Modal';
import { ShareSuccess } from '../ShareOptions/ShareSuccess';
type RoadmapHeaderProps = {};
export function RoadmapHeader(props: RoadmapHeaderProps) {
const $canManageCurrentRoadmap = useStore(canManageCurrentRoadmap);
const $currentRoadmap = useStore(currentRoadmap);
const {
title,
description,
_id: roadmapId,
creator,
team,
visibility,
} = useStore(currentRoadmap) || {};
const [isSharing, setIsSharing] = useState(false);
const [isSharingWithOthers, setIsSharingWithOthers] = useState(false);
const toast = useToast();
async function deleteResource() {
pageProgressMessage.set('Deleting roadmap');
const teamId = $currentRoadmap?.teamId;
const baseApiUrl = import.meta.env.PUBLIC_API_URL;
let error, response;
if (teamId) {
({ error, response } = await httpPut<TeamResourceConfig>(
`${baseApiUrl}/v1-delete-team-resource-config/${teamId}`,
{
resourceId: roadmapId,
resourceType: 'roadmap',
}
));
} else {
({ error, response } = await httpDelete<TeamResourceConfig>(
`${baseApiUrl}/v1-delete-roadmap/${roadmapId}`
));
}
if (error || !response) {
toast.error(error?.message || 'Something went wrong');
return;
}
toast.success('Roadmap removed');
if (!teamId) {
window.location.href = '/account/roadmaps';
} else {
window.location.href = `/team/roadmaps?t=${teamId}`;
}
}
const avatarUrl = creator?.avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${creator?.avatar}`
: '/images/default-avatar.png';
const sharingWithOthersModal = isSharingWithOthers && (
<Modal
onClose={() => setIsSharingWithOthers(false)}
wrapperClassName="max-w-lg"
bodyClassName="p-4 flex flex-col"
>
<ShareSuccess
visibility="public"
roadmapId={roadmapId!}
description={description}
onClose={() => setIsSharingWithOthers(false)}
isSharingWithOthers={true}
/>
</Modal>
);
return (
<div className="border-b">
<div className="container relative py-5 sm:py-12">
{creator?.name && (
<div className="-mb-1 flex items-center gap-1.5 text-sm text-gray-500">
<img
alt={creator.name}
src={avatarUrl}
className="h-5 w-5 rounded-full"
/>
<span>
Created by&nbsp;
<span className="font-semibold text-gray-900">
{creator?.name}
</span>
{team && (
<>
&nbsp;from&nbsp;
<span className="font-semibold text-gray-900">
{team?.name}
</span>
</>
)}
</span>
</div>
)}
<div className="mb-3 mt-4 sm:mb-4">
<h1 className="text-2xl font-bold sm:mb-2 sm:text-4xl">{title}</h1>
<p className="mt-0.5 text-sm text-gray-500 sm:text-lg">
{description}
</p>
</div>
<div className="flex justify-between gap-2 sm:gap-0">
<div className="flex gap-1 sm:gap-2">
<a
href="/roadmaps"
className="rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm"
aria-label="Back to All Roadmaps"
>
&larr;<span className="hidden sm:inline">&nbsp;All Roadmaps</span>
</a>
<button
data-guest-required
data-popup="login-popup"
className="inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm"
aria-label="Subscribe for Updates"
>
<span className="ml-2">Subscribe</span>
</button>
</div>
<div className="flex items-center gap-2">
{$canManageCurrentRoadmap && (
<>
{isSharing && $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,
});
}}
/>
)}
<a
href={`${import.meta.env.PUBLIC_EDITOR_APP_URL}/${
$currentRoadmap?._id
}`}
target="_blank"
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
>
<Shapes className="mr-1.5 h-4 w-4 stroke-[2.5]" />
<span className="hidden sm:inline-block">Edit Roadmap</span>
<span className="sm:hidden">Edit</span>
</a>
<button
onClick={() => setIsSharing(true)}
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
>
<Lock className="mr-1.5 h-4 w-4 stroke-[2.5]" />
Sharing
</button>
<RoadmapActionButton
onDelete={() => {
const confirmation = window.confirm(
'Are you sure you want to delete this roadmap?'
);
if (!confirmation) {
return;
}
deleteResource().finally(() => null);
}}
/>
</>
)}
{!$canManageCurrentRoadmap && visibility === 'public' && (
<>
{sharingWithOthersModal}
<button
onClick={() => setIsSharingWithOthers(true)}
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
>
<Lock className="mr-1.5 h-4 w-4 stroke-[2.5]" />
Share with Others
</button>
</>
)}
</div>
</div>
<RoadmapHint
roadmapTitle={title!}
hasTNSBanner={false}
roadmapId={roadmapId!}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { cn } from '../../lib/classname';
import { ResourceProgressStats } from './ResourceProgressStats';
type RoadmapHintProps = {
roadmapId: string;
roadmapTitle: string;
hasTNSBanner?: boolean;
tnsBannerLink?: string;
};
export function RoadmapHint(props: RoadmapHintProps) {
const {
roadmapTitle,
roadmapId,
hasTNSBanner = false,
tnsBannerLink = '',
} = props;
return (
<div
className={cn('mb-0 mt-4 rounded-md border-0 sm:mt-7 sm:border', {
'sm:-mb-[82px]': hasTNSBanner,
'sm:-mb-[65px]': !hasTNSBanner,
})}
>
{hasTNSBanner && (
<div className="hidden border-b bg-gray-100 px-2 py-1.5 sm:block">
<p className="text-sm">
Get the latest {roadmapTitle} news from our sister site{' '}
<a
href={tnsBannerLink}
target="_blank"
className="font-semibold underline"
>
TheNewStack.io
</a>
</p>
</div>
)}
<ResourceProgressStats
isSecondaryBanner={hasTNSBanner}
resourceId={roadmapId}
resourceType="roadmap"
/>
</div>
);
}

View File

@@ -0,0 +1,134 @@
import { useEffect, useState } from 'react';
import { httpGet } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page';
import {
CreateRoadmapModal,
type RoadmapDocument,
} from './CreateRoadmap/CreateRoadmapModal';
import { PersonalRoadmapList } from './PersonalRoadmapList';
import { useToast } from '../../hooks/use-toast';
import { SharedRoadmapList } from './SharedRoadmapList';
import type { FriendshipStatus } from '../Befriend';
export type FriendUserType = {
id: string;
name: string;
avatar: string;
status: FriendshipStatus;
};
export type GetRoadmapListResponse = {
personalRoadmaps: (RoadmapDocument & {
topics: number;
})[];
sharedRoadmaps: (RoadmapDocument & {
topics: number;
creator: FriendUserType;
})[];
};
type TabType = {
label: string;
value: 'personal' | 'shared';
};
const tabTypes: TabType[] = [
{ label: 'Personal', value: 'personal' },
{ label: 'Shared by Friends', value: 'shared' },
];
export function RoadmapListPage() {
const toast = useToast();
const [isLoading, setIsLoading] = useState(true);
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
const [activeTab, setActiveTab] = useState<TabType['value']>('personal');
const [allRoadmaps, setAllRoadmaps] = useState<GetRoadmapListResponse>({
personalRoadmaps: [],
sharedRoadmaps: [],
});
async function loadRoadmapList() {
setIsLoading(true);
const { response, error } = await httpGet<GetRoadmapListResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-roadmap-list`
);
if (error || !response) {
console.error(error);
toast.error(error?.message || 'Something went wrong, please try again');
return;
}
setAllRoadmaps(
response! || {
personalRoadmaps: [],
sharedRoadmaps: [],
}
);
}
useEffect(() => {
loadRoadmapList().finally(() => {
setIsLoading(false);
pageProgressMessage.set('');
});
}, []);
if (isLoading) {
return null;
}
return (
<div>
{isCreatingRoadmap && (
<CreateRoadmapModal onClose={() => setIsCreatingRoadmap(false)} />
)}
<div className="mb-6 flex flex-col justify-between gap-2 sm:flex-row sm:items-center sm:gap-0">
<div className="flex grow items-center gap-2">
{tabTypes.map((tab) => {
return (
<button
key={tab.value}
className={`relative flex w-full items-center justify-center whitespace-nowrap rounded-md border p-1 px-3 text-sm sm:w-auto ${
activeTab === tab.value ? ' border-gray-400 bg-gray-200 ' : ''
} w-full sm:w-auto`}
onClick={() => setActiveTab(tab.value)}
>
{tab.label}
</button>
);
})}
</div>
<button
className={`relative flex w-full items-center justify-center rounded-md border p-1 px-3 text-sm sm:w-auto`}
onClick={() => setIsCreatingRoadmap(true)}
>
+ Create Roadmap
</button>
</div>
<div className="mt-4">
{activeTab === 'personal' && (
<PersonalRoadmapList
roadmaps={allRoadmaps?.personalRoadmaps}
setAllRoadmaps={setAllRoadmaps}
onDelete={(roadmapId) => {
setAllRoadmaps({
...allRoadmaps,
personalRoadmaps: allRoadmaps.personalRoadmaps.filter(
(r) => r._id !== roadmapId
),
});
}}
/>
)}
{activeTab === 'shared' && (
<SharedRoadmapList roadmaps={allRoadmaps?.sharedRoadmaps} />
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,162 @@
import { useState } from 'react';
import { useStore } from '@nanostores/react';
import { Check, Copy, Loader2 } from 'lucide-react';
import { Modal } from '../Modal';
import type { AllowedRoadmapVisibility } from './CreateRoadmap/CreateRoadmapModal';
import { cn } from '../../lib/classname';
import { httpPatch } from '../../lib/http';
import { useToast } from '../../hooks/use-toast';
import { useCopyText } from '../../hooks/use-copy-text';
import { currentRoadmap, isCurrentRoadmapPersonal } from '../../stores/roadmap';
type ShareRoadmapModalProps = {
onClose: () => void;
};
export const allowedVisibilityLabels: {
id: AllowedRoadmapVisibility;
label: string;
}[] = [
{
id: 'me',
label: 'Only visible to me',
},
{
id: 'public',
label: 'Anyone with the link',
},
{
id: 'team',
label: 'Visible to team members',
},
{
id: 'friends',
label: 'Only friends can view',
},
];
export function ShareRoadmapModal(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 [visibility, setVisibility] = useState($currentRoadmap?.visibility);
const [isLoading, setIsLoading] = useState(false);
async function updateVisibility(newVisibility: AllowedRoadmapVisibility) {
setIsLoading(true);
const { response, error } = await httpPatch(
`${import.meta.env.PUBLIC_API_URL}/v1-update-roadmap-visibility/${
$currentRoadmap?._id
}`,
{
visibility: newVisibility,
}
);
if (error) {
console.error(error);
toast.error(error?.message || 'Something went wrong, please try again');
setIsLoading(false);
return;
}
setIsLoading(false);
toast.success('Visibility updated');
setVisibility(newVisibility);
currentRoadmap.set({
...$currentRoadmap!,
visibility: newVisibility,
});
}
function handleCopy() {
const isDev = import.meta.env.DEV;
const url = new URL(
isDev ? 'http://localhost:3000/r' : 'https://roadmap.sh/r'
);
url.searchParams.set('id', roadmapId);
copyText(url.toString());
}
return (
<Modal onClose={onClose}>
<div className="p-4 pb-0">
<h1 className="text-lg font-medium leading-5 text-gray-900">
Updating {$currentRoadmap?.title}
</h1>
</div>
<ul className="mt-4 border-t">
{allowedVisibilityLabels.map((v) => {
if (v.id === 'team' && $isCurrentRoadmapPersonal) {
return null;
} else if (v.id === 'friends' && !$isCurrentRoadmapPersonal) {
return null;
}
return (
<li key={v.id}>
<button
disabled={v.id === visibility || isLoading}
key={v.id}
className={cn(
'relative flex w-full items-center border-b p-2.5 px-4 text-sm text-gray-700 hover:bg-gray-200 hover:text-gray-900 disabled:cursor-not-allowed',
v.id === visibility &&
'bg-gray-900 text-white hover:bg-gray-900 hover:text-white'
)}
onClick={() => updateVisibility(v.id)}
>
{v.label}
{v.id === visibility && (
<span className="absolute bottom-0 right-0 top-0 flex w-8 items-center justify-center">
<span className="h-2 w-2 rounded-full bg-green-500" />
</span>
)}
</button>
</li>
);
})}
</ul>
<div className="flex items-center justify-between p-4">
<button
disabled={isLoading}
className="flex h-9 items-center 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 disabled:cursor-not-allowed disabled:opacity-70"
onClick={onClose}
>
{isLoading ? (
<>
<Loader2 size={14} className="mr-2 animate-spin stroke-[2.5]" />
Saving
</>
) : (
'Cancel'
)}
</button>
<button
className="flex h-9 items-center justify-center rounded-md border border-transparent bg-gray-900 px-4 py-2 text-sm font-medium text-white outline-none hover:bg-gray-800 focus:bg-gray-800"
onClick={handleCopy}
>
{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,118 @@
import { ExternalLinkIcon, Map, Plus } from 'lucide-react';
import RoadmapIcon from '../../icons/roadmap.svg';
import type { GetRoadmapListResponse } from './RoadmapListPage';
type GroupByCreator = {
creator: GetRoadmapListResponse['sharedRoadmaps'][number]['creator'];
roadmaps: GetRoadmapListResponse['sharedRoadmaps'];
};
type SharedRoadmapListProps = {
roadmaps: GetRoadmapListResponse['sharedRoadmaps'];
};
export function SharedRoadmapList(props: SharedRoadmapListProps) {
const { roadmaps: sharedRoadmaps } = props;
const allUniqueCreatorIds = new Set(
sharedRoadmaps.map((roadmap) => roadmap.creator.id)
);
const groupByCreator: GroupByCreator[] = [];
for (const creatorId of allUniqueCreatorIds) {
const creator = sharedRoadmaps.find(
(roadmap) => roadmap.creator.id === creatorId
)?.creator;
if (!creator) {
continue;
}
groupByCreator.push({
creator,
roadmaps: sharedRoadmaps.filter(
(roadmap) => roadmap.creator.id === creatorId
),
});
}
if (sharedRoadmaps.length === 0) {
return (
<div className="flex flex-col items-center p-4 py-20">
<Map 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">
Roadmaps from your friends will appear here
</p>
</div>
);
}
return (
<div>
<div className="mb-3 flex items-center justify-between">
<span className={'text-sm text-gray-400'}>
{sharedRoadmaps.length} shared roadmap(s)
</span>
</div>
<div>
<ul className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{groupByCreator.map((group) => {
const creator = group.creator;
return (
<li
key={creator.id}
className="flex flex-col items-start overflow-hidden rounded-md border border-gray-300"
>
<div className="relative flex w-full items-center gap-3 p-3">
<img
src={
creator.avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${
creator.avatar
}`
: '/images/default-avatar.png'
}
alt={creator.name || ''}
className="h-8 w-8 rounded-full"
/>
<div>
<h3 className="truncate font-medium">{creator.name}</h3>
<p className="truncate text-sm text-gray-500">
{group?.roadmaps?.length || 0} shared roadmap(s)
</p>
</div>
</div>
<ul className="w-full">
{group?.roadmaps?.map((roadmap) => {
return (
<li
key={roadmap._id}
className="relative flex w-full border-t"
>
<a
href={`/r?id=${roadmap._id}`}
className="group inline-grid w-full grid-cols-[auto,16px] items-center justify-between gap-2 px-3 py-2 text-sm text-gray-600 transition-colors hover:bg-gray-100 hover:text-black"
target={'_blank'}
>
<span className="w-full truncate">
{roadmap.title}
</span>
<ExternalLinkIcon
size={16}
className="opacity-20 transition-opacity group-hover:opacity-100"
/>
</a>
</li>
);
})}
</ul>
</li>
);
})}
</ul>
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
export function SkeletonRoadmapHeader() {
return (
<div className="border-b">
<div className="container relative py-5 sm:py-12">
<div className="flex items-center gap-1.5">
<div className="h-4 w-4 animate-pulse rounded-full bg-gray-300" />
<div className="h-5 w-5/12 animate-pulse rounded-md bg-gray-200" />
</div>
<div className="mb-3 mt-4 sm:mb-4">
<div className="h-8 w-1/2 animate-pulse rounded-md bg-gray-300 sm:mb-2 sm:h-10" />
<div className="mt-0.5 h-5 w-1/3 animate-pulse rounded-md bg-gray-200 sm:h-7" />
</div>
<div className="flex justify-between gap-2 sm:gap-0">
<div className="h-7 w-[35.04px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-32" />
<div className="flex items-center gap-2">
<div className="h-7 w-[60.52px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[139.71px]" />
<div className="h-7 w-[71.48px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[100.34px]" />
<div className="h-7 w-[32px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[89.73px]" />
</div>
</div>
<div className="mb-0 mt-4 rounded-md border-0 sm:-mb-[65px] sm:mt-7 sm:border">
<div
data-progress-nums-container
className="striped-loader relative hidden h-8 items-center justify-between rounded-md bg-white sm:flex"
/>
<div
data-progress-nums-container
className="striped-loader relative -mb-2 flex h-[34px] items-center justify-between rounded-md border bg-white px-2 py-1.5 text-sm text-gray-700 sm:hidden"
/>
</div>
</div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import {useEffect, useState} from 'preact/hooks';
import { type FormEvent, useEffect, useState } from 'react';
import { httpDelete } from '../../lib/http';
import { logout } from '../Navigation/navigation';
@@ -10,9 +10,9 @@ export function DeleteAccountForm() {
useEffect(() => {
setError('');
setConfirmationText('');
}, [])
}, []);
const handleSubmit = async (e: Event) => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setError('');
@@ -53,7 +53,7 @@ export function DeleteAccountForm() {
type="text"
name="delete-account"
id="delete-account"
className="mt-2 block w-full rounded-md border border-gray-300 py-2 px-3 outline-none placeholder:text-gray-400 focus:border-gray-400"
className="mt-2 block w-full rounded-md border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:border-gray-400"
placeholder={'Type "delete" to confirm'}
required
autoFocus

View File

@@ -12,6 +12,6 @@ import { DeleteAccountForm } from './DeleteAccountForm';
<p class="text-black font-medium -mb-2 mt-3 text-base">Please type "delete" to confirm.</p>
<DeleteAccountForm client:only />
<DeleteAccountForm client:only="react" />
</div>
</Popup>

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { type FormEvent, useEffect, useRef, useState } from 'react';
import { httpDelete } from '../lib/http';
import type { TeamDocument } from './CreateTeam/CreateTeamForm';
import { useTeamId } from '../hooks/use-team-id';
@@ -34,7 +34,7 @@ export function DeleteTeamPopup(props: DeleteTeamPopupProps) {
inputEl.current?.focus();
}, []);
const handleSubmit = async (e: Event) => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setError('');
@@ -69,18 +69,18 @@ export function DeleteTeamPopup(props: DeleteTeamPopupProps) {
return (
<>
<div class="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="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"
>
<h2 class="text-2xl font-semibold text-black">Delete Team</h2>
<h2 className="text-2xl font-semibold text-black">Delete Team</h2>
<p className="text-gray-500">
This will permanently delete your team and all associated data.
</p>
<p class="-mb-2 mt-3 text-base font-medium text-black">
<p className="-mb-2 mt-3 text-base font-medium text-black">
Please type "delete" to confirm.
</p>
<form onSubmit={handleSubmit}>

View File

@@ -22,7 +22,7 @@ if (faqs.length === 0) {
<div class='border-t bg-gray-100 mt-8'>
<div class='container'>
<div class='flex justify-between relative -top-5'>
<h1 class='text-sm sm:text-base font-medium py-1 px-3 border bg-white rounded-md'>Frequently Asked Questions</h1>
<h2 class='text-sm sm:text-base font-medium py-1 px-3 border bg-white rounded-md'>Frequently Asked Questions</h2>
</div>
<div class='flex flex-col gap-1 pb-14'>

View File

@@ -16,8 +16,8 @@ export function FavoriteIcon(props: FavoriteIconProps) {
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M5.93682 0.5H2.06282C1.63546 0.500094 1.22423 0.663195 0.912987 0.956045C0.601741 1.2489 0.413919 1.64944 0.387822 2.076L0.00182198 8.461C-0.012178 8.6905 0.0548218 8.9185 0.191822 9.104L0.242322 9.1665C0.575322 9.5485 1.15132 9.6165 1.56582 9.31L3.99982 7.5115L6.43382 9.31C6.58413 9.42115 6.76305 9.48708 6.94954 9.50006C7.13603 9.51303 7.32235 9.4725 7.4866 9.38323C7.65085 9.29397 7.78621 9.15967 7.87677 8.99613C7.96733 8.83258 8.00932 8.64659 7.99782 8.46L7.61232 2.0765C7.58622 1.64981 7.39835 1.24914 7.08701 0.956192C6.77567 0.663248 6.36431 0.500094 5.93682 0.5ZM5.93682 1.25C6.42732 1.25 6.83382 1.632 6.86382 2.122L7.24932 8.506C7.25216 8.55018 7.24229 8.59425 7.22089 8.63301C7.19949 8.67176 7.16745 8.70359 7.12854 8.72472C7.08964 8.74585 7.0455 8.75542 7.00134 8.75228C6.95718 8.74914 6.91484 8.73343 6.87932 8.707L4.27582 6.783C4.19591 6.72397 4.09917 6.69211 3.99982 6.69211C3.90047 6.69211 3.80373 6.72397 3.72382 6.783L1.11982 8.707C1.0843 8.73343 1.04196 8.74914 0.9978 8.75228C0.953639 8.75542 0.909502 8.74585 0.8706 8.72472C0.831697 8.70359 0.799653 8.67176 0.778252 8.63301C0.756851 8.59425 0.746986 8.55018 0.749822 8.506L1.13632 2.122C1.16632 1.632 1.57232 1.25 2.06282 1.25H5.93682Z"
fill="currentColor"
/>
@@ -35,8 +35,8 @@ export function FavoriteIcon(props: FavoriteIconProps) {
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
fillRule="evenodd"
clipRule="evenodd"
d="M5.93682 0.5H2.06282C1.63546 0.500094 1.22423 0.663195 0.912987 0.956045C0.601741 1.2489 0.413919 1.64944 0.387822 2.076L0.00182198 8.461C-0.012178 8.6905 0.0548218 8.9185 0.191822 9.104L0.242322 9.1665C0.575322 9.5485 1.15132 9.6165 1.56582 9.31L3.99982 7.5115L6.43382 9.31C6.58413 9.42115 6.76305 9.48708 6.94954 9.50006C7.13603 9.51303 7.32235 9.4725 7.4866 9.38323C7.65085 9.29397 7.78621 9.15967 7.87677 8.99613C7.96733 8.83258 8.00932 8.64659 7.99782 8.46L7.61232 2.0765C7.58622 1.64981 7.39835 1.24914 7.08701 0.956192C6.77567 0.663248 6.36431 0.500094 5.93682 0.5Z"
fill="currentColor"
/>

View File

@@ -6,11 +6,18 @@ export interface FeaturedItemType {
isNew?: boolean;
url: string;
text: string;
allowBookmark?: boolean;
}
export interface Props extends FeaturedItemType {}
const { isUpcoming = false, isNew = false, text, url } = Astro.props;
const {
isUpcoming = false,
isNew = false,
text,
url,
allowBookmark = true,
} = Astro.props;
---
<a
@@ -26,11 +33,17 @@ const { isUpcoming = false, isNew = false, text, url } = Astro.props;
{text}
</span>
<MarkFavorite
resourceId={url.split('/').pop()!}
resourceType={url.includes('best-practices') ? 'best-practice' : 'roadmap'}
client:load
/>
{
allowBookmark && (
<MarkFavorite
resourceId={url.split('/').pop()!}
resourceType={
url.includes('best-practices') ? 'best-practice' : 'roadmap'
}
client:only='react'
/>
)
}
{
isNew && (

View File

@@ -1,18 +1,26 @@
---
import FeaturedItem, { FeaturedItemType } from './FeaturedItem.astro';
import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton';
import FeaturedItem, { type FeaturedItemType } from './FeaturedItem.astro';
export interface Props {
featuredItems: FeaturedItemType[];
heading: string;
showCreateRoadmap?: boolean;
allowBookmark?: boolean;
}
const { featuredItems, heading } = Astro.props;
const {
featuredItems,
heading,
showCreateRoadmap,
allowBookmark = true,
} = Astro.props;
---
<div class='relative border-b border-b-[#1e293c] py-10 sm:py-14'>
<div class='container'>
<h2
class='text-md font-regular absolute flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 -top-[17px] sm:left-1/2 sm:-translate-x-1/2'
class='text-md font-regular absolute -top-[17px] flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2'
>
{heading}
</h2>
@@ -22,6 +30,7 @@ const { featuredItems, heading } = Astro.props;
featuredItems.map((featuredItem) => (
<li>
<FeaturedItem
allowBookmark={allowBookmark}
text={featuredItem.text}
url={featuredItem.url}
isNew={featuredItem.isNew}
@@ -30,6 +39,13 @@ const { featuredItems, heading } = Astro.props;
</li>
))
}
{
showCreateRoadmap && (
<li>
<CreateRoadmapButton client:load className='min-h-[54px]' />
</li>
)
}
</ul>
</div>
</div>

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