Compare commits

..

111 Commits

Author SHA1 Message Date
Kamran Ahmed
4d735df233 chore: disable teams 2023-07-20 19:23:41 +01:00
Kamran Ahmed
1ecab3981e Update team forms 2023-07-20 19:08:13 +01:00
Kamran Ahmed
7ea9943544 Team pages responsiveness 2023-07-20 18:58:34 +01:00
Kamran Ahmed
d2f76c4726 Responsiveness of progress tab 2023-07-20 18:19:33 +01:00
Kamran Ahmed
758072cca9 Merge branch 'master' of github.com:kamranahmedse/developer-roadmap into feat/team 2023-07-20 17:50:29 +01:00
Kamran Ahmed
c565dfa5a7 Add toast instead of alert 2023-07-20 17:48:37 +01:00
Kamran Ahmed
1b8cf1734e Add toast 2023-07-20 17:47:18 +01:00
Kamran Ahmed
8376b543cc Update 2023-07-20 13:00:00 +01:00
Arik Chakma
cf31795f58 wip: user avatar in progress 2023-07-20 07:41:10 +06:00
Kamran Ahmed
0dd21e15d1 Update button styles 2023-07-20 02:23:20 +01:00
Kamran Ahmed
598ee44168 Merge branch 'feat/team' of github.com:kamranahmedse/developer-roadmap into feat/team 2023-07-20 02:05:41 +01:00
Arik Chakma
57de0720c5 chore: add external link 2023-07-20 06:13:06 +06:00
Arik Chakma
ae14d560a1 fix: progress details 2023-07-20 05:59:54 +06:00
Kamran Ahmed
c5e18ed461 Remove header from progress 2023-07-20 00:58:51 +01:00
Kamran Ahmed
f7a33d8991 Refactor toast 2023-07-20 00:48:46 +01:00
Kamran Ahmed
b65982d7e1 Merge branch 'feat/team' of github.com:kamranahmedse/developer-roadmap into feat/team 2023-07-20 00:45:13 +01:00
Kamran Ahmed
76facf70ec Refactor delete team 2023-07-20 00:45:06 +01:00
Arik Chakma
1e8751deae fix: refactor group logic 2023-07-20 04:38:30 +06:00
Arik Chakma
2103102c96 wip: group by roadmap items 2023-07-20 04:14:28 +06:00
Arik Chakma
2328931bc9 Merge branch 'feat/team' of https://github.com/kamranahmedse/developer-roadmap into feat/team 2023-07-20 02:46:15 +06:00
Arik Chakma
340733d979 wip: error handling 2023-07-20 02:45:03 +06:00
Kamran Ahmed
9dbcb50161 Merge branch 'feat/team' of github.com:kamranahmedse/developer-roadmap into feat/team 2023-07-19 21:29:56 +01:00
Kamran Ahmed
02c79dca56 Add store for user teams 2023-07-19 21:29:48 +01:00
Arik Chakma
805699f956 wip: error handling 2023-07-20 02:25:51 +06:00
Kamran Ahmed
02dd91e35a Merge branch 'feat/team' of github.com:kamranahmedse/developer-roadmap into feat/team 2023-07-19 21:22:15 +01:00
Kamran Ahmed
6157ec9c35 Permissions 2023-07-19 21:22:09 +01:00
Arik Chakma
3b318b4ca7 wip: add toast 2023-07-20 01:59:44 +06:00
Kamran Ahmed
65a98de213 Update team progress page 2023-07-19 20:30:44 +01:00
Kamran Ahmed
1254e8e7f9 Add update member functionality 2023-07-19 20:10:31 +01:00
Kamran Ahmed
10d105241f Merge branch 'feat/team' of github.com:kamranahmedse/developer-roadmap into feat/team 2023-07-19 19:17:15 +01:00
Kamran Ahmed
23651e070a Invite member changes 2023-07-19 19:17:08 +01:00
Arik Chakma
e8aaee7375 chore: team versions mob 2023-07-19 22:40:24 +06:00
Kamran Ahmed
952a514d1d Open user progress in new page 2023-07-19 17:33:33 +01:00
Kamran Ahmed
1303167be4 Add more popup changes 2023-07-19 17:28:58 +01:00
Kamran Ahmed
d4995a9092 Merge branch 'feat/team' of github.com:kamranahmedse/developer-roadmap into feat/team 2023-07-19 17:17:00 +01:00
Kamran Ahmed
783bfeea36 Add team roadmaps and removal 2023-07-19 17:16:51 +01:00
Arik Chakma
87ba7252e7 wip: team version responsive 2023-07-19 07:40:41 +06:00
Kamran Ahmed
442bf489c8 Add roadmap removal and updation 2023-07-19 00:31:13 +01:00
Kamran Ahmed
b6682710dd Ignore progress tracking on removed items 2023-07-18 23:50:24 +01:00
Kamran Ahmed
8f8f26514b Add team version loading 2023-07-18 23:39:07 +01:00
Kamran Ahmed
df3a66fe22 Merge branch 'feat/team' of github.com:kamranahmedse/developer-roadmap into feat/team 2023-07-18 22:52:53 +01:00
Kamran Ahmed
fa5a71e4b9 Team roadmaps 2023-07-18 22:52:47 +01:00
Arik Chakma
339cd8cc1e fix: removed node css 2023-07-19 03:08:23 +06:00
Arik Chakma
52670089af chore: modal responsive 2023-07-19 02:58:53 +06:00
Arik Chakma
c9599ef9e3 wip: team progress responsive 2023-07-19 02:53:46 +06:00
Arik Chakma
8fdd9ea653 fix: progress renderer 2023-07-19 01:58:42 +06:00
Arik Chakma
db07a161ae wip: team versions 2023-07-19 01:09:38 +06:00
Kamran Ahmed
edb79d466c Member progress modal 2023-07-18 17:48:58 +01:00
Kamran Ahmed
18d2e18bb3 Add member progress modal 2023-07-18 17:39:44 +01:00
Kamran Ahmed
42d43baa71 Fix: show only team roadmaps 2023-07-18 16:14:47 +01:00
Kamran Ahmed
55c0233dc2 Handle draft team 2023-07-18 15:48:14 +01:00
Kamran Ahmed
180c990bb6 Member progress page 2023-07-18 15:33:25 +01:00
Kamran Ahmed
72efdca722 Fix invalid image size 2023-07-18 13:00:10 +01:00
Kamran Ahmed
d9b386e9f1 Member listing and options page 2023-07-18 05:30:46 +01:00
Kamran Ahmed
1501fa631e Invitation workflow 2023-07-18 05:11:27 +01:00
Kamran Ahmed
fdb2c23911 Team creation 2023-07-17 17:51:10 +01:00
Kamran Ahmed
3f00c70c36 Team creation 2023-07-17 17:46:14 +01:00
Kamran Ahmed
762dff1d42 Add team invite form 2023-07-17 15:56:41 +01:00
Kamran Ahmed
6405744ec6 Search icon color 2023-07-16 20:20:24 +01:00
Kamran Ahmed
a8c20ba372 Team roadmaps 2023-07-16 20:05:58 +01:00
Kamran Ahmed
71bb3d4d3f Team skill rendering 2023-07-16 20:00:11 +01:00
Kamran Ahmed
0de4b38bb3 Team skill rendering 2023-07-16 19:53:08 +01:00
Kamran Ahmed
ac585b3f0b Team resource config updation 2023-07-16 19:42:05 +01:00
Kamran Ahmed
e214119c45 Add resource modification popup 2023-07-16 19:14:02 +01:00
Kamran Ahmed
5f43ae1e61 Roadmap selector 2023-07-15 21:44:05 +01:00
Kamran Ahmed
6f3693bf1b Allow removing roadmaps 2023-07-15 21:10:21 +01:00
Kamran Ahmed
3bbb1d7541 Roadmap addition to backend 2023-07-15 20:44:30 +01:00
Kamran Ahmed
18b0563c75 wip: refactor team creation 2023-07-15 17:02:58 +01:00
Kamran Ahmed
102ec5021f wip: refactor team creation 2023-07-14 01:35:19 +01:00
Kamran Ahmed
62c24ca8e1 wip: refactor team creation 2023-07-13 14:30:13 +01:00
Kamran Ahmed
2914433f0f wip: refactor team creation 2023-07-13 14:17:12 +01:00
Kamran Ahmed
afd319f88f wip: refactor team creation 2023-07-13 14:02:02 +01:00
Kamran Ahmed
b06f08fc67 wip: refactor team creation 2023-07-12 22:48:42 +01:00
Kamran Ahmed
485182a9cf Update team response format 2023-07-12 12:30:52 +01:00
Kamran Ahmed
6716c0375d Fix team sidebar dropdown not working on mobile 2023-07-12 12:11:36 +01:00
Kamran Ahmed
3f763051bb Add spinner to team dropdown 2023-07-11 19:12:50 +01:00
Kamran Ahmed
fef7a2e6e0 Responsiveness and teams page 2023-07-11 18:47:32 +01:00
Kamran Ahmed
75891d5eb1 Merge branch 'master' of github.com:kamranahmedse/developer-roadmap into feat/team 2023-07-11 15:42:02 +01:00
Arik Chakma
fdf67ae9f5 chore: team member resource progress map 2023-07-11 02:28:27 +06:00
Arik Chakma
9c7dd705c3 chore: team sidebar icon 2023-07-11 00:59:06 +06:00
Arik Chakma
62a9fbbc0e Merge branch 'master' into feat/team 2023-07-11 00:44:09 +06:00
Arik Chakma
dec256866e chore: team progress design 2023-07-11 00:39:57 +06:00
Arik Chakma
a1df2ff992 chore: max team members 2023-07-10 21:36:10 +06:00
Arik Chakma
8bd092e489 chore: auth hydrate 2023-07-10 21:29:54 +06:00
Arik Chakma
843445e59d chore: avatar sidebar 2023-07-10 20:48:30 +06:00
Arik Chakma
6fe4db8109 chore: team member page design 2023-07-10 20:08:40 +06:00
Arik Chakma
528cf0f380 chore: empty state 2023-07-10 18:48:18 +06:00
Arik Chakma
b06655ecb3 fix: bugs 2023-07-10 16:56:54 +06:00
Arik Chakma
e3b057dde7 fix: layout shift 2023-07-10 13:40:55 +06:00
Arik Chakma
d5b9836465 chore: update team 2023-07-10 10:49:16 +06:00
Arik Chakma
591fe5daa8 chore: delete team 2023-07-10 10:31:49 +06:00
Arik Chakma
99f8f32d66 chore: update team logo 2023-07-08 22:07:52 +06:00
Arik Chakma
9b08945d62 chore: resend invite dropdown 2023-07-08 18:55:07 +06:00
Arik Chakma
06e1e12ce8 chore: delete member 2023-07-08 02:29:03 +06:00
Arik Chakma
4c347b9df0 chore: notification count 2023-07-08 01:47:45 +06:00
Arik Chakma
271ddc1e5e chore: invite notifications 2023-07-08 00:33:32 +06:00
Arik Chakma
490a4509a0 chore: respond invite 2023-07-07 22:55:23 +06:00
Arik Chakma
ad23bb1bf8 chore: respond invite 2023-07-07 21:41:43 +06:00
Arik Chakma
7e15ccd231 chore: update team 2023-07-07 00:56:38 +06:00
Arik Chakma
5c10caa19a fix: search bug 2023-07-07 00:11:27 +06:00
Arik Chakma
2f9eacdff9 chore: team member progress 2023-07-06 19:40:01 +06:00
Arik Chakma
fd7ddc3819 chore: team progress page 2023-07-06 03:50:47 +06:00
Arik Chakma
3a1529e7f4 chore: search selector active on focus 2023-07-06 02:50:16 +06:00
Arik Chakma
f4db87bed3 chore: team sidebar 2023-07-06 02:32:05 +06:00
Arik Chakma
e648425505 fix: set team size 2023-07-05 23:39:27 +06:00
Arik Chakma
f2c4da4aad chore: team size 2023-07-05 23:35:15 +06:00
Arik Chakma
db268b2966 fix: tab keydown in selector 2023-07-05 00:35:40 +06:00
Arik Chakma
c88024906f chore: create team 2023-07-05 00:19:53 +06:00
Arik Chakma
c7b5bb426c chore: slugify identifier 2023-07-05 00:11:51 +06:00
Arik Chakma
00be0781aa chore: select resources 2023-07-05 00:07:08 +06:00
Arik Chakma
a27d41b718 chore: team sidebar 2023-07-04 04:29:43 +06:00
1502 changed files with 38359 additions and 395969 deletions

