Compare commits

...

189 Commits

Author SHA1 Message Date
Arik Chakma
fce5545b60 fix: add signup form 2024-02-14 15:52:15 +06:00
Arik Chakma
19f5885e62 refactor: remove dead code 2024-02-14 14:56:18 +06:00
Arik Chakma
5c771c806d fix: disable login buttons 2024-02-14 14:51:06 +06:00
Kamran Ahmed
81a14e90eb Add MLOps roadmap pdf 2024-02-13 19:33:44 +00:00
Kamran Ahmed
51dd58f7ed Fix broken URL for aws roadmap 2024-02-13 19:14:21 +00:00
Kamran Ahmed
28a0fca90d Add content for mlops 2024-02-13 18:55:27 +00:00
Kamran Ahmed
be8495a60a Add mlops roadmap 2024-02-13 18:55:27 +00:00
Abderrahmane BENAISSA
cf5ac18aa1 Fix: Argo CD documentation link (#5162) 2024-02-11 15:48:22 +00:00
dev-aly3n
0c7bc0e330 fix foundry in blockchain roadmap (#5164) 2024-02-11 15:47:43 +00:00
p134c0d3
94ba9e7451 Fix typo on getting started page (#5166)
* Getting Started - Frontend Type Fix

* Remove package-lock.json from .gitignore

* Update .gitignore

* Delete package-lock.json

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
2024-02-11 15:45:56 +00:00
Kamran Ahmed
0de6ed6028 Add videos and guides links to nav 2024-02-11 09:58:14 +00:00
Kamran Ahmed
ef0c9f3db2 Fix typo 2024-02-11 00:30:40 +00:00
Kamran Ahmed
468e92bac1 Update navigation UI 2024-02-10 22:53:03 +00:00
Kamran Ahmed
733e9cb5af Add get started page 2024-02-10 22:53:03 +00:00
Arik Chakma
714ca8c49f feat: add all page views (#5154) 2024-02-09 16:14:03 +00:00
Dharun
63c3850f0e fix: table alignment (#5151)
Small Alignment correction
2024-02-07 21:47:21 +06:00
dev-aly3n
e2f7abe69a fix all nodejs doc redirection from .dev to .org (#5134) 2024-02-04 22:09:29 +06:00
dev-aly3n
780402afd6 fix: broken link in python and also add another similar resource (#5045)
* fix: broken link in python and also add another similar resource

* add new resource to binary search trees in python

* Update src/data/roadmaps/python/content/101-data-structures-and-algorithms/103-binary-search-trees.md

---------

Co-authored-by: Arik Chakma <arikchangma@gmail.com>
2024-02-04 22:02:18 +06:00
Kamran Ahmed
2b74f70ef9 Update anchor text 2024-02-01 21:39:31 +05:00
Kamran Ahmed
1eab06f1f5 Link to languages article from backend roadmap 2024-01-28 00:14:34 +05:00
Arik Chakma
d2b7704370 feat: upgrade package to latest versions (#5077)
* feat: upgrade packages

* fix: duplicate hero roadmap keys
2024-01-26 21:01:11 +05:00
dev-aly3n
a5d5f63677 fix: remove the non-English youtube video from blockchain roadmap cryptography (#5088) 2024-01-25 22:52:18 +06:00
dev-aly3n
dfc7821a44 fix: replace the broken youtube link with the correct one in ux-design (#5014) 2024-01-25 21:44:51 +06:00
dev-aly3n
1cee0b36dc fix: broken link in frontend performance recommended guides (#5061) 2024-01-25 21:43:13 +06:00
Frédéric Gaudreau
0d1f916535 fix: dead esbuild youtube link (#5078)
fixing a dead youtube link caused by a change of channel name:
https://github.com/kamranahmedse/developer-roadmap/issues/5076
2024-01-24 22:02:00 +06:00
Kamran Ahmed
ce0f2a4ee4 Add beginner backend roadmap 2024-01-22 21:51:30 +05:00
Kamran Ahmed
bf89b013d1 Add spring tile 2024-01-22 16:01:55 +05:00
Kamran Ahmed
5bcb3e282d Update new tags on roadmaps 2024-01-22 15:59:06 +05:00
Arik Chakma
747652c0f3 feat: add Node.js questions (#5056)
* feat: add nodejs questions

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

Added a link to a tutorial for Gradle for complete beginners

* Update 104-using-gradle.md

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

* Update 104-using-gradle.md

---------

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

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

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

---------

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

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

* Update 102-javascript-versions.md

* fix: add link text

---------

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

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

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

---------

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

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

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

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

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

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

to:

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

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

Kimberly Brehm's Linear Algebra Course

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

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

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

---------

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

* reset the package-lock.json file

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

* Fix Best Practcies roadmaps articels

* Restore files

---------

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

* Team roadmap icon fix

* Personal roadmap list and empty friends

* Fix invite friend

* Fix user progress modal

* Friends and notification pages

* Friends and notification pages

* Update

* Fix progress modal

---------

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

View File

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

5
.gitignore vendored
View File

@@ -29,6 +29,5 @@ pnpm-debug.log*
tests-examples
*.csv
/renderer/*
!/renderer/index.tsx
!/renderer/renderer.ts
/editor/*
!/editor/readonly-editor.tsx

3
.npmrc
View File

@@ -1 +1,2 @@
auto-install-peers=true
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

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "roadmap.sh",
"type": "module",
"version": "0.0.1",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "astro dev --port 3000",
@@ -16,53 +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",
"generate-renderer": "sh scripts/generate-renderer.sh",
"test:e2e": "playwright test"
},
"dependencies": {
"@astrojs/react": "^3.0.0",
"@astrojs/sitemap": "^1.3.3",
"@astrojs/tailwind": "^5.0.0",
"@fingerprintjs/fingerprintjs": "^3.4.1",
"@astrojs/react": "^3.0.9",
"@astrojs/sitemap": "^3.0.5",
"@astrojs/tailwind": "^5.1.0",
"@fingerprintjs/fingerprintjs": "^4.2.1",
"@nanostores/react": "^0.7.1",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"astro": "^3.0.5",
"astro-compress": "^2.0.8",
"clsx": "^2.0.0",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"astro": "^4.2.1",
"astro-compress": "^2.2.8",
"clsx": "^2.1.0",
"dracula-prism": "^2.1.13",
"jose": "^4.14.4",
"jose": "^5.2.0",
"js-cookie": "^3.0.5",
"lucide-react": "^0.274.0",
"nanoid": "^4.0.2",
"nanostores": "^0.9.2",
"node-html-parser": "^6.1.5",
"npm-check-updates": "^16.10.12",
"lucide-react": "^0.300.0",
"nanoid": "^5.0.4",
"nanostores": "^0.9.5",
"node-html-parser": "^6.1.12",
"npm-check-updates": "^16.14.12",
"prismjs": "^1.29.0",
"react": "^18.0.0",
"react": "^18.2.0",
"react-confetti": "^6.1.0",
"react-dom": "^18.0.0",
"reactflow": "^11.8.3",
"rehype-external-links": "^2.1.0",
"react-dom": "^18.2.0",
"reactflow": "^11.10.2",
"rehype-external-links": "^3.0.0",
"roadmap-renderer": "^1.0.6",
"slugify": "^1.6.6",
"tailwind-merge": "^1.14.0",
"tailwindcss": "^3.3.3"
"tailwind-merge": "^2.2.1",
"tailwindcss": "^3.4.1",
"zustand": "^4.5.0"
},
"devDependencies": {
"@playwright/test": "^1.35.1",
"@tailwindcss/typography": "^0.5.9",
"@types/js-cookie": "^3.0.3",
"@types/prismjs": "^1.26.0",
"@playwright/test": "^1.41.1",
"@tailwindcss/typography": "^0.5.10",
"@types/js-cookie": "^3.0.6",
"@types/prismjs": "^1.26.3",
"csv-parser": "^3.0.0",
"gh-pages": "^5.0.0",
"gh-pages": "^6.1.1",
"js-yaml": "^4.1.0",
"markdown-it": "^13.0.1",
"openai": "^3.3.0",
"prettier": "^2.8.8",
"prettier-plugin-astro": "^0.10.0",
"prettier-plugin-tailwindcss": "^0.3.0"
"markdown-it": "^14.0.0",
"openai": "^4.25.0",
"prettier": "^3.2.4",
"prettier-plugin-astro": "^0.12.3",
"prettier-plugin-tailwindcss": "^0.5.11"
}
}

4273
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

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

After

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

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

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

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

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

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

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

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

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

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

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

Before

Width:  |  Height:  |  Size: 123 B

After

Width:  |  Height:  |  Size: 123 B

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

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

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

Before

Width:  |  Height:  |  Size: 267 B

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 561 KiB

BIN
public/roadmaps/aws.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

BIN
public/roadmaps/mlops.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
public/roadmaps/rust.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

View File

@@ -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)
@@ -39,6 +39,7 @@ Here is the list of available roadmaps with more being actively worked upon.
- [QA Roadmap](https://roadmap.sh/qa)
- [Python Roadmap](https://roadmap.sh/python)
- [Software Architect Roadmap](https://roadmap.sh/software-architect)
- [Game Developer Roadmap](https://roadmap.sh/game-developer) / [Server Side Game Developer](https://roadmap.sh/server-side-game-developer)
- [Software Design and Architecture Roadmap](https://roadmap.sh/software-design-architecture)
- [JavaScript Roadmap](https://roadmap.sh/javascript)
- [TypeScript Roadmap](https://roadmap.sh/typescript)
@@ -52,6 +53,7 @@ Here is the list of available roadmaps with more being actively worked upon.
- [Android Roadmap](https://roadmap.sh/android)
- [Flutter Roadmap](https://roadmap.sh/flutter)
- [Go Roadmap](https://roadmap.sh/golang)
- [Rust Roadmap](https://roadmap.sh/rust)
- [Java Roadmap](https://roadmap.sh/java)
- [Spring Boot Roadmap](https://roadmap.sh/spring-boot)
- [Design System Roadmap](https://roadmap.sh/design-system)
@@ -66,14 +68,20 @@ Here is the list of available roadmaps with more being actively worked upon.
- [UX Design Roadmap](https://roadmap.sh/ux-design)
- [Docker Roadmap](https://roadmap.sh/docker)
- [Prompt Engineering Roadmap](https://roadmap.sh/prompt-engineering)
- [Technical Writer Roadmap](https://roadmap.sh/technical-writer)
We have also added a new form of visual content covering best practices:
There are also interactive best practices:
- [Code Review Best Practices](https://roadmap.sh/best-practices/code-review)
- [Frontend Performance Best Practices](https://roadmap.sh/best-practices/frontend-performance)
- [API Security Best Practices](https://roadmap.sh/best-practices/api-security)
- [AWS Best Practices](https://roadmap.sh/best-practices/aws)
..and questions to help you test, rate and improve your knowledge
- [JavaScript Questions](https://roadmap.sh/questions/javascript)
- [React Questions](https://roadmap.sh/questions/react)
![](https://i.imgur.com/waxVImv.png)
## Share with the community

View File

@@ -8,17 +8,17 @@ if [ ! -d ".temp/web-draw" ]; then
git clone git@github.com:roadmapsh/web-draw.git .temp/web-draw
fi
rm -rf renderer
mkdir renderer
rm -rf editor
mkdir editor
# copy the files at /src/editor/renderer/* to /renderer
# copy the files at /src/editor/* to /editor
# while replacing any existing files
cp -rf .temp/web-draw/src/editor/renderer/* renderer
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 renderer -type f \( -name "*.ts" -o -name "*.tsx" \) -print0 | while IFS= read -r -d '' file; do
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
@@ -28,6 +28,5 @@ find renderer -type f \( -name "*.ts" -o -name "*.tsx" \) -print0 | while IFS= r
done
# ignore the worktree changes for the renderer directory
git update-index --skip-worktree renderer/*
# ignore the worktree changes for the editor directory
git update-index --assume-unchanged editor/readonly-editor.tsx

View File

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

View File

@@ -97,7 +97,7 @@ const sidebarLinks = [
}`}
>
<AstroIcon icon={'users'} class={`h-4 w-4 mr-2`} />
Teams
Teams
</a>
</li>
{
@@ -167,13 +167,12 @@ const sidebarLinks = [
{sidebarLink.title}
</span>
{sidebarLink.isNew &&
!isActive && (
<span class='relative mr-1 flex items-center'>
<span class='relative rounded-full bg-gray-200 p-1 text-xs' />
<span class='absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs' />
</span>
)}
{sidebarLink.isNew && !isActive && (
<span class='relative mr-1 flex items-center'>
<span class='relative rounded-full bg-gray-200 p-1 text-xs' />
<span class='absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs' />
</span>
)}
{sidebarLink.id === 'friends' && (
<SidebarFriendsCounter client:load />

View File

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

View File

@@ -0,0 +1,15 @@
import { PartyPopper } from 'lucide-react';
export function AppChecklist() {
return (
<div className="fixed bottom-6 right-3">
<a
href="/get-started"
className="flex items-center gap-2 rounded-full border border-slate-900 bg-white py-2 pl-3 pr-4 text-sm font-medium hover:bg-zinc-200"
>
<PartyPopper className="relative -top-[2px] h-[20px] w-[20px] text-purple-600" />
Welcome! Start here
</a>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import { useState } from 'react';
import { GitHubButton } from './GitHubButton';
import { GoogleButton } from './GoogleButton';
import { LinkedInButton } from './LinkedInButton';
import { EmailLoginForm } from './EmailLoginForm';
import { EmailSignupForm } from './EmailSignupForm';
type AuthenticationFormProps = {
type?: 'login' | 'signup';
};
export function AuthenticationForm(props: AuthenticationFormProps) {
const { type = 'login' } = props;
const [isDisabled, setIsDisabled] = useState(false);
return (
<>
<div className="flex w-full flex-col gap-2">
<GitHubButton isDisabled={isDisabled} setIsDisabled={setIsDisabled} />
<GoogleButton isDisabled={isDisabled} setIsDisabled={setIsDisabled} />
<LinkedInButton isDisabled={isDisabled} setIsDisabled={setIsDisabled} />
</div>
<div className="flex w-full items-center gap-2 py-6 text-sm text-slate-600">
<div className="h-px w-full bg-slate-200" />
OR
<div className="h-px w-full bg-slate-200" />
</div>
{type === 'login' ? (
<EmailLoginForm isDisabled={isDisabled} setIsDisabled={setIsDisabled} />
) : (
<EmailSignupForm
isDisabled={isDisabled}
setIsDisabled={setIsDisabled}
/>
)}
</>
);
}

View File

@@ -4,7 +4,14 @@ import { useState } from 'react';
import { httpPost } from '../../lib/http';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
export function EmailLoginForm() {
type EmailLoginFormProps = {
isDisabled?: boolean;
setIsDisabled?: (isDisabled: boolean) => void;
};
export function EmailLoginForm(props: EmailLoginFormProps) {
const { isDisabled, setIsDisabled } = props;
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [error, setError] = useState('');
@@ -14,6 +21,7 @@ export function EmailLoginForm() {
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setIsDisabled?.(true);
setError('');
const { response, error } = await httpPost<{ token: string }>(
@@ -21,7 +29,7 @@ export function EmailLoginForm() {
{
email,
password,
}
},
);
// Log the user in and reload the page
@@ -39,12 +47,13 @@ export function EmailLoginForm() {
// @todo use proper types
if ((error as any).type === 'user_not_verified') {
window.location.href = `/verification-pending?email=${encodeURIComponent(
email
email,
)}`;
return;
}
setIsLoading(false);
setIsDisabled?.(false);
setError(error?.message || 'Something went wrong. Please try again later.');
};
@@ -92,7 +101,7 @@ export function EmailLoginForm() {
<button
type="submit"
disabled={isLoading}
disabled={isLoading || isDisabled}
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
>
{isLoading ? 'Please wait...' : 'Continue'}

View File

@@ -1,7 +1,14 @@
import { type FormEvent, useState } from 'react';
import { httpPost } from '../../lib/http';
export function EmailSignupForm() {
type EmailSignupFormProps = {
isDisabled?: boolean;
setIsDisabled?: (isDisabled: boolean) => void;
};
export function EmailSignupForm(props: EmailSignupFormProps) {
const { isDisabled, setIsDisabled } = props;
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
@@ -13,6 +20,7 @@ export function EmailSignupForm() {
e.preventDefault();
setIsLoading(true);
setIsDisabled?.(true);
setError('');
const { response, error } = await httpPost<{ status: 'ok' }>(
@@ -21,20 +29,21 @@ export function EmailSignupForm() {
email,
password,
name,
}
},
);
if (error || response?.status !== 'ok') {
setIsLoading(false);
setIsDisabled?.(false);
setError(
error?.message || 'Something went wrong. Please try again later.'
error?.message || 'Something went wrong. Please try again later.',
);
return;
}
window.location.href = `/verification-pending?email=${encodeURIComponent(
email
email,
)}`;
};
@@ -90,7 +99,7 @@ export function EmailSignupForm() {
<button
type="submit"
disabled={isLoading}
disabled={isLoading || isDisabled}
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
>
{isLoading ? 'Please wait...' : 'Continue to Verify Email'}

View File

@@ -1,19 +1,23 @@
import { useEffect, useState } from 'react';
import GitHubIcon from '../../icons/github.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
import Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { httpGet } from '../../lib/http';
import { Spinner } from '../ReactIcons/Spinner.tsx';
type GitHubButtonProps = {};
type GitHubButtonProps = {
isDisabled?: boolean;
setIsDisabled?: (isDisabled: boolean) => void;
};
const GITHUB_REDIRECT_AT = 'githubRedirectAt';
const GITHUB_LAST_PAGE = 'githubLastPage';
export function GitHubButton(props: GitHubButtonProps) {
const { isDisabled, setIsDisabled } = props;
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const icon = isLoading ? SpinnerIcon : GitHubIcon;
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
@@ -26,16 +30,18 @@ export function GitHubButton(props: GitHubButtonProps) {
}
setIsLoading(true);
setIsDisabled?.(true);
httpGet<{ token: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-github-callback${
window.location.search
}`
}`,
)
.then(({ response, error }) => {
if (!response?.token) {
const errMessage = error?.message || 'Something went wrong.';
setError(errMessage);
setIsLoading(false);
setIsDisabled?.(false);
return;
}
@@ -74,22 +80,25 @@ export function GitHubButton(props: GitHubButtonProps) {
.catch((err) => {
setError('Something went wrong. Please try again later.');
setIsLoading(false);
setIsDisabled?.(false);
});
}, []);
const handleClick = async () => {
setIsLoading(true);
setIsDisabled?.(true);
const { response, error } = await httpGet<{ loginUrl: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-github-login`
`${import.meta.env.PUBLIC_API_URL}/v1-github-login`,
);
if (error || !response?.loginUrl) {
setError(
error?.message || 'Something went wrong. Please try again later.'
error?.message || 'Something went wrong. Please try again later.',
);
setIsLoading(false);
setIsDisabled?.(false);
return;
}
@@ -97,7 +106,7 @@ export function GitHubButton(props: GitHubButtonProps) {
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname
window.location.pathname,
)
? window.location.pathname + window.location.search
: window.location.pathname;
@@ -113,14 +122,14 @@ export function GitHubButton(props: GitHubButtonProps) {
<>
<button
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}
disabled={isLoading || isDisabled}
onClick={handleClick}
>
<img
src={icon.src}
alt="GitHub"
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>
{isLoading ? (
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
) : (
<GitHubIcon className={'h-[18px] w-[18px]'} />
)}
Continue with GitHub
</button>
{error && (

View File

@@ -1,19 +1,23 @@
import { useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import GoogleIcon from '../../icons/google.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { httpGet } from '../../lib/http';
import { Spinner } from '../ReactIcons/Spinner.tsx';
import { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx';
type GoogleButtonProps = {};
type GoogleButtonProps = {
isDisabled?: boolean;
setIsDisabled?: (isDisabled: boolean) => void;
};
const GOOGLE_REDIRECT_AT = 'googleRedirectAt';
const GOOGLE_LAST_PAGE = 'googleLastPage';
export function GoogleButton(props: GoogleButtonProps) {
const { isDisabled, setIsDisabled } = props;
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const icon = isLoading ? SpinnerIcon : GoogleIcon;
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
@@ -26,15 +30,17 @@ export function GoogleButton(props: GoogleButtonProps) {
}
setIsLoading(true);
setIsDisabled?.(true);
httpGet<{ token: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-google-callback${
window.location.search
}`
}`,
)
.then(({ response, error }) => {
if (!response?.token) {
setError(error?.message || 'Something went wrong.');
setIsLoading(false);
setIsDisabled?.(false);
return;
}
@@ -73,18 +79,21 @@ export function GoogleButton(props: GoogleButtonProps) {
.catch((err) => {
setError('Something went wrong. Please try again later.');
setIsLoading(false);
setIsDisabled?.(false);
});
}, []);
const handleClick = () => {
setIsLoading(true);
setIsDisabled?.(true);
httpGet<{ loginUrl: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-google-login`
`${import.meta.env.PUBLIC_API_URL}/v1-google-login`,
)
.then(({ response, error }) => {
if (!response?.loginUrl) {
setError(error?.message || 'Something went wrong.');
setIsLoading(false);
setIsDisabled?.(false);
return;
}
@@ -93,7 +102,7 @@ export function GoogleButton(props: GoogleButtonProps) {
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname
window.location.pathname,
)
? window.location.pathname + window.location.search
: window.location.pathname;
@@ -107,6 +116,7 @@ export function GoogleButton(props: GoogleButtonProps) {
.catch((err) => {
setError('Something went wrong. Please try again later.');
setIsLoading(false);
setIsDisabled?.(false);
});
};
@@ -114,14 +124,14 @@ export function GoogleButton(props: GoogleButtonProps) {
<>
<button
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}
disabled={isLoading || isDisabled}
onClick={handleClick}
>
<img
src={icon.src}
alt="Google"
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>
{isLoading ? (
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
) : (
<GoogleIcon className={'h-[18px] w-[18px]'} />
)}
Continue with Google
</button>
{error && (

View File

@@ -1,19 +1,23 @@
import { useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import LinkedIn from '../../icons/linkedin.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { httpGet } from '../../lib/http';
import { Spinner } from '../ReactIcons/Spinner.tsx';
import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx';
type LinkedInButtonProps = {};
type LinkedInButtonProps = {
isDisabled?: boolean;
setIsDisabled?: (isDisabled: boolean) => void;
};
const LINKEDIN_REDIRECT_AT = 'linkedInRedirectAt';
const LINKEDIN_LAST_PAGE = 'linkedInLastPage';
export function LinkedInButton(props: LinkedInButtonProps) {
const { isDisabled, setIsDisabled } = props;
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const icon = isLoading ? SpinnerIcon : LinkedIn;
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
@@ -26,15 +30,17 @@ export function LinkedInButton(props: LinkedInButtonProps) {
}
setIsLoading(true);
setIsDisabled?.(true);
httpGet<{ token: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-callback${
window.location.search
}`
}`,
)
.then(({ response, error }) => {
if (!response?.token) {
setError(error?.message || 'Something went wrong.');
setIsLoading(false);
setIsDisabled?.(false);
return;
}
@@ -73,18 +79,21 @@ export function LinkedInButton(props: LinkedInButtonProps) {
.catch((err) => {
setError('Something went wrong. Please try again later.');
setIsLoading(false);
setIsDisabled?.(false);
});
}, []);
const handleClick = () => {
setIsLoading(true);
setIsDisabled?.(true);
httpGet<{ loginUrl: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-login`
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-login`,
)
.then(({ response, error }) => {
if (!response?.loginUrl) {
setError(error?.message || 'Something went wrong.');
setIsLoading(false);
setIsDisabled?.(false);
return;
}
@@ -93,7 +102,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname
window.location.pathname,
)
? window.location.pathname + window.location.search
: window.location.pathname;
@@ -107,6 +116,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
.catch((err) => {
setError('Something went wrong. Please try again later.');
setIsLoading(false);
setIsDisabled?.(false);
});
};
@@ -114,14 +124,14 @@ export function LinkedInButton(props: LinkedInButtonProps) {
<>
<button
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}
disabled={isLoading || isDisabled}
onClick={handleClick}
>
<img
src={icon.src}
alt="Google"
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>
{isLoading ? (
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
) : (
<LinkedInIcon className={'h-[18px] w-[18px]'} />
)}
Continue with LinkedIn
</button>
{error && (

View File

@@ -1,14 +1,10 @@
---
import Popup from '../Popup/Popup.astro';
import { EmailLoginForm } from './EmailLoginForm';
import Divider from './Divider.astro';
import { GitHubButton } from './GitHubButton';
import { GoogleButton } from './GoogleButton';
import { LinkedInButton } from './LinkedInButton';
import { AuthenticationForm } from './AuthenticationForm';
---
<Popup id='login-popup' title='' subtitle=''>
<div class='text-center'>
<div class='mb-7 text-center'>
<p class='mb-3 text-2xl font-semibold leading-5 text-slate-900'>
Login to your account
</p>
@@ -16,19 +12,9 @@ import { LinkedInButton } from './LinkedInButton';
You must be logged in to perform this action.
</p>
</div>
<div class='mt-7 flex flex-col gap-2'>
<GitHubButton client:load />
<GoogleButton client:load />
<LinkedInButton client:load />
</div>
<Divider />
<EmailLoginForm client:load />
<AuthenticationForm client:load />
<div class='mt-6 text-center text-sm text-slate-600'>
Don't have an account?{' '}
<a href='/signup' class='font-medium text-[#4285f4]'>Sign up</a>
<a href='/signup' class='font-medium text-[#4285f4]'> Sign up</a>
</div>
</Popup>

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ import { useToast } from '../../hooks/use-toast';
export type TeamResourceConfig = {
isCustomResource: boolean;
title: string;
description?: string;
visibility?: AllowedRoadmapVisibility;
resourceId: string;
resourceType: string;

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ 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 {replaceChildren} from "../../lib/dom.ts";
export type ProgressMapProps = {
teamId: string;
@@ -81,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) => {

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

@@ -38,7 +38,7 @@ export function CreateRoadmapButton(props: CreateRoadmapButtonProps) {
<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
className,
)}
onClick={toggleCreateRoadmapHandler}
>

View File

@@ -29,6 +29,7 @@ export interface RoadmapDocument {
description?: string;
creatorId: string;
teamId?: string;
isDiscoverable: boolean;
type: AllowedCustomRoadmapType;
visibility: AllowedRoadmapVisibility;
sharedFriendIds?: string[];
@@ -61,7 +62,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
async function handleSubmit(
e: FormEvent<HTMLFormElement> | MouseEvent<HTMLButtonElement>,
redirect: boolean = true
redirect: boolean = true,
) {
e.preventDefault();
if (isLoading) {
@@ -84,7 +85,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
}),
nodes: [],
edges: [],
}
},
);
if (error) {
@@ -95,9 +96,9 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
toast.success('Roadmap created successfully');
if (redirect) {
window.location.href = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${
response?._id
}`;
window.location.href = `${
import.meta.env.PUBLIC_EDITOR_APP_URL
}/${response?._id}`;
return;
}
@@ -144,7 +145,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
name="title"
id="title"
required
className="block w-full rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm"
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)}
@@ -164,7 +165,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
name="description"
required
className={cn(
'block 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',
'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"
@@ -185,7 +186,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
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'
!teamId && 'w-full',
)}
>
Cancel
@@ -212,7 +213,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
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'
teamId ? 'hidden sm:flex' : 'w-full',
)}
>
{isLoading ? (

View File

@@ -7,13 +7,12 @@ import {
httpPost,
} from '../../lib/http';
import { RoadmapHeader } from './RoadmapHeader';
import { RoadmapRenderer } from './RoadmapRenderer';
import { TopicDetail } from '../TopicDetail/TopicDetail';
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
import { currentRoadmap } from '../../stores/roadmap';
import { UserProgressModal } from '../UserProgress/UserProgressModal';
import { RestrictedPage } from './RestrictedPage';
import { isLoggedIn } from '../../lib/jwt';
import { FlowRoadmapRenderer } from './FlowRoadmapRenderer';
export const allowedLinkTypes = [
'video',
@@ -54,14 +53,20 @@ export type GetRoadmapResponse = RoadmapDocument & {
export function hideRoadmapLoader() {
const loaderEl = document.querySelector(
'[data-roadmap-loader]'
'[data-roadmap-loader]',
) as HTMLElement;
if (loaderEl) {
loaderEl.remove();
}
}
export function CustomRoadmap() {
type CustomRoadmapProps = {
isEmbed?: boolean;
};
export function CustomRoadmap(props: CustomRoadmapProps) {
const { isEmbed = false } = props;
const { id, secret } = getUrlParams() as { id: string; secret: string };
const [isLoading, setIsLoading] = useState(true);
@@ -72,14 +77,15 @@ export function CustomRoadmap() {
setIsLoading(true);
const roadmapUrl = new URL(
`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${id}`
`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${id}`,
);
if (secret) {
roadmapUrl.searchParams.set('secret', secret);
}
const { response, error } = await httpGet<GetRoadmapResponse>(
roadmapUrl.toString()
roadmapUrl.toString(),
);
if (error || !response) {
@@ -95,19 +101,10 @@ export function CustomRoadmap() {
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) {
@@ -120,14 +117,9 @@ export function CustomRoadmap() {
return (
<>
<RoadmapHeader />
<RoadmapRenderer roadmap={roadmap!} />
<TopicDetail canSubmitContribution={false} />
<UserProgressModal
resourceId={roadmap?._id!}
resourceType="roadmap"
isCustomResource={true}
/>
{!isEmbed && <RoadmapHeader />}
<FlowRoadmapRenderer isEmbed={isEmbed} roadmap={roadmap!} />
<TopicDetail isEmbed={isEmbed} canSubmitContribution={false} />
</>
);
}

View File

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

View File

@@ -1,16 +1,18 @@
import {CircleSlash, PenSquare, Shapes} from 'lucide-react';
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 } = props;
const { roadmapId, canManage, className } = props;
const editUrl = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${roadmapId}`;
return (
<div className="flex h-full items-center justify-center">
<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>
@@ -18,9 +20,9 @@ export function EmptyRoadmap(props: EmptyRoadmapProps) {
{canManage && (
<a
href={editUrl}
className="mt-4 rounded-md bg-gray-500 px-4 py-2 font-medium text-white hover:bg-gray-600 flex items-center"
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="inline-block mr-2 h-4 w-4" />
<Shapes className="mr-2 inline-block h-4 w-4" />
Edit Roadmap
</a>
)}

View File

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

View File

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

View File

@@ -7,17 +7,18 @@ import {
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';
import {RoadmapIcon} from "../ReactIcons/RoadmapIcon.tsx";
type PersonalRoadmapListType = {
roadmaps: GetRoadmapListResponse['personalRoadmaps'];
@@ -60,6 +61,8 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) {
const shareSettingsModal = selectedRoadmap && (
<ShareOptionsModal
isDiscoverable={selectedRoadmap.isDiscoverable}
description={selectedRoadmap.description}
visibility={selectedRoadmap.visibility}
sharedFriendIds={selectedRoadmap.sharedFriendIds}
sharedTeamMemberIds={selectedRoadmap.sharedTeamMemberIds}
@@ -88,11 +91,8 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) {
if (roadmapList.length === 0) {
return (
<div className="flex flex-col items-center p-4 py-20">
<img
alt="roadmap"
src={RoadmapIcon.src}
className="mb-4 h-24 w-24 opacity-10"
/>
<RoadmapIcon className="mb-4 h-24 w-24 opacity-10" />
<h3 className="mb-1 text-2xl font-bold text-gray-900">No roadmaps</h3>
<p className="text-base text-gray-500">
Create a roadmap to get started
@@ -140,7 +140,7 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
return (
<li
className="grid grid-cols-1 p-2.5 sm:grid-cols-[auto_110px]"
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">
@@ -172,10 +172,20 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
}}
/>
<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-gray-300 bg-white px-2 py-1.5 text-xs hover:bg-gray-50 focus:outline-none'
'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'}
>

View File

@@ -24,6 +24,8 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) {
<>
{isSharing && $canManageCurrentRoadmap && $currentRoadmap && (
<ShareOptionsModal
isDiscoverable={$currentRoadmap.isDiscoverable}
description={$currentRoadmap?.description}
visibility={$currentRoadmap?.visibility}
teamId={$currentRoadmap?.teamId}
roadmapId={$currentRoadmap?._id!}
@@ -41,7 +43,7 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) {
<div
data-progress-nums-container=""
className={cn(
'striped-loader relative hidden items-center justify-between bg-white px-2 py-1.5 sm:flex',
'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,

View File

@@ -23,7 +23,7 @@ export function RoadmapActionButton(props: RoadmapActionButtonProps) {
<button
disabled={false}
onClick={() => setIsOpen(!isOpen)}
className="inline-flex items-center justify-center rounded-md bg-gray-500 py-1.5 pl-2 pr-2 text-xs font-medium text-white hover:bg-gray-600 sm:pl-1.5 sm:pr-3 sm:text-sm"
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>
@@ -32,7 +32,7 @@ export function RoadmapActionButton(props: RoadmapActionButtonProps) {
{isOpen && (
<div
ref={menuRef}
className="align-right absolute right-0 top-full z-50 mt-1 w-[140px] rounded-md bg-slate-800 px-2 py-2 text-white shadow-md"
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 && (

View File

@@ -9,6 +9,9 @@ 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';
import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx';
type RoadmapHeaderProps = {};
@@ -22,9 +25,11 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
_id: roadmapId,
creator,
team,
visibility,
} = useStore(currentRoadmap) || {};
const [isSharing, setIsSharing] = useState(false);
const [isSharingWithOthers, setIsSharingWithOthers] = useState(false);
const toast = useToast();
async function deleteResource() {
@@ -40,11 +45,11 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
{
resourceId: roadmapId,
resourceType: 'roadmap',
}
},
));
} else {
({ error, response } = await httpDelete<TeamResourceConfig>(
`${baseApiUrl}/v1-delete-roadmap/${roadmapId}`
`${baseApiUrl}/v1-delete-roadmap/${roadmapId}`,
));
}
@@ -65,6 +70,22 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
? `${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">
@@ -82,7 +103,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
</span>
{team && (
<>
&nbsp;in&nbsp;
&nbsp;from&nbsp;
<span className="font-semibold text-gray-900">
{team?.name}
</span>
@@ -99,7 +120,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
</div>
<div className="flex justify-between gap-2 sm:gap-0">
<div className="flex gap-1 sm:gap-2">
<div className="flex justify-stretch gap-1 sm:gap-2">
<a
href="/roadmaps"
className="rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm"
@@ -108,70 +129,85 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
&larr;<span className="hidden sm:inline">&nbsp;All Roadmaps</span>
</a>
<button
data-guest-required
data-popup="login-popup"
className="inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm"
aria-label="Subscribe for Updates"
>
<span className="ml-2">Subscribe</span>
</button>
<ShareRoadmapButton
roadmapId={roadmapId!}
description={description!}
pageUrl={`https://roadmap.sh/r?id=${roadmapId}`}
allowEmbed={true}
/>
</div>
{$canManageCurrentRoadmap && (
<div className="flex items-center gap-2">
{isSharing && $currentRoadmap && (
<ShareOptionsModal
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 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);
}}
/>
)}
</>
)}
<a
href={`${import.meta.env.PUBLIC_EDITOR_APP_URL}/${
$currentRoadmap?._id
}`}
target="_blank"
className="inline-flex items-center justify-center rounded-md bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:bg-gray-300 hover:border-gray-300 sm:px-3 sm:text-sm border border-gray-300"
>
<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 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:bg-gray-300 hover:border-gray-300 sm:px-3 sm:text-sm border border-gray-300"
>
<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);
}}
/>
</div>
)}
{!$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

View File

@@ -1,53 +0,0 @@
svg text tspan {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeSpeed;
}
svg > g[data-type='topic'],
svg > g[data-type='subtopic'],
svg > g > g[data-type='link-item'],
svg > g[data-type='button'] {
cursor: pointer;
}
svg > g[data-type='topic']:hover > rect {
fill: #d6d700;
}
svg > g[data-type='subtopic']:hover > rect {
fill: #f3c950;
}
svg > g[data-type='button']:hover {
opacity: 0.8;
}
svg .done rect {
fill: #cbcbcb !important;
}
svg .done text,
svg .skipped text {
text-decoration: line-through;
}
svg > g[data-type='topic'].learning > rect + text,
svg > g[data-type='topic'].done > rect + text {
fill: black;
}
svg > g[data-type='subtipic'].done > rect + text,
svg > g[data-type='subtipic'].learning > rect + text {
fill: #cbcbcb;
}
svg .learning rect {
fill: #dad1fd !important;
}
svg .learning text {
text-decoration: underline;
}
svg .skipped rect {
fill: #496b69 !important;
}

View File

@@ -1,177 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Renderer } from '../../../renderer';
import './RoadmapRenderer.css';
import {
renderResourceProgress,
updateResourceProgress,
type ResourceProgressType,
renderTopicProgress,
refreshProgressCounters,
} from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page';
import { useToast } from '../../hooks/use-toast';
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
import { EmptyRoadmap } from './EmptyRoadmap';
type RoadmapRendererProps = {
roadmap: RoadmapDocument;
};
type RoadmapNodeDetails = {
nodeId: string;
nodeType: string;
targetGroup: SVGElement;
};
export function getNodeDetails(
svgElement: SVGElement
): RoadmapNodeDetails | null {
const targetGroup = (svgElement?.closest('g') as SVGElement) || {};
const nodeId = targetGroup?.dataset?.nodeId;
const nodeType = targetGroup?.dataset?.type;
if (!nodeId || !nodeType) return null;
return { nodeId, nodeType, targetGroup };
}
export const allowedClickableNodeTypes = [
'topic',
'subtopic',
'button',
'link-item',
];
export function RoadmapRenderer(props: RoadmapRendererProps) {
const { roadmap } = props;
const roadmapRef = useRef<HTMLDivElement>(null);
const roadmapId = roadmap._id!;
const toast = useToast();
const [hideRenderer, setHideRenderer] = useState(false);
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 handleSvgClick = useCallback((e: MouseEvent) => {
const target = e.target as SVGElement;
const { nodeId, nodeType, targetGroup } = getNodeDetails(target) || {};
if (!nodeId || !nodeType || !allowedClickableNodeTypes.includes(nodeType))
return;
if (nodeType === 'button' || nodeType === 'link-item') {
const link = targetGroup?.dataset?.link || '';
const isExternalLink = link.startsWith('http');
if (isExternalLink) {
window.open(link, '_blank');
} else {
window.location.href = link;
}
return;
}
const isCurrentStatusLearning = targetGroup?.classList.contains('learning');
const isCurrentStatusSkipped = targetGroup?.classList.contains('skipped');
if (e.shiftKey) {
e.preventDefault();
updateTopicStatus(
nodeId,
isCurrentStatusLearning ? 'pending' : 'learning'
);
return;
} else if (e.altKey) {
e.preventDefault();
updateTopicStatus(nodeId, isCurrentStatusSkipped ? 'pending' : 'skipped');
return;
}
window.dispatchEvent(
new CustomEvent('roadmap.node.click', {
detail: {
topicId: nodeId,
resourceId: roadmap?._id,
resourceType: 'roadmap',
isCustomResource: true,
},
})
);
}, []);
const handleSvgRightClick = useCallback((e: MouseEvent) => {
e.preventDefault();
const target = e.target as SVGElement;
const { nodeId, nodeType, targetGroup } = getNodeDetails(target) || {};
if (!nodeId || !nodeType || !allowedClickableNodeTypes.includes(nodeType))
return;
if (nodeType === 'button' || nodeType === 'link-item') {
return;
}
const isCurrentStatusDone = targetGroup?.classList.contains('done');
updateTopicStatus(nodeId, isCurrentStatusDone ? 'pending' : 'done');
}, []);
useEffect(() => {
if (!roadmapRef?.current) return;
roadmapRef?.current?.addEventListener('click', handleSvgClick);
roadmapRef?.current?.addEventListener('contextmenu', handleSvgRightClick);
return () => {
roadmapRef?.current?.removeEventListener('click', handleSvgClick);
roadmapRef?.current?.removeEventListener(
'contextmenu',
handleSvgRightClick
);
};
}, []);
return (
<div className="flex grow bg-gray-50 pb-8 pt-4 sm:pt-12">
<div className="container !max-w-[1000px]">
<Renderer
ref={roadmapRef}
roadmap={{ nodes: roadmap?.nodes!, edges: roadmap?.edges! }}
onRendered={() => {
renderResourceProgress('roadmap', roadmapId).then(() => {
if (roadmap?.nodes?.length === 0) {
setHideRenderer(true);
roadmapRef?.current?.classList.add('hidden');
}
});
}}
/>
{hideRenderer && (
<EmptyRoadmap roadmapId={roadmapId} canManage={roadmap.canManage} />
)}
</div>
</div>
);
}

View File

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

View File

@@ -48,7 +48,7 @@ import Icon from './AstroIcon.astro';
<span class='mx-2 text-gray-400'>by</span>
<a
class='font-regular rounded-md bg-blue-600 px-1.5 py-1 text-sm hover:bg-blue-700'
href='https://twitter.com/intent/user?screen_name=kamrify'
href='https://twitter.com/kamrify'
target='_blank'
>
<span class='hidden sm:inline'>@kamrify</span>

View File

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

View File

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

View File

@@ -7,9 +7,13 @@ import {
renderTopicProgress,
updateResourceProgress,
} from '../../lib/resource-progress';
import type { ResourceProgressType, ResourceType } from '../../lib/resource-progress';
import type {
ResourceProgressType,
ResourceType,
} from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page';
import { showLoginPopup } from '../../lib/popup';
import { replaceChildren } from '../../lib/dom.ts';
export class Renderer {
resourceId: string;
@@ -88,12 +92,13 @@ export class Renderer {
});
})
.then((svg) => {
this.containerEl?.replaceChildren(svg);
replaceChildren(this.containerEl!, svg);
// this.containerEl?.replaceChildren(svg);
})
.then(() => {
return renderResourceProgress(
this.resourceType as ResourceType,
this.resourceId
this.resourceId,
);
})
.catch((error) => {
@@ -112,19 +117,6 @@ export class Renderer {
});
}
trackVisit() {
if (!isLoggedIn()) {
return;
}
window.setTimeout(() => {
httpPost(`${import.meta.env.PUBLIC_API_URL}/v1-visit`, {
resourceId: this.resourceId,
resourceType: this.resourceType,
}).then(() => null);
}, 0);
}
onDOMLoaded() {
if (!this.prepareConfig()) {
return;
@@ -133,15 +125,13 @@ export class Renderer {
const urlParams = new URLSearchParams(window.location.search);
const roadmapType = urlParams.get('r');
this.trackVisit();
if (roadmapType) {
this.switchRoadmap(`/${roadmapType}.json`);
} else {
this.jsonToSvg(
this.resourceType === 'roadmap'
? `/${this.resourceId}.json`
: `/best-practices/${this.resourceId}.json`
: `/best-practices/${this.resourceId}.json`,
);
}
}
@@ -181,7 +171,7 @@ export class Renderer {
resourceType: this.resourceType as ResourceType,
topicId,
},
newStatus
newStatus,
)
.then(() => {
renderTopicProgress(topicId, newStatus);
@@ -213,9 +203,14 @@ export class Renderer {
const isCurrentStatusDone = targetGroup.classList.contains('done');
const normalizedGroupId = groupId.replace(/^\d+-/, '');
if (normalizedGroupId.startsWith('ext_link:')) {
return;
}
this.updateTopicStatus(
normalizedGroupId,
!isCurrentStatusDone ? 'done' : 'pending'
!isCurrentStatusDone ? 'done' : 'pending',
);
}
@@ -241,9 +236,12 @@ export class Renderer {
action: `${this.resourceType} / ${this.resourceId}`,
label: externalLink,
});
window.open(`https://${externalLink}`);
} else {
window.location.href = `https://${externalLink}`;
}
window.open(`https://${externalLink}`);
return;
}
@@ -263,7 +261,7 @@ export class Renderer {
resourceType: this.resourceType,
resourceId: this.resourceId,
},
})
}),
);
return;
}
@@ -278,7 +276,7 @@ export class Renderer {
e.preventDefault();
this.updateTopicStatus(
normalizedGroupId,
!isCurrentStatusLearning ? 'learning' : 'pending'
!isCurrentStatusLearning ? 'learning' : 'pending',
);
return;
}
@@ -287,7 +285,7 @@ export class Renderer {
e.preventDefault();
this.updateTopicStatus(
normalizedGroupId,
!isCurrentStatusSkipped ? 'skipped' : 'pending'
!isCurrentStatusSkipped ? 'skipped' : 'pending',
);
return;
@@ -300,7 +298,7 @@ export class Renderer {
resourceId: this.resourceId,
resourceType: this.resourceType,
},
})
}),
);
}

View File

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

View File

@@ -7,9 +7,10 @@ import type { FriendshipStatus } from '../Befriend';
import { useToast } from '../../hooks/use-toast';
import { EmptyFriends } from './EmptyFriends';
import { FriendProgressItem } from './FriendProgressItem';
import UserIcon from '../../icons/user.svg';
import { UserProgressModal } from '../UserProgress/UserProgressModal';
import { InviteFriendPopup } from './InviteFriendPopup';
import { UserCustomProgressModal } from '../UserProgress/UserCustomProgressModal';
import { UserIcon } from 'lucide-react';
type FriendResourceProgress = {
updatedAt: string;
@@ -63,7 +64,7 @@ export function FriendsPage() {
async function loadFriends() {
const { response, error } = await httpGet<ListFriendsResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-list-friends`
`${import.meta.env.PUBLIC_API_URL}/v1-list-friends`,
);
if (error || !response) {
@@ -88,15 +89,15 @@ export function FriendsPage() {
const befriendUrl = `${baseUrl}/befriend?u=${user?.id}`;
const selectedGroupingType = groupingTypes.find(
(grouping) => grouping.value === selectedGrouping
(grouping) => grouping.value === selectedGrouping,
);
const filteredFriends = friends.filter((friend) =>
selectedGroupingType?.statuses.includes(friend.status)
const filteredFriends = friends.filter(
(friend) => selectedGroupingType?.statuses.includes(friend.status),
);
const receivedRequests = friends.filter(
(friend) => friend.status === 'received'
(friend) => friend.status === 'received',
);
if (isLoading) {
@@ -107,6 +108,25 @@ export function FriendsPage() {
return <EmptyFriends befriendUrl={befriendUrl} />;
}
const progressModal =
showFriendProgress && showFriendProgress?.isCustomResource ? (
<UserCustomProgressModal
userId={showFriendProgress?.friend.userId}
resourceId={showFriendProgress.resourceId}
resourceType="roadmap"
isCustomResource={true}
onClose={() => setShowFriendProgress(undefined)}
/>
) : (
<UserProgressModal
userId={showFriendProgress?.friend.userId}
resourceId={showFriendProgress?.resourceId!}
resourceType={'roadmap'}
onClose={() => setShowFriendProgress(undefined)}
isCustomResource={showFriendProgress?.isCustomResource}
/>
);
return (
<div>
{showInviteFriendPopup && (
@@ -116,15 +136,7 @@ export function FriendsPage() {
/>
)}
{showFriendProgress && (
<UserProgressModal
userId={showFriendProgress.friend.userId}
resourceId={showFriendProgress.resourceId}
resourceType={'roadmap'}
onClose={() => setShowFriendProgress(undefined)}
isCustomResource={showFriendProgress.isCustomResource}
/>
)}
{showFriendProgress && progressModal}
<div className="mb-4 flex flex-col items-stretch justify-between gap-2 sm:flex-row sm:items-center sm:gap-0">
<div className="flex items-center gap-2">
@@ -191,11 +203,8 @@ export function FriendsPage() {
{filteredFriends.length === 0 && (
<div className="flex flex-col items-center justify-center py-12">
<img
src={UserIcon.src}
alt="Empty Friends"
className="mb-3 w-12 opacity-20"
/>
<UserIcon size={'60px'} className="mb-3 w-12 opacity-20" />
<h2 className="text-lg font-semibold">
{selectedGrouping === 'active' && 'No friends yet'}
{selectedGrouping === 'sent' && 'No requests sent'}

View File

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

View File

@@ -0,0 +1,78 @@
import { ExternalLink, Globe2, type LucideIcon } from 'lucide-react';
type RoadmapCardProps = {
title: string;
description: string;
icon: LucideIcon;
icon2?: LucideIcon;
link: string;
isUpcoming?: boolean;
};
export function RoadmapCard(props: RoadmapCardProps) {
const {
isUpcoming,
link,
title,
description,
icon: Icon,
icon2: Icon2,
} = props;
if (isUpcoming) {
return (
<div className="group relative block rounded-xl border border-gray-300 bg-gradient-to-br from-gray-100 to-gray-50 p-5 overflow-hidden">
<div className="mb-2 sm:mb-5 flex flex-row items-center">
<div className="flex h-7 w-7 sm:h-9 sm:w-9 items-center justify-center rounded-full bg-gray-900 text-white">
<Icon className="h-3 sm:h-5" />
</div>
{Icon2 && (
<>
<span className="mx-2 text-gray-400">+</span>
<div className="flex h-7 w-7 sm:h-9 sm:w-9 items-center justify-center rounded-full bg-gray-900 text-white">
<Icon2 className="h-3 sm:h-5" />
</div>
</>
)}
</div>
<span className="mb-0.5 block text-lg sm:text-xl font-semibold sm:mb-2">
{title}
</span>
<span className="text-sm text-gray-500">{description}</span>
<div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-100/70">
<span className="text-sm bg-black rounded-lg text-white font-semibold py-1 px-2 -rotate-45 transform">
Coming soon
</span>
</div>
</div>
);
}
return (
<a
href={link}
target={'_blank'}
className="group relative block rounded-xl border border-gray-300 bg-gradient-to-br from-gray-100 to-gray-50
p-3.5 sm:p-5 transition-colors duration-200 ease-in-out hover:cursor-pointer hover:border-black/30 hover:bg-gray-50/70 hover:shadow-sm"
>
<div className="mb-2 sm:mb-5 flex flex-row items-center">
<div className="flex h-7 w-7 sm:h-9 sm:w-9 items-center justify-center rounded-full bg-gray-900 text-white">
<Icon className="h-4 sm:h-5" />
</div>
{Icon2 && (
<>
<span className="mx-2 text-gray-400">+</span>
<div className="flex h-7 w-7 sm:h-9 sm:w-9 items-center justify-center rounded-full bg-gray-900 text-white">
<Icon2 className="h-4 sm:h-5" />
</div>
</>
)}
</div>
<ExternalLink className="lucide lucide-external-link absolute right-2 top-2 h-4 text-gray-300 transition group-hover:text-gray-700" />
<span className="mb-0 block text-lg sm:text-xl font-semibold sm:mb-2">
{title}
</span>
<span className="text-sm text-gray-500">{description}</span>
</a>
);
}

View File

@@ -0,0 +1,63 @@
import { ExternalLink } from 'lucide-react';
type RoadmapMultiCardProps = {
roadmaps: {
title: string;
link: string;
}[];
description: string;
secondaryRoadmaps?: {
title: string;
link: string;
}[];
secondaryDescription?: string;
};
export function RoadmapMultiCard(props: RoadmapMultiCardProps) {
const { roadmaps, description, secondaryRoadmaps, secondaryDescription } = props;
return (
<div
className="relative flex flex-col overflow-hidden rounded-xl border border-gray-300 bg-gradient-to-br from-gray-100
to-gray-50 ease-in-out"
>
<div className="flex flex-col divide-y">
{roadmaps.map((roadmap, index) => (
<a
target={'_blank'}
key={index}
href={roadmap.link}
className="group text-sm sm:text-base flex w-full items-center justify-between gap-2 bg-gradient-to-br from-gray-100 to-gray-50 px-4 sm:px-5 py-2 transition-colors duration-200"
>
{roadmap.title}
<ExternalLink className="lucide lucide-external-link h-4 text-gray-300 transition group-hover:text-gray-700" />
</a>
))}
</div>
<p className="flex-grow bg-gray-200/70 p-4 sm:p-5 text-sm text-gray-500">
{description}
</p>
{secondaryRoadmaps && (
<div className="flex flex-col divide-y">
{secondaryRoadmaps.map((roadmap, index) => (
<a
target={'_blank'}
key={index}
href={roadmap.link}
className="group text-sm sm:text-base flex w-full items-center justify-between gap-2 bg-gradient-to-br from-gray-100 to-gray-50 px-5 py-2 transition-colors duration-200"
>
{roadmap.title}
<ExternalLink className="lucide lucide-external-link h-4 text-gray-300 transition group-hover:text-gray-700" />
</a>
))}
</div>
)}
{secondaryDescription && (
<p className="flex-grow bg-gray-200/70 p-4 sm:p-5 text-sm text-gray-500">
{secondaryDescription}
</p>
)}
</div>
);
}

View File

@@ -0,0 +1,29 @@
import { type ReactNode } from 'react';
import { SectionBadge } from './SectionBadge.tsx';
type RoleRoadmapsProps = {
badge: string;
title: string;
description: string;
children: ReactNode;
};
export function RoleRoadmaps(props: RoleRoadmapsProps) {
const { badge, title, description, children } = props;
return (
<div className="bg-gradient-to-b from-gray-100 to-white py-5 sm:py-8 md:py-12">
<div className="container">
<div className="text-left">
<SectionBadge title={badge} />
</div>
<div className="my-4 sm:my-7 text-left">
<h2 className="mb-1 text-xl sm:text-3xl font-semibold">{title}</h2>
<p className="text-sm sm:text-base text-gray-500">{description}</p>
<div className="mt-4 sm:mt-7 grid sm:grid-cols-2 md:grid-cols-3 gap-3">{children}</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,12 @@
type SectionBadgeProps = {
title: string;
};
export function SectionBadge(props: SectionBadgeProps) {
const { title } = props;
return (
<span className="rounded-full bg-black px-3 py-1 text-sm text-white">
{title}
</span>
);
}

View File

@@ -0,0 +1,31 @@
import { useState } from 'react';
type TipItemProps = {
title: string;
description: string;
};
export function TipItem(props: TipItemProps) {
const { title, description } = props;
const [isToggled, setIsToggled] = useState(false);
return (
<div className="flex flex-col">
{!isToggled && (
<div
onClick={() => setIsToggled(true)}
className="cursor-pointer rounded-lg sm:rounded-xl bg-black px-3 py-2 text-sm sm:text-base text-white"
>
{title}
</div>
)}
{isToggled && (
<p
className="rounded-lg sm:rounded-xl bg-gray-200 px-3 py-2 text-black text-sm sm:text-base"
>
{description}
</p>
)}
</div>
);
}

View File

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

View File

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

View File

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

View File

@@ -4,12 +4,14 @@ import { isLoggedIn } from '../../lib/jwt';
import { AccountDropdownList } from './AccountDropdownList';
import { DropdownTeamList } from './DropdownTeamList';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
export function AccountDropdown() {
const dropdownRef = useRef(null);
const [showDropdown, setShowDropdown] = useState(false);
const [isTeamsOpen, setIsTeamsOpen] = useState(false);
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
useOutsideClick(dropdownRef, () => {
setShowDropdown(false);
@@ -22,6 +24,14 @@ export function AccountDropdown() {
return (
<div className="relative z-50 animate-fade-in">
{isCreatingRoadmap && (
<CreateRoadmapModal
onClose={() => {
setIsCreatingRoadmap(false);
}}
/>
)}
<button
className="flex h-8 w-40 items-center justify-center gap-1.5 rounded-full bg-gradient-to-r from-purple-500 to-purple-700 px-4 py-2 text-sm font-medium text-white hover:from-purple-500 hover:to-purple-600"
onClick={() => {
@@ -43,7 +53,13 @@ export function AccountDropdown() {
{isTeamsOpen ? (
<DropdownTeamList setIsTeamsOpen={setIsTeamsOpen} />
) : (
<AccountDropdownList setIsTeamsOpen={setIsTeamsOpen} />
<AccountDropdownList
onCreateRoadmap={() => {
setIsCreatingRoadmap(true);
setShowDropdown(false);
}}
setIsTeamsOpen={setIsTeamsOpen}
/>
)}
</div>
)}

View File

@@ -1,54 +1,76 @@
import { ChevronRight } from 'lucide-react';
import { ChevronRight, LogOut, Map, Plus, User2, Users2 } from 'lucide-react';
import { logout } from './navigation';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
import { useState } from 'react';
type AccountDropdownListProps = {
onCreateRoadmap: () => void;
setIsTeamsOpen: (isOpen: boolean) => void;
};
export function AccountDropdownList(props: AccountDropdownListProps) {
const { setIsTeamsOpen } = props;
const { setIsTeamsOpen, onCreateRoadmap } = props;
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
return (
<ul>
<li className="px-1">
<a
href="/account"
className="block rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex items-center gap-2 rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<User2 className="h-4 w-4 stroke-[2.5px] text-slate-400 group-hover:text-white" />
Profile
</a>
</li>
<li className="px-1">
<a
href="/account/friends"
className="block rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex items-center gap-2 rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<Users2 className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
Friends
</a>
</li>
<li className="px-1">
<li className="mt-1 border-t border-t-gray-700/60 px-1 pt-1">
<button
onClick={() => {
onCreateRoadmap();
}}
className="group flex w-full items-center gap-2 rounded py-2 pl-3 pr-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<Plus className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
New Roadmap
</button>
</li>
<li className="border-b border-b-gray-700/60 px-1 pb-1">
<a
href="/account/roadmaps"
className="block rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex items-center gap-2 rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
<Map className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
Roadmaps
</a>
</li>
<li className="px-1">
<li className="px-1 pt-1">
<button
className="group flex w-full items-center justify-between rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex w-full items-center justify-between rounded py-2 pl-3 pr-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
onClick={() => setIsTeamsOpen(true)}
>
Teams
<span className="flex items-center gap-2.5">
<Users2 className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
Teams
</span>
<ChevronRight className="h-4 w-4 shrink-0 stroke-[2.5px] text-slate-400 group-hover:text-white" />
</button>
</li>
<li className="px-1">
<button
className="block w-full rounded pl-4 pr-2 py-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
className="group flex gap-2 items-center w-full rounded py-2 pl-3 pr-2 text-left text-sm font-medium text-slate-100 hover:bg-slate-700"
type="button"
onClick={logout}
>
<LogOut className="h-4 w-4 stroke-[2px] text-slate-400 group-hover:text-white" />
Logout
</button>
</li>

View File

@@ -1,61 +1,38 @@
---
import { Menu } from 'lucide-react';
import Icon from '../AstroIcon.astro';
import { NavigationDropdown } from '../NavigationDropdown';
import { AccountDropdown } from './AccountDropdown';
---
<div class='bg-slate-900 py-5 text-white sm:py-8'>
<nav class='container flex items-center justify-between'>
<a
class='flex items-center text-lg font-medium text-white'
href='/'
aria-label='roadmap.sh'
>
<Icon icon='logo' />
</a>
<div class='flex items-center gap-5'>
<a
class='flex items-center text-lg font-medium text-white'
href='/'
aria-label='roadmap.sh'
>
<Icon icon='logo' />
</a>
<!-- Desktop navigation items -->
<ul class='hidden space-x-5 sm:flex sm:items-center'>
<li>
<a href='/roadmaps' class='text-gray-400 hover:text-white'>Roadmaps</a>
</li>
<li>
<a href='/best-practices' class='text-gray-400 hover:text-white'
>Best Practices</a
>
</li>
<li class='hidden lg:inline'>
<a href='/questions' class='text-gray-400 hover:text-white'>Questions</a
>
</li>
<li>
<a href='/teams' class='group relative text-blue-300 hover:text-white'>
Teams
<span
class='ml-0.5 hidden rounded-sm border-black bg-blue-300 px-1 py-0.5 text-xs font-semibold uppercase text-black group-hover:bg-white md:inline'
>
New
</span>
<span class='inline md:hidden absolute -right-[11px] top-0'>
<span class='relative flex h-2 w-2'>
<span
class='absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75'
></span>
<span class='relative inline-flex h-2 w-2 rounded-full bg-sky-500'
></span>
</span>
</span>
<!-- Desktop navigation items -->
<div class='hidden space-x-5 sm:flex sm:items-center'>
<NavigationDropdown client:load />
<a href='/get-started' class='text-gray-400 hover:text-white'>
Start Here
</a>
</li>
<li>
<kbd
<a href='/teams' class='text-gray-400 hover:text-white'> Teams</a>
<button
data-command-menu
class='hidden items-center rounded-md border border-gray-800 px-2.5 py-1 text-sm text-gray-400 hover:cursor-pointer hover:bg-gray-800 sm:flex'
class='hidden items-center rounded-md border border-gray-800 px-2.5 py-1.5 text-sm text-gray-400 hover:cursor-pointer hover:bg-gray-800 sm:flex'
>
<Icon icon='search' class='mr-2 h-3 w-3' />
<kbd class='mr-1 font-sans'>⌘</kbd><kbd class='font-sans'>K</kbd>
</kbd>
</li>
</ul>
<Icon icon='search' class='h-3 w-3' />
<span class='ml-2'>Search</span>
</button>
</div>
</div>
<ul class='hidden h-8 w-[172px] items-center justify-end gap-5 sm:flex'>
<li data-guest-required class='hidden'>
<a href='/login' class='text-gray-400 hover:text-white'>Login</a>

View File

@@ -0,0 +1,105 @@
import {
BookOpen,
BookOpenText,
CheckSquare,
FileQuestion,
Menu,
Shirt,
Video,
Waypoints,
} from 'lucide-react';
import { useRef, useState } from 'react';
import { cn } from '../lib/classname.ts';
import { useOutsideClick } from '../hooks/use-outside-click.ts';
const links = [
{
link: '/roadmaps',
label: 'Roadmaps',
description: 'Step by step learning paths',
Icon: Waypoints,
},
{
link: '/best-practices',
label: 'Best Practices',
description: "Do's and don'ts",
Icon: CheckSquare,
},
{
link: '/questions',
label: 'Questions',
description: 'Test and Practice your knowledge',
Icon: FileQuestion,
},
{
link: '/guides',
label: 'Guides',
description: 'In-depth articles and tutorials',
Icon: BookOpenText,
},
{
link: 'https://youtube.com/@roadmapsh',
label: 'Videos',
description: 'Animated and interactive content',
Icon: Video,
isExternal: true,
},
{
link: 'https://cottonbureau.com/people/roadmapsh',
label: 'Shop',
description: 'Get some cool swag',
Icon: Shirt,
isExternal: true,
},
];
export function NavigationDropdown() {
const dropdownRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
useOutsideClick(dropdownRef, () => {
setIsOpen(false);
});
return (
<div className="relative flex items-center" ref={dropdownRef}>
<button
className={cn('text-gray-400 hover:text-white', {
'text-white': isOpen,
})}
onClick={() => setIsOpen(true)}
onMouseOver={() => setIsOpen(true)}
>
<Menu className="h-5 w-5" />
</button>
<div
className={cn(
'absolute pointer-events-none left-0 top-full z-[999] mt-2 w-48 min-w-[320px] -translate-y-1 rounded-lg bg-slate-800 py-2 opacity-0 shadow-xl transition-all duration-100',
{
'pointer-events-auto translate-y-2.5 opacity-100': isOpen,
},
)}
>
{links.map((link) => (
<a
href={link.link}
target={link.isExternal ? '_blank' : undefined}
rel={link.isExternal ? 'noopener noreferrer' : undefined}
key={link.link}
className="group flex items-center gap-3 px-4 py-2.5 text-gray-400 transition-colors hover:bg-slate-700"
>
<span className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-slate-600 transition-colors group-hover:bg-slate-500 group-hover:text-slate-100">
<link.Icon className="inline-block h-5 w-5" />
</span>
<span className="flex flex-col">
<span className="font-medium text-slate-300 transition-colors group-hover:text-slate-100">
{link.label}
</span>
<span className="text-sm">{link.description}</span>
</span>
</a>
))}
</div>
</div>
);
}

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import CloseIcon from '../icons/close.svg';
import { httpGet } from '../lib/http';
import { sponsorHidden } from '../stores/page';
import { useStore } from '@nanostores/react';
import { X } from 'lucide-react';
export type PageSponsorType = {
company: string;
@@ -46,7 +46,7 @@ export function PageSponsor(props: PageSponsorProps) {
`${import.meta.env.PUBLIC_API_URL}/v1-get-sponsor`,
{
href: window.location.pathname,
}
},
);
if (error) {
@@ -101,11 +101,11 @@ export function PageSponsor(props: PageSponsorProps) {
sponsorHidden.set(true);
}}
>
<img alt="Close" className="h-4 w-4" src={CloseIcon.src} />
<X className="h-4 w-4" />
</span>
<img
src={imageUrl}
className="block h-[150px] w-[104.89px] object-contain lg:h-[169px] lg:w-[118.18px]"
className="block h-[150px] object-fill lg:h-[169px] lg:w-[118.18px]"
alt="Sponsor Banner"
/>
<span className="flex flex-1 flex-col justify-between text-sm">

View File

@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import { isLoggedIn } from '../../lib/jwt';
import { httpPost } from '../../lib/http';
import type { ResourceType } from '../../lib/resource-progress';
type PageVisitProps = {
resourceId?: string;
resourceType?: ResourceType;
};
export function PageVisit(props: PageVisitProps) {
const { resourceId, resourceType } = props;
useEffect(() => {
if (!isLoggedIn()) {
return;
}
httpPost(`${import.meta.env.PUBLIC_API_URL}/v1-visit`, {
...(resourceType && { resourceType, resourceId }),
}).finally(() => {});
}, []);
return null;
}

View File

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

View File

@@ -23,7 +23,7 @@ function ProgressStatButton(props: ProgressStatButtonProps) {
<button
disabled={isDisabled}
onClick={onClick}
className="group relative text-sm sm:text-base flex flex-1 items-center overflow-hidden rounded-md sm:rounded-xl border border-gray-300 bg-white py-2 px-2 sm:py-3 sm:px-4 text-black transition-colors hover:border-black disabled:pointer-events-none disabled:opacity-50"
className="group relative flex flex-1 items-center overflow-hidden rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black disabled:pointer-events-none disabled:opacity-50 sm:rounded-xl sm:px-4 sm:py-3 sm:text-base"
>
{icon}
<span className="flex flex-grow justify-between">
@@ -31,7 +31,7 @@ function ProgressStatButton(props: ProgressStatButtonProps) {
<span>{count}</span>
</span>
<span className="absolute top-full left-0 right-0 flex h-full items-center justify-center border border-black bg-black text-white transition-all duration-200 group-hover:top-0">
<span className="absolute left-0 right-0 top-full flex h-full items-center justify-center border border-black bg-black text-white transition-all duration-200 group-hover:top-0">
Restart Asking
</span>
</button>
@@ -62,7 +62,7 @@ export function QuestionFinished(props: QuestionFinishedProps) {
<span className="inline sm:hidden">questions</span>
</p>
<div className="mt-5 mb-5 flex w-full flex-col gap-1.5 sm:gap-3 px-2 sm:flex-row sm:px-16">
<div className="mb-5 mt-5 flex w-full flex-col gap-1.5 px-2 sm:flex-row sm:gap-3 sm:px-16">
<ProgressStatButton
icon={<ThumbsUp className="mr-1 h-4" />}
label="Knew"
@@ -85,10 +85,10 @@ export function QuestionFinished(props: QuestionFinishedProps) {
onClick={() => onReset('skip')}
/>
</div>
<div className="mt-2 mb-4 sm:mb-0 text-sm">
<div className="mb-4 mt-2 text-sm sm:mb-0">
<button
onClick={() => onReset('reset')}
className="flex items-center gap-0.5 text-red-700 hover:text-black text-sm sm:text-base"
className="flex items-center gap-0.5 text-sm text-red-700 hover:text-black sm:text-base"
>
<RefreshCcw className="mr-1 h-4" />
Restart Asking

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