Compare commits

..

94 Commits

Author SHA1 Message Date
Arik Chakma
eaf5aee2f5 wip: add more questions 2023-09-26 20:10:11 +06:00
Arik Chakma
aee10fac37 wip: add more questions 2023-09-26 19:48:50 +06:00
Arik Chakma
c70ee5c5f3 wip: add more questions 2023-09-26 19:16:29 +06:00
Arik Chakma
b39de5f670 wip: add more questions 2023-09-26 01:28:01 +06:00
Arik Chakma
802b84ad79 wip: add more questions 2023-09-26 01:04:48 +06:00
Arik Chakma
ff000c87ed wip: add more questions 2023-09-26 00:32:14 +06:00
Arik Chakma
1604cb9d8c wip: add more questions 2023-09-25 18:23:17 +06:00
Arik Chakma
3fab75d44c wip: add more questions 2023-09-25 17:22:21 +06:00
Arik Chakma
7fb089259d wip: add another question 2023-09-24 22:09:09 +06:00
Arik Chakma
6713b059e1 wip: add more questions 2023-09-24 22:01:49 +06:00
Arik Chakma
e9651c6afe wip: add more questions 2023-09-24 21:44:33 +06:00
Arik Chakma
96fe0a5439 wip: add more questions 2023-09-24 21:37:29 +06:00
Arik Chakma
0393a658a7 wip: add more questions 2023-09-24 20:51:45 +06:00
Arik Chakma
4d0143f137 wip: add more questions 2023-09-23 21:42:13 +06:00
Arik Chakma
66eff7af70 wip: add more questions 2023-09-23 18:29:05 +06:00
Arik Chakma
0331e1f782 wip: add more questions 2023-09-23 17:02:44 +06:00
Arik Chakma
0318fe48e3 wip: add more question 2023-09-23 16:04:32 +06:00
Arik Chakma
00ba8a73c1 wip: add more questions 2023-09-23 15:34:50 +06:00
Arik Chakma
81a9baedd0 fix: set example 2023-09-23 14:03:55 +06:00
Arik Chakma
50e26e4fe2 wip: add more questions 2023-09-23 14:01:09 +06:00
Arik Chakma
56473b129c wip: add more questions 2023-09-23 12:24:23 +06:00
Arik Chakma
1dd53d8994 wip: add more questions 2023-09-23 10:15:10 +06:00
Arik Chakma
1b639c433c wip: add more questions 2023-09-23 09:19:09 +06:00
Arik Chakma
041facdc61 wip: add ternary operator 2023-09-22 21:08:41 +06:00
Arik Chakma
e4d770e256 wip: add more questions 2023-09-22 19:17:58 +06:00
Arik Chakma
81bbb42e34 Add Javascript questions 2023-09-22 17:48:48 +06:00
Kamran Ahmed
b92ae9b836 Increase line height of question answers 2023-09-22 05:27:06 +01:00
Kamran Ahmed
83df0da6b4 Enable indexing of question pages 2023-09-22 05:22:45 +01:00
Kamran Ahmed
a58b78bfe9 Hide account dropdown when user clicks anywhere 2023-09-22 05:20:28 +01:00
Kamran Ahmed
2fa41f583e Add react questions 2023-09-22 05:15:52 +01:00
Kamran Ahmed
80819f8914 UI fixes for questions 2023-09-22 05:08:24 +01:00
Arik Chakma
edcf0e683d Add react questions (#4492)
* Add more questions

* wip: add lazy, conditional questions

* wip: Add RSC questions

* wip: add component's lifecycle

* wip: add dependency array question

* wip: add comment and state

* chore: add more questions

* wip: add list question

* wip: add directive questions

* fix: conventions and examples

* wip: add custom hook question

* wip: add hydration question

* wip: add error boundary example

* wip: add strict mode question

* wip: investigating slow react app

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

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

---------

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

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

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
auto-install-peers=true

View File

@@ -45,22 +45,6 @@ export default defineConfig({
format: 'file',
},
integrations: [
{
name: 'client-authenticated',
hooks: {
'astro:config:setup'(options) {
options.addClientDirective({
name: 'authenticated',
entrypoint: fileURLToPath(
new URL(
'./src/directives/client-authenticated.mjs',
import.meta.url
)
),
});
},
},
},
tailwind({
config: {
applyBaseStyles: false,
@@ -71,6 +55,7 @@ export default defineConfig({
serialize: serializeSitemap,
}),
compress({
HTML: false,
CSS: false,
JavaScript: false,
}),

View File

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

View File

@@ -4,7 +4,7 @@
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "astro dev",
"dev": "astro dev --port 3000",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
@@ -21,31 +21,36 @@
"test:e2e": "playwright test"
},
"dependencies": {
"@astrojs/react": "^2.2.2",
"@astrojs/react": "^3.0.0",
"@astrojs/sitemap": "^1.3.3",
"@astrojs/tailwind": "^3.1.3",
"@astrojs/tailwind": "^5.0.0",
"@fingerprintjs/fingerprintjs": "^3.4.1",
"@nanostores/react": "^0.7.1",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"astro": "^2.6.6",
"astro": "^3.0.5",
"astro-compress": "^2.0.8",
"dracula-prism": "^2.1.13",
"jose": "^4.14.4",
"js-cookie": "^3.0.5",
"lucide-react": "^0.274.0",
"nanostores": "^0.9.2",
"node-html-parser": "^6.1.5",
"npm-check-updates": "^16.10.12",
"prismjs": "^1.29.0",
"react": "^18.0.0",
"react-confetti": "^6.1.0",
"react-dom": "^18.0.0",
"rehype-external-links": "^2.1.0",
"roadmap-renderer": "^1.0.6",
"slugify": "^1.6.6",
"tailwindcss": "^3.3.2"
"tailwindcss": "^3.3.3"
},
"devDependencies": {
"@playwright/test": "^1.35.1",
"@tailwindcss/typography": "^0.5.9",
"@types/js-cookie": "^3.0.3",
"@types/prismjs": "^1.26.0",
"csv-parser": "^3.0.0",
"gh-pages": "^5.0.0",
"js-yaml": "^4.1.0",

941
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,7 @@ Here is the list of available roadmaps with more being actively worked upon.
- [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)
- [Software Design and Architecture Roadmap](https://roadmap.sh/software-design-architecture)
- [JavaScript Roadmap](https://roadmap.sh/javascript)
@@ -50,7 +51,6 @@ Here is the list of available roadmaps with more being actively worked upon.
- [GraphQL Roadmap](https://roadmap.sh/graphql)
- [Android Roadmap](https://roadmap.sh/android)
- [Flutter Roadmap](https://roadmap.sh/flutter)
- [Python Roadmap](https://roadmap.sh/python)
- [Go Roadmap](https://roadmap.sh/golang)
- [Java Roadmap](https://roadmap.sh/java)
- [Spring Boot Roadmap](https://roadmap.sh/spring-boot)

View File

@@ -6,7 +6,7 @@ export function EmptyActivity() {
<div className="flex flex-col items-center p-7 text-center">
<img
alt="no roadmaps"
src={RoadmapIcon}
src={RoadmapIcon.src}
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>

View File

@@ -4,7 +4,7 @@ import { useState } from 'react';
import { httpPost } from '../../lib/http';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
const EmailLoginForm = () => {
export function EmailLoginForm() {
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [error, setError] = useState('');
@@ -99,6 +99,4 @@ const EmailLoginForm = () => {
</button>
</form>
);
};
export default EmailLoginForm;
}

View File

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

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { type FormEvent, useState } from 'react';
import { httpPost } from '../../lib/http';
export function ForgotPasswordForm() {
@@ -7,7 +7,7 @@ export function ForgotPasswordForm() {
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const handleSubmit = async (e: Event) => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setError('');

View File

@@ -1,5 +1,4 @@
import { useEffect, useState } from 'react';
import GitHubIcon from '../../icons/github.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import Cookies from 'js-cookie';
@@ -91,10 +90,11 @@ export function GitHubButton(props: GitHubButtonProps) {
// For non authentication pages, we want to redirect back to the page
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath =
['/respond-invite', '/befriend'].includes(window.location.pathname)
? window.location.pathname + window.location.search
: window.location.pathname;
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname
)
? window.location.pathname + window.location.search
: window.location.pathname;
localStorage.setItem(GITHUB_REDIRECT_AT, Date.now().toString());
localStorage.setItem(GITHUB_LAST_PAGE, pagePath);
@@ -111,7 +111,7 @@ export function GitHubButton(props: GitHubButtonProps) {
onClick={handleClick}
>
<img
src={icon as any}
src={icon.src}
alt="GitHub"
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>

View File

@@ -111,7 +111,7 @@ export function GoogleButton(props: GoogleButtonProps) {
onClick={handleClick}
>
<img
src={icon as any}
src={icon.src}
alt="Google"
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>

View File

@@ -86,10 +86,11 @@ export function LinkedInButton(props: LinkedInButtonProps) {
// For non authentication pages, we want to redirect back to the page
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath =
['/respond-invite', '/befriend'].includes(window.location.pathname)
? window.location.pathname + window.location.search
: window.location.pathname;
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname
)
? window.location.pathname + window.location.search
: window.location.pathname;
localStorage.setItem(LINKEDIN_REDIRECT_AT, Date.now().toString());
localStorage.setItem(LINKEDIN_LAST_PAGE, pagePath);
@@ -111,7 +112,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
onClick={handleClick}
>
<img
src={icon as any}
src={icon.src}
alt="Google"
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
/>

View File

@@ -1,6 +1,6 @@
---
import Popup from '../Popup/Popup.astro';
import EmailLoginForm from './EmailLoginForm';
import { EmailLoginForm } from './EmailLoginForm';
import Divider from './Divider.astro';
import { GitHubButton } from './GitHubButton';
import { GoogleButton } from './GoogleButton';
@@ -9,9 +9,9 @@ import { LinkedInButton } from './LinkedInButton';
<Popup id='login-popup' title='' subtitle=''>
<div class='text-center'>
<h2 class='mb-3 text-2xl font-semibold leading-5 text-slate-900'>
<p class='mb-3 text-2xl font-semibold leading-5 text-slate-900'>
Login to your account
</h2>
</p>
<p class='mt-2 text-sm leading-4 text-slate-600'>
You must be logged in to perform this action.
</p>

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import { type FormEvent, useEffect, useState } from 'react';
import { httpPost } from '../../lib/http';
import Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
export default function ResetPasswordForm() {
export function ResetPasswordForm() {
const [code, setCode] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirm, setPasswordConfirm] = useState('');
@@ -21,7 +21,7 @@ export default function ResetPasswordForm() {
}
}, []);
const handleSubmit = async (e: Event) => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);

View File

@@ -1,10 +1,9 @@
import SpinnerIcon from '../../icons/spinner.svg';
import ErrorIcon from '../../icons/error.svg';
import { useEffect, useState } from 'react';
import Cookies from 'js-cookie';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
import { httpPost } from '../../lib/http';
import ErrorIcon from '../../icons/error.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
export function TriggerVerifyAccount() {
const [isLoading, setIsLoading] = useState(true);
@@ -59,14 +58,14 @@ export function TriggerVerifyAccount() {
{isLoading && (
<img
alt={'Please wait.'}
src={SpinnerIcon}
src={SpinnerIcon.src}
className={'mx-auto h-16 w-16 animate-spin'}
/>
)}
{error && (
<img
alt={'Please wait.'}
src={ErrorIcon}
src={ErrorIcon.src}
className={'mx-auto h-16 w-16'}
/>
)}

View File

@@ -1,5 +1,5 @@
import VerifyLetterIcon from '../../icons/verify-letter.svg';
import { useEffect, useState } from 'react';
import VerifyLetterIcon from '../../icons/verify-letter.svg';
import { httpPost } from '../../lib/http';
export function VerificationEmailMessage() {
@@ -39,7 +39,7 @@ export function VerificationEmailMessage() {
<div className="mx-auto max-w-md text-center">
<img
alt="Verify Email"
src={VerifyLetterIcon as any}
src={VerifyLetterIcon.src}
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">

View File

@@ -141,7 +141,7 @@ export function Befriend() {
const isMe = currentUser?.id === user.id;
return (
<div className="container max-w-[400px] text-center">
<div className="container !max-w-[400px] text-center">
<img
alt={'join team'}
src={userAvatar}

View File

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

View File

@@ -2,6 +2,7 @@ import { Fragment, useEffect, useRef, useState } from 'react';
import { useKeydown } from '../../hooks/use-keydown';
import { useOutsideClick } from '../../hooks/use-outside-click';
import BestPracticesIcon from '../../icons/best-practices.svg';
import ClipboardIcon from '../../icons/clipboard.svg';
import GuideIcon from '../../icons/guide.svg';
import HomeIcon from '../../icons/home.svg';
import RoadmapIcon from '../../icons/roadmap.svg';
@@ -22,13 +23,13 @@ export type PageType = {
};
const defaultPages: PageType[] = [
{ id: 'home', url: '/', title: 'Home', group: 'Pages', icon: HomeIcon },
{ id: 'home', url: '/', title: 'Home', group: 'Pages', icon: HomeIcon.src },
{
id: 'account',
url: '/account',
title: 'Account',
group: 'Pages',
icon: UserIcon,
icon: UserIcon.src,
isProtected: true,
},
{
@@ -36,7 +37,7 @@ const defaultPages: PageType[] = [
url: '/team',
title: 'Teams',
group: 'Pages',
icon: GroupIcon,
icon: GroupIcon.src,
isProtected: true,
},
{
@@ -44,28 +45,35 @@ const defaultPages: PageType[] = [
url: '/roadmaps',
title: 'Roadmaps',
group: 'Pages',
icon: RoadmapIcon,
icon: RoadmapIcon.src,
},
{
id: 'best-practices',
url: '/best-practices',
title: 'Best Practices',
group: 'Pages',
icon: BestPracticesIcon,
icon: BestPracticesIcon.src,
},
{
id: 'questions',
url: '/questions',
title: 'Questions',
group: 'Pages',
icon: ClipboardIcon.src,
},
{
id: 'guides',
url: '/guides',
title: 'Guides',
group: 'Pages',
icon: GuideIcon,
icon: GuideIcon.src,
},
{
id: 'videos',
url: '/videos',
title: 'Videos',
group: 'Pages',
icon: VideoIcon,
icon: VideoIcon.src,
},
];
@@ -221,7 +229,7 @@ export function CommandMenu() {
{page.icon && (
<img
alt={page.title}
src={page.icon as any}
src={page.icon}
className="mr-2 h-4 w-4"
/>
)}

View File

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

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ export function NotDropdown(props: NotDropdownProps) {
<img
alt={singularName}
src={ChevronDownIcon}
src={ChevronDownIcon.src}
className={'relative top-[1px] h-[17px] w-[17px] opacity-40'}
/>
</div>

View File

@@ -79,7 +79,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
className="popup-close absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-100 hover:text-gray-900"
onClick={onClose}
>
<img alt={'close'} src={CloseIcon} className="h-4 w-4" />
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
<span className="sr-only">Close modal</span>
</button>
<input

View File

@@ -9,13 +9,13 @@ export const validTeamTypes = [
{
value: 'company',
label: 'Company',
icon: BuildingIcon,
icon: BuildingIcon.src,
description: 'Track the skills and learning progress of the tech team at your company',
},
{
value: 'study_group',
label: 'Study Group',
icon: UsersIcon,
icon: UsersIcon.src,
description: 'Invite your friends or course-mates and track your learning progress together',
},
] as const;

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { AppError, httpPost, httpPut } from '../../lib/http';
import { type FormEvent, useEffect, useRef, useState } from 'react';
import { type AppError, httpPost, httpPut } from '../../lib/http';
import type { ValidTeamType } from './Step0';
import type { TeamDocument } from './CreateTeamForm';
import { NextButton } from './NextButton';
@@ -49,7 +49,7 @@ export function Step1(props: Step1Props) {
team?.teamSize || ('' as any)
);
const handleSubmit = async (e: Event) => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
if (!name || !selectedTeamType) {
@@ -124,7 +124,7 @@ export function Step1(props: Step1Props) {
<form onSubmit={handleSubmit}>
<div className="flex w-full flex-col">
<label
for="name"
htmlFor="name"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
>
{selectedTeamType === 'company' ? 'Company Name' : 'Group Name'}
@@ -147,7 +147,7 @@ export function Step1(props: Step1Props) {
{selectedTeamType === 'company' && (
<div className="mt-4 flex w-full flex-col">
<label
for="website"
htmlFor="website"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
>
Website
@@ -168,7 +168,7 @@ export function Step1(props: Step1Props) {
{selectedTeamType === 'company' && (
<div className="mt-4 flex w-full flex-col">
<label for="website" className="text-sm leading-none text-slate-500">
<label htmlFor="website" className="text-sm leading-none text-slate-500">
Company LinkedIn URL
</label>
<input
@@ -187,7 +187,7 @@ export function Step1(props: Step1Props) {
)}
<div className="mt-4 flex w-full flex-col">
<label for="website" className="text-sm leading-none text-slate-500">
<label htmlFor="website" className="text-sm leading-none text-slate-500">
GitHub Organization URL
</label>
<input
@@ -205,7 +205,7 @@ export function Step1(props: Step1Props) {
{selectedTeamType === 'company' && (
<div className="mt-4 flex w-full flex-col">
<label
for="team-size"
htmlFor="team-size"
className='text-sm leading-none text-slate-500 after:text-red-400 after:content-["*"]'
>
Tech Team Size
@@ -237,7 +237,7 @@ export function Step1(props: Step1Props) {
</div>
)}
<div className="mt-4 flex flex-col md:flex-row items-center justify-between gap-2">
<div className="mt-4 flex flex-col items-center justify-between gap-2 md:flex-row">
<button
type="button"
onClick={onBack}

View File

@@ -1,4 +1,4 @@
import { RoadmapSelector, TeamResourceConfig } from './RoadmapSelector';
import { RoadmapSelector, type TeamResourceConfig } from './RoadmapSelector';
import type { TeamDocument } from './CreateTeamForm';
type Step2Props = {

View File

@@ -1,7 +1,7 @@
import type { TeamDocument } from './CreateTeamForm';
import { NextButton } from './NextButton';
import { TrashIcon } from '../ReactIcons/TrashIcon';
import { AllowedRoles, RoleDropdown } from './RoleDropdown';
import { type AllowedRoles, RoleDropdown } from './RoleDropdown';
import { useEffect, useRef, useState } from 'react';
import { httpPost } from '../../lib/http';

View File

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

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { type FormEvent, useEffect, useRef, useState } from 'react';
import { httpDelete } from '../lib/http';
import type { TeamDocument } from './CreateTeam/CreateTeamForm';
import { useTeamId } from '../hooks/use-team-id';
@@ -34,7 +34,7 @@ export function DeleteTeamPopup(props: DeleteTeamPopupProps) {
inputEl.current?.focus();
}, []);
const handleSubmit = async (e: Event) => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setError('');

View File

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

View File

@@ -4,15 +4,16 @@ import FeaturedItem, { FeaturedItemType } from './FeaturedItem.astro';
export interface Props {
featuredItems: FeaturedItemType[];
heading: string;
allowBookmark?: boolean;
}
const { featuredItems, heading } = Astro.props;
const { featuredItems, heading, allowBookmark = true } = Astro.props;
---
<div class='relative border-b border-b-[#1e293c] py-10 sm:py-14'>
<div class='container'>
<h2
class='text-md font-regular absolute flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 -top-[17px] sm:left-1/2 sm:-translate-x-1/2'
class='text-md font-regular absolute -top-[17px] flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2'
>
{heading}
</h2>
@@ -22,6 +23,7 @@ const { featuredItems, heading } = Astro.props;
featuredItems.map((featuredItem) => (
<li>
<FeaturedItem
allowBookmark={allowBookmark}
text={featuredItem.text}
url={featuredItem.url}
isNew={featuredItem.isNew}

View File

@@ -96,6 +96,7 @@ export function MarkFavorite({
return (
<button
aria-label={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
onClick={toggleFavoriteHandler}
tabIndex={-1}
className={`${isFavorite ? '' : 'opacity-30 hover:opacity-100'} ${

View File

@@ -1,4 +1,4 @@
import { FormEvent, useEffect, useRef, useState } from 'react';
import { type FormEvent, useEffect, useRef, useState } from 'react';
import { useToast } from '../../hooks/use-toast';
import { useTeamId } from '../../hooks/use-team-id';
import { useOutsideClick } from '../../hooks/use-outside-click';

View File

@@ -31,8 +31,7 @@ import Icon from './AstroIcon.astro';
<a
class='px-2 py-1.5 transition-colors hover:text-white sm:border-b-0 sm:px-0 sm:py-0'
href='https://youtube.com/theroadmap?sub_confirmation=1'
target='_blank'>YouTube</a
>
target='_blank'>YouTube</a>
</p>
<div class='flex flex-col justify-between gap-12 sm:flex-row'>
@@ -68,6 +67,7 @@ import Icon from './AstroIcon.astro';
<a href='/privacy' class='hover:text-white'>Privacy</a>
<span class='mx-1.5'>&middot;</span>
<a
aria-label="Subscribe to YouTube channel"
href='https://youtube.com/theroadmap?sub_confirmation=1'
target='_blank'
class='hover:text-white'
@@ -75,6 +75,7 @@ import Icon from './AstroIcon.astro';
<AstroIcon icon='youtube' class='inline-block h-5 w-5' />
</a>
<a
aria-label="Follow on Twitter"
href='https://twitter.com/roadmapsh'
target='_blank'
class='ml-2 hover:text-white'

View File

@@ -5,10 +5,9 @@ import {
refreshProgressCounters,
renderResourceProgress,
renderTopicProgress,
ResourceProgressType,
ResourceType,
updateResourceProgress,
} from '../../lib/resource-progress';
import type { ResourceProgressType, ResourceType } from '../../lib/resource-progress';
import { pageProgressMessage } from '../../stores/page';
import { showLoginPopup } from '../../lib/popup';

View File

@@ -15,7 +15,7 @@ export function EmptyFriends(props: EmptyFriendsProps) {
<div className="mx-auto flex flex-col items-center p-7 text-center">
<img
alt="no friends"
src={UserPlusIcon as any}
src={UserPlusIcon.src}
className="mb-2 h-[60px] w-[60px] opacity-10 sm:h-[120px] sm:w-[120px]"
/>
<h2 className="text-lg font-bold sm:text-xl">Invite your Friends</h2>
@@ -44,7 +44,7 @@ export function EmptyFriends(props: EmptyFriendsProps) {
copyText(befriendUrl);
}}
>
<img src={CopyIcon as any} className="h-4 w-4" alt="Invite Friends" />
<img src={CopyIcon.src} className="h-4 w-4" alt="Invite Friends" />
{isCopied ? 'Copied' : 'Copy'}
</button>
</div>

View File

@@ -188,7 +188,7 @@ export function FriendsPage() {
{filteredFriends.length === 0 && (
<div className="flex flex-col items-center justify-center py-12">
<img
src={UserIcon}
src={UserIcon.src}
alt="Empty Friends"
className="mb-3 w-12 opacity-20"
/>

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef } from 'react';
import type { MouseEvent } from 'react';
import { useRef } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click';
import CopyIcon from '../../icons/copy.svg';
import { useCopyText } from '../../hooks/use-copy-text';
@@ -38,8 +39,8 @@ export function InviteFriendPopup(props: InviteFriendPopupProps) {
readOnly={true}
className="mt-2 block w-full rounded-md border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:border-gray-400"
value={befriendUrl}
onClick={(e) => {
e?.target?.select();
onClick={(e: MouseEvent<HTMLInputElement>) => {
(e?.target as HTMLInputElement)?.select();
copyText(befriendUrl);
}}
/>
@@ -53,7 +54,11 @@ export function InviteFriendPopup(props: InviteFriendPopupProps) {
copyText(befriendUrl);
}}
>
<img src={CopyIcon} className="h-4 w-4" alt="Invite Friends" />
<img
src={CopyIcon.src}
className="h-4 w-4"
alt="Invite Friends"
/>
{isCopied ? 'Copied' : 'Copy URL'}
</button>
</div>

View File

@@ -1,5 +1,4 @@
import { httpGet } from '../../lib/http';
import type { TeamListResponse } from '../TeamDropdown/TeamDropdown';
import { useEffect, useState } from 'react';
type GetFriendCountsResponse = {

View File

@@ -16,15 +16,22 @@ const { url, title, description, isNew } = Astro.props;
class='relative flex h-full flex-col rounded-md border border-gray-200 bg-white p-2.5 hover:border-gray-400 sm:rounded-lg sm:p-5'
>
<span
class='font-semibold text-md mb-0 text-gray-900 hover:text-black sm:mb-1.5 sm:text-xl'
class='text-md mb-0 font-semibold text-gray-900 hover:text-black sm:mb-1.5 sm:text-xl'
>
{title}
</span>
<span class='hidden text-sm leading-normal text-gray-400 sm:block' set:html={description} />
<span
class='hidden text-sm leading-normal text-gray-400 sm:block'
set:html={description}
/>
{
isNew && (
<span class='absolute bottom-1 right-1 rounded-sm bg-yellow-300 px-1 py-0.5 text-xs font-medium uppercase text-yellow-900 sm:px-1.5'>
<span class='flex items-center gap-1.5 absolute bottom-1.5 right-1 rounded-sm text-xs font-semibold uppercase text-purple-500 sm:px-1.5'>
<span class='relative flex h-2 w-2'>
<span class='absolute inline-flex h-full w-full animate-ping rounded-full bg-purple-400 opacity-75' />
<span class='relative inline-flex h-2 w-2 rounded-full bg-purple-500' />
</span>
New
</span>
)

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { EmptyProgress } from './EmptyProgress';
import { httpGet } from '../../lib/http';
import { ProgressList } from './ProgressList';
import { HeroRoadmaps } from './HeroRoadmaps';
import {isLoggedIn} from "../../lib/jwt";
export type UserProgressResponse = {
@@ -122,7 +122,7 @@ export function FavoriteRoadmaps() {
<div className="container min-h-full">
{!isLoading && progress.length == 0 && <EmptyProgress />}
{progress.length > 0 && (
<ProgressList progress={progress} isLoading={isLoading} />
<HeroRoadmaps customRoadmaps={[]} progress={progress} isLoading={isLoading} />
)}
</div>
</div>

View File

@@ -0,0 +1,155 @@
import type { UserProgressResponse } from './FavoriteRoadmaps';
import { CheckIcon } from '../ReactIcons/CheckIcon';
import { MarkFavorite } from '../FeaturedItems/MarkFavorite';
import { Spinner } from '../ReactIcons/Spinner';
import type { ResourceType } from '../../lib/resource-progress';
import { MapIcon } from 'lucide-react';
type ProgressRoadmapProps = {
url: string;
percentageDone: number;
allowFavorite?: boolean;
resourceId: string;
resourceType: ResourceType;
resourceTitle: string;
isFavorite?: boolean;
};
function HeroRoadmap(props: ProgressRoadmapProps) {
const {
url,
percentageDone,
resourceType,
resourceId,
resourceTitle,
isFavorite,
allowFavorite = true,
} = props;
return (
<a
href={url}
className="relative flex flex-col overflow-hidden rounded-md border border-slate-800 bg-slate-900 p-3 text-sm text-slate-400 hover:border-slate-600 hover:text-slate-300"
>
<span className="relative z-20">{resourceTitle}</span>
<span
className="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]"
style={{ width: `${percentageDone}%` }}
></span>
{allowFavorite && (
<MarkFavorite
resourceId={resourceId}
resourceType={resourceType}
favorite={isFavorite}
/>
)}
</a>
);
}
type ProgressTitleProps = {
icon: any;
isLoading?: boolean;
title: string;
};
export function HeroTitle(props: ProgressTitleProps) {
const { isLoading = false, title, icon } = props;
return (
<p className="mb-4 flex items-center text-sm text-gray-400">
{!isLoading && icon}
{isLoading && (
<span className="mr-1.5">
<Spinner />
</span>
)}
{title}
</p>
);
}
type ProgressListProps = {
progress: UserProgressResponse;
showCustomRoadmaps?: boolean;
customRoadmaps: any[]; // @fixme implement this
isLoading?: boolean;
};
export function HeroRoadmaps(props: ProgressListProps) {
const {
progress,
isLoading = false,
customRoadmaps = [{} /* @fixme implement this */],
showCustomRoadmaps = false,
} = props;
return (
<div className="relative pb-12 pt-4 sm:pt-7">
{
<HeroTitle
icon={
(<CheckIcon additionalClasses="mr-1.5 h-[14px] w-[14px]" />) as any
}
isLoading={isLoading}
title="Your progress and favorite roadmaps."
/>
}
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
{progress.map((resource) => (
<HeroRoadmap
key={resource.resourceId}
resourceId={resource.resourceId}
resourceType={resource.resourceType}
resourceTitle={resource.resourceTitle}
isFavorite={resource.isFavorite}
percentageDone={
((resource.skipped + resource.done) / resource.total) * 100
}
url={
resource.resourceType === 'roadmap'
? `/${resource.resourceId}`
: `/best-practices/${resource.resourceId}`
}
/>
))}
</div>
{showCustomRoadmaps && (
<div className="mt-5">
{
<HeroTitle
icon={<MapIcon className="mr-1.5 h-[14px] w-[14px]" />}
title="Your custom roadmaps"
/>
}
{customRoadmaps.length === 0 && (
<p className="rounded-md border border-dashed border-gray-800 p-2 text-sm text-gray-600">
You haven't created any custom roadmaps yet.{' '}
<button className="text-gray-500 underline underline-offset-2 hover:text-gray-400">
Create one!
</button>
</p>
)}
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
{customRoadmaps.map((customRoadmap) => (
<HeroRoadmap
resourceId={'343434'}
resourceType={'roadmap'}
resourceTitle={'Frontend Roadmap Revised'}
percentageDone={50}
url={`/r?${'34343434'}`}
allowFavorite={false}
/>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -1,61 +0,0 @@
import type { UserProgressResponse } from './FavoriteRoadmaps';
import { CheckIcon } from '../ReactIcons/CheckIcon';
import { MarkFavorite } from '../FeaturedItems/MarkFavorite';
import { Spinner } from '../ReactIcons/Spinner';
type ProgressListProps = {
progress: UserProgressResponse;
isLoading?: boolean;
};
export function ProgressList(props: ProgressListProps) {
const { progress, isLoading = false } = props;
return (
<div className="relative pb-12 pt-4 sm:pt-7">
<p className="mb-4 flex items-center text-sm text-gray-400">
{!isLoading && (
<CheckIcon additionalClasses={'mr-1.5 w-[14px] h-[14px]'} />
)}
{isLoading && (
<span className="mr-1.5">
<Spinner />
</span>
)}
Your progress and favorite roadmaps.
</p>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
{progress.map((resource) => {
const url =
resource.resourceType === 'roadmap'
? `/${resource.resourceId}`
: `/best-practices/${resource.resourceId}`;
const percentageDone =
((resource.skipped + resource.done) / resource.total) * 100;
return (
<a
key={resource.resourceId}
href={url}
className="relative flex flex-col overflow-hidden rounded-md border border-slate-800 bg-slate-900 p-3 text-sm text-slate-400 hover:border-slate-600 hover:text-slate-300"
>
<span className="relative z-20">{resource.resourceTitle}</span>
<span
className="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]"
style={{ width: `${percentageDone}%` }}
></span>
<MarkFavorite
resourceId={resource.resourceId}
resourceType={resource.resourceType}
favorite={resource.isFavorite}
/>
</a>
);
})}
</div>
</div>
);
}

View File

@@ -24,10 +24,10 @@ import AccountDropdown from './AccountDropdown.astro';
>
</li>
<li class='hidden lg:inline'>
<a href='/guides' class='text-gray-400 hover:text-white'>Guides</a>
<a href='/questions' class='text-gray-400 hover:text-white'>Questions</a>
</li>
<li class='hidden lg:inline'>
<a href='/videos' class='text-gray-400 hover:text-white'>Videos</a>
<a href='/guides' class='text-gray-400 hover:text-white'>Guides</a>
</li>
<li>
<kbd

View File

@@ -19,6 +19,8 @@ function bindEvents() {
...target.closest('button')?.dataset,
};
const accountDropdown = document.querySelector('[data-account-dropdown]');
// If the user clicks on the logout button, remove the token cookie
if (dataset.logoutButton !== undefined) {
e.preventDefault();
@@ -27,6 +29,12 @@ function bindEvents() {
document.querySelector('[data-mobile-nav]')?.classList.remove('hidden');
} else if (dataset.closeMobileNav !== undefined) {
document.querySelector('[data-mobile-nav]')?.classList.add('hidden');
} else if (
accountDropdown &&
!target?.closest('[data-account-dropdown]') &&
!accountDropdown.classList.contains('hidden')
) {
accountDropdown.classList.add('hidden');
}
});

View File

@@ -91,14 +91,14 @@ export function NotificationPage() {
className="inline-flex border p-1 rounded hover:bg-gray-50 disabled:opacity-75"
onClick={() => respondInvitation('accept', notification?._id!)}
>
<img src={AcceptIcon} className="h-4 w-4" />
<img src={AcceptIcon.src} className="h-4 w-4" />
</button>
<button type="button"
disabled={isLoading}
className="inline-flex border p-1 rounded hover:bg-gray-50 disabled:opacity-75"
onClick={() => respondInvitation('reject', notification?._id!)}
>
<img src={XIcon} className="h-4 w-4" />
<img alt={'Close'} src={XIcon.src} className="h-4 w-4" />
</button>
</div>
</div>

View File

@@ -6,8 +6,8 @@ const starCount = await getFormattedStars('kamranahmedse/developer-roadmap');
---
<div class='py-6 sm:py-16 border-b border-t text-left sm:text-center bg-white'>
<div class='max-w-[600px] container'>
<h2 class='text-2xl sm:text-5xl font-bold'>Community</h2>
<div class='!max-w-[600px] container'>
<p class='text-2xl sm:text-5xl font-bold'>Community</p>
<p class='text-gray-600 text-sm sm:text-lg leading-relaxed my-2.5 sm:my-5'>
roadmap.sh is the <a
href='https://github.com/search?o=desc&q=stars%3A%3E100000&s=stars&type=Repositories'

View File

@@ -31,7 +31,7 @@ export function PageProgress(props: Props) {
<div className="fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-white bg-opacity-75">
<div className="flex items-center justify-center rounded-md border bg-white px-4 py-2 ">
<img
src={SpinnerIcon as any}
src={SpinnerIcon.src}
alt="Loading"
className="h-4 w-4 animate-spin fill-blue-600 text-gray-200 sm:h-4 sm:w-4"
/>

View File

@@ -101,7 +101,7 @@ export function PageSponsor(props: PageSponsorProps) {
sponsorHidden.set(true);
}}
>
<img alt="Close" className="h-4 w-4" src={CloseIcon as any} />
<img alt="Close" className="h-4 w-4" src={CloseIcon.src} />
</span>
<img
src={imageUrl}

View File

@@ -5,9 +5,9 @@ import Popup from './Popup/Popup.astro';
<Popup id='progress-help' title='' subtitle=''>
<div class='-mt-2.5'>
<h2 class='mb-3 text-2xl font-semibold leading-5 text-gray-900'>
<p class='mb-3 text-2xl font-semibold leading-5 text-gray-900'>
Track your Progress
</h2>
</p>
<p class='text-sm leading-4 text-gray-600'>
Login and use one of the options listed below.
</p>

View File

@@ -0,0 +1,142 @@
/**
* atom-dark theme for `prism.js`
* Based on Atom's `atom-dark` theme: https://github.com/atom/atom-dark-syntax
* @author Joe Gibson (@gibsjose)
*/
code[class*="language-"],
pre[class*="language-"] {
color: #c5c8c6;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
font-family: Inconsolata, Monaco, Consolas, 'Courier New', Courier, monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #1d1f21;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #7C7C7C;
}
.token.punctuation {
color: #c5c8c6;
}
.namespace {
opacity: .7;
}
.token.property,
.token.keyword,
.token.tag {
color: #96CBFE;
}
.token.class-name {
color: #FFFFB6;
text-decoration: underline;
}
.token.boolean,
.token.constant {
color: #99CC99;
}
.token.symbol,
.token.deleted {
color: #f92672;
}
.token.number {
color: #FF73FD;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #A8FF60;
}
.token.variable {
color: #C6C5FE;
}
.token.operator {
color: #EDEDED;
}
.token.entity {
color: #FFFFB6;
cursor: help;
}
.token.url {
color: #96CBFE;
}
.language-css .token.string,
.style .token.string {
color: #87C38A;
}
.token.atrule,
.token.attr-value {
color: #F9EE98;
}
.token.function {
color: #DAD085;
}
.token.regex {
color: #E9C062;
}
.token.important {
color: #fd971f;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}

View File

@@ -0,0 +1,125 @@
import { Fragment, useEffect, useRef, useState } from 'react';
import type { QuestionType } from '../../lib/question-group';
import { markdownToHtml } from '../../lib/markdown';
import Prism from 'prismjs';
import './PrismAtom.css';
type QuestionCardProps = {
question: QuestionType;
};
export function QuestionCard(props: QuestionCardProps) {
const { question } = props;
const [isAnswerVisible, setIsAnswerVisible] = useState<boolean>(false);
const answerRef = useRef<HTMLDivElement>(null);
const questionRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// set the height of the question width to the height of the answer
// width if the answer is visible and the question height is less than
// the answer height
if (isAnswerVisible) {
Prism.highlightAll();
const answerHeight = answerRef.current?.clientHeight || 0;
const questionHeight = questionRef.current?.clientHeight || 0;
if (answerHeight > questionHeight) {
questionRef.current!.style.height = `${answerHeight}px`;
}
} else {
questionRef.current!.style.height = `auto`;
}
// if the user has scrolled down and the top of the answer is not
// visible, scroll to the top of the answer
const questionTop =
(questionRef.current?.getBoundingClientRect().top || 0) - 147;
if (questionTop < 0) {
window.scrollTo({
top: window.scrollY + questionTop - 10,
});
}
}, [isAnswerVisible]);
useEffect(() => {
setIsAnswerVisible(false);
}, [question]);
return (
<>
<div
ref={questionRef}
className={`flex flex-grow flex-col items-center justify-center py-5 sm:py-8`}
>
<div className="hidden text-gray-400 sm:block">
{question.topics?.map((topic, counter) => {
const totalTopics = question.topics?.length || 0;
return (
<Fragment key={topic}>
<span className="capitalize">{topic}</span>
{counter !== totalTopics - 1 && (
<span className="mx-2">&middot;</span>
)}
</Fragment>
);
})}
</div>
<div className="mx-auto flex max-w-[550px] flex-1 items-center justify-center py-3 sm:py-8">
<p className="px-4 text-xl font-semibold !leading-snug text-black sm:text-3xl">
{question.question}
</p>
</div>
<div className="text-center">
<button
onClick={() => {
setIsAnswerVisible(true);
}}
className="cursor-pointer text-sm text-gray-500 underline underline-offset-4 transition-colors hover:text-black sm:text-base"
>
Click to Reveal the Answer
</button>
</div>
</div>
<div
ref={answerRef}
className={`absolute left-0 right-0 flex flex-col items-center justify-center rounded-[7px] bg-neutral-100 py-4 text-sm leading-normal text-black transition-all duration-300 sm:py-8 sm:text-xl ${
isAnswerVisible ? 'top-0 min-h-[248px] sm:min-h-[398px]' : 'top-full'
}`}
>
{!question.isLongAnswer && (
<div
className={`mx-auto flex max-w-[600px] flex-grow flex-col items-center justify-center py-0 px-5 text-center text-base [&>p]:leading-relaxed sm:text-xl`}
dangerouslySetInnerHTML={{
__html: markdownToHtml(question.answer, false),
}}
/>
)}
{question.isLongAnswer && (
<div
className={`qa-answer prose prose-sm prose-quoteless mx-auto flex w-full max-w-[600px] flex-grow flex-col items-start justify-center py-0 px-4 text-left text-sm prose-h1:mb-2.5 prose-h1:mt-7 prose-h2:mb-3 prose-h2:mt-0 prose-h3:mb-[5px] prose-h3:mt-[10px] prose-p:mb-2 prose-p:mt-0 prose-blockquote:font-normal prose-blockquote:not-italic prose-blockquote:text-gray-700 prose-pre:!mb-6 prose-pre:w-full prose-ul:my-2 prose-li:m-0 prose-li:mb-0.5 sm:px-5 sm:text-lg sm:prose-p:mb-4`}
dangerouslySetInnerHTML={{
__html: markdownToHtml(question.answer, false),
}}
/>
)}
<div className="mt-7 text-center">
<button
onClick={() => {
setIsAnswerVisible(false);
}}
className="cursor-pointer text-sm text-gray-500 underline underline-offset-4 transition-colors hover:text-black sm:text-base"
>
Hide the Answer
</button>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,99 @@
import type { ReactNode } from 'react';
import {
PartyPopper,
RefreshCcw,
SkipForward,
Sparkles,
ThumbsUp,
} from 'lucide-react';
import type { QuestionProgressType } from './QuestionsList';
type ProgressStatButtonProps = {
isDisabled?: boolean;
icon: ReactNode;
label: string;
count: number;
onClick: () => void;
};
function ProgressStatButton(props: ProgressStatButtonProps) {
const { icon, label, count, onClick, isDisabled = false } = props;
return (
<button
disabled={isDisabled}
onClick={onClick}
className="group relative text-sm sm:text-base flex flex-1 items-center overflow-hidden rounded-md sm:rounded-xl border border-gray-300 bg-white py-2 px-2 sm:py-3 sm:px-4 text-black transition-colors hover:border-black disabled:pointer-events-none disabled:opacity-50"
>
{icon}
<span className="flex flex-grow justify-between">
<span>{label}</span>
<span>{count}</span>
</span>
<span className="absolute top-full left-0 right-0 flex h-full items-center justify-center border border-black bg-black text-white transition-all duration-200 group-hover:top-0">
Restart Asking
</span>
</button>
);
}
type QuestionFinishedProps = {
knowCount: number;
didNotKnowCount: number;
skippedCount: number;
totalCount: number;
onReset: (type: QuestionProgressType | 'reset') => void;
};
export function QuestionFinished(props: QuestionFinishedProps) {
const { knowCount, didNotKnowCount, skippedCount, totalCount, onReset } =
props;
return (
<div className="relative flex flex-grow flex-col items-center justify-center px-4 sm:px-0">
<PartyPopper className="mb-4 mt-10 h-14 w-14 text-gray-300 sm:mt-0 sm:h-24 sm:w-24" />
<h1 className="text-lg font-semibold text-gray-700 sm:text-2xl">
Questions Finished
</h1>
<p className="mt-0 text-sm text-gray-500 sm:mt-2 sm:text-base">
Click below revisit{' '}
<span className="hidden sm:inline">specific or all questions</span>{' '}
<span className="inline sm:hidden">questions</span>
</p>
<div className="mt-5 mb-5 flex w-full flex-col gap-1.5 sm:gap-3 px-2 sm:flex-row sm:px-16">
<ProgressStatButton
icon={<ThumbsUp className="mr-1 h-4" />}
label="Knew"
count={knowCount}
isDisabled={knowCount === 0}
onClick={() => onReset('know')}
/>
<ProgressStatButton
icon={<Sparkles className="mr-1 h-4" />}
label="Learned"
count={didNotKnowCount}
isDisabled={didNotKnowCount === 0}
onClick={() => onReset('dontKnow')}
/>
<ProgressStatButton
icon={<SkipForward className="mr-1 h-4" />}
label="Skipped"
count={skippedCount}
isDisabled={skippedCount === 0}
onClick={() => onReset('skip')}
/>
</div>
<div className="mt-2 mb-4 sm:mb-0 text-sm">
<button
onClick={() => onReset('reset')}
className="flex items-center gap-0.5 text-red-700 hover:text-black text-sm sm:text-base"
>
<RefreshCcw className="mr-1 h-4" />
Restart Asking
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,12 @@
import {Spinner} from "../ReactIcons/Spinner";
export function QuestionLoader() {
return (
<div className="flex flex-grow flex-col items-center justify-center">
<p className="text-xl font-medium text-gray-500 flex items-center gap-3.5">
<Spinner isDualRing={false} innerFill='#6b7280' className="h-5 w-5" />
Please wait ..
</p>
</div>
);
}

View File

@@ -0,0 +1,286 @@
import { useEffect, useRef, useState } from 'react';
import { QuestionsProgress } from './QuestionsProgress';
import { CheckCircle, SkipForward, Sparkles } from 'lucide-react';
import { QuestionCard } from './QuestionCard';
import { QuestionLoader } from './QuestionLoader';
import { isLoggedIn } from '../../lib/jwt';
import type { QuestionType } from '../../lib/question-group';
import { httpGet, httpPut } from '../../lib/http';
import { useToast } from '../../hooks/use-toast';
import { QuestionFinished } from './QuestionFinished';
import { Confetti } from '../Confetti';
type UserQuestionProgress = {
know: string[];
dontKnow: string[];
skip: string[];
};
export type QuestionProgressType = keyof UserQuestionProgress;
type QuestionsListProps = {
groupId: string;
questions: QuestionType[];
};
export function QuestionsList(props: QuestionsListProps) {
const { questions: unshuffledQuestions, groupId } = props;
const toast = useToast();
const [isLoading, setIsLoading] = useState(true);
const [showConfetti, setShowConfetti] = useState(false);
const [questions, setQuestions] = useState<QuestionType[]>();
const [pendingQuestions, setPendingQuestions] = useState<QuestionType[]>([]);
const [userProgress, setUserProgress] = useState<UserQuestionProgress>();
const containerRef = useRef<HTMLDivElement>(null);
async function fetchUserProgress(): Promise<
UserQuestionProgress | undefined
> {
if (!isLoggedIn()) {
return;
}
const { response, error } = await httpGet<UserQuestionProgress>(
`${
import.meta.env.PUBLIC_API_URL
}/v1-get-user-question-progress/${groupId}`
);
if (error) {
toast.error(error.message || 'Error fetching user progress');
return;
}
return response;
}
async function loadQuestions() {
const userProgress = await fetchUserProgress();
setUserProgress(userProgress);
const knownQuestions = userProgress?.know || [];
const didNotKnowQuestions = userProgress?.dontKnow || [];
const skipQuestions = userProgress?.skip || [];
const pendingQuestions = unshuffledQuestions.filter((question) => {
return (
!knownQuestions.includes(question.id) &&
!didNotKnowQuestions.includes(question.id) &&
!skipQuestions.includes(question.id)
);
});
// Shuffle and set pending questions
setPendingQuestions(pendingQuestions.sort(() => Math.random() - 0.5));
setQuestions(unshuffledQuestions);
setIsLoading(false);
}
async function resetProgress(type: QuestionProgressType | 'reset' = 'reset') {
let knownQuestions = userProgress?.know || [];
let didNotKnowQuestions = userProgress?.dontKnow || [];
let skipQuestions = userProgress?.skip || [];
if (!isLoggedIn()) {
if (type === 'know') {
knownQuestions = [];
} else if (type === 'dontKnow') {
didNotKnowQuestions = [];
} else if (type === 'skip') {
skipQuestions = [];
} else if (type === 'reset') {
knownQuestions = [];
didNotKnowQuestions = [];
skipQuestions = [];
}
} else {
setIsLoading(true);
const { response, error } = await httpPut<UserQuestionProgress>(
`${
import.meta.env.PUBLIC_API_URL
}/v1-reset-question-progress/${groupId}`,
{
status: type,
}
);
if (error) {
toast.error(error.message || 'Error resetting progress');
return;
}
knownQuestions = response?.know || [];
didNotKnowQuestions = response?.dontKnow || [];
skipQuestions = response?.skip || [];
}
const pendingQuestions = unshuffledQuestions.filter((question) => {
return (
!knownQuestions.includes(question.id) &&
!didNotKnowQuestions.includes(question.id) &&
!skipQuestions.includes(question.id)
);
});
setUserProgress({
know: knownQuestions,
dontKnow: didNotKnowQuestions,
skip: skipQuestions,
});
setPendingQuestions(pendingQuestions.sort(() => Math.random() - 0.5));
setIsLoading(false);
}
async function updateQuestionStatus(
status: QuestionProgressType,
questionId: string
) {
setIsLoading(true);
let newProgress = userProgress || { know: [], dontKnow: [], skip: [] };
if (!isLoggedIn()) {
if (status === 'know') {
newProgress.know.push(questionId);
} else if (status == 'dontKnow') {
newProgress.dontKnow.push(questionId);
} else if (status == 'skip') {
newProgress.skip.push(questionId);
}
} else {
const { response, error } = await httpPut<UserQuestionProgress>(
`${
import.meta.env.PUBLIC_API_URL
}/v1-update-question-status/${groupId}`,
{
status,
questionId,
questionGroupId: groupId,
}
);
if (error || !response) {
toast.error(error?.message || 'Error marking question status');
return;
}
newProgress = response;
}
const updatedQuestionList = pendingQuestions.filter(
(q) => q.id !== questionId
);
setUserProgress(newProgress);
setPendingQuestions(updatedQuestionList);
setIsLoading(false);
if (updatedQuestionList.length === 0) {
setShowConfetti(true);
}
}
useEffect(() => {
loadQuestions().then(() => null);
}, [unshuffledQuestions]);
const knowCount = userProgress?.know.length || 0;
const dontKnowCount = userProgress?.dontKnow.length || 0;
const skipCount = userProgress?.skip.length || 0;
const hasProgress = knowCount > 0 || dontKnowCount > 0 || skipCount > 0;
const currQuestion = pendingQuestions[0];
const hasFinished = !isLoading && hasProgress && !currQuestion;
return (
<div className="mb-0 sm:mb-40 gap-3 text-center">
<QuestionsProgress
knowCount={knowCount}
didNotKnowCount={dontKnowCount}
skippedCount={skipCount}
totalCount={unshuffledQuestions?.length || questions?.length}
isLoading={isLoading}
showLoginAlert={!isLoggedIn() && hasProgress}
onResetClick={() => {
resetProgress('reset').finally(() => null);
}}
/>
{showConfetti && containerRef.current && (
<Confetti
pieces={100}
element={containerRef.current}
onDone={() => {
setShowConfetti(false);
}}
/>
)}
<div
ref={containerRef}
className="relative mb-4 flex min-h-[250px] w-full overflow-hidden rounded-lg border border-gray-300 bg-white sm:min-h-[400px]"
>
{hasFinished && (
<QuestionFinished
totalCount={unshuffledQuestions?.length || questions?.length || 0}
knowCount={knowCount}
didNotKnowCount={dontKnowCount}
skippedCount={skipCount}
onReset={(type: QuestionProgressType | 'reset') => {
resetProgress(type).finally(() => null);
}}
/>
)}
{!isLoading && currQuestion && <QuestionCard question={currQuestion} />}
{isLoading && <QuestionLoader />}
</div>
<div
className={`flex flex-col gap-1 sm:gap-3 transition-opacity duration-300 sm:flex-row ${
hasFinished ? 'opacity-0' : 'opacity-100'
}`}
>
<button
disabled={isLoading || !currQuestion}
onClick={(e) => {
e.stopPropagation();
e.preventDefault()
updateQuestionStatus('know', currQuestion.id).finally(() => null);
}}
className="flex flex-1 items-center rounded-md sm:rounded-lg border border-gray-300 bg-white text-sm sm:text-base py-2 px-2 sm:py-3 sm:px-4 text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50"
>
<CheckCircle className="mr-1 h-4 text-current" />
Already Know that
</button>
<button
onClick={() => {
updateQuestionStatus('dontKnow', currQuestion.id).finally(
() => null
);
}}
disabled={isLoading || !currQuestion}
className="flex flex-1 items-center rounded-md sm:rounded-lg border border-gray-300 bg-white text-sm sm:text-base py-2 px-2 sm:py-3 sm:px-4 text-black transition-colors hover:border-black hover:bg-black hover:text-white disabled:pointer-events-none disabled:opacity-50"
>
<Sparkles className="mr-1 h-4 text-current" />
Didn't Know that
</button>
<button
onClick={() => {
updateQuestionStatus('skip', currQuestion.id).finally(() => null);
}}
disabled={isLoading || !currQuestion}
data-next-question="skip"
className="flex flex-1 items-center rounded-md sm:rounded-lg border border-red-600 text-sm sm:text-base py-2 px-2 sm:py-3 sm:px-4 text-red-600 hover:bg-red-600 hover:text-white disabled:pointer-events-none disabled:opacity-50"
>
<SkipForward className="mr-1 h-4" />
Skip Question
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,101 @@
import { CheckCircle, RotateCcw, SkipForward, Sparkles } from 'lucide-react';
import { showLoginPopup } from '../../lib/popup';
type QuestionsProgressProps = {
isLoading?: boolean;
showLoginAlert?: boolean;
knowCount?: number;
didNotKnowCount?: number;
totalCount?: number;
skippedCount?: number;
onResetClick?: () => void;
};
export function QuestionsProgress(props: QuestionsProgressProps) {
const {
showLoginAlert,
isLoading = false,
knowCount = 0,
didNotKnowCount = 0,
totalCount = 0,
skippedCount = 0,
onResetClick = () => null,
} = props;
const totalSolved = knowCount + didNotKnowCount + skippedCount;
const donePercentage = (totalSolved / totalCount) * 100;
return (
<div className="mb-3 sm:mb-5 overflow-hidden rounded-lg border border-gray-300 bg-white p-4 sm:p-6">
<div className="mb-3 flex items-center text-gray-600">
<div className="relative w-full flex-1 rounded-xl bg-gray-200 p-1">
<div
className="duration-400 absolute bottom-0 left-0 top-0 rounded-xl bg-slate-800 transition-[width]"
style={{
width: `${donePercentage}%`,
}}
/>
</div>
<span className="ml-3 text-sm">
{totalSolved} / {totalCount}
</span>
</div>
<div className="relative -left-1 flex flex-col gap-2 text-sm text-black sm:flex-row sm:gap-3">
<span className="flex items-center">
<CheckCircle className="mr-1 h-4" />
<span>Knew</span>
<span className="ml-2 rounded-md bg-gray-200/80 px-1.5 font-medium text-black">
<span className="tabular-nums">{knowCount}</span>{' '}
<span className="hidden lg:inline">Questions</span>
<span className="inline sm:hidden">Questions</span>
</span>
</span>
<span className="flex items-center">
<Sparkles className="mr-1 h-4" />
<span>Learnt</span>
<span className="ml-2 rounded-md bg-gray-200/80 px-1.5 font-medium text-black">
<span className="tabular-nums">{didNotKnowCount}</span>{' '}
<span className="hidden lg:inline">Questions</span>
<span className="inline sm:hidden">Questions</span>
</span>
</span>
<span className="flex items-center">
<SkipForward className="mr-1 h-4" />
<span>Skipped</span>
<span className="ml-2 rounded-md bg-gray-200/80 px-1.5 font-medium text-black">
<span className="tabular-nums">{skippedCount}</span>{' '}
<span className="hidden lg:inline">Questions</span>
<span className="inline sm:hidden">Questions</span>
</span>
</span>
<button
disabled={isLoading}
onClick={onResetClick}
className="flex items-center text-red-600 transition-opacity duration-300 hover:text-red-900 disabled:opacity-50"
>
<RotateCcw className="mr-1 h-4" />
Reset
<span className='inline lg:hidden'>Progress</span>
</button>
</div>
{showLoginAlert && (
<p className="-mx-6 mt-6 -mb-6 border-t bg-yellow-100 py-3 text-sm text-yellow-900">
You progress is not saved. Please{' '}
<button
onClick={() => {
showLoginPopup();
}}
className="underline-offset-3 font-medium underline hover:text-black"
>
login to save your progress.
</button>
</p>
)}
</div>
);
}

View File

@@ -18,7 +18,7 @@ const relatedRoadmapDetails = await getRoadmapsByIds(relatedRoadmaps);
<div class='border-t bg-gray-100'>
<div class='container'>
<div class='flex justify-between relative -top-5'>
<h2 class='text-md font-medium py-1 px-3 border bg-white rounded-md'>Related Roadmaps</h2>
<span class='text-md font-medium py-1 px-3 border bg-white rounded-md'>Related Roadmaps</span>
<a href='/roadmaps' class='text-md font-medium py-1 px-3 border bg-white rounded-md hover:bg-gray-50'>
<span class='hidden sm:inline'>All Roadmaps &rarr;</span>
<span class='inline sm:hidden'>More &rarr;</span>

View File

@@ -5,10 +5,10 @@ import { ProgressShareButton } from './UserProgress/ProgressShareButton';
export interface Props {
resourceId: string;
resourceType: ResourceType;
isSecondaryBanner?: boolean;
hasSecondaryBanner?: boolean;
}
const { isSecondaryBanner = false, resourceId, resourceType } = Astro.props;
const { hasSecondaryBanner = false, resourceId, resourceType } = Astro.props;
---
<div
@@ -16,8 +16,8 @@ const { isSecondaryBanner = false, resourceId, resourceType } = Astro.props;
class:list={[
'hidden sm:flex justify-between px-2 bg-white items-center py-1.5 relative striped-loader bg-white',
{
'rounded-bl-md rounded-br-md': isSecondaryBanner,
'rounded-md': !isSecondaryBanner,
'rounded-tl-md rounded-tr-md': hasSecondaryBanner,
'rounded-md': !hasSecondaryBanner,
},
]}
>

View File

@@ -87,7 +87,7 @@ export function RespondInviteForm() {
<div className="container text-center">
<img
alt={'error'}
src={ErrorIcon}
src={ErrorIcon.src}
className="mx-auto mb-4 mt-24 w-20 opacity-20"
/>
@@ -112,7 +112,7 @@ export function RespondInviteForm() {
<div className="container text-center">
<img
alt={'join team'}
src={BuildingIcon}
src={BuildingIcon.src}
className="mx-auto mb-4 mt-24 w-20 opacity-20"
/>

View File

@@ -24,7 +24,7 @@ export function Editor(props: EditorProps) {
</span>
)}
<img src={CopyIcon} alt="Copy" className="inline-block h-4 w-4" />
<img src={CopyIcon.src} alt="Copy" className="inline-block h-4 w-4" />
</button>
</div>
<textarea

View File

@@ -146,7 +146,7 @@ export function RoadCardPage() {
className="flex cursor-pointer items-center justify-center rounded border border-gray-300 p-1.5 px-2 text-sm font-medium disabled:bg-blue-50"
onClick={() => copyText(badgeUrl.toString())}
>
<img alt="Copy" src={CopyIcon} className="mr-1" />
<img alt="Copy" src={CopyIcon.src} className="mr-1" />
{isCopied ? 'Copied!' : 'Copy Link'}
</button>

View File

@@ -8,6 +8,7 @@ import YouTubeAlert from './YouTubeAlert.astro';
import ProgressHelpPopup from './ProgressHelpPopup.astro';
import { MarkFavorite } from './FeaturedItems/MarkFavorite';
import { TeamVersions } from './TeamVersions/TeamVersions';
import { RoadmapFrontmatter } from '../lib/roadmap';
export interface Props {
title: string;
@@ -17,6 +18,7 @@ export interface Props {
roadmapId: string;
isUpcoming?: boolean;
hasSearch?: boolean;
question?: RoadmapFrontmatter['question'];
hasTopics?: boolean;
}
@@ -29,25 +31,43 @@ const {
hasSearch = false,
note,
hasTopics = false,
question,
} = Astro.props;
const isRoadmapReady = !isUpcoming;
const roadmapTitle =
roadmapId === 'devops'
? 'DevOps'
: `${roadmapId.charAt(0).toUpperCase()}${roadmapId.slice(1)}`;
const hasTnsBanner = !!tnsBannerLink;
---
<LoginPopup />
<ProgressHelpPopup />
<div class='border-b'>
<div class='container relative py-5 sm:py-12'>
<div class='relative border-b'>
<div
class:list={[
'container relative py-5',
{
'sm:py-16': hasTnsBanner,
'sm:py-12': !hasTnsBanner,
},
]}
>
<div class='mb-3 mt-0 sm:mb-4'>
<h1 class='mb-0.5 text-2xl font-bold sm:mb-2 sm:text-4xl'>
{title}
<MarkFavorite
resourceId={roadmapId}
resourceType='roadmap'
className='text-gray-500 !opacity-100 hover:text-gray-600 [&>svg]:stroke-[0.4] [&>svg]:stroke-gray-400 hover:[&>svg]:stroke-gray-600 [&>svg]:h-4 [&>svg]:w-4 sm:[&>svg]:h-5 sm:[&>svg]:w-5 ml-1.5 relative focus:outline-0'
client:only="react"
/>
<span class='relative top-0 sm:-top-1'>
<MarkFavorite
resourceId={roadmapId}
resourceType='roadmap'
className='text-gray-500 !opacity-100 hover:text-gray-600 [&>svg]:stroke-[0.4] [&>svg]:stroke-gray-400 hover:[&>svg]:stroke-gray-600 [&>svg]:h-4 [&>svg]:w-4 sm:[&>svg]:h-4 sm:[&>svg]:w-4 ml-1.5 relative focus:outline-0'
client:only='react'
/>
</span>
</h1>
<p class='text-sm text-gray-500 sm:text-lg'>{description}</p>
</div>
@@ -121,7 +141,7 @@ const isRoadmapReady = !isUpcoming;
<TeamVersions
resourceType='roadmap'
resourceId={roadmapId}
client:only
client:only='react'
/>
{
@@ -144,12 +164,31 @@ const isRoadmapReady = !isUpcoming;
<!-- Desktop: Roadmap Resources - Alert -->
{
hasTopics && (
<RoadmapHint roadmapId={roadmapId} tnsBannerLink={tnsBannerLink} />
<RoadmapHint
tnsBannerLink={tnsBannerLink}
titleQuestion={question?.title}
titleAnswer={question?.description}
roadmapId={roadmapId}
/>
)
}
{hasSearch && <TopicSearch />}
</div>
{
tnsBannerLink && (
<div class='absolute left-0 right-0 top-0 hidden border-b border-b-gray-200 px-2 py-1.5 sm:block'>
<p class='py-0.5 text-center text-sm'>
<span class='badge mr-1'>Partner</span>
Get the latest {roadmapTitle} news from our sister site{' '}
<a href={tnsBannerLink} target='_blank' class='font-medium underline'>
TheNewStack.io
</a>
</p>
</div>
)
}
</div>
{note && <RoadmapNote text={note} />}

View File

@@ -1,47 +1,57 @@
---
import AstroIcon from './AstroIcon.astro';
import Icon from './AstroIcon.astro';
import { RoadmapTitleQuestion } from './RoadmapTitleQuestion.tsx';
import ResourceProgressStats from './ResourceProgressStats.astro';
export interface Props {
roadmapId: string;
tnsBannerLink?: string;
titleQuestion?: string;
titleAnswer?: string;
}
const { roadmapId, tnsBannerLink = '' } = Astro.props;
const hasTNSBanner = !!tnsBannerLink;
const roadmapTitle =
roadmapId === 'devops'
? 'DevOps'
: `${roadmapId.charAt(0).toUpperCase()}${roadmapId.slice(1)}`;
const {
roadmapId,
titleQuestion = '',
titleAnswer = '',
tnsBannerLink,
} = Astro.props;
const hasTitleQuestion = titleQuestion && titleAnswer;
const hasTnsBanner = !!tnsBannerLink;
---
<div
class:list={[
'mt-4 sm:mt-7 border-0 sm:border rounded-md mb-0',
{
'sm:-mb-[82px]': hasTNSBanner,
'sm:-mb-[65px]': !hasTNSBanner,
},
'mt-4 sm:mt-7 border-0 sm:border rounded-md mb-0 bg-white',
...(hasTnsBanner
? [
{
'sm:-mb-[110px]': hasTitleQuestion,
'sm:-mb-[81px]': !hasTitleQuestion,
},
]
: [
{
'sm:-mb-[88px]': hasTitleQuestion,
'sm:-mb-[65px]': !hasTitleQuestion,
},
]),
]}
>
<ResourceProgressStats
resourceId={roadmapId}
resourceType='roadmap'
hasSecondaryBanner={hasTitleQuestion}
/>
{
hasTNSBanner && (
<div class='hidden border-b bg-gray-100 px-2 py-1.5 sm:block'>
<p class='text-sm'>
Get the latest {roadmapTitle} news from our sister site{' '}
<a
href={tnsBannerLink}
target='_blank'
class='font-semibold underline'
>
TheNewStack.io
</a>
</p>
</div>
hasTitleQuestion && (
<RoadmapTitleQuestion
client:load
question={titleQuestion}
answer={titleAnswer}
/>
)
}
<ResourceProgressStats isSecondaryBanner={hasTNSBanner} resourceId={roadmapId} resourceType='roadmap' />
</div>
</div>

View File

@@ -0,0 +1,70 @@
import { ChevronDown, ChevronUp, GraduationCap } from 'lucide-react';
import { useRef, useState } from 'react';
import { useOutsideClick } from '../hooks/use-outside-click';
import { markdownToHtml } from '../lib/markdown';
type RoadmapTitleQuestionProps = {
question: string;
answer: string;
};
export function RoadmapTitleQuestion(props: RoadmapTitleQuestionProps) {
const { question, answer } = props;
const [isAnswerVisible, setIsAnswerVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useOutsideClick(ref, () => {
setIsAnswerVisible(false);
});
return (
<div className="relative hidden border-t text-sm font-medium sm:block">
{isAnswerVisible && (
<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>
)}
<h2
className="z-50 flex cursor-pointer items-center px-2 py-2.5 font-medium text-base"
aria-expanded={isAnswerVisible ? 'true' : 'false'}
onClick={(e) => {
e.preventDefault();
setIsAnswerVisible(!isAnswerVisible);
}}
>
<span className="flex items-center flex-grow">
<GraduationCap className="mr-2 inline-block h-6 w-6" />
{question}
</span>
<span className="flex-shrink-0 text-gray-400">
<ChevronDown className={`inline-block h-5 w-5`} />
</span>
</h2>
<div
className={`absolute left-0 right-0 top-0 z-50 mt-0 rounded-md border bg-white ${
isAnswerVisible ? 'block' : 'hidden'
}`}
ref={ref}
>
{isAnswerVisible && (
<h2
className="flex cursor-pointer items-center border-b px-[7px] py-[9px] text-base font-medium"
onClick={() => setIsAnswerVisible(false)}
>
<span className="flex flex-grow items-center">
<GraduationCap className="mr-2 inline-block h-6 w-6" />
{question}
</span>
<span className="flex-shrink-0 text-gray-400">
<ChevronUp className={`inline-block h-5 w-5`} />
</span>
</h2>
)}
<div
className="bg-gray-100 [&>p]:text-gray-800 p-3 text-base [&>h2]:mb-2 [&>h2]:mt-5 [&>h2]:text-[17px] [&>h2]:font-medium [&>p:last-child]:mb-0 [&>p>a]:font-semibold [&>p>a]:underline [&>p>a]:underline-offset-2 [&>p]:mb-3 [&>p]:font-normal [&>p]:leading-relaxed"
dangerouslySetInnerHTML={{ __html: markdownToHtml(answer, false) }}
></div>
</div>
</div>
);
}

View File

@@ -140,7 +140,7 @@ export function TeamDropdown() {
{isLoading && 'Loading ..'}
</span>
</div>
<img alt={'show dropdown'} src={ChevronDown} className="h-4 w-4" />
<img alt={'show dropdown'} src={ChevronDown.src} className="h-4 w-4" />
</button>
{showDropdown && (

View File

@@ -1,8 +1,8 @@
import { FormEvent, useEffect, useRef, useState } from 'react';
import { type FormEvent, useEffect, useRef, useState } from 'react';
import { httpPost } from '../../lib/http';
import { useTeamId } from '../../hooks/use-team-id';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { AllowedRoles, RoleDropdown } from '../CreateTeam/RoleDropdown';
import { type AllowedRoles, RoleDropdown } from '../CreateTeam/RoleDropdown';
type InviteMemberPopupProps = {
onInvited: () => void;

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { type FormEvent, useEffect, useRef, useState } from 'react';
import { httpDelete } from '../../lib/http';
import { useTeamId } from '../../hooks/use-team-id';
import { useOutsideClick } from '../../hooks/use-outside-click';
@@ -23,7 +23,7 @@ export function LeaveTeamPopup(props: LeaveTeamPopupProps) {
confirmationEl?.current?.focus();
}, []);
const handleSubmit = async (e: Event) => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setError('');

View File

@@ -81,7 +81,7 @@ export function MemberActionDropdown({
onClick={() => setIsOpen(!isOpen)}
className="ml-2 flex items-center opacity-60 transition-opacity hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-30"
>
<img alt="menu" src={MoreIcon} className="h-4 w-4" />
<img alt="menu" src={MoreIcon.src} className="h-4 w-4" />
</button>
{isOpen && (

View File

@@ -1,8 +1,8 @@
import { FormEvent, useRef, useState } from 'react';
import { type FormEvent, useRef, useState } from 'react';
import { httpPut } from '../../lib/http';
import { useTeamId } from '../../hooks/use-team-id';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { AllowedRoles, RoleDropdown } from '../CreateTeam/RoleDropdown';
import { type AllowedRoles, RoleDropdown } from '../CreateTeam/RoleDropdown';
import type { TeamMemberDocument } from './TeamMembersPage';
type InviteMemberPopupProps = {

View File

@@ -31,7 +31,7 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
>
<img
alt={'link'}
src={ExternalLinkIcon}
src={ExternalLinkIcon.src}
className="ml-2 h-4 w-4 opacity-20 transition-opacity group-hover:opacity-100"
/>
</a>

View File

@@ -8,8 +8,8 @@ import type { TeamMember } from './TeamProgressPage';
import { httpGet } from '../../lib/http';
import {
renderTopicProgress,
ResourceProgressType,
ResourceType,
type ResourceProgressType,
type ResourceType,
updateResourceProgress,
} from '../../lib/resource-progress';
import CloseIcon from '../../icons/close.svg';
@@ -413,7 +413,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
}`}
onClick={onClose}
>
<img alt={'close'} src={CloseIcon} className="h-4 w-4" />
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
<span className="sr-only">Close modal</span>
</button>
</div>

View File

@@ -188,7 +188,7 @@ export function TeamRoadmaps() {
{addRoadmapModal}
<img
alt="roadmap"
src={RoadmapIcon}
src={RoadmapIcon.src}
className="mb-4 h-24 w-24 opacity-10"
/>
<h3 className="mb-1 text-2xl font-bold text-gray-900">No roadmaps</h3>
@@ -259,7 +259,7 @@ export function TeamRoadmaps() {
<img
alt={'link'}
src={ExternalLinkIcon}
src={ExternalLinkIcon.src}
className="ml-2 h-4 w-4 opacity-20 transition-opacity group-hover:opacity-100"
/>
</a>
@@ -332,7 +332,7 @@ export function TeamRoadmaps() {
>
<img
alt="add"
src={PlusIcon}
src={PlusIcon.src}
className="mb-1 h-6 w-6 opacity-20 transition-opacity group-hover:opacity-100"
/>
<span className="text-sm text-gray-400 transition-colors focus:outline-none group-hover:text-black">

View File

@@ -1,4 +1,4 @@
import { FormEvent, useEffect, useState } from 'react';
import { type FormEvent, useEffect, useState } from 'react';
import { httpGet, httpPut } from '../../lib/http';
import { Spinner } from '../ReactIcons/Spinner';
import UploadProfilePicture from '../UpdateProfile/UploadProfilePicture';
@@ -9,7 +9,6 @@ import { DeleteTeamPopup } from '../DeleteTeamPopup';
import { $isCurrentTeamAdmin } from '../../stores/team';
import { useStore } from '@nanostores/react';
import { useToast } from '../../hooks/use-toast';
export function UpdateTeamForm() {
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
@@ -25,8 +24,6 @@ export function UpdateTeamForm() {
const [gitHub, setGitHub] = useState('');
const [teamType, setTeamType] = useState('');
const [teamSize, setTeamSize] = useState('');
const [roadmaps, setRoadmaps] = useState<string[]>([]);
const [bestPractices, setBestPractices] = useState<string[]>([]);
const validTeamSizes = [
'0-1',
'2-10',

View File

@@ -29,26 +29,26 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
title: 'Progress',
href: `/team/progress?t=${teamId}`,
id: 'progress',
icon: TeamProgress,
icon: TeamProgress.src,
},
{
title: 'Roadmaps',
href: `/team/roadmaps?t=${teamId}`,
id: 'roadmaps',
icon: MapIcon,
icon: MapIcon.src,
hasWarning: currentTeam?.roadmaps?.length === 0,
},
{
title: 'Members',
href: `/team/members?t=${teamId}`,
id: 'members',
icon: GroupIcon,
icon: GroupIcon.src,
},
{
title: 'Settings',
href: `/team/settings?t=${teamId}`,
id: 'settings',
icon: SettingsIcon,
icon: SettingsIcon.src,
},
];
@@ -66,7 +66,7 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
sidebarLinks.find((sidebarLink) => sidebarLink.id === activePageId)
?.title
}
<img alt="menu" src={ChevronDown} className="h-4 w-4" />
<img alt="menu" src={ChevronDown.src} className="h-4 w-4" />
</button>
{menuShown && (
<ul
@@ -80,7 +80,7 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
activePageId === 'team' ? 'bg-slate-100' : ''
}`}
>
<img alt={'teams'} src={GroupIcon} className={`mr-2 h-4 w-4`} />
<img alt={'teams'} src={GroupIcon.src} className={`mr-2 h-4 w-4`} />
Personal Account / Teams
</a>
</li>
@@ -113,7 +113,7 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
>
<img
alt={'menu icon'}
src={ChatIcon}
src={ChatIcon.src}
className="mr-2 h-4 w-4"
/>
Send Feedback
@@ -174,7 +174,7 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
className="mr-3 mt-4 flex items-center justify-center rounded-md border p-2 text-sm text-gray-500 transition-colors hover:border-gray-300 hover:bg-gray-50 hover:text-black"
onClick={() => setShowFeedbackPopup(true)}
>
<img alt={'feedback'} src={ChatIcon} className="mr-2 h-4 w-4" />
<img alt={'feedback'} src={ChatIcon.src} className="mr-2 h-4 w-4" />
Send Feedback
</button>
</nav>

View File

@@ -144,7 +144,7 @@ export function TeamVersions(props: TeamVersionsProps) {
</span>
<img
alt="Dropdown"
src={DropdownIcon as any}
src={DropdownIcon.src}
className="h-3 w-3 sm:h-4 sm:w-4"
/>
</div>

View File

@@ -12,9 +12,9 @@ import {
isTopicDone,
refreshProgressCounters,
renderTopicProgress,
ResourceType,
updateResourceProgress as updateResourceProgressApi,
} from '../../lib/resource-progress';
import type { ResourceType } from '../../lib/resource-progress';
import { pageProgressMessage, sponsorHidden } from '../../stores/page';
import { TopicProgressButton } from './TopicProgressButton';
import { ContributionForm } from './ContributionForm';
@@ -147,7 +147,7 @@ export function TopicDetail() {
{isLoading && (
<div className="flex w-full justify-center">
<img
src={SpinnerIcon as any}
src={SpinnerIcon.src}
alt="Loading"
className="h-6 w-6 animate-spin fill-blue-600 text-gray-200 sm:h-12 sm:w-12"
/>
@@ -192,7 +192,7 @@ export function TopicDetail() {
setIsContributing(false);
}}
>
<img alt="Close" className="h-5 w-5" src={CloseIcon as any} />
<img alt="Close" className="h-5 w-5" src={CloseIcon.src} />
</button>
</div>
@@ -206,7 +206,8 @@ export function TopicDetail() {
{/* Contribution */}
<div className="mt-8 flex-1 border-t">
<p className="mb-2 mt-2 text-sm leading-relaxed text-gray-400">
Help others learn by submitting links to learn more about this topic{' '}
Help others learn by submitting links to learn more about this
topic{' '}
</p>
<button
onClick={() => {

View File

@@ -5,13 +5,12 @@ import DownIcon from '../../icons/down.svg';
import SpinnerIcon from '../../icons/spinner.svg';
import { isLoggedIn } from '../../lib/jwt';
import {
ResourceProgressType,
ResourceType,
getTopicStatus,
refreshProgressCounters,
renderTopicProgress,
updateResourceProgress,
} from '../../lib/resource-progress';
import type { ResourceProgressType, ResourceType } from '../../lib/resource-progress';
import { showLoginPopup } from '../../lib/popup';
import { useToast } from '../../hooks/use-toast';
@@ -165,7 +164,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
if (isUpdatingProgress) {
return (
<button className="inline-flex cursor-default items-center rounded-md border border-gray-300 bg-white p-1 px-2 text-sm text-black">
<img alt="Check" className="h-4 w-4 animate-spin" src={SpinnerIcon} />
<img alt="Check" className="h-4 w-4 animate-spin" src={SpinnerIcon.src} />
<span className="ml-2">Updating Status..</span>
</button>
);
@@ -189,7 +188,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
onClick={() => setShowChangeStatus(true)}
>
<span className="mr-0.5">Update Status</span>
<img alt="Check" className="h-4 w-4" src={DownIcon} />
<img alt="Check" className="h-4 w-4" src={DownIcon.src} />
</button>
{showChangeStatus && (

View File

@@ -1,4 +1,4 @@
import { FormEvent, useEffect, useState } from 'react';
import { type FormEvent, useEffect, useState } from 'react';
import { httpGet, httpPost } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page';

View File

@@ -1,4 +1,4 @@
import { FormEvent, useEffect, useState } from 'react';
import { type FormEvent, useEffect, useState } from 'react';
import { httpGet, httpPost } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page';
import UploadProfilePicture from './UploadProfilePicture';

View File

@@ -1,5 +1,5 @@
import Cookies from 'js-cookie';
import { ChangeEvent, FormEvent, useEffect, useRef, useState } from 'react';
import { type ChangeEvent, type FormEvent, useEffect, useRef, useState } from 'react';
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
interface PreviewFile extends File {

View File

@@ -292,7 +292,7 @@ export function UserProgressModal(props: ProgressMapProps) {
className={`absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-gray-100 bg-transparent p-1.5 text-sm text-gray-400 hover:text-gray-900 lg:hidden`}
onClick={onClose}
>
<img alt={'close'} src={CloseIcon as any} className="h-4 w-4" />
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
<span className="sr-only">Close modal</span>
</button>
</div>

View File

@@ -2,4 +2,4 @@
> Set up automated security auditing.
It's important to keep track of changes in your infrastructure's security settings. One way to do this is to first set up a security auditer role ([JSON template](https://gist.github.com/bigsnarfdude/d0758b4fd335085623be)), which will give anyone assigned that role read-only access to any security related settings on your account. You can then use this rather [fantastic Python script](https://gist.github.com/jlevy/cce1b44fc24f94599d0a4b3e613cc15d), which will go over all the items in your account and produce a canonical output showing your configuration. You set up a cronjob somewhere to run this script, and compare its output to the output from the previous run. Any differences will show you exactly what has been changed in your security configuration. It's useful to set this up and just have it email you the diff of any changes. (Source: Intrusion Detection in the Cloud - [Presentation](http://awsmedia.s3.amazonaws.com/SEC402.pdf))
It's important to keep track of changes in your infrastructure's security settings. One way to do this is to first set up a security auditer role ([JSON template](https://gist.github.com/bigsnarfdude/d0758b4fd335085623be)), which will give anyone assigned that role read-only access to any security related settings on your account. You can then use this rather [fantastic Python script](https://gist.github.com/jlevy/cce1b44fc24f94599d0a4b3e613cc15d), which will go over all the items in your account and produce a canonical output showing your configuration. You set up a cronjob somewhere to run this script, and compare its output to the output from the previous run. Any differences will show you exactly what has been changed in your security configuration. It's useful to set this up and just have it email you the diff of any changes. (Source: Intrusion Detection in the Cloud - [Presentation](https://awsmedia.s3.amazonaws.com/SEC402.pdf))

View File

@@ -6,6 +6,6 @@ Removing all unnecessary spaces, comments and attributes will reduce the size of
Most of the frameworks have plugins to facilitate the minification of the webpages. You can use a bunch of NPM modules that can do the job for you automatically.
- [HTML minifier | Minify Code](http://minifycode.com/html-minifier/)
- [HTML minifier | Code Beautify](https://codebeautify.org/minify-html)
- [Online HTML Compressor](http://refresh-sf.com)
- [Experimenting with HTML minifier — Perfection Kills](http://perfectionkills.com/experimenting-with-html-minifier/#use_short_doctype)

View File

@@ -1,12 +0,0 @@
[
{
"id": "what-is-server",
"question": "What is a Server",
"answer": {
"heading": "",
"answer": "",
"list": [],
"file": "what-is-server.md"
}
}
]

View File

@@ -1 +0,0 @@
## What is Server

View File

@@ -0,0 +1,27 @@
Let's see how we can use the `alert`, `prompt` and `confirm` functions to interact with the user.
## alert()
The `alert()` method displays an alert box with a specified message and an OK button.
```js
alert('Hello World!');
```
## prompt()
The `prompt()` method displays a dialog box that prompts the visitor for input. A prompt box is often used if you want the user to input a value before entering a page. The `prompt()` method returns the input value if the user clicks OK. If the user clicks Cancel, the method returns `null`.
```js
const name = prompt('What is your name?');
console.log(name);
```
## confirm()
The `confirm()` method displays a dialog box with a specified message, along with an OK and a Cancel button. This is often used to confirm or verify something from the user.
```js
const result = confirm('Are you sure?');
console.log(result); // true/false
```

View File

@@ -0,0 +1,32 @@
You can add a new element to the DOM using the `appendChild` or `insertBefore` method.
## appendChild
The `appendChild` method adds a new element as the last child of the specified parent element.
```js
const roadmapWrapper = document.querySelector('.roadmap-wrapper');
const roadmap = document.createElement('div');
roadmap.id = 'javascript-roadmap';
roadmapWrapper.appendChild(roadmapTitle);
```
In the example above, the `roadmap` element is added as the last child of the `roadmapWrapper` element.
## insertBefore
The `insertBefore` method adds a new element before the specified child element.
```js
const roadmapWrapper = document.querySelector('.roadmap-wrapper');
const roadmap = document.createElement('div');
roadmap.id = 'javascript-roadmap';
const roadmapTitle = document.querySelector('#roadmap-title');
roadmapWrapper.insertBefore(roadmap, roadmapTitle);
```
In the example above, the `roadmap` element is added before the `roadmapTitle` element.

View File

@@ -0,0 +1,27 @@
The difference between Asynchronous and Synchronous code is that Asynchronous code does not block the execution of the program while Synchronous code does.
## Asynchronous code
Asynchronous code is executed in the background and it does not block the execution of the program. It is usually used to perform tasks that take a long time to complete, such as network requests.
```js
console.log('Before');
setTimeout(() => {
console.log('Hello');
}, 1000);
console.log('After');
```
## Synchronous code
Synchronous code is executed in sequence and it blocks the execution of the program until it is completed. If a task takes a long time to complete, everything else waits.
```js
console.log('Before');
for (let i = 0; i < 1000000000; i++) {}
console.log('After');
```

View File

@@ -0,0 +1,28 @@
You can use `break` and `continue` in loops to alter the flow of the loop. `break` will stop the loop from continuing, and `continue` will skip the current iteration and continue the loop.
```js
for (let i = 0; i < 5; i++) {
if (i === 1) {
continue; // skips the rest of the code in the loop
}
console.log(`i: ${i}`);
}
// Output:
// i: 0
// i: 2
// i: 3
// i: 4
```
```js
for (let i = 0; i < 5; i++) {
if (i === 1) {
break; // stops the loop
}
console.log(`i: ${i}`);
}
// Output:
// i: 0
```

View File

@@ -0,0 +1,48 @@
**Callback hell**, often referred to as **Pyramid of Doom**, describes a situation in JavaScript where multiple nested callbacks become difficult to manage, leading to unreadable and unmaintainable code. It often arises when performing multiple asynchronous operations that depend on the completion of previous operations. The code starts to take on a pyramidal shape due to the nesting.
## Example of callback hell
```js
callAsync1(function () {
callAsync2(function () {
callAsync3(function () {
callAsync4(function () {
callAsync5(function () {
// ...
});
});
});
});
});
```
## Strategies to avoid callback hell
Developers can address or avoid callback hell by using strategies like modularizing the code into named functions, using asynchronous control flow libraries, or leveraging modern JavaScript features like Promises and `async/await` to write more linear, readable asynchronous code.
### Promise chaining
```js
callAsync1()
.then(() => callAsync2())
.then(() => callAsync3())
.then(() => callAsync4())
.then(() => callAsync5())
.catch((err) => console.error(err));
```
### Async/await
```js
async function asyncCall() {
try {
await callAsync1();
await callAsync2();
await callAsync3();
await callAsync4();
await callAsync5();
} catch (err) {
console.error(err);
}
}
```

View File

@@ -0,0 +1,18 @@
A closure is a function that has access to its outer function scope even after the outer function has returned. This means a closure can remember and access variables and arguments of its outer function even after the function has finished.
```js
function outer() {
const name = 'Roadmap';
function inner() {
console.log(name);
}
return inner;
}
const closure = outer();
closure(); // Roadmap
```
In the above example, the `inner` function has access to the `name` variable of the `outer` function even after the `outer` function has returned. Therefore, the `inner` function forms a closure.

View File

@@ -0,0 +1,8 @@
The Comma Operator `,` evaluates each of its operands (from left to right) and returns the value of the last operand.
```js
let x = 1;
x = (x++, x);
console.log(x); // 2
```

View File

@@ -0,0 +1,9 @@
To create a new DOM element, you can use the `document.createElement` method. It accepts a tag name as an argument and returns a new element with the specified tag name. You can set attributes to the element.
```js
const div = document.createElement('div');
div.id = 'roadmap-wrapper';
div.setAttribute('data-id', 'javascript');
console.log(div); // <div id="roadmap-wrapper" data-id="javascript"></div>
```

View File

@@ -0,0 +1,33 @@
You can use the `CustomEvent` constructor to create a custom event. The `CustomEvent` constructor accepts two arguments: the event name and an optional object that specifies the event options. And you can use the `dispatchEvent` method to dispatch the custom event on the target element/document.
## Creating Custom Events
```js
const event = new CustomEvent('roadmap-updated', {
detail: { name: 'JavaScript' },
});
element.dispatchEvent(event);
```
## Listening for Custom Events
You can listen for custom events using the `addEventListener` method. The `addEventListener` method accepts the event name and a callback function that is called when the event is dispatched.
```js
element.addEventListener('roadmap-updated', (event) => {
console.log(event.detail); // { name: 'JavaScript' }
});
```
## Removing Event Listeners
You can remove event listeners using the `removeEventListener` method. The `removeEventListener` method accepts the event name and the callback function that was used to add the event listener.
```js
function handleEvent(event) {
console.log(event.detail); // { name: 'JavaScript' }
}
element.addEventListener('roadmap-updated', handleEvent);
element.removeEventListener('roadmap-updated', handleEvent);
```

View File

@@ -0,0 +1,38 @@
Debugging JavaScript code can be achieved through various methods and tools. Here's a basic guide:
## Console Logging:
You can use `console.log()`, `console.warn()`, `console.error()`, etc., to print values, variables, or messages to the browser's developer console.
```js
console.log('Value of x:', x);
```
## Browser Developer Tools:
Most modern browsers come equipped with developer tools. You can access these tools by pressing `F12` or right-clicking on the web page and selecting `Inspect` or `Inspect Element`.
- **Sources Tab**: Allows you to see the loaded scripts, set breakpoints, and step through the code.
- **Console Tab**: Displays console outputs and allows for interactive JavaScript execution.
- **Network Tab**: Helps in checking network requests and responses.
## Setting Breakpoints:
In the `Sources` tab of the browser's developer tools, you can click on a line number to set a breakpoint. The code execution will pause at this line, allowing you to inspect variables, the call stack, and continue step-by-step.
## Debugger Statement:
Inserting the `debugger;` statement in your code will act as a breakpoint when the browser developer tools are open. Execution will pause at the `debugger;` line.
```js
function myFunction() {
debugger; // Execution will pause here when dev tools are open
// ... rest of the code
}
```
## Call Stack and Scope:
In the developer tools, when paused on a breakpoint or `debugger;` statement, you can inspect the `call stack` to see the sequence of function calls. The `Scope` panel will show you the values of local and global variables.
Remember, debugging is an iterative process. It often involves setting breakpoints, checking variables, adjusting code, and re-running to ensure correctness.

View File

@@ -0,0 +1,25 @@
The main difference between `defer` and `async` is the order of execution.
## Defer attribute
A `<script>` element with a `defer` attribute, it will continue to load the HTML page and render it while the script is being downloaded. The script is executed after the HTML page has been completely parsed. `defer` scripts maintain their order in the document.
```html
<script defer src="script1.js"></script>
<script defer src="script2.js"></script>
```
In the example above, `script1.js` will be executed before `script2.js`. The browser will download both scripts in parallel, but `script1.js` will be executed after the HTML page has been parsed and `script2.js` will be executed after `script1.js` has been executed.
## Async attribute
On the other hand, A `<script>` element with an `async` attribute, it will pause the HTML parser and execute the script immediately after it has been downloaded. The HTML parsing will resume after the script has been executed.
```html
<script async src="script1.js"></script>
<script async src="script2.js"></script>
```
In the example above, the browser will download both scripts in parallel, and execute them as soon as they are downloaded. The order of execution is not guaranteed.
To know more you can check [this diagram](https://roadmap.sh/guides/avoid-render-blocking-javascript-with-async-defer) from us that explains the difference between `defer` and `async` in a visual way.

View File

@@ -0,0 +1,14 @@
The `do...while` statement creates a loop that executes a block of code once, before checking if the condition is `true`, then it will repeat the loop as long as the condition is `true`.
```js
let i = 0;
do {
console.log(i);
i++;
} while (i < 3);
// 0
// 1
// 2
```

View File

@@ -0,0 +1,7 @@
The `==` equality operator converts the operands if they are not of the same type, then applies strict comparison. The `===` strict equality operator only considers values equal that have the same type.
```js
console.log(1 == '1'); // true
console.log(1 === '1'); // false
console.log(1 === 1); // true
```

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