Compare commits

..

160 Commits

Author SHA1 Message Date
Arik Chakma
e6cbf55a38 feat: add more questions 2024-01-19 23:41:49 +06:00
Arik Chakma
d66d4bcb8a feat: add nodejs questions 2024-01-19 01:01:38 +06: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
1077 changed files with 88439 additions and 24445 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.8",
"@astrojs/sitemap": "^3.0.3",
"@astrojs/tailwind": "^5.0.4",
"@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",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"astro": "^4.0.7",
"astro-compress": "^2.2.3",
"clsx": "^2.0.0",
"dracula-prism": "^2.1.13",
"jose": "^4.14.4",
"jose": "^5.1.3",
"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.11",
"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.1",
"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.0",
"tailwindcss": "^3.4.0",
"zustand": "^4.4.7"
},
"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.40.1",
"@tailwindcss/typography": "^0.5.10",
"@types/js-cookie": "^3.0.6",
"@types/prismjs": "^1.26.3",
"csv-parser": "^3.0.0",
"gh-pages": "^5.0.0",
"gh-pages": "^6.1.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.24.1",
"prettier": "^3.1.1",
"prettier-plugin-astro": "^0.12.2",
"prettier-plugin-tailwindcss": "^0.5.9"
}
}

3406
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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: 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.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 561 KiB

BIN
public/roadmaps/aws.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

BIN
public/roadmaps/rust.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

View File

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