View File

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

View File

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

View File

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

View File

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

4
.gitignore vendored
View File

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

2
.npmrc
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
{
"name": "roadmap.sh",
"type": "module",
"version": "1.0.0",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "astro dev --port 3000",
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
@@ -16,54 +16,40 @@
"roadmap-links": "node scripts/roadmap-links.cjs",
"roadmap-dirs": "node scripts/roadmap-dirs.cjs",
"roadmap-content": "node scripts/roadmap-content.cjs",
"generate-renderer": "sh scripts/generate-renderer.sh",
"best-practice-dirs": "node scripts/best-practice-dirs.cjs",
"best-practice-content": "node scripts/best-practice-content.cjs",
"test:e2e": "playwright test"
},
"dependencies": {
"@astrojs/react": "^3.0.9",
"@astrojs/sitemap": "^3.0.5",
"@astrojs/tailwind": "^5.1.0",
"@fingerprintjs/fingerprintjs": "^4.2.1",
"@nanostores/react": "^0.7.1",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"astro": "^4.2.1",
"astro-compress": "^2.2.8",
"clsx": "^2.1.0",
"dracula-prism": "^2.1.13",
"jose": "^5.2.0",
"@astrojs/preact": "^2.2.1",
"@astrojs/sitemap": "^1.3.3",
"@astrojs/tailwind": "^3.1.3",
"@fingerprintjs/fingerprintjs": "^3.4.1",
"@nanostores/preact": "^0.5.0",
"astro": "^2.6.6",
"astro-compress": "^1.1.47",
"jose": "^4.14.4",
"js-cookie": "^3.0.5",
"lucide-react": "^0.300.0",
"nanoid": "^5.0.4",
"nanostores": "^0.9.5",
"node-html-parser": "^6.1.12",
"npm-check-updates": "^16.14.12",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-confetti": "^6.1.0",
"react-dom": "^18.2.0",
"reactflow": "^11.10.2",
"rehype-external-links": "^3.0.0",
"nanostores": "^0.9.2",
"node-html-parser": "^6.1.5",
"npm-check-updates": "^16.10.12",
"preact": "^10.15.1",
"rehype-external-links": "^2.1.0",
"roadmap-renderer": "^1.0.6",
"slugify": "^1.6.6",
"tailwind-merge": "^2.2.1",
"tailwindcss": "^3.4.1",
"zustand": "^4.5.0"
"tailwindcss": "^3.3.2"
},
"devDependencies": {
"@playwright/test": "^1.41.1",
"@tailwindcss/typography": "^0.5.10",
"@types/js-cookie": "^3.0.6",
"@types/prismjs": "^1.26.3",
"@playwright/test": "^1.35.1",
"@tailwindcss/typography": "^0.5.9",
"@types/js-cookie": "^3.0.3",
"csv-parser": "^3.0.0",
"gh-pages": "^6.1.1",
"gh-pages": "^5.0.0",
"js-yaml": "^4.1.0",
"markdown-it": "^14.0.0",
"openai": "^4.25.0",
"prettier": "^3.2.4",
"prettier-plugin-astro": "^0.12.3",
"prettier-plugin-tailwindcss": "^0.5.11"
"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"
}
}