@@ -21,7 +21,7 @@ export function EmailLoginForm() {
{
email,
password,
}
},
);
// Log the user in and reload the page
@@ -39,7 +39,7 @@ 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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {
@@ -96,7 +102,10 @@ export function CustomRoadmap() {
}
async function trackVisit() {
if (!isLoggedIn()) return;
if (!isLoggedIn() || isEmbed) {
return;
}
await httpPost(`${import.meta.env.PUBLIC_API_URL}/v1-visit`, {
resourceId: id,
resourceType: 'roadmap',
@@ -120,14 +129,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,7 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) {
const shareSettingsModal = selectedRoadmap && (
<ShareOptionsModal
isDiscoverable={selectedRoadmap.isDiscoverable}
description={selectedRoadmap.description}
visibility={selectedRoadmap.visibility}
sharedFriendIds={selectedRoadmap.sharedFriendIds}
@@ -89,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
@@ -141,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">
@@ -173,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,7 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) {
<>
{isSharing && $canManageCurrentRoadmap && $currentRoadmap && (
<ShareOptionsModal
isDiscoverable={$currentRoadmap.isDiscoverable}
description={$currentRoadmap?.description}
visibility={$currentRoadmap?.visibility}
teamId={$currentRoadmap?.teamId}
@@ -42,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,71 +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
description={$currentRoadmap?.description}
visibility={$currentRoadmap?.visibility}
teamId={$currentRoadmap?.teamId}
roadmapId={$currentRoadmap?._id!}
sharedFriendIds={$currentRoadmap?.sharedFriendIds || []}
sharedTeamMemberIds={
$currentRoadmap?.sharedTeamMemberIds || []
}
onClose={() => setIsSharing(false)}
onShareSettingsUpdate={(settings) => {
currentRoadmap.set({
...$currentRoadmap,
...settings,
});
<div 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 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);
}}
/>
</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) => {
@@ -141,7 +146,7 @@ export class Renderer {
this.jsonToSvg(
this.resourceType === 'roadmap'
? `/${this.resourceId}.json`
: `/best-practices/${this.resourceId}.json`
: `/best-practices/${this.resourceId}.json`,
);
}
}
@@ -181,7 +186,7 @@ export class Renderer {
resourceType: this.resourceType as ResourceType,
topicId,
},
newStatus
newStatus,
)
.then(() => {
renderTopicProgress(topicId, newStatus);
@@ -213,9 +218,14 @@ export class Renderer {
const isCurrentStatusDone = targetGroup.classList.contains('done');
const normalizedGroupId = groupId.replace(/^\d+-/, '');
if (normalizedGroupId.startsWith('ext_link:')) {
return;
}
this.updateTopicStatus(
normalizedGroupId,
!isCurrentStatusDone ? 'done' : 'pending'
!isCurrentStatusDone ? 'done' : 'pending',
);
}
@@ -241,9 +251,12 @@ export class Renderer {
action: `${this.resourceType} / ${this.resourceId}`,
label: externalLink,
});
window.open(`https://${externalLink}`);
} else {
window.location.href = `https://${externalLink}`;
}
window.open(`https://${externalLink}`);
return;
}
@@ -263,7 +276,7 @@ export class Renderer {
resourceType: this.resourceType,
resourceId: this.resourceId,
},
})
}),
);
return;
}
@@ -278,7 +291,7 @@ export class Renderer {
e.preventDefault();
this.updateTopicStatus(
normalizedGroupId,
!isCurrentStatusLearning ? 'learning' : 'pending'
!isCurrentStatusLearning ? 'learning' : 'pending',
);
return;
}
@@ -287,7 +300,7 @@ export class Renderer {
e.preventDefault();
this.updateTopicStatus(
normalizedGroupId,
!isCurrentStatusSkipped ? 'skipped' : 'pending'
!isCurrentStatusSkipped ? 'skipped' : 'pending',
);
return;
@@ -300,7 +313,7 @@ export class Renderer {
resourceId: this.resourceId,
resourceType: this.resourceType,
},
})
}),
);
}

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import CloseIcon from '../icons/close.svg';
import { httpGet } from '../lib/http';
import { sponsorHidden } from '../stores/page';
import { useStore } from '@nanostores/react';
import { X } from 'lucide-react';
export type PageSponsorType = {
company: string;
@@ -46,7 +46,7 @@ export function PageSponsor(props: PageSponsorProps) {
`${import.meta.env.PUBLIC_API_URL}/v1-get-sponsor`,
{
href: window.location.pathname,
}
},
);
if (error) {
@@ -101,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

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

View File

@@ -46,7 +46,7 @@ export function QuestionsList(props: QuestionsListProps) {
const { response, error } = await httpGet<UserQuestionProgress>(
`${
import.meta.env.PUBLIC_API_URL
}/v1-get-user-question-progress/${groupId}`
}/v1-get-user-question-progress/${groupId}`,
);
if (error) {
@@ -106,7 +106,7 @@ export function QuestionsList(props: QuestionsListProps) {
}/v1-reset-question-progress/${groupId}`,
{
status: type,
}
},
);
if (error) {
@@ -139,7 +139,7 @@ export function QuestionsList(props: QuestionsListProps) {
async function updateQuestionStatus(
status: QuestionProgressType,
questionId: string
questionId: string,
) {
setIsLoading(true);
let newProgress = userProgress || { know: [], dontKnow: [], skip: [] };
@@ -161,7 +161,7 @@ export function QuestionsList(props: QuestionsListProps) {
status,
questionId,
questionGroupId: groupId,
}
},
);
if (error || !response) {
@@ -173,7 +173,7 @@ export function QuestionsList(props: QuestionsListProps) {
}
const updatedQuestionList = pendingQuestions.filter(
(q) => q.id !== questionId
(q) => q.id !== questionId,
);
setUserProgress(newProgress);
@@ -198,7 +198,7 @@ export function QuestionsList(props: QuestionsListProps) {
const hasFinished = !isLoading && hasProgress && !currQuestion;
return (
<div className="mb-0 sm:mb-40 gap-3 text-center">
<div className="mb-0 gap-3 text-center sm:mb-40">
<QuestionsProgress
knowCount={knowCount}
didNotKnowCount={dontKnowCount}
@@ -241,7 +241,7 @@ export function QuestionsList(props: QuestionsListProps) {
</div>
<div
className={`flex flex-col gap-1 sm:gap-3 transition-opacity duration-300 sm:flex-row ${
className={`flex flex-col gap-1 transition-opacity duration-300 sm:flex-row sm:gap-3 ${
hasFinished ? 'opacity-0' : 'opacity-100'
}`}
>
@@ -249,10 +249,10 @@ export function QuestionsList(props: QuestionsListProps) {
disabled={isLoading || !currQuestion}
onClick={(e) => {
e.stopPropagation();
e.preventDefault()
e.preventDefault();
updateQuestionStatus('know', currQuestion.id).finally(() => null);
}}
className="flex flex-1 items-center rounded-md sm:rounded-lg border border-gray-300 bg-white text-sm sm:text-base py-2 px-2 sm:py-3 sm:px-4 text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50"
className="flex flex-1 items-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
>
<CheckCircle className="mr-1 h-4 text-current" />
Already Know that
@@ -260,11 +260,11 @@ export function QuestionsList(props: QuestionsListProps) {
<button
onClick={() => {
updateQuestionStatus('dontKnow', currQuestion.id).finally(
() => null
() => null,
);
}}
disabled={isLoading || !currQuestion}
className="flex flex-1 items-center rounded-md sm:rounded-lg border border-gray-300 bg-white text-sm sm:text-base py-2 px-2 sm:py-3 sm:px-4 text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50"
className="flex flex-1 items-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
>
<Sparkles className="mr-1 h-4 text-current" />
Didn't Know that
@@ -275,7 +275,7 @@ export function QuestionsList(props: QuestionsListProps) {
}}
disabled={isLoading || !currQuestion}
data-next-question="skip"
className="flex flex-1 items-center rounded-md sm:rounded-lg border border-red-600 text-sm sm:text-base py-2 px-2 sm:py-3 sm:px-4 text-red-600 hover:bg-red-600 hover:text-white disabled:pointer-events-none disabled:opacity-50"
className="flex flex-1 items-center rounded-md border border-red-600 px-2 py-2 text-sm text-red-600 hover:bg-red-600 hover:text-white disabled:pointer-events-none disabled:opacity-50 sm:rounded-lg sm:px-4 sm:py-3 sm:text-base"
>
<SkipForward className="mr-1 h-4" />
Skip Question

View File

@@ -26,7 +26,7 @@ export function QuestionsProgress(props: QuestionsProgressProps) {
const donePercentage = (totalSolved / totalCount) * 100;
return (
<div className="mb-3 sm:mb-5 overflow-hidden rounded-lg border border-gray-300 bg-white p-4 sm:p-6">
<div className="mb-3 overflow-hidden rounded-lg border border-gray-300 bg-white p-4 sm:mb-5 sm:p-6">
<div className="mb-3 flex items-center text-gray-600">
<div className="relative w-full flex-1 rounded-xl bg-gray-200 p-1">
<div
@@ -79,12 +79,12 @@ export function QuestionsProgress(props: QuestionsProgressProps) {
>
<RotateCcw className="mr-1 h-4" />
Reset
<span className='inline lg:hidden'>Progress</span>
<span className="inline lg:hidden">Progress</span>
</button>
</div>
{showLoginAlert && (
<p className="-mx-6 mt-6 -mb-6 border-t bg-yellow-100 py-3 text-sm text-yellow-900">
<p className="-mx-6 -mb-6 mt-6 border-t bg-yellow-100 py-3 text-sm text-yellow-900">
You progress is not saved. Please{' '}
<button
onClick={() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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