4659
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,8 +0,0 @@
<svg width="63" height="24" viewBox="0 0 63 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="63" height="24" rx="7" fill="#563AFF"/>
<path d="M27.2629 16.7273H25.2856L28.2984 8H30.6763L33.6848 16.7273H31.7075L29.5214 9.99432H29.4533L27.2629 16.7273ZM27.1393 13.2969H31.8098V14.7372H27.1393V13.2969Z" fill="white"/>
<path d="M37.829 16.7273H34.7352V8H37.8545C38.7324 8 39.4881 8.17472 40.1216 8.52415C40.7551 8.87074 41.2423 9.36932 41.5832 10.0199C41.927 10.6705 42.0989 11.4489 42.0989 12.3551C42.0989 13.2642 41.927 14.0455 41.5832 14.6989C41.2423 15.3523 40.7523 15.8537 40.1131 16.2031C39.4767 16.5526 38.7153 16.7273 37.829 16.7273ZM36.5804 15.1463H37.7523C38.2977 15.1463 38.7565 15.0497 39.1287 14.8565C39.5037 14.6605 39.7849 14.358 39.9724 13.9489C40.1628 13.5369 40.2579 13.0057 40.2579 12.3551C40.2579 11.7102 40.1628 11.1832 39.9724 10.7741C39.7849 10.3651 39.5051 10.0639 39.1329 9.87074C38.7608 9.67756 38.302 9.58097 37.7565 9.58097H36.5804V15.1463Z" fill="white"/>
<path d="M46.5594 16.7273H43.4657V8H46.585C47.4628 8 48.2185 8.17472 48.8521 8.52415C49.4856 8.87074 49.9728 9.36932 50.3137 10.0199C50.6574 10.6705 50.8293 11.4489 50.8293 12.3551C50.8293 13.2642 50.6574 14.0455 50.3137 14.6989C49.9728 15.3523 49.4827 15.8537 48.8435 16.2031C48.2072 16.5526 47.4458 16.7273 46.5594 16.7273ZM45.3109 15.1463H46.4827C47.0282 15.1463 47.487 15.0497 47.8592 14.8565C48.2342 14.6605 48.5154 14.358 48.7029 13.9489C48.8932 13.5369 48.9884 13.0057 48.9884 12.3551C48.9884 11.7102 48.8932 11.1832 48.7029 10.7741C48.5154 10.3651 48.2356 10.0639 47.8634 9.87074C47.4913 9.67756 47.0324 9.58097 46.487 9.58097H45.3109V15.1463Z" fill="white"/>
<path d="M10 12H18" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 8V16" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,5 +0,0 @@
<svg width="89" height="24" viewBox="0 0 89 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="89" height="24" rx="7" fill="black"/>
<path d="M23.8217 17V7.54545H27.5518C28.2659 7.54545 28.8752 7.67318 29.38 7.92862C29.8878 8.18099 30.274 8.53954 30.5387 9.00426C30.8065 9.46591 30.9403 10.0091 30.9403 10.6339C30.9403 11.2617 30.8049 11.8018 30.5341 12.2543C30.2633 12.7036 29.8709 13.0483 29.3569 13.2884C28.846 13.5284 28.2274 13.6484 27.5011 13.6484H25.0036V12.0419H27.1779C27.5595 12.0419 27.8765 11.9896 28.1289 11.8849C28.3813 11.7803 28.569 11.6233 28.6921 11.4141C28.8183 11.2048 28.8814 10.9447 28.8814 10.6339C28.8814 10.32 28.8183 10.0553 28.6921 9.83984C28.569 9.62441 28.3797 9.46129 28.1243 9.3505C27.8719 9.23662 27.5534 9.17969 27.1687 9.17969H25.8207V17H23.8217ZM28.9276 12.6974L31.2773 17H29.0707L26.7717 12.6974H28.9276ZM32.353 17V7.54545H38.7237V9.19354H34.3519V11.4464H38.396V13.0945H34.3519V15.3519H38.7422V17H32.353ZM40.3129 7.54545H42.7781L45.3818 13.8977H45.4926L48.0963 7.54545H50.5615V17H48.6226V10.8462H48.5441L46.0974 16.9538H44.7771L42.3303 10.8232H42.2519V17H40.3129V7.54545ZM60.8967 12.2727C60.8967 13.3037 60.7012 14.1809 60.3104 14.9041C59.9226 15.6274 59.3932 16.1798 58.7223 16.5614C58.0545 16.94 57.3035 17.1293 56.4695 17.1293C55.6293 17.1293 54.8752 16.9384 54.2074 16.5568C53.5395 16.1752 53.0117 15.6228 52.6239 14.8995C52.2362 14.1763 52.0423 13.3007 52.0423 12.2727C52.0423 11.2417 52.2362 10.3646 52.6239 9.64134C53.0117 8.91809 53.5395 8.36719 54.2074 7.98864C54.8752 7.60701 55.6293 7.41619 56.4695 7.41619C57.3035 7.41619 58.0545 7.60701 58.7223 7.98864C59.3932 8.36719 59.9226 8.91809 60.3104 9.64134C60.7012 10.3646 60.8967 11.2417 60.8967 12.2727ZM58.87 12.2727C58.87 11.6049 58.77 11.0417 58.57 10.5831C58.373 10.1245 58.0945 9.77675 57.7344 9.53977C57.3743 9.30279 56.9527 9.1843 56.4695 9.1843C55.9863 9.1843 55.5646 9.30279 55.2045 9.53977C54.8445 9.77675 54.5644 10.1245 54.3643 10.5831C54.1674 11.0417 54.0689 11.6049 54.0689 12.2727C54.0689 12.9406 54.1674 13.5038 54.3643 13.9624C54.5644 14.4209 54.8445 14.7687 55.2045 15.0057C55.5646 15.2427 55.9863 15.3612 56.4695 15.3612C56.9527 15.3612 57.3743 15.2427 57.7344 15.0057C58.0945 14.7687 58.373 14.4209 58.57 13.9624C58.77 13.5038 58.87 12.9406 58.87 12.2727ZM63.5523 7.54545L65.8374 14.7287H65.9252L68.2149 7.54545H70.4308L67.1716 17H64.5956L61.3318 7.54545H63.5523ZM71.5688 17V7.54545H77.9395V9.19354H73.5677V11.4464H77.6118V13.0945H73.5677V15.3519H77.958V17H71.5688Z" fill="white"/>
<path d="M8 12L17 12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 203 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

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

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

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

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

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

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

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

Before

Width:  |  Height:  |  Size: 123 B

After

Width:  |  Height:  |  Size: 123 B

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

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

0
public/manifest/icon32.png Normal file → Executable 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.

Before

Width:  |  Height:  |  Size: 561 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 KiB

View File

@@ -9,8 +9,8 @@
<a href="https://roadmap.sh/best-practices">
<img src="https://img.shields.io/badge/%E2%9C%A8-Best%20Practices-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="best practices" />
</a>
<a href="https://roadmap.sh/questions">
<img src="https://img.shields.io/badge/%E2%9C%A8-Questions-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="videos" />
<a href="https://youtube.com/theroadmap?sub_confirmation=1">
<img src="https://img.shields.io/badge/%E2%9C%A8-Videos-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="videos" />
</a>
<a href="https://www.youtube.com/channel/UCA0H2KIWgWTwpTFjSxp0now?sub_confirmation=1">
<img src="https://img.shields.io/badge/%E2%9C%A8-YouTube%20Channel-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="roadmaps" />
@@ -24,7 +24,7 @@
Roadmaps are now interactive, you can click the nodes to read more about the topics.
### [View all Roadmaps](https://roadmap.sh) &nbsp;&middot;&nbsp; [Best Practices](https://roadmap.sh/best-practices) &nbsp;&middot;&nbsp; [Questions](https://roadmap.sh/questions)
### [View all Roadmaps](https://roadmap.sh)
![](https://i.imgur.com/waxVImv.png)
@@ -35,11 +35,8 @@ Here is the list of available roadmaps with more being actively worked upon.
- [DevOps Roadmap](https://roadmap.sh/devops) / [DevOps Beginner Roadmap](https://roadmap.sh/devops?r=devops-beginner)
- [Full Stack Roadmap](https://roadmap.sh/full-stack)
- [Computer Science Roadmap](https://roadmap.sh/computer-science)
- [AI and Data Scientist Roadmap](https://roadmap.sh/ai-data-scientist)
- [QA Roadmap](https://roadmap.sh/qa)
- [Python Roadmap](https://roadmap.sh/python)
- [Software Architect Roadmap](https://roadmap.sh/software-architect)
- [Game Developer Roadmap](https://roadmap.sh/game-developer) / [Server Side Game Developer](https://roadmap.sh/server-side-game-developer)
- [Software Design and Architecture Roadmap](https://roadmap.sh/software-design-architecture)
- [JavaScript Roadmap](https://roadmap.sh/javascript)
- [TypeScript Roadmap](https://roadmap.sh/typescript)
@@ -52,8 +49,8 @@ Here is the list of available roadmaps with more being actively worked upon.
- [GraphQL Roadmap](https://roadmap.sh/graphql)
- [Android Roadmap](https://roadmap.sh/android)
- [Flutter Roadmap](https://roadmap.sh/flutter)
- [Python Roadmap](https://roadmap.sh/python)
- [Go Roadmap](https://roadmap.sh/golang)
- [Rust Roadmap](https://roadmap.sh/rust)
- [Java Roadmap](https://roadmap.sh/java)
- [Spring Boot Roadmap](https://roadmap.sh/spring-boot)
- [Design System Roadmap](https://roadmap.sh/design-system)
@@ -68,20 +65,14 @@ Here is the list of available roadmaps with more being actively worked upon.
- [UX Design Roadmap](https://roadmap.sh/ux-design)
- [Docker Roadmap](https://roadmap.sh/docker)
- [Prompt Engineering Roadmap](https://roadmap.sh/prompt-engineering)
- [Technical Writer Roadmap](https://roadmap.sh/technical-writer)
There are also interactive best practices:
We have also added a new form of visual content covering 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

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

View File

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

View File

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

View File

@@ -19,12 +19,13 @@ if (!allowedRoadmapIds.includes(roadmapId)) {
}
const ROADMAP_CONTENT_DIR = path.join(ALL_ROADMAPS_DIR, roadmapId, 'content');
const OpenAI = require('openai');
const openai = new OpenAI({
const { Configuration, OpenAIApi } = require('openai');
const configuration = new Configuration({
apiKey: OPEN_AI_API_KEY,
});
const openai = new OpenAIApi(configuration);
function getFilesInFolder(folderPath, fileList = {}) {
const files = fs.readdirSync(folderPath);
@@ -59,16 +60,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 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.`;
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.`;
if (!childTopic) {
prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${parentTopic}". Write me a brief paragraph for that. 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.`;
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.`;
}
console.log(`Generating '${childTopic || parentTopic}'...`);
return new Promise((resolve, reject) => {
openai.chat.completions
.create({
openai
.createChatCompletion({
model: 'gpt-4',
messages: [
{
@@ -78,7 +79,7 @@ function writeTopicContent(currTopicUrl) {
],
})
.then((response) => {
const article = response.choices[0].message.content;
const article = response.data.choices[0].message.content;
resolve(article);
})
@@ -91,7 +92,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) {
@@ -137,14 +138,15 @@ async function writeFileForGroup(group, topicUrlToPathMapping) {
async function run() {
const topicUrlToPathMapping = getFilesInFolder(ROADMAP_CONTENT_DIR);
const roadmapJson = require(
path.join(ALL_ROADMAPS_DIR, `${roadmapId}/${roadmapId}`),
);
const roadmapJson = require(path.join(
ALL_ROADMAPS_DIR,
`${roadmapId}/${roadmapId}`
));
const groups = roadmapJson?.mockup?.controls?.control?.filter(
(control) =>
control.typeID === '__group__' &&
!control.properties?.controlName?.startsWith('ext_link'),
!control.properties?.controlName?.startsWith('ext_link')
);
if (!OPEN_AI_API_KEY) {

View File

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

View File

@@ -1,8 +1,6 @@
---
import AstroIcon from './AstroIcon.astro';
import { TeamDropdown } from './TeamDropdown/TeamDropdown';
import { SidebarFriendsCounter } from './Friends/SidebarFriendsCounter';
import { Map } from 'lucide-react';
export interface Props {
activePageId: string;
@@ -23,32 +21,11 @@ const sidebarLinks = [
classes: 'h-3 w-4',
},
},
{
href: '/account/friends',
title: 'Friends',
id: 'friends',
isNew: false,
icon: {
glyph: 'users',
classes: 'h-4 w-4',
},
},
{
href: '/account/roadmaps',
title: 'Roadmaps',
id: 'roadmaps',
isNew: true,
icon: {
glyph: 'users',
classes: 'h-4 w-4',
component: Map,
},
},
{
href: '/account/road-card',
title: 'Card',
id: 'road-card',
isNew: false,
isNew: true,
icon: {
glyph: 'badge',
classes: 'h-4 w-4',
@@ -89,17 +66,17 @@ const sidebarLinks = [
id='settings-menu-dropdown'
class='absolute left-0 right-0 z-10 mt-1 hidden space-y-1.5 bg-white p-2 shadow-lg'
>
<li>
<a
href='/team'
class={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200 ${
activePageId === 'team' ? 'bg-slate-100' : ''
}`}
>
<AstroIcon icon={'users'} class={`h-4 w-4 mr-2`} />
Teams
</a>
</li>
<!--<li>-->
<!-- <a-->
<!-- href='/team'-->
<!-- class={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200 ${-->
<!-- activePageId === 'team' ? 'bg-slate-100' : ''-->
<!-- }`}-->
<!-- >-->
<!-- <AstroIcon icon={'users'} class={`h-4 w-4 mr-2`} />-->
<!-- Teams-->
<!-- </a>-->
<!--</li>-->
{
sidebarLinks.map((sidebarLink) => {
const isActive = activePageId === sidebarLink.id;
@@ -112,16 +89,10 @@ const sidebarLinks = [
isActive ? 'bg-slate-100' : ''
}`}
>
{sidebarLink.icon.component ? (
<sidebarLink.icon.component
className={`${sidebarLink.icon.classes} mr-2`}
/>
) : (
<AstroIcon
icon={sidebarLink.icon.glyph}
class={`${sidebarLink.icon.classes} mr-2`}
/>
)}
<AstroIcon
icon={sidebarLink.icon.glyph}
class={`${sidebarLink.icon.classes} mr-2`}
/>
{sidebarLink.title}
</a>
</li>
@@ -154,16 +125,10 @@ const sidebarLinks = [
}`}
>
<span class='flex flex-grow items-center'>
{sidebarLink.icon.component ? (
<sidebarLink.icon.component
className={`${sidebarLink.icon.classes} mr-2`}
/>
) : (
<AstroIcon
icon={sidebarLink.icon.glyph}
class={`${sidebarLink.icon.classes} mr-2`}
/>
)}
<AstroIcon
icon={sidebarLink.icon.glyph}
class={`${sidebarLink.icon.classes} mr-2`}
/>
{sidebarLink.title}
</span>
@@ -173,10 +138,6 @@ const sidebarLinks = [
<span class='absolute bottom-0 left-0 right-0 top-0 animate-ping rounded-full bg-gray-400 p-1 text-xs' />
</span>
)}
{sidebarLink.id === 'friends' && (
<SidebarFriendsCounter client:load />
)}
</a>
</li>
);
@@ -188,12 +149,7 @@ const sidebarLinks = [
}
<!-- /End Desktop Sidebar -->
<div
class:list={[
'grow px-0 py-0 md:py-10',
{ 'md:px-10': hasDesktopSidebar, 'md:px-5': !hasDesktopSidebar },
]}
>
<div class:list={['grow px-0 py-0 md:py-10', { 'md:px-10': hasDesktopSidebar, 'md:px-5': !hasDesktopSidebar }]}>
<slot />
</div>
</div>

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,7 @@
import { useState } from 'preact/hooks';
import { httpPost } from '../../lib/http';
import { getRelativeTimeString } from '../../lib/date';
import { useToast } from '../../hooks/use-toast';
import { ProgressShareButton } from '../UserProgress/ProgressShareButton';
import { useState } from 'react';
import { getUser } from '../../lib/jwt';
type ResourceProgressType = {
resourceType: 'roadmap' | 'best-practice';
@@ -16,17 +14,14 @@ type ResourceProgressType = {
skippedCount: number;
onCleared?: () => void;
showClearButton?: boolean;
isCustomResource: boolean;
};
export function ResourceProgress(props: ResourceProgressType) {
const { showClearButton = true, isCustomResource } = props;
const { showClearButton = true } = props;
const toast = useToast();
const [isClearing, setIsClearing] = useState(false);
const [isConfirming, setIsConfirming] = useState(false);
const userId = getUser()?.id;
const {
updatedAt,
resourceType,
@@ -56,8 +51,8 @@ export function ResourceProgress(props: ResourceProgressType) {
return;
}
localStorage.removeItem(`${resourceType}-${resourceId}-${userId}-favorite`);
localStorage.removeItem(`${resourceType}-${resourceId}-${userId}-progress`);
localStorage.removeItem(`${resourceType}-${resourceId}-favorite`);
localStorage.removeItem(`${resourceType}-${resourceId}-progress`);
setIsClearing(false);
setIsConfirming(false);
@@ -66,15 +61,11 @@ export function ResourceProgress(props: ResourceProgressType) {
}
}
let url =
const url =
resourceType === 'roadmap'
? `/${resourceId}`
: `/best-practices/${resourceId}`;
if (isCustomResource) {
url = `/r?id=${resourceId}`;
}
const totalMarked = doneCount + skippedCount;
const progressPercentage = Math.round((totalMarked / totalCount) * 100);
@@ -97,7 +88,7 @@ export function ResourceProgress(props: ResourceProgressType) {
{getRelativeTimeString(updatedAt)}
</span>
</a>
<div className="sm:space-between flex flex-row items-start rounded-b-md border border-t-0 px-2 py-2 text-xs text-gray-500">
<p className="sm:space-between flex flex-row items-start rounded-b-md border border-t-0 px-2 py-2 text-xs text-gray-500">
<span className="hidden flex-1 gap-1 sm:flex">
{doneCount > 0 && (
<>
@@ -116,56 +107,44 @@ export function ResourceProgress(props: ResourceProgressType) {
)}
<span>{totalCount} total</span>
</span>
<div className="flex w-full items-center justify-between gap-2 sm:w-auto sm:justify-start">
<ProgressShareButton
resourceType={resourceType}
resourceId={resourceId}
isCustomResource={isCustomResource}
className="text-xs font-normal"
shareIconClassName="w-2.5 h-2.5 stroke-2"
checkIconClassName="w-2.5 h-2.5"
/>
<span className={'hidden sm:block'}>&bull;</span>
{showClearButton && (
<>
{!isConfirming && (
<button
className="text-red-500 hover:text-red-800"
onClick={() => setIsConfirming(true)}
disabled={isClearing}
>
{!isClearing && (
<>
Clear Progress <span>&times;</span>
</>
)}
{showClearButton && (
<>
{!isConfirming && (
{isClearing && 'Processing...'}
</button>
)}
{isConfirming && (
<span>
Are you sure?{' '}
<button
className="text-red-500 hover:text-red-800"
onClick={() => setIsConfirming(true)}
disabled={isClearing}
onClick={clearProgress}
className="ml-1 mr-1 text-red-500 underline hover:text-red-800"
>
{!isClearing && (
<>
Clear Progress <span>&times;</span>
</>
)}
{isClearing && 'Processing...'}
Yes
</button>{' '}
<button
onClick={() => setIsConfirming(false)}
className="text-red-500 underline hover:text-red-800"
>
No
</button>
)}
{isConfirming && (
<span>
Are you sure?{' '}
<button
onClick={clearProgress}
className="ml-1 mr-1 text-red-500 underline hover:text-red-800"
>
Yes
</button>{' '}
<button
onClick={() => setIsConfirming(false)}
className="text-red-500 underline hover:text-red-800"
>
No
</button>
</span>
)}
</>
)}
</div>
</div>
</span>
)}
</>
)}
</p>
</div>
);
}

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,19 @@
import Cookies from 'js-cookie';
import type { FormEvent } from 'react';
import { useState } from 'react';
import type { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import { httpPost } from '../../lib/http';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
type EmailLoginFormProps = {
isDisabled?: boolean;
setIsDisabled?: (isDisabled: boolean) => void;
};
export function EmailLoginForm(props: EmailLoginFormProps) {
const { isDisabled, setIsDisabled } = props;
const EmailLoginForm: FunctionComponent<{}> = () => {
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
const handleFormSubmit = async (e: Event) => {
e.preventDefault();
setIsLoading(true);
setIsDisabled?.(true);
setError('');
const { response, error } = await httpPost<{ token: string }>(
@@ -29,7 +21,7 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
{
email,
password,
},
}
);
// Log the user in and reload the page
@@ -37,7 +29,6 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
path: '/',
expires: 30,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
window.location.reload();
@@ -47,13 +38,12 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
// @todo use proper types
if ((error as any).type === 'user_not_verified') {
window.location.href = `/verification-pending?email=${encodeURIComponent(
email,
email
)}`;
return;
}
setIsLoading(false);
setIsDisabled?.(false);
setError(error?.message || 'Something went wrong. Please try again later.');
};
@@ -86,7 +76,7 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
onInput={(e) => setPassword(String((e.target as any).value))}
/>
<p className="mb-3 mt-2 text-sm text-gray-500">
<p class="mb-3 mt-2 text-sm text-gray-500">
<a
href="/forgot-password"
className="text-blue-800 hover:text-blue-600"
@@ -101,11 +91,13 @@ export function EmailLoginForm(props: EmailLoginFormProps) {
<button
type="submit"
disabled={isLoading || isDisabled}
disabled={isLoading}
className="inline-flex w-full items-center justify-center rounded-lg bg-black p-2 py-3 text-sm font-medium text-white outline-none focus:ring-2 focus:ring-black focus:ring-offset-1 disabled:bg-gray-400"
>
{isLoading ? 'Please wait...' : 'Continue'}
</button>
</form>
);
}
};
export default EmailLoginForm;

View File

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

View File

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

View File

@@ -1,23 +1,20 @@
import { useEffect, useState } from 'react';
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
import { useEffect, useState } from 'preact/hooks';
import GitHubIcon from '../../icons/github.svg';
import SpinnerIcon from '../../icons/spinner.svg';
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 = {
isDisabled?: boolean;
setIsDisabled?: (isDisabled: boolean) => void;
};
type GitHubButtonProps = {};
const GITHUB_REDIRECT_AT = 'githubRedirectAt';
const GITHUB_LAST_PAGE = 'githubLastPage';
export function GitHubButton(props: GitHubButtonProps) {
const { isDisabled, setIsDisabled } = props;
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const icon = isLoading ? SpinnerIcon : GitHubIcon;
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
@@ -30,18 +27,16 @@ export function GitHubButton(props: GitHubButtonProps) {
}
setIsLoading(true);
setIsDisabled?.(true);
httpGet<{ token: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-github-callback${
window.location.search
}`,
}`
)
.then(({ response, error }) => {
if (!response?.token) {
const errMessage = error?.message || 'Something went wrong.';
setError(errMessage);
setIsLoading(false);
setIsDisabled?.(false);
return;
}
@@ -62,54 +57,43 @@ export function GitHubButton(props: GitHubButtonProps) {
}
}
const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) {
localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl;
}
localStorage.removeItem(GITHUB_REDIRECT_AT);
localStorage.removeItem(GITHUB_LAST_PAGE);
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
path: '/',
expires: 30,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
window.location.href = redirectUrl;
})
.catch((err) => {
setError('Something went wrong. Please try again later.');
setIsLoading(false);
setIsDisabled?.(false);
});
}, []);
const handleClick = async () => {
setIsLoading(true);
setIsDisabled?.(true);
const { response, error } = await httpGet<{ loginUrl: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-github-login`,
`${import.meta.env.PUBLIC_API_URL}/v1-github-login`
);
if (error || !response?.loginUrl) {
setError(
error?.message || 'Something went wrong. Please try again later.',
error?.message || 'Something went wrong. Please try again later.'
);
setIsLoading(false);
setIsDisabled?.(false);
return;
}
// For non authentication pages, we want to redirect back to the page
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname,
)
? window.location.pathname + window.location.search
: window.location.pathname;
const pagePath =
window.location.pathname === '/respond-invite'
? window.location.pathname + window.location.search
: window.location.pathname;
localStorage.setItem(GITHUB_REDIRECT_AT, Date.now().toString());
localStorage.setItem(GITHUB_LAST_PAGE, pagePath);
@@ -121,15 +105,15 @@ export function GitHubButton(props: GitHubButtonProps) {
return (
<>
<button
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoading || isDisabled}
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoading}
onClick={handleClick}
>
{isLoading ? (
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
) : (
<GitHubIcon className={'h-[18px] w-[18px]'} />
)}
<img
src={icon}
alt="GitHub"
class={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>
Continue with GitHub
</button>
{error && (

View File

@@ -1,23 +1,19 @@
import { useEffect, useState } from 'react';
import { useEffect, useState } from 'preact/hooks';
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 = {
isDisabled?: boolean;
setIsDisabled?: (isDisabled: boolean) => void;
};
type GoogleButtonProps = {};
const GOOGLE_REDIRECT_AT = 'googleRedirectAt';
const GOOGLE_LAST_PAGE = 'googleLastPage';
export function GoogleButton(props: GoogleButtonProps) {
const { isDisabled, setIsDisabled } = props;
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const icon = isLoading ? SpinnerIcon : GoogleIcon;
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
@@ -30,17 +26,15 @@ export function GoogleButton(props: GoogleButtonProps) {
}
setIsLoading(true);
setIsDisabled?.(true);
httpGet<{ token: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-google-callback${
window.location.search
}`,
}`
)
.then(({ response, error }) => {
if (!response?.token) {
setError(error?.message || 'Something went wrong.');
setIsLoading(false);
setIsDisabled?.(false);
return;
}
@@ -61,39 +55,29 @@ export function GoogleButton(props: GoogleButtonProps) {
}
}
const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) {
localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl;
}
localStorage.removeItem(GOOGLE_REDIRECT_AT);
localStorage.removeItem(GOOGLE_LAST_PAGE);
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
path: '/',
expires: 30,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
window.location.href = redirectUrl;
})
.catch((err) => {
setError('Something went wrong. Please try again later.');
setIsLoading(false);
setIsDisabled?.(false);
});
}, []);
const handleClick = () => {
setIsLoading(true);
setIsDisabled?.(true);
httpGet<{ loginUrl: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-google-login`,
`${import.meta.env.PUBLIC_API_URL}/v1-google-login`
)
.then(({ response, error }) => {
if (!response?.loginUrl) {
setError(error?.message || 'Something went wrong.');
setIsLoading(false);
setIsDisabled?.(false);
return;
}
@@ -101,11 +85,10 @@ export function GoogleButton(props: GoogleButtonProps) {
// For non authentication pages, we want to redirect back to the page
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname,
)
? window.location.pathname + window.location.search
: window.location.pathname;
const pagePath =
window.location.pathname === '/respond-invite'
? window.location.pathname + window.location.search
: window.location.pathname;
localStorage.setItem(GOOGLE_REDIRECT_AT, Date.now().toString());
localStorage.setItem(GOOGLE_LAST_PAGE, pagePath);
@@ -116,22 +99,21 @@ export function GoogleButton(props: GoogleButtonProps) {
.catch((err) => {
setError('Something went wrong. Please try again later.');
setIsLoading(false);
setIsDisabled?.(false);
});
};
return (
<>
<button
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoading || isDisabled}
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoading}
onClick={handleClick}
>
{isLoading ? (
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
) : (
<GoogleIcon className={'h-[18px] w-[18px]'} />
)}
<img
src={icon}
alt="Google"
class={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>
Continue with Google
</button>
{error && (

View File

@@ -1,23 +1,19 @@
import { useEffect, useState } from 'react';
import { useEffect, useState } from 'preact/hooks';
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 = {
isDisabled?: boolean;
setIsDisabled?: (isDisabled: boolean) => void;
};
type LinkedInButtonProps = {};
const LINKEDIN_REDIRECT_AT = 'linkedInRedirectAt';
const LINKEDIN_LAST_PAGE = 'linkedInLastPage';
export function LinkedInButton(props: LinkedInButtonProps) {
const { isDisabled, setIsDisabled } = props;
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const icon = isLoading ? SpinnerIcon : LinkedIn;
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
@@ -30,17 +26,15 @@ export function LinkedInButton(props: LinkedInButtonProps) {
}
setIsLoading(true);
setIsDisabled?.(true);
httpGet<{ token: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-callback${
window.location.search
}`,
}`
)
.then(({ response, error }) => {
if (!response?.token) {
setError(error?.message || 'Something went wrong.');
setIsLoading(false);
setIsDisabled?.(false);
return;
}
@@ -61,39 +55,29 @@ export function LinkedInButton(props: LinkedInButtonProps) {
}
}
const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) {
localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl;
}
localStorage.removeItem(LINKEDIN_REDIRECT_AT);
localStorage.removeItem(LINKEDIN_LAST_PAGE);
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
path: '/',
expires: 30,
domain: import.meta.env.DEV ? 'localhost' : '.roadmap.sh',
});
window.location.href = redirectUrl;
})
.catch((err) => {
setError('Something went wrong. Please try again later.');
setIsLoading(false);
setIsDisabled?.(false);
});
}, []);
const handleClick = () => {
setIsLoading(true);
setIsDisabled?.(true);
httpGet<{ loginUrl: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-login`,
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-login`
)
.then(({ response, error }) => {
if (!response?.loginUrl) {
setError(error?.message || 'Something went wrong.');
setIsLoading(false);
setIsDisabled?.(false);
return;
}
@@ -101,11 +85,10 @@ export function LinkedInButton(props: LinkedInButtonProps) {
// For non authentication pages, we want to redirect back to the page
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname,
)
? window.location.pathname + window.location.search
: window.location.pathname;
const pagePath =
window.location.pathname === '/respond-invite'
? window.location.pathname + window.location.search
: window.location.pathname;
localStorage.setItem(LINKEDIN_REDIRECT_AT, Date.now().toString());
localStorage.setItem(LINKEDIN_LAST_PAGE, pagePath);
@@ -116,22 +99,21 @@ export function LinkedInButton(props: LinkedInButtonProps) {
.catch((err) => {
setError('Something went wrong. Please try again later.');
setIsLoading(false);
setIsDisabled?.(false);
});
};
return (
<>
<button
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoading || isDisabled}
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded border border-slate-300 bg-white p-2 text-sm font-medium text-black outline-none focus:ring-2 focus:ring-[#333] focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoading}
onClick={handleClick}
>
{isLoading ? (
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
) : (
<LinkedInIcon className={'h-[18px] w-[18px]'} />
)}
<img
src={icon}
alt="Google"
class={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>
Continue with LinkedIn
</button>
{error && (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,368 +0,0 @@
import { useEffect, useState } from 'react';
import { httpDelete, httpGet, httpPost } from '../lib/http';
import { pageProgressMessage } from '../stores/page';
import { isLoggedIn } from '../lib/jwt';
import { showLoginPopup } from '../lib/popup';
import { getUrlParams } from '../lib/browser';
import { CheckIcon } from './ReactIcons/CheckIcon';
import { DeleteUserIcon } from './ReactIcons/DeleteUserIcon';
import { useToast } from '../hooks/use-toast';
import { useAuth } from '../hooks/use-auth';
import { AddedUserIcon } from './ReactIcons/AddedUserIcon';
import { StopIcon } from './ReactIcons/StopIcon';
import { ErrorIcon } from './ReactIcons/ErrorIcon';
export type FriendshipStatus =
| 'none'
| 'sent'
| 'received'
| 'accepted'
| 'rejected'
| 'got_rejected';
type UserResponse = {
id: string;
links: Record<string, string>;
avatar: string;
name: string;
status: FriendshipStatus;
};
export function Befriend() {
const { u: inviteId } = getUrlParams();
const toast = useToast();
const currentUser = useAuth();
const [isConfirming, setIsConfirming] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
const [user, setUser] = useState<UserResponse>();
const isAuthenticated = isLoggedIn();
async function loadUser(userId: string) {
const { response, error } = await httpGet<UserResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-friend/${userId}`
);
if (error || !response) {
setError(error?.message || 'Something went wrong');
return;
}
if (response.status === 'accepted') {
window.location.href = '/account/friends?c=fa';
return;
}
setUser(response);
}
useEffect(() => {
if (inviteId) {
loadUser(inviteId).finally(() => {
pageProgressMessage.set('');
setIsLoading(false);
});
} else {
setIsLoading(false);
setError('Missing invite ID in URL');
pageProgressMessage.set('');
}
}, [inviteId]);
async function addFriend(userId: string, successMessage: string) {
pageProgressMessage.set('Please wait...');
setError('');
const { response, error } = await httpPost<UserResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-add-friend/${userId}`,
{}
);
if (error || !response) {
setError(error?.message || 'Something went wrong');
return;
}
if (response.status === 'accepted') {
window.location.href = '/account/friends?c=fa';
return;
}
setUser(response);
}
async function deleteFriend(userId: string, successMessage: string) {
pageProgressMessage.set('Please wait...');
setError('');
const { response, error } = await httpDelete<UserResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-delete-friend/${userId}`,
{}
);
if (error || !response) {
setError(error?.message || 'Something went wrong');
return;
}
setUser(response);
toast.success(successMessage);
}
if (isLoading) {
return null;
}
if (!user) {
return (
<div className="container text-center">
<ErrorIcon additionalClasses="mx-auto mb-4 mt-24 w-20 opacity-20" />
<h2 className={'mb-1 text-2xl font-bold'}>Error</h2>
<p className="mb-4 text-base leading-6 text-gray-600">
{error || 'There was a problem, please try again.'}
</p>
<div>
<a
href="/"
className="flex-grow cursor-pointer rounded-lg bg-gray-200 px-3 py-2 text-center"
>
Back to home
</a>
</div>
</div>
);
}
const userAvatar = user.avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${user.avatar}`
: '/images/default-avatar.png';
const isMe = currentUser?.id === user.id;
return (
<div className="container !max-w-[400px] text-center">
<img
alt={'join team'}
src={userAvatar}
className="mx-auto mb-4 mt-24 w-28 rounded-full"
/>
<h2 className={'mb-1 text-3xl font-bold'}>{user.name}</h2>
<p className="mb-6 text-base leading-6 text-gray-600">
After you add {user.name} as a friend, you will be able to view each
other's skills and progress.
</p>
<div className="mx-auto w-full duration-500 sm:max-w-md">
<div className="flex w-full flex-col items-center gap-2">
{user.status === 'none' && (
<button
disabled={isMe}
onClick={() => {
if (!isAuthenticated) {
return showLoginPopup();
}
addFriend(user.id, 'Friend request sent').finally(() => {
pageProgressMessage.set('');
});
}}
type="button"
className="w-full flex-grow cursor-pointer rounded-lg bg-black px-3 py-2 text-center text-white disabled:cursor-not-allowed disabled:opacity-40"
>
{isMe ? "You can't add yourself" : 'Add Friend'}
</button>
)}
{user.status === 'sent' && (
<>
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<CheckIcon additionalClasses="mr-2 h-4 w-4" />
Request Sent
</span>
{!isConfirming && (
<button
onClick={() => {
setIsConfirming(true);
}}
type="button"
className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700"
>
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Withdraw Request
</button>
)}
{isConfirming && (
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '}
<button
className="ml-2 text-red-700 underline"
onClick={() => {
deleteFriend(user.id, 'Friend request withdrawn').finally(
() => {
pageProgressMessage.set('');
}
);
}}
>
Yes
</button>{' '}
<button
onClick={() => {
setIsConfirming(false);
}}
className="ml-2 text-red-600 underline"
>
No
</button>
</span>
)}
</>
)}
{user.status === 'accepted' && (
<>
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<AddedUserIcon additionalClasses="mr-2 h-5 w-5" />
You are friends
</span>
{!isConfirming && (
<button
onClick={() => {
setIsConfirming(true);
}}
type="button"
className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700"
>
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Remove Friend
</button>
)}
{isConfirming && (
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '}
<button
className="ml-2 text-red-700 underline"
onClick={() => {
deleteFriend(user.id, 'Friend removed').finally(() => {
pageProgressMessage.set('');
});
}}
>
Yes
</button>{' '}
<button
onClick={() => {
setIsConfirming(false);
}}
className="ml-2 text-red-600 underline"
>
No
</button>
</span>
)}
</>
)}
{user.status === 'rejected' && (
<>
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 px-3 py-2 text-center text-black">
<DeleteUserIcon additionalClasses="mr-2 h-4 w-4" />
Request Rejected
</span>
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Changed your mind?{' '}
<button
className="ml-2 text-red-700 underline"
onClick={() => {
addFriend(user.id, 'Friend request accepted').finally(
() => {
pageProgressMessage.set('');
}
);
}}
>
Click here to Accept
</button>
</span>
</>
)}
{user.status === 'got_rejected' && (
<>
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-500 px-3 py-2 text-center text-red-500">
<StopIcon additionalClasses="mr-2 h-4 w-4" />
Request Rejected
</span>
</>
)}
{user.status === 'received' && (
<>
<button
onClick={() => {
addFriend(user.id, 'Friend request accepted').finally(() => {
pageProgressMessage.set('');
});
}}
className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-gray-800 bg-gray-800 px-3 py-2 text-center text-white hover:bg-black"
>
<CheckIcon additionalClasses="mr-2 h-4 w-4" />
Accept Request
</button>
{!isConfirming && (
<button
onClick={() => {
setIsConfirming(true);
}}
type="button"
className="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-white px-3 py-2 text-center text-red-600 hover:bg-red-100"
>
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Reject Request
</button>
)}
{isConfirming && (
<span className="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '}
<button
className="ml-2 text-red-700 underline"
onClick={() => {
deleteFriend(user.id, 'Friend request rejected').finally(
() => {
pageProgressMessage.set('');
}
);
}}
>
Yes
</button>{' '}
<button
onClick={() => {
setIsConfirming(false);
}}
className="ml-2 text-red-600 underline"
>
No
</button>
</span>
)}
</>
)}
</div>
</div>
{error && (
<p className="mt-2 rounded-lg bg-red-100 p-2 text-red-700">{error}</p>
)}
</div>
);
}

View File

@@ -3,9 +3,8 @@ import ResourceProgressStats from './ResourceProgressStats.astro';
export interface Props {
bestPracticeId: string;
}
const { bestPracticeId } = Astro.props;
---
<div class='mt-4 sm:mt-7 border-0 sm:border rounded-md mb-0 sm:-mb-[65px]'>
<ResourceProgressStats resourceId={bestPracticeId} resourceType='best-practice' />
<ResourceProgressStats />
</div>

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useEffect, useState } from 'preact/hooks';
import { Stepper } from '../Stepper';
import { Step0, type ValidTeamType } from './Step0';
import { Step1, type ValidTeamSize } from './Step1';
import { Step0, ValidTeamType } from './Step0';
import { Step1, ValidTeamSize } from './Step1';
import { Step2 } from './Step2';
import { httpGet } from '../../lib/http';
import { getUrlParams, setUrlParams } from '../../lib/browser';
@@ -190,14 +190,14 @@ export function CreateTeamForm() {
}
return (
<div className={'mx-auto max-w-[700px] py-1 md:py-6'}>
<div className={'mb-3 md:mb-8 pb-3 md:pb-0 border-b md:border-b-0 flex flex-col items-start md:items-center'}>
<h1 className={'text-xl md:text-4xl font-bold'}>Create Team</h1>
<p className={'mt-1 md:mt-2 text-sm md:text-base text-gray-500'}>
<div className={'mx-auto max-w-[700px] py-6'}>
<div className={'mb-8 flex flex-col items-center'}>
<h1 className={'text-4xl font-bold'}>Create Team</h1>
<p className={'mt-2 text-gray-500'}>
Complete the steps below to create your team
</p>
</div>
<div className="mb-8 mt-8 hidden sm:flex w-full">
<div className="mb-8 mt-8 flex w-full">
<Stepper
activeIndex={stepIndex}
completeSteps={completedSteps}

View File

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

View File

@@ -1,43 +0,0 @@
import { ChevronDownIcon } from '../ReactIcons/ChevronDownIcon';
type NotDropdownProps = {
onClick: () => void;
selectedCount: number;
singularName: string;
pluralName: string;
};
export function NotDropdown(props: NotDropdownProps) {
const { onClick, selectedCount, singularName, pluralName } = props;
const singularOrPlural = selectedCount === 1 ? singularName : pluralName;
return (
<div
className="flex cursor-text items-center justify-between rounded-md border border-gray-300 px-3 py-2.5 hover:border-gray-400/50 hover:bg-gray-50"
role="button"
onClick={onClick}
>
{selectedCount > 0 && (
<div className="flex flex-col">
<p className="mb-1.5 text-base font-medium text-gray-800">
{selectedCount} {singularOrPlural} selected
</p>
<p className="text-sm text-gray-400">
Click to add or change selection
</p>
</div>
)}
{selectedCount === 0 && (
<div className="flex flex-col">
<p className="text-base text-gray-400">
Click to select {pluralName}
</p>
</div>
)}
<ChevronDownIcon className="relative top-[1px] h-[17px] w-[17px] opacity-40" />
</div>
);
}

View File

@@ -1,53 +1,35 @@
import { useEffect, useState } from 'react';
import { useEffect, useState } from 'preact/hooks';
import { SearchSelector } from '../SearchSelector';
import { httpGet, httpPut } from '../../lib/http';
import type { PageType } from '../CommandMenu/CommandMenu';
import SearchIcon from '../../icons/search.svg';
import { pageProgressMessage } from '../../stores/page';
import type { TeamDocument } from './CreateTeamForm';
import { UpdateTeamResourceModal } from './UpdateTeamResourceModal';
import { SelectRoadmapModal } from './SelectRoadmapModal';
import { Map, Shapes } from 'lucide-react';
import type {
AllowedRoadmapVisibility,
RoadmapDocument,
} from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { useToast } from '../../hooks/use-toast';
export type TeamResourceConfig = {
isCustomResource: boolean;
title: string;
description?: string;
visibility?: AllowedRoadmapVisibility;
resourceId: string;
resourceType: string;
removed: string[];
topics?: number;
sharedTeamMemberIds: string[];
sharedFriendIds: string[];
}[];
type RoadmapSelectorProps = {
teamId: string;
teamResources: TeamResourceConfig;
setTeamResources: (config: TeamResourceConfig) => void;
team: TeamDocument;
teamResourceConfig: TeamResourceConfig;
setTeamResourceConfig: (config: TeamResourceConfig) => void;
};
export function RoadmapSelector(props: RoadmapSelectorProps) {
const { teamId, teamResources = [], setTeamResources } = props;
const { team, teamResourceConfig = [], setTeamResourceConfig } = props;
const toast = useToast();
const [removingRoadmapId, setRemovingRoadmapId] = useState<string>('');
const [showSelectRoadmapModal, setShowSelectRoadmapModal] = useState(false);
const [allRoadmaps, setAllRoadmaps] = useState<PageType[]>([]);
const [changingRoadmapId, setChangingRoadmapId] = useState<string>('');
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState<boolean>(false);
const [error, setError] = useState<string>('');
async function loadAllRoadmaps() {
const { error, response } = await httpGet<PageType[]>(`/pages.json`);
if (error) {
toast.error(error.message || 'Something went wrong. Please try again!');
setError(error.message || 'Something went wrong. Please try again!');
return;
}
@@ -68,15 +50,15 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
}
async function deleteResource(roadmapId: string) {
if (!teamId) {
if (!team?._id) {
return;
}
pageProgressMessage.set(`Deleting resource`);
const { error, response } = await httpPut<TeamResourceConfig>(
`${
import.meta.env.PUBLIC_API_URL
}/v1-delete-team-resource-config/${teamId}`,
`${import.meta.env.PUBLIC_API_URL}/v1-delete-team-resource-config/${
team._id
}`,
{
resourceId: roadmapId,
resourceType: 'roadmap',
@@ -88,7 +70,7 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
return;
}
setTeamResources(response);
setTeamResourceConfig(response);
}
async function onRemove(resourceId: string) {
@@ -100,17 +82,17 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
}
async function addTeamResource(roadmapId: string) {
if (!teamId) {
if (!team?._id) {
return;
}
pageProgressMessage.set(`Adding roadmap to team`);
const { error, response } = await httpPut<TeamResourceConfig>(
`${
import.meta.env.PUBLIC_API_URL
}/v1-update-team-resource-config/${teamId}`,
`${import.meta.env.PUBLIC_API_URL}/v1-update-team-resource-config/${
team._id
}`,
{
teamId: teamId,
teamId: team._id,
resourceId: roadmapId,
resourceType: 'roadmap',
removed: [],
@@ -122,25 +104,13 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
return;
}
setTeamResources(response);
setTeamResourceConfig(response);
}
useEffect(() => {
loadAllRoadmaps().finally(() => {});
loadAllRoadmaps().finally();
}, []);
function handleCustomRoadmapCreated(roadmap: RoadmapDocument) {
const { _id: roadmapId } = roadmap;
if (!roadmapId) {
return;
}
loadAllRoadmaps().finally(() => {});
addTeamResource(roadmapId).finally(() => {
pageProgressMessage.set('');
});
}
return (
<div>
{changingRoadmapId && (
@@ -148,195 +118,102 @@ export function RoadmapSelector(props: RoadmapSelectorProps) {
onClose={() => setChangingRoadmapId('')}
resourceId={changingRoadmapId}
resourceType={'roadmap'}
teamId={teamId}
setTeamResourceConfig={setTeamResources}
teamId={team?._id!}
setTeamResourceConfig={setTeamResourceConfig}
defaultRemovedItems={
teamResources.find((c) => c.resourceId === changingRoadmapId)
teamResourceConfig.find((c) => c.resourceId === changingRoadmapId)
?.removed || []
}
/>
)}
{showSelectRoadmapModal && (
<SelectRoadmapModal
onClose={() => setShowSelectRoadmapModal(false)}
teamResourceConfig={teamResources}
allRoadmaps={allRoadmaps}
teamId={teamId}
onRoadmapAdd={(roadmapId) => {
addTeamResource(roadmapId).finally(() => {
pageProgressMessage.set('');
});
}}
onRoadmapRemove={(roadmapId) => {
onRemove(roadmapId).finally(() => {});
}}
/>
)}
<div className="my-3 flex items-center gap-4">
{isCreatingRoadmap && (
<CreateRoadmapModal
teamId={teamId}
onClose={() => setIsCreatingRoadmap(false)}
onCreated={(roadmap: RoadmapDocument) => {
handleCustomRoadmapCreated(roadmap);
setIsCreatingRoadmap(false);
}}
<SearchSelector
placeholder={`Search Roadmaps ..`}
onSelect={(option) => {
const roadmapId = option.value;
addTeamResource(roadmapId).finally(() => {
pageProgressMessage.set('');
});
}}
options={allRoadmaps
.filter((roadmap) => {
return !teamResourceConfig
.map((c) => c.resourceId)
.includes(roadmap.id);
})
.map((roadmap) => ({
value: roadmap.id,
label: roadmap.title,
}))}
searchInputId={'roadmap-input'}
inputClassName="mt-2 block w-full rounded-md border px-3 py-2 shadow-sm outline-none placeholder:text-gray-400 focus:ring-2 focus:ring-black focus:ring-offset-1"
/>
{!teamResourceConfig.length && (
<div className="mt-4 rounded-md border px-4 py-12 text-center text-sm text-gray-700">
<img
alt={'search'}
src={SearchIcon}
className={'mx-auto mb-5 h-[42px] w-[42px] opacity-10'}
/>
)}
<button
className="flex h-10 grow items-center justify-center gap-2 rounded-md border border-black bg-white text-black transition-colors hover:bg-black hover:text-white"
onClick={() => {
setShowSelectRoadmapModal(true);
}}
>
<Map className="h-4 w-4 stroke-[2.5]" />
Pick from our roadmaps
</button>
<span className="text-base text-gray-400">or</span>
<button
className="flex h-10 grow items-center justify-center gap-2 rounded-md border border-black bg-white text-black transition-colors hover:bg-black hover:text-white"
onClick={() => {
setIsCreatingRoadmap(true);
}}
>
<Shapes className="h-4 w-4 stroke-[2.5]" />
Create Custom Roadmap
</button>
</div>
{!teamResources.length && (
<div className="flex min-h-[240px] flex-col items-center justify-center rounded-lg border">
<Map className="mb-2 h-12 w-12 text-gray-300" />
<p className={'text-lg font-semibold'}>No roadmaps selected.</p>
<p className={'text-base text-gray-400'}>
Pick from{' '}
<span
onClick={() => setShowSelectRoadmapModal(true)}
className="cursor-pointer underline"
>
our roadmaps
</span>{' '}
or{' '}
<span
onClick={() => {
setIsCreatingRoadmap(true);
}}
className="cursor-pointer underline"
>
create a new one
</span>
.
<span className="block text-lg font-semibold text-black">
No roadmaps selected.
</span>
<p className={'text-sm text-gray-400'}>
Please search and add roadmaps from above
</p>
</div>
)}
{teamResources.length > 0 && (
<div className="mb-3 grid grid-cols-1 flex-wrap gap-2.5 sm:grid-cols-3">
{teamResources.map(
({
isCustomResource,
title: roadmapTitle,
resourceId,
removed: removedTopics,
topics,
}) => {
return (
<div
className="relative flex flex-col items-start overflow-hidden rounded-md border border-gray-300"
key={resourceId}
>
<div className={'w-full flex-grow px-3 pb-2 pt-4'}>
<span className="mb-0.5 block text-base font-medium leading-snug text-black">
{roadmapTitle}
{teamResourceConfig.length > 0 && (
<div className="mt-4 grid grid-cols-3 flex-wrap gap-2.5">
{teamResourceConfig.map(({ resourceId, removed: removedTopics }) => {
const roadmapTitle =
allRoadmaps.find((roadmap) => roadmap.id === resourceId)?.title ||
'...';
return (
<div className="flex flex-col items-start rounded-md border border-gray-300">
<div className={'w-full px-3 pb-2 pt-4'}>
<span className="mb-0.5 block text-base font-medium leading-none text-black">
{roadmapTitle}
</span>
{removedTopics.length > 0 ? (
<span className={'text-xs leading-none text-gray-900'}>
{removedTopics.length} topic
{removedTopics.length > 1 ? 's' : ''} removed
</span>
) : (
<span className="text-xs italic leading-none text-gray-400/60">
No changes made ..
</span>
{removedTopics.length > 0 || (topics && topics > 0) ? (
<span className={'text-xs leading-none text-gray-400'}>
{isCustomResource ? (
<>
Custom &middot; {topics} topic
{topics && topics > 1 ? 's' : ''}
</>
) : (
<>
{removedTopics.length} topic
{removedTopics.length > 1 ? 's' : ''} removed
</>
)}
</span>
) : (
<span className="text-xs italic leading-none text-gray-400/60">
{isCustomResource
? 'Placeholder roadmap.'
: 'No changes made ..'}
</span>
)}
</div>
{removingRoadmapId === resourceId && (
<div
className={
'flex w-full items-center justify-end p-3 text-sm'
}
>
<span className="text-xs text-gray-500">
Are you sure?{' '}
<button
onClick={() => onRemove(resourceId)}
className="mx-0.5 text-red-500 underline underline-offset-1"
>
Yes
</button>{' '}
<button
onClick={() => setRemovingRoadmapId('')}
className="text-red-500 underline underline-offset-1"
>
No
</button>
</span>
</div>
)}
{(!removingRoadmapId || removingRoadmapId !== resourceId) && (
<div className={'flex w-full justify-between p-3'}>
<button
type="button"
className={
'text-xs text-gray-500 underline hover:text-black focus:outline-none'
}
onClick={() => {
if (isCustomResource) {
window.open(
`${
import.meta.env.PUBLIC_EDITOR_APP_URL
}/${resourceId}`,
'_blank'
);
return;
}
setChangingRoadmapId(resourceId);
}}
>
Customize
</button>
<button
type="button"
className={
'text-xs text-red-500 underline hover:text-black'
}
onClick={() => setRemovingRoadmapId(resourceId)}
>
Remove
</button>
</div>
)}
</div>
);
}
)}
<div className={'flex w-full justify-between p-3'}>
<button
type="button"
className={
'text-xs text-gray-500 underline hover:text-black focus:outline-none'
}
onClick={() => setChangingRoadmapId(resourceId)}
>
Customize
</button>
<button
type="button"
className={
'text-xs text-red-500 underline hover:text-black'
}
onClick={() => onRemove(resourceId)}
>
Remove
</button>
</div>
</div>
);
})}
</div>
)}
</div>

View File

@@ -1,5 +1,5 @@
import { ChevronDownIcon } from '../ReactIcons/ChevronDownIcon';
import { useRef, useState } from 'react';
import { useRef, useState } from 'preact/hooks';
import { useOutsideClick } from '../../hooks/use-outside-click';
const allowedRoles = [
@@ -86,7 +86,10 @@ export function RoleDropdown(props: RoleDropdownProps) {
}`}
>
<span
className={`capitalize`}>
className={`capitalize ${
selectedRole === 'admin' ? 'text-blue-600' : ''
} ${selectedRole === 'manager' ? 'text-cyan-600' : ''}`}
>
{selectedRole || 'Select Role'}
</span>
<ChevronDownIcon

View File

@@ -1,154 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click';
import type { PageType } from '../CommandMenu/CommandMenu';
import type { TeamResourceConfig } from './RoadmapSelector';
import { SelectRoadmapModalItem } from './SelectRoadmapModalItem';
import { XIcon } from 'lucide-react';
export type SelectRoadmapModalProps = {
teamId: string;
allRoadmaps: PageType[];
onClose: () => void;
teamResourceConfig: TeamResourceConfig;
onRoadmapAdd: (roadmapId: string) => void;
onRoadmapRemove: (roadmapId: string) => void;
};
export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
const {
onClose,
allRoadmaps,
onRoadmapAdd,
onRoadmapRemove,
teamResourceConfig,
} = props;
const popupBodyEl = useRef<HTMLDivElement>(null);
const searchInputEl = useRef<HTMLInputElement>(null);
const [searchResults, setSearchResults] = useState<PageType[]>(allRoadmaps);
const [searchText, setSearchText] = useState('');
useKeydown('Escape', () => {
onClose();
});
useOutsideClick(popupBodyEl, () => {
onClose();
});
useEffect(() => {
if (!searchInputEl.current) {
return;
}
searchInputEl.current.focus();
}, [searchInputEl]);
useEffect(() => {
if (searchText.length === 0) {
setSearchResults(allRoadmaps);
return;
}
const searchResults = allRoadmaps.filter((roadmap) => {
return (
roadmap.title.toLowerCase().includes(searchText.toLowerCase()) ||
roadmap.id.toLowerCase().includes(searchText.toLowerCase())
);
});
setSearchResults(searchResults);
}, [searchText, allRoadmaps]);
const roleBasedRoadmaps = searchResults.filter(
(roadmap) => roadmap?.metadata?.tags?.includes('role-roadmap'),
);
const skillBasedRoadmaps = searchResults.filter(
(roadmap) => roadmap?.metadata?.tags?.includes('skill-roadmap'),
);
return (
<div className="fixed left-0 right-0 top-0 z-50 h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div className="relative mx-auto h-full w-full max-w-2xl p-4 md:h-auto">
<div
ref={popupBodyEl}
className="popup-body relative mt-4 overflow-hidden rounded-lg bg-white shadow"
>
<button
type="button"
className="popup-close absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-100 hover:text-gray-900"
onClick={onClose}
>
<XIcon className="h-4 w-4" />
<span className="sr-only">Close modal</span>
</button>
<input
ref={searchInputEl}
type="text"
placeholder="Search roadmaps"
className="block w-full border-b px-5 pb-3.5 pt-4 outline-none placeholder:text-gray-400"
value={searchText}
onInput={(e) => setSearchText((e.target as HTMLInputElement).value)}
/>
<div className="min-h-[200px] p-4">
<span className="block pb-3 text-xs uppercase text-gray-400">
Role Based Roadmaps
</span>
{roleBasedRoadmaps.length === 0 && (
<p className="mb-1 flex h-full items-start text-sm italic text-gray-400"></p>
)}
{roleBasedRoadmaps.length > 0 && (
<div className="mb-5 flex flex-wrap items-center gap-2">
{roleBasedRoadmaps.map((roadmap) => {
const isSelected = !!teamResourceConfig?.find(
(r) => r.resourceId === roadmap.id,
);
return (
<SelectRoadmapModalItem
key={roadmap.id}
title={roadmap.title}
isSelected={isSelected}
onClick={() => {
if (isSelected) {
onRoadmapRemove(roadmap.id);
} else {
onRoadmapAdd(roadmap.id);
}
}}
/>
);
})}
</div>
)}
<span className="block pb-3 text-xs uppercase text-gray-400">
Skill Based Roadmaps
</span>
<div className="flex flex-wrap items-center gap-2">
{skillBasedRoadmaps.map((roadmap) => {
const isSelected = !!teamResourceConfig.find(
(r) => r.resourceId === roadmap.id,
);
return (
<SelectRoadmapModalItem
key={roadmap.id}
title={roadmap.title}
isSelected={isSelected}
onClick={() => {
if (isSelected) {
onRoadmapRemove(roadmap.id);
} else {
onRoadmapAdd(roadmap.id);
}
}}
/>
);
})}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,34 +0,0 @@
import type { SelectRoadmapModalProps } from './SelectRoadmapModal';
type SelectRoadmapModalItemProps = {
title: string;
isSelected: boolean;
onClick: () => void;
};
export function SelectRoadmapModalItem(props: SelectRoadmapModalItemProps) {
const { isSelected, onClick, title } = props;
return (
<button
className={`group flex min-h-[35px] items-stretch overflow-hidden rounded-md text-sm ${
!isSelected
? 'border border-gray-300 hover:bg-gray-100'
: 'bg-black text-white transition-colors hover:bg-gray-700'
}`}
onClick={onClick}
>
<span className="flex items-center px-3">{title}</span>
{isSelected && (
<span className="flex items-center bg-gray-700 px-3 text-xs text-white transition-colors">
&times;
</span>
)}
{!isSelected && (
<span className="flex items-center bg-gray-100 px-2.5 text-xs text-gray-500">
+
</span>
)}
</button>
);
}

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