Compare commits
89 Commits
feat/versi
...
fix/markdo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5420d2f56 | ||
|
|
966d5fedb5 | ||
|
|
243778cf11 | ||
|
|
9c9c59911b | ||
|
|
7a93301b5b | ||
|
|
aa056c1da8 | ||
|
|
13d1879977 | ||
|
|
aca3163ba9 | ||
|
|
5e80d9d4d8 | ||
|
|
0fc28c482a | ||
|
|
837d2ac782 | ||
|
|
68c62bacc2 | ||
|
|
720438e619 | ||
|
|
3afab1aa70 | ||
|
|
f40585f992 | ||
|
|
9232d03e24 | ||
|
|
01cb4b5131 | ||
|
|
50f02b504a | ||
|
|
2d12bffe46 | ||
|
|
3b1762cd91 | ||
|
|
d9be6e3c8b | ||
|
|
b65328ebc9 | ||
|
|
5da5924b6c | ||
|
|
b35a169315 | ||
|
|
9d05c64f50 | ||
|
|
e94296cdd4 | ||
|
|
7a4796508d | ||
|
|
e0f5d6f436 | ||
|
|
d103bc629c | ||
|
|
cb9943191e | ||
|
|
eaa567dfe0 | ||
|
|
277713e16b | ||
|
|
5ed49b965c | ||
|
|
a27aaf6e2d | ||
|
|
be02cc59ea | ||
|
|
068847af08 | ||
|
|
c6c91ef8fe | ||
|
|
8fb3e7983b | ||
|
|
80ec1a1c4b | ||
|
|
76d1ca1333 | ||
|
|
40357f7956 | ||
|
|
581f4a76a4 | ||
|
|
ef1a3031c4 | ||
|
|
3774f3c5ec | ||
|
|
b11da48f41 | ||
|
|
5edda5654c | ||
|
|
505077a545 | ||
|
|
9f4967929f | ||
|
|
27cb89494f | ||
|
|
ec556915e4 | ||
|
|
c61e44119d | ||
|
|
6f46d723bc | ||
|
|
ee6e3e4029 | ||
|
|
6e9fe97e5c | ||
|
|
13af03c930 | ||
|
|
78692ff13f | ||
|
|
54d7388b09 | ||
|
|
b609c43055 | ||
|
|
d83fe1279b | ||
|
|
fb3cb85c14 | ||
|
|
82dbca95fb | ||
|
|
7e702ee385 | ||
|
|
08fbb730ab | ||
|
|
cd80338fa6 | ||
|
|
fa33d0c339 | ||
|
|
8ec9a6e675 | ||
|
|
16853df928 | ||
|
|
c15d139d54 | ||
|
|
4e5cc5bd35 | ||
|
|
a36bca2f42 | ||
|
|
10b688049d | ||
|
|
0db92f6418 | ||
|
|
dccaa66ed4 | ||
|
|
3deee4dfc3 | ||
|
|
980e243124 | ||
|
|
044046e044 | ||
|
|
793764c3a3 | ||
|
|
abc8a97676 | ||
|
|
79355cd876 | ||
|
|
2809b81920 | ||
|
|
204a9577cd | ||
|
|
577e724aa7 | ||
|
|
14a1544ed4 | ||
|
|
14ea7ba0ad | ||
|
|
5e7ec4f8d8 | ||
|
|
417badc6ea | ||
|
|
0558957673 | ||
|
|
7f6a42a0c5 | ||
|
|
cc258b7612 |
@@ -1,4 +1,4 @@
|
||||
name: Deployment to GH Pages
|
||||
name: App Deployment
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
11028
package-lock.json
generated
56
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "roadmap.sh",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev --port 3000",
|
||||
@@ -22,48 +22,48 @@
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/react": "^3.0.3",
|
||||
"@astrojs/sitemap": "^3.0.2",
|
||||
"@astrojs/tailwind": "^5.0.2",
|
||||
"@fingerprintjs/fingerprintjs": "^4.1.0",
|
||||
"@astrojs/react": "^3.0.7",
|
||||
"@astrojs/sitemap": "^3.0.3",
|
||||
"@astrojs/tailwind": "^5.0.3",
|
||||
"@fingerprintjs/fingerprintjs": "^4.2.1",
|
||||
"@nanostores/react": "^0.7.1",
|
||||
"@types/react": "^18.2.31",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"astro": "^3.3.3",
|
||||
"astro-compress": "^2.1.5",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"astro": "^4.0.3",
|
||||
"astro-compress": "^2.2.3",
|
||||
"clsx": "^2.0.0",
|
||||
"dracula-prism": "^2.1.13",
|
||||
"jose": "^4.15.4",
|
||||
"jose": "^5.1.3",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.288.0",
|
||||
"nanoid": "^5.0.2",
|
||||
"nanostores": "^0.9.4",
|
||||
"node-html-parser": "^6.1.10",
|
||||
"npm-check-updates": "^16.14.6",
|
||||
"lucide-react": "^0.294.0",
|
||||
"nanoid": "^5.0.4",
|
||||
"nanostores": "^0.9.5",
|
||||
"node-html-parser": "^6.1.11",
|
||||
"npm-check-updates": "^16.14.11",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"reactflow": "^11.9.4",
|
||||
"reactflow": "^11.10.1",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"roadmap-renderer": "^1.0.6",
|
||||
"slugify": "^1.6.6",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"zustand": "^4.4.4"
|
||||
"tailwind-merge": "^2.1.0",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/js-cookie": "^3.0.5",
|
||||
"@types/prismjs": "^1.26.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
"csv-parser": "^3.0.0",
|
||||
"gh-pages": "^6.0.0",
|
||||
"gh-pages": "^6.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"markdown-it": "^13.0.2",
|
||||
"openai": "^4.13.0",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-astro": "^0.12.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.6"
|
||||
"markdown-it": "^14.0.0",
|
||||
"openai": "^4.20.1",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier-plugin-astro": "^0.12.2",
|
||||
"prettier-plugin-tailwindcss": "^0.5.9"
|
||||
}
|
||||
}
|
||||
|
||||
2005
pnpm-lock.yaml
generated
3
public/images/hackernews.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32">
|
||||
<path fill="#94a3b8" d="M5 5v22h22V5zm2 2h18v18H7zm4.5 4l3.5 6v5h2v-5l3.5-6h-2L16 15.281L13.5 11z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 203 B |
BIN
public/images/partners/zilliz.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
1
public/images/reddit.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1024 1024"><path fill="#94a3b8" d="M288 568a56 56 0 1 0 112 0a56 56 0 1 0-112 0zm338.7 119.7c-23.1 18.2-68.9 37.8-114.7 37.8s-91.6-19.6-114.7-37.8c-14.4-11.3-35.3-8.9-46.7 5.5s-8.9 35.3 5.5 46.7C396.3 771.6 457.5 792 512 792s115.7-20.4 155.9-52.1a33.25 33.25 0 1 0-41.2-52.2zM960 456c0-61.9-50.1-112-112-112c-42.1 0-78.7 23.2-97.9 57.6c-57.6-31.5-127.7-51.8-204.1-56.5L612.9 195l127.9 36.9c11.5 32.6 42.6 56.1 79.2 56.1c46.4 0 84-37.6 84-84s-37.6-84-84-84c-32 0-59.8 17.9-74 44.2L603.5 123a33.2 33.2 0 0 0-39.6 18.4l-90.8 203.9c-74.5 5.2-142.9 25.4-199.2 56.2A111.94 111.94 0 0 0 176 344c-61.9 0-112 50.1-112 112c0 45.8 27.5 85.1 66.8 102.5c-7.1 21-10.8 43-10.8 65.5c0 154.6 175.5 280 392 280s392-125.4 392-280c0-22.6-3.8-44.5-10.8-65.5C932.5 541.1 960 501.8 960 456zM820 172.5a31.5 31.5 0 1 1 0 63a31.5 31.5 0 0 1 0-63zM120 456c0-30.9 25.1-56 56-56a56 56 0 0 1 50.6 32.1c-29.3 22.2-53.5 47.8-71.5 75.9a56.23 56.23 0 0 1-35.1-52zm392 381.5c-179.8 0-325.5-95.6-325.5-213.5S332.2 410.5 512 410.5S837.5 506.1 837.5 624S691.8 837.5 512 837.5zM868.8 508c-17.9-28.1-42.2-53.7-71.5-75.9c9-18.9 28.3-32.1 50.6-32.1c30.9 0 56 25.1 56 56c.1 23.5-14.5 43.7-35.1 52zM624 568a56 56 0 1 0 112 0a56 56 0 1 0-112 0z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
0
public/manifest/apple-touch-icon.png
Executable file → Normal file
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
0
public/manifest/favicon.ico
Executable file → Normal file
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
0
public/manifest/icon152.png
Executable file → Normal file
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
0
public/manifest/icon16.png
Executable file → Normal file
|
Before Width: | Height: | Size: 123 B After Width: | Height: | Size: 123 B |
0
public/manifest/icon196.png
Executable file → Normal file
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
0
public/manifest/icon32.png
Executable file → Normal file
|
Before Width: | Height: | Size: 267 B After Width: | Height: | Size: 267 B |
BIN
public/pdfs/roadmaps/aws.pdf
Normal file
BIN
public/pdfs/roadmaps/game-developer.pdf
Normal file
BIN
public/pdfs/roadmaps/rust.pdf
Normal file
BIN
public/pdfs/roadmaps/server-side-game-developer.pdf
Normal file
BIN
public/pdfs/roadmaps/technical-writer.pdf
Normal file
BIN
public/roadmaps/aws.png
Normal file
|
After Width: | Height: | Size: 636 KiB |
BIN
public/roadmaps/game-developer.png
Normal file
|
After Width: | Height: | Size: 614 KiB |
BIN
public/roadmaps/rust.png
Normal file
|
After Width: | Height: | Size: 599 KiB |
BIN
public/roadmaps/technical-writer.png
Normal file
|
After Width: | Height: | Size: 522 KiB |
@@ -39,6 +39,7 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [QA Roadmap](https://roadmap.sh/qa)
|
||||
- [Python Roadmap](https://roadmap.sh/python)
|
||||
- [Software Architect Roadmap](https://roadmap.sh/software-architect)
|
||||
- [Game Developer Roadmap](https://roadmap.sh/game-developer) / [Server Side Game Developer](https://roadmap.sh/server-side-game-developer)
|
||||
- [Software Design and Architecture Roadmap](https://roadmap.sh/software-design-architecture)
|
||||
- [JavaScript Roadmap](https://roadmap.sh/javascript)
|
||||
- [TypeScript Roadmap](https://roadmap.sh/typescript)
|
||||
@@ -52,6 +53,7 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [Android Roadmap](https://roadmap.sh/android)
|
||||
- [Flutter Roadmap](https://roadmap.sh/flutter)
|
||||
- [Go Roadmap](https://roadmap.sh/golang)
|
||||
- [Rust Roadmap](https://roadmap.sh/rust)
|
||||
- [Java Roadmap](https://roadmap.sh/java)
|
||||
- [Spring Boot Roadmap](https://roadmap.sh/spring-boot)
|
||||
- [Design System Roadmap](https://roadmap.sh/design-system)
|
||||
@@ -66,6 +68,7 @@ Here is the list of available roadmaps with more being actively worked upon.
|
||||
- [UX Design Roadmap](https://roadmap.sh/ux-design)
|
||||
- [Docker Roadmap](https://roadmap.sh/docker)
|
||||
- [Prompt Engineering Roadmap](https://roadmap.sh/prompt-engineering)
|
||||
- [Technical Writer Roadmap](https://roadmap.sh/technical-writer)
|
||||
|
||||
There are also interactive best practices:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-#!/usr/bin/env bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
@@ -59,9 +59,9 @@ function writeTopicContent(currTopicUrl) {
|
||||
|
||||
const roadmapTitle = roadmapId.replace(/-/g, ' ');
|
||||
|
||||
let prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${childTopic}". Write me a brief paragraph for that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output.`;
|
||||
let prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${childTopic}". Write me a brief paragraph for that. Your output should be strictly markdown. Do not include anything other than the description in your output. I already know the benefits of each so do not add benefits in the output.`;
|
||||
if (!childTopic) {
|
||||
prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${parentTopic}". Write me a brief paragraph for that. Content should be in markdown. I already know the benefits of each so do not add benefits in the output.`;
|
||||
prompt = `I am reading a guide about "${roadmapTitle}". I am on the topic "${parentTopic}". I want to know more about "${parentTopic}". Write me a brief paragraph for that. Your output should be strictly markdown. Do not include anything other than the description in your output. I already know the benefits of each so do not add benefits in the output.`;
|
||||
}
|
||||
|
||||
console.log(`Generating '${childTopic || parentTopic}'...`);
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import RoadmapIcon from '../../icons/roadmap.svg';
|
||||
import { RoadmapIcon } from "../ReactIcons/RoadmapIcon";
|
||||
|
||||
export function EmptyActivity() {
|
||||
return (
|
||||
<div className="rounded-md">
|
||||
<div className="flex flex-col items-center p-7 text-center">
|
||||
<img
|
||||
alt="no roadmaps"
|
||||
src={RoadmapIcon.src}
|
||||
className="mb-2 w-[60px] h-[60px] sm:h-[120px] sm:w-[120px] opacity-10"
|
||||
/>
|
||||
<RoadmapIcon className="mb-2 w-[60px] h-[60px] sm:h-[120px] sm:w-[120px] opacity-10" />
|
||||
|
||||
<h2 className="text-lg sm:text-xl font-bold">No Progress</h2>
|
||||
<p className="my-1 sm:my-2 max-w-[400px] text-gray-500 text-sm sm:text-base">
|
||||
Progress will appear here as you start tracking your{' '}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import GitHubIcon from '../../icons/github.svg';
|
||||
import SpinnerIcon from '../../icons/spinner.svg';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||
import Cookies from 'js-cookie';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
|
||||
type GitHubButtonProps = {};
|
||||
|
||||
@@ -13,7 +13,6 @@ const GITHUB_LAST_PAGE = 'githubLastPage';
|
||||
export function GitHubButton(props: GitHubButtonProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const icon = isLoading ? SpinnerIcon : GitHubIcon;
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -29,7 +28,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
httpGet<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-github-callback${
|
||||
window.location.search
|
||||
}`
|
||||
}`,
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
if (!response?.token) {
|
||||
@@ -81,12 +80,12 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
setIsLoading(true);
|
||||
|
||||
const { response, error } = await httpGet<{ loginUrl: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-github-login`
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-github-login`,
|
||||
);
|
||||
|
||||
if (error || !response?.loginUrl) {
|
||||
setError(
|
||||
error?.message || 'Something went wrong. Please try again later.'
|
||||
error?.message || 'Something went wrong. Please try again later.',
|
||||
);
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -97,7 +96,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
// the user was on before they clicked the social login button
|
||||
if (!['/login', '/signup'].includes(window.location.pathname)) {
|
||||
const pagePath = ['/respond-invite', '/befriend'].includes(
|
||||
window.location.pathname
|
||||
window.location.pathname,
|
||||
)
|
||||
? window.location.pathname + window.location.search
|
||||
: window.location.pathname;
|
||||
@@ -116,11 +115,11 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
disabled={isLoading}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<img
|
||||
src={icon.src}
|
||||
alt="GitHub"
|
||||
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
|
||||
) : (
|
||||
<GitHubIcon className={'h-[18px] w-[18px]'} />
|
||||
)}
|
||||
Continue with GitHub
|
||||
</button>
|
||||
{error && (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import GoogleIcon from '../../icons/google.svg';
|
||||
import SpinnerIcon from '../../icons/spinner.svg';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { GoogleIcon } from '../ReactIcons/GoogleIcon.tsx';
|
||||
|
||||
type GoogleButtonProps = {};
|
||||
|
||||
@@ -13,7 +13,6 @@ const GOOGLE_LAST_PAGE = 'googleLastPage';
|
||||
export function GoogleButton(props: GoogleButtonProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const icon = isLoading ? SpinnerIcon : GoogleIcon;
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -29,7 +28,7 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
httpGet<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-google-callback${
|
||||
window.location.search
|
||||
}`
|
||||
}`,
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
if (!response?.token) {
|
||||
@@ -79,7 +78,7 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
const handleClick = () => {
|
||||
setIsLoading(true);
|
||||
httpGet<{ loginUrl: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-google-login`
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-google-login`,
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
if (!response?.loginUrl) {
|
||||
@@ -93,7 +92,7 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
// the user was on before they clicked the social login button
|
||||
if (!['/login', '/signup'].includes(window.location.pathname)) {
|
||||
const pagePath = ['/respond-invite', '/befriend'].includes(
|
||||
window.location.pathname
|
||||
window.location.pathname,
|
||||
)
|
||||
? window.location.pathname + window.location.search
|
||||
: window.location.pathname;
|
||||
@@ -117,11 +116,11 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
disabled={isLoading}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<img
|
||||
src={icon.src}
|
||||
alt="Google"
|
||||
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
|
||||
) : (
|
||||
<GoogleIcon className={'h-[18px] w-[18px]'} />
|
||||
)}
|
||||
Continue with Google
|
||||
</button>
|
||||
{error && (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import LinkedIn from '../../icons/linkedin.svg';
|
||||
import SpinnerIcon from '../../icons/spinner.svg';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { Spinner } from '../ReactIcons/Spinner.tsx';
|
||||
import { LinkedInIcon } from '../ReactIcons/LinkedInIcon.tsx';
|
||||
|
||||
type LinkedInButtonProps = {};
|
||||
|
||||
@@ -13,7 +13,6 @@ const LINKEDIN_LAST_PAGE = 'linkedInLastPage';
|
||||
export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const icon = isLoading ? SpinnerIcon : LinkedIn;
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -29,7 +28,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
httpGet<{ token: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-callback${
|
||||
window.location.search
|
||||
}`
|
||||
}`,
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
if (!response?.token) {
|
||||
@@ -79,7 +78,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
const handleClick = () => {
|
||||
setIsLoading(true);
|
||||
httpGet<{ loginUrl: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-login`
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-linkedin-login`,
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
if (!response?.loginUrl) {
|
||||
@@ -93,7 +92,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
// the user was on before they clicked the social login button
|
||||
if (!['/login', '/signup'].includes(window.location.pathname)) {
|
||||
const pagePath = ['/respond-invite', '/befriend'].includes(
|
||||
window.location.pathname
|
||||
window.location.pathname,
|
||||
)
|
||||
? window.location.pathname + window.location.search
|
||||
: window.location.pathname;
|
||||
@@ -117,11 +116,11 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
disabled={isLoading}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<img
|
||||
src={icon.src}
|
||||
alt="Google"
|
||||
className={`h-[18px] w-[18px] ${isLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<Spinner className={'h-[18px] w-[18px]'} isDualRing={false} />
|
||||
) : (
|
||||
<LinkedInIcon className={'h-[18px] w-[18px]'} />
|
||||
)}
|
||||
Continue with LinkedIn
|
||||
</button>
|
||||
{error && (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import ErrorIcon from '../../icons/error.svg';
|
||||
import SpinnerIcon from '../../icons/spinner.svg';
|
||||
import { TOKEN_COOKIE_NAME } from '../../lib/jwt';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import { ErrorIcon2 } from '../ReactIcons/ErrorIcon2';
|
||||
|
||||
export function TriggerVerifyAccount() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -16,7 +16,7 @@ export function TriggerVerifyAccount() {
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-verify-account`,
|
||||
{
|
||||
code,
|
||||
}
|
||||
},
|
||||
)
|
||||
.then(({ response, error }) => {
|
||||
if (!response?.token) {
|
||||
@@ -55,20 +55,8 @@ export function TriggerVerifyAccount() {
|
||||
return (
|
||||
<div className="mx-auto flex max-w-md flex-col items-center pt-0 sm:pt-12">
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
{isLoading && (
|
||||
<img
|
||||
alt={'Please wait.'}
|
||||
src={SpinnerIcon.src}
|
||||
className={'mx-auto h-16 w-16 animate-spin'}
|
||||
/>
|
||||
)}
|
||||
{error && (
|
||||
<img
|
||||
alt={'Please wait.'}
|
||||
src={ErrorIcon.src}
|
||||
className={'mx-auto h-16 w-16'}
|
||||
/>
|
||||
)}
|
||||
{isLoading && <Spinner className="mx-auto h-16 w-16" />}
|
||||
{error && <ErrorIcon2 className="mx-auto h-16 w-16" />}
|
||||
<h2 className="mb-1 mt-4 text-center text-xl font-semibold sm:mb-3 sm:mt-4 sm:text-2xl">
|
||||
Verifying your account
|
||||
</h2>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import VerifyLetterIcon from '../../icons/verify-letter.svg';
|
||||
import { httpPost } from '../../lib/http';
|
||||
import { VerifyLetterIcon } from '../ReactIcons/VerifyLetterIcon';
|
||||
|
||||
export function VerificationEmailMessage() {
|
||||
const [email, setEmail] = useState('..');
|
||||
@@ -37,11 +37,7 @@ export function VerificationEmailMessage() {
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-md text-center">
|
||||
<img
|
||||
alt="Verify Email"
|
||||
src={VerifyLetterIcon.src}
|
||||
className="mx-auto mb-4 h-20 w-40 sm:h-40"
|
||||
/>
|
||||
<VerifyLetterIcon className="mx-auto mb-4 h-20 w-40 sm:h-40" />
|
||||
<h2 className="my-2 text-center text-xl font-semibold sm:my-5 sm:text-2xl">
|
||||
Verify your email address
|
||||
</h2>
|
||||
|
||||
@@ -1,35 +1,47 @@
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Fragment,
|
||||
type ReactElement,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import BestPracticesIcon from '../../icons/best-practices.svg';
|
||||
import ClipboardIcon from '../../icons/clipboard.svg';
|
||||
import GuideIcon from '../../icons/guide.svg';
|
||||
import HomeIcon from '../../icons/home.svg';
|
||||
import RoadmapIcon from '../../icons/roadmap.svg';
|
||||
import UserIcon from '../../icons/user.svg';
|
||||
import GroupIcon from '../../icons/group.svg';
|
||||
import VideoIcon from '../../icons/video.svg';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { BestPracticesIcon } from '../ReactIcons/BestPracticesIcon.tsx';
|
||||
import { UserIcon } from '../ReactIcons/UserIcon.tsx';
|
||||
import { GroupIcon } from '../ReactIcons/GroupIcon.tsx';
|
||||
import { RoadmapIcon } from '../ReactIcons/RoadmapIcon.tsx';
|
||||
import { ClipboardIcon } from '../ReactIcons/ClipboardIcon.tsx';
|
||||
import { GuideIcon } from '../ReactIcons/GuideIcon.tsx';
|
||||
import { HomeIcon } from '../ReactIcons/HomeIcon.tsx';
|
||||
import { VideoIcon } from '../ReactIcons/VideoIcon.tsx';
|
||||
|
||||
export type PageType = {
|
||||
id: string;
|
||||
url: string;
|
||||
title: string;
|
||||
group: string;
|
||||
icon?: string;
|
||||
icon?: ReactElement;
|
||||
isProtected?: boolean;
|
||||
metadata?: Record<string, any>;
|
||||
};
|
||||
|
||||
const defaultPages: PageType[] = [
|
||||
{ id: 'home', url: '/', title: 'Home', group: 'Pages', icon: HomeIcon.src },
|
||||
{
|
||||
id: 'home',
|
||||
url: '/',
|
||||
title: 'Home',
|
||||
group: 'Pages',
|
||||
icon: <HomeIcon className="mr-2 h-4 w-4 stroke-2" />,
|
||||
},
|
||||
{
|
||||
id: 'account',
|
||||
url: '/account',
|
||||
title: 'Account',
|
||||
group: 'Pages',
|
||||
icon: UserIcon.src,
|
||||
icon: <UserIcon className="mr-2 h-4 w-4 stroke-2" />,
|
||||
isProtected: true,
|
||||
},
|
||||
{
|
||||
@@ -37,7 +49,7 @@ const defaultPages: PageType[] = [
|
||||
url: '/team',
|
||||
title: 'Teams',
|
||||
group: 'Pages',
|
||||
icon: GroupIcon.src,
|
||||
icon: <GroupIcon className="mr-2 h-4 w-4 stroke-2" />,
|
||||
isProtected: true,
|
||||
},
|
||||
{
|
||||
@@ -45,7 +57,7 @@ const defaultPages: PageType[] = [
|
||||
url: '/account/friends',
|
||||
title: 'Friends',
|
||||
group: 'Pages',
|
||||
icon: GroupIcon.src,
|
||||
icon: <GroupIcon className="mr-2 h-4 w-4 stroke-2" />,
|
||||
isProtected: true,
|
||||
},
|
||||
{
|
||||
@@ -53,14 +65,14 @@ const defaultPages: PageType[] = [
|
||||
url: '/roadmaps',
|
||||
title: 'Roadmaps',
|
||||
group: 'Pages',
|
||||
icon: RoadmapIcon.src,
|
||||
icon: <RoadmapIcon className="mr-2 h-4 w-4 stroke-2" />,
|
||||
},
|
||||
{
|
||||
id: 'account-roadmaps',
|
||||
url: '/account/roadmaps',
|
||||
title: 'Custom Roadmaps',
|
||||
group: 'Pages',
|
||||
icon: RoadmapIcon.src,
|
||||
icon: <RoadmapIcon className="mr-2 h-4 w-4 stroke-2" />,
|
||||
isProtected: true,
|
||||
},
|
||||
{
|
||||
@@ -68,28 +80,28 @@ const defaultPages: PageType[] = [
|
||||
url: '/best-practices',
|
||||
title: 'Best Practices',
|
||||
group: 'Pages',
|
||||
icon: BestPracticesIcon.src,
|
||||
icon: <BestPracticesIcon className="mr-2 h-4 w-4 stroke-2" />,
|
||||
},
|
||||
{
|
||||
id: 'questions',
|
||||
url: '/questions',
|
||||
title: 'Questions',
|
||||
group: 'Pages',
|
||||
icon: ClipboardIcon.src,
|
||||
icon: <ClipboardIcon className="mr-2 h-4 w-4 stroke-2" />,
|
||||
},
|
||||
{
|
||||
id: 'guides',
|
||||
url: '/guides',
|
||||
title: 'Guides',
|
||||
group: 'Pages',
|
||||
icon: GuideIcon.src,
|
||||
icon: <GuideIcon className="mr-2 h-4 w-4 stroke-2" />,
|
||||
},
|
||||
{
|
||||
id: 'videos',
|
||||
url: '/videos',
|
||||
title: 'Videos',
|
||||
group: 'Pages',
|
||||
icon: VideoIcon.src,
|
||||
icon: <VideoIcon className="mr-2 h-4 w-4 stroke-2" />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -199,7 +211,7 @@ export function CommandMenu() {
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
const canGoPrev = activeCounter > 0;
|
||||
setActiveCounter(
|
||||
canGoPrev ? activeCounter - 1 : searchResults.length - 1
|
||||
canGoPrev ? activeCounter - 1 : searchResults.length - 1,
|
||||
);
|
||||
} else if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
@@ -242,13 +254,7 @@ export function CommandMenu() {
|
||||
{!page.icon && (
|
||||
<span className="mr-2 text-gray-400">{page.group}</span>
|
||||
)}
|
||||
{page.icon && (
|
||||
<img
|
||||
alt={page.title}
|
||||
src={page.icon}
|
||||
className="mr-2 h-4 w-4"
|
||||
/>
|
||||
)}
|
||||
{page.icon && page.icon}
|
||||
{page.title}
|
||||
</a>
|
||||
</Fragment>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ChevronDownIcon from '../../icons/chevron-down.svg';
|
||||
import { ChevronDownIcon } from '../ReactIcons/ChevronDownIcon';
|
||||
|
||||
type NotDropdownProps = {
|
||||
onClick: () => void;
|
||||
@@ -37,11 +37,7 @@ export function NotDropdown(props: NotDropdownProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<img
|
||||
alt={singularName}
|
||||
src={ChevronDownIcon.src}
|
||||
className={'relative top-[1px] h-[17px] w-[17px] opacity-40'}
|
||||
/>
|
||||
<ChevronDownIcon className="relative top-[1px] h-[17px] w-[17px] opacity-40" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu';
|
||||
import type { TeamResourceConfig } from './RoadmapSelector';
|
||||
import CloseIcon from '../../icons/close.svg';
|
||||
import { SelectRoadmapModalItem } from './SelectRoadmapModalItem';
|
||||
import { XIcon } from 'lucide-react';
|
||||
|
||||
export type SelectRoadmapModalProps = {
|
||||
teamId: string;
|
||||
@@ -60,11 +60,11 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
|
||||
setSearchResults(searchResults);
|
||||
}, [searchText, allRoadmaps]);
|
||||
|
||||
const roleBasedRoadmaps = searchResults.filter((roadmap) =>
|
||||
roadmap?.metadata?.tags?.includes('role-roadmap')
|
||||
const roleBasedRoadmaps = searchResults.filter(
|
||||
(roadmap) => roadmap?.metadata?.tags?.includes('role-roadmap'),
|
||||
);
|
||||
const skillBasedRoadmaps = searchResults.filter((roadmap) =>
|
||||
roadmap?.metadata?.tags?.includes('skill-roadmap')
|
||||
const skillBasedRoadmaps = searchResults.filter(
|
||||
(roadmap) => roadmap?.metadata?.tags?.includes('skill-roadmap'),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -79,7 +79,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
|
||||
className="popup-close absolute right-2.5 top-3 ml-auto inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-100 hover:text-gray-900"
|
||||
onClick={onClose}
|
||||
>
|
||||
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
|
||||
<XIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Close modal</span>
|
||||
</button>
|
||||
<input
|
||||
@@ -101,7 +101,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
|
||||
<div className="mb-5 flex flex-wrap items-center gap-2">
|
||||
{roleBasedRoadmaps.map((roadmap) => {
|
||||
const isSelected = !!teamResourceConfig?.find(
|
||||
(r) => r.resourceId === roadmap.id
|
||||
(r) => r.resourceId === roadmap.id,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -127,7 +127,7 @@ export function SelectRoadmapModal(props: SelectRoadmapModalProps) {
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{skillBasedRoadmaps.map((roadmap) => {
|
||||
const isSelected = !!teamResourceConfig.find(
|
||||
(r) => r.resourceId === roadmap.id
|
||||
(r) => r.resourceId === roadmap.id,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import BuildingIcon from '../../icons/building.svg';
|
||||
import UsersIcon from '../../icons/users.svg';
|
||||
import type { TeamDocument } from './CreateTeamForm';
|
||||
import { httpPut } from '../../lib/http';
|
||||
import { useState } from 'react';
|
||||
import { NextButton } from './NextButton';
|
||||
import { BuildingIcon } from '../ReactIcons/BuildingIcon.tsx';
|
||||
import { UsersIcon } from '../ReactIcons/UsersIcon.tsx';
|
||||
|
||||
export const validTeamTypes = [
|
||||
{
|
||||
value: 'company',
|
||||
label: 'Company',
|
||||
icon: BuildingIcon.src,
|
||||
icon: BuildingIcon,
|
||||
description:
|
||||
'Track the skills and learning progress of the tech team at your company',
|
||||
},
|
||||
{
|
||||
value: 'study_group',
|
||||
label: 'Study Group',
|
||||
icon: UsersIcon.src,
|
||||
icon: UsersIcon,
|
||||
description:
|
||||
'Invite your friends or course-mates and track your learning progress together',
|
||||
},
|
||||
@@ -56,7 +56,7 @@ export function Step0(props: Step0Props) {
|
||||
teamSize: team.teamSize,
|
||||
linkedInUrl: team?.links?.linkedIn || undefined,
|
||||
}),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -76,21 +76,20 @@ export function Step0(props: Step0Props) {
|
||||
{validTeamTypes.map((validTeamType) => (
|
||||
<button
|
||||
key={validTeamType.value}
|
||||
className={`flex flex-grow flex-col items-center rounded-lg border px-5 pt-12 pb-10 ${
|
||||
className={`flex flex-grow flex-col items-center rounded-lg border px-5 pb-10 pt-12 ${
|
||||
validTeamType.value == selectedTeamType
|
||||
? 'border-gray-400 bg-gray-100'
|
||||
: 'border-gray-300 hover:border-gray-400 hover:bg-gray-50'
|
||||
}`}
|
||||
onClick={() => setSelectedTeamType(validTeamType.value)}
|
||||
>
|
||||
<img
|
||||
key={validTeamType.value}
|
||||
alt={validTeamType.label}
|
||||
src={validTeamType.icon}
|
||||
className={`mb-3 h-12 w-12 opacity-10 ${
|
||||
validTeamType.value === selectedTeamType ? 'opacity-100' : ''
|
||||
}`}
|
||||
/>
|
||||
{
|
||||
<validTeamType.icon
|
||||
className={`mb-3 h-12 w-12 opacity-10 ${
|
||||
validTeamType.value === selectedTeamType ? 'opacity-100' : ''
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
<span className="mb-2 block text-2xl font-bold">
|
||||
{validTeamType.label}
|
||||
</span>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import type { TeamResourceConfig } from './RoadmapSelector';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import {replaceChildren} from "../../lib/dom.ts";
|
||||
|
||||
export type ProgressMapProps = {
|
||||
teamId: string;
|
||||
@@ -81,7 +82,8 @@ export function UpdateTeamResourceModal(props: ProgressMapProps) {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
|
||||
containerEl.current?.replaceChildren(svg);
|
||||
replaceChildren(containerEl.current!, svg);
|
||||
// containerEl.current?.replaceChildren(svg);
|
||||
|
||||
// Render team configuration
|
||||
removedItems.forEach((topicId: string) => {
|
||||
|
||||
145
src/components/CreateVersion/CreateVersion.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { httpGet, httpPost } from '../../lib/http';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { GitFork, Loader2, Map } from 'lucide-react';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import type { RoadmapDocument } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
|
||||
|
||||
type CreateVersionProps = {
|
||||
roadmapId: string;
|
||||
};
|
||||
|
||||
export function CreateVersion(props: CreateVersionProps) {
|
||||
const { roadmapId } = props;
|
||||
|
||||
const toast = useToast();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isConfirming, setIsConfirming] = useState(false);
|
||||
const [userVersion, setUserVersion] = useState<RoadmapDocument>();
|
||||
|
||||
async function loadMyVersion() {
|
||||
if (!isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const { response, error } = await httpGet<RoadmapDocument>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-my-version/${roadmapId}`,
|
||||
{},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
setUserVersion(response);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadMyVersion().finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
async function createVersion() {
|
||||
if (isCreating || !roadmapId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
showLoginPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreating(true);
|
||||
const { response, error } = await httpPost<{ roadmapId: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-create-version/${roadmapId}`,
|
||||
{},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
setIsCreating(false);
|
||||
toast.error(error?.message || 'Failed to create version');
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = `${
|
||||
import.meta.env.PUBLIC_EDITOR_APP_URL
|
||||
}/${response?.roadmapId}`;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-[30px] w-[312px] animate-pulse rounded-md bg-gray-300"></div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && userVersion?._id) {
|
||||
return (
|
||||
<div className={'flex items-center'}>
|
||||
<a
|
||||
href={`/r?id=${userVersion._id}`}
|
||||
className="flex items-center rounded-md border border-blue-400 bg-gray-50 px-2.5 py-1 text-xs font-medium text-blue-600 hover:bg-blue-100 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:hover:bg-gray-100 max-sm:hidden sm:text-sm"
|
||||
>
|
||||
<Map size="15px" className="mr-1.5" />
|
||||
Visit your own version of this Roadmap
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isConfirming) {
|
||||
return (
|
||||
<p className="flex h-[30px] items-center text-sm text-red-500">
|
||||
Create and edit a custom roadmap from this roadmap?
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsConfirming(false);
|
||||
createVersion().finally(() => null);
|
||||
}}
|
||||
className="ml-2 font-semibold underline underline-offset-2"
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<span className="text-xs"> / </span>
|
||||
<button
|
||||
className="font-semibold underline underline-offset-2"
|
||||
onClick={() => setIsConfirming(false)}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
disabled={isCreating}
|
||||
className="flex items-center justify-center rounded-md border border-gray-300 bg-gray-50 px-2.5 py-1 text-xs font-medium text-black hover:bg-gray-200 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:hover:bg-gray-100 max-sm:hidden sm:text-sm"
|
||||
onClick={() => {
|
||||
if (!isLoggedIn()) {
|
||||
showLoginPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConfirming(true);
|
||||
}}
|
||||
>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-3 w-3 animate-spin stroke-[2.5]" />
|
||||
Please wait ..
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GitFork className="mr-1.5" size="16px" />
|
||||
Create your own version of this roadmap
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -53,14 +53,20 @@ export type GetRoadmapResponse = RoadmapDocument & {
|
||||
|
||||
export function hideRoadmapLoader() {
|
||||
const loaderEl = document.querySelector(
|
||||
'[data-roadmap-loader]'
|
||||
'[data-roadmap-loader]',
|
||||
) as HTMLElement;
|
||||
if (loaderEl) {
|
||||
loaderEl.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export function CustomRoadmap() {
|
||||
type CustomRoadmapProps = {
|
||||
isEmbed?: boolean;
|
||||
};
|
||||
|
||||
export function CustomRoadmap(props: CustomRoadmapProps) {
|
||||
const { isEmbed = false } = props;
|
||||
|
||||
const { id, secret } = getUrlParams() as { id: string; secret: string };
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -71,14 +77,15 @@ export function CustomRoadmap() {
|
||||
setIsLoading(true);
|
||||
|
||||
const roadmapUrl = new URL(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${id}`
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-roadmap/${id}`,
|
||||
);
|
||||
|
||||
if (secret) {
|
||||
roadmapUrl.searchParams.set('secret', secret);
|
||||
}
|
||||
|
||||
const { response, error } = await httpGet<GetRoadmapResponse>(
|
||||
roadmapUrl.toString()
|
||||
roadmapUrl.toString(),
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -95,7 +102,10 @@ export function CustomRoadmap() {
|
||||
}
|
||||
|
||||
async function trackVisit() {
|
||||
if (!isLoggedIn()) return;
|
||||
if (!isLoggedIn() || isEmbed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await httpPost(`${import.meta.env.PUBLIC_API_URL}/v1-visit`, {
|
||||
resourceId: id,
|
||||
resourceType: 'roadmap',
|
||||
@@ -119,9 +129,9 @@ export function CustomRoadmap() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<RoadmapHeader />
|
||||
<FlowRoadmapRenderer roadmap={roadmap!} />
|
||||
<TopicDetail canSubmitContribution={false} />
|
||||
{!isEmbed && <RoadmapHeader />}
|
||||
<FlowRoadmapRenderer isEmbed={isEmbed} roadmap={roadmap!} />
|
||||
<TopicDetail isEmbed={isEmbed} canSubmitContribution={false} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
81
src/components/CustomRoadmap/EmbedRoadmapModal.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
|
||||
import { Modal } from '../Modal';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { useCopyText } from '../../hooks/use-copy-text';
|
||||
import { currentRoadmap, isCurrentRoadmapPersonal } from '../../stores/roadmap';
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
|
||||
type ShareRoadmapModalProps = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function EmbedRoadmapModal(props: ShareRoadmapModalProps) {
|
||||
const { onClose } = props;
|
||||
|
||||
const toast = useToast();
|
||||
const $currentRoadmap = useStore(currentRoadmap);
|
||||
const $isCurrentRoadmapPersonal = useStore(isCurrentRoadmapPersonal);
|
||||
const roadmapId = $currentRoadmap?._id!;
|
||||
|
||||
const { copyText, isCopied } = useCopyText();
|
||||
|
||||
const isDev = import.meta.env.DEV;
|
||||
const baseUrl = isDev ? 'http://localhost:3000' : 'https://roadmap.sh';
|
||||
|
||||
const embedHtml = `<iframe src="${baseUrl}/r/embed?id=${roadmapId}" width="100%" height="500px" frameBorder="0"\n></iframe>`;
|
||||
|
||||
return (
|
||||
<Modal onClose={onClose} wrapperClassName={'max-w-[500px]'}>
|
||||
<div className="p-4 pb-0">
|
||||
<h1 className="text-xl font-semibold leading-5 text-gray-900">
|
||||
Embed Roadmap
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pt-3">
|
||||
<p className={'mb-2 text-sm text-gray-500'}>
|
||||
Copy the following HTML code and paste it into your website.
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={embedHtml}
|
||||
readOnly={true}
|
||||
onClick={(e) => {
|
||||
e.currentTarget.select();
|
||||
copyText(embedHtml);
|
||||
}}
|
||||
className="w-full resize-none rounded-md border bg-gray-50 p-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between px-4 pb-4 pt-2">
|
||||
<button
|
||||
className={cn(
|
||||
'flex h-9 w-full items-center justify-center rounded-md border border-transparent px-4 py-2 text-sm font-medium text-white outline-none',
|
||||
{
|
||||
'bg-green-500 hover:bg-green-600 focus:bg-green-600': isCopied,
|
||||
'bg-gray-900 hover:bg-gray-800 focus:bg-gray-800': !isCopied,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
copyText(embedHtml);
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<Check size={14} className="mr-2 stroke-[2.5]" />
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={14} className="mr-2 stroke-[2.5]" />
|
||||
Copy Link
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,27 @@
|
||||
import { ReadonlyEditor } from '../../../editor/readonly-editor';
|
||||
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
|
||||
import {
|
||||
renderResourceProgress,
|
||||
updateResourceProgress,
|
||||
type ResourceProgressType,
|
||||
renderTopicProgress,
|
||||
refreshProgressCounters,
|
||||
renderResourceProgress,
|
||||
renderTopicProgress,
|
||||
type ResourceProgressType,
|
||||
updateResourceProgress,
|
||||
} from '../../lib/resource-progress';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import type { Node } from 'reactflow';
|
||||
import { useCallback, type MouseEvent, useMemo, useState, useRef } from 'react';
|
||||
import { type MouseEvent, useCallback, useRef, useState } from 'react';
|
||||
import { EmptyRoadmap } from './EmptyRoadmap';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { totalRoadmapNodes } from '../../stores/roadmap.ts';
|
||||
|
||||
type FlowRoadmapRendererProps = {
|
||||
isEmbed?: boolean;
|
||||
roadmap: RoadmapDocument;
|
||||
};
|
||||
|
||||
export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
|
||||
const { roadmap } = props;
|
||||
const { roadmap, isEmbed = false } = props;
|
||||
const roadmapId = String(roadmap._id!);
|
||||
|
||||
const [hideRenderer, setHideRenderer] = useState(false);
|
||||
@@ -31,6 +33,10 @@ export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
|
||||
topicId: string,
|
||||
newStatus: ResourceProgressType,
|
||||
) {
|
||||
if (isEmbed) {
|
||||
return;
|
||||
}
|
||||
|
||||
pageProgressMessage.set('Updating progress');
|
||||
updateResourceProgress(
|
||||
{
|
||||
@@ -138,6 +144,12 @@ export function FlowRoadmapRenderer(props: FlowRoadmapRendererProps) {
|
||||
)}
|
||||
onRendered={() => {
|
||||
renderResourceProgress('roadmap', roadmapId).then(() => {
|
||||
totalRoadmapNodes.set(
|
||||
roadmap?.nodes?.filter((node) => {
|
||||
return ['topic', 'subtopic'].includes(node.type);
|
||||
}).length || 0,
|
||||
);
|
||||
|
||||
if (roadmap?.nodes?.length === 0) {
|
||||
setHideRenderer(true);
|
||||
editorWrapperRef?.current?.classList.add('hidden');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import MoreIcon from '../../icons/more-vertical.svg';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { Lock, MoreVertical, Shapes, Trash2 } from 'lucide-react';
|
||||
import { MoreVerticalIcon } from '../ReactIcons/MoreVerticalIcon.tsx';
|
||||
|
||||
type PersonalRoadmapActionDropdownProps = {
|
||||
onDelete?: () => void;
|
||||
@@ -9,7 +9,9 @@ type PersonalRoadmapActionDropdownProps = {
|
||||
onUpdateSharing?: () => void;
|
||||
};
|
||||
|
||||
export function PersonalRoadmapActionDropdown(props: PersonalRoadmapActionDropdownProps) {
|
||||
export function PersonalRoadmapActionDropdown(
|
||||
props: PersonalRoadmapActionDropdownProps,
|
||||
) {
|
||||
const { onDelete, onUpdateSharing, onCustomize } = props;
|
||||
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
@@ -26,7 +28,7 @@ export function PersonalRoadmapActionDropdown(props: PersonalRoadmapActionDropdo
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="hidden items-center opacity-60 transition-opacity hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-30 sm:flex"
|
||||
>
|
||||
<img alt="menu" src={MoreIcon.src} className="h-4 w-4" />
|
||||
<MoreVerticalIcon className={'h-4 w-4'} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
||||
@@ -14,11 +14,11 @@ import {
|
||||
type AllowedRoadmapVisibility,
|
||||
type RoadmapDocument,
|
||||
} from './CreateRoadmap/CreateRoadmapModal';
|
||||
import RoadmapIcon from '../../icons/roadmap.svg';
|
||||
import { PersonalRoadmapActionDropdown } from './PersonalRoadmapActionDropdown';
|
||||
import type { GetRoadmapListResponse } from './RoadmapListPage';
|
||||
import { useState, type Dispatch, type SetStateAction } from 'react';
|
||||
import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal';
|
||||
import {RoadmapIcon} from "../ReactIcons/RoadmapIcon.tsx";
|
||||
|
||||
type PersonalRoadmapListType = {
|
||||
roadmaps: GetRoadmapListResponse['personalRoadmaps'];
|
||||
@@ -91,11 +91,8 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) {
|
||||
if (roadmapList.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center p-4 py-20">
|
||||
<img
|
||||
alt="roadmap"
|
||||
src={RoadmapIcon.src}
|
||||
className="mb-4 h-24 w-24 opacity-10"
|
||||
/>
|
||||
<RoadmapIcon className="mb-4 h-24 w-24 opacity-10" />
|
||||
|
||||
<h3 className="mb-1 text-2xl font-bold text-gray-900">No roadmaps</h3>
|
||||
<p className="text-base text-gray-500">
|
||||
Create a roadmap to get started
|
||||
@@ -175,26 +172,26 @@ function CustomRoadmapItem(props: CustomRoadmapItemProps) {
|
||||
}}
|
||||
/>
|
||||
|
||||
<a
|
||||
href={`/r?id=${roadmap._id}`}
|
||||
className={
|
||||
'ml-2 flex items-center gap-2 rounded-md border border-gray-300 bg-white px-2 py-1.5 text-xs hover:bg-gray-50 focus:outline-none'
|
||||
}
|
||||
target={'_blank'}
|
||||
>
|
||||
<ExternalLink className="inline-block h-4 w-4" />
|
||||
Visit
|
||||
</a>
|
||||
<a
|
||||
href={editorLink}
|
||||
className={
|
||||
'ml-2 flex items-center gap-2 rounded-md border border-gray-800 bg-gray-900 px-2.5 py-1.5 text-xs text-white hover:bg-gray-800 focus:outline-none'
|
||||
'ml-2 flex items-center gap-2 rounded-md border border-gray-300 bg-white px-2.5 py-1.5 text-xs text-black hover:bg-gray-50 focus:outline-none'
|
||||
}
|
||||
target={'_blank'}
|
||||
>
|
||||
<PenSquare className="inline-block h-4 w-4" />
|
||||
Edit
|
||||
</a>
|
||||
<a
|
||||
href={`/r?id=${roadmap._id}`}
|
||||
className={
|
||||
'ml-2 flex items-center gap-2 rounded-md border border-blue-400 bg-white px-2 py-1.5 text-xs hover:bg-blue-50 focus:outline-none text-blue-600'
|
||||
}
|
||||
target={'_blank'}
|
||||
>
|
||||
<ExternalLink className="inline-block h-4 w-4" />
|
||||
Visit
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { RoadmapActionButton } from './RoadmapActionButton';
|
||||
import { Lock, Shapes } from 'lucide-react';
|
||||
import { Modal } from '../Modal';
|
||||
import { ShareSuccess } from '../ShareOptions/ShareSuccess';
|
||||
import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx';
|
||||
|
||||
type RoadmapHeaderProps = {};
|
||||
|
||||
@@ -44,11 +45,11 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
|
||||
{
|
||||
resourceId: roadmapId,
|
||||
resourceType: 'roadmap',
|
||||
}
|
||||
},
|
||||
));
|
||||
} else {
|
||||
({ error, response } = await httpDelete<TeamResourceConfig>(
|
||||
`${baseApiUrl}/v1-delete-roadmap/${roadmapId}`
|
||||
`${baseApiUrl}/v1-delete-roadmap/${roadmapId}`,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -119,7 +120,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-2 sm:gap-0">
|
||||
<div className="flex gap-1 sm:gap-2">
|
||||
<div className="flex justify-stretch gap-1 sm:gap-2">
|
||||
<a
|
||||
href="/roadmaps"
|
||||
className="rounded-md bg-gray-500 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-600 sm:text-sm"
|
||||
@@ -128,14 +129,12 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
|
||||
←<span className="hidden sm:inline"> All Roadmaps</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
data-guest-required
|
||||
data-popup="login-popup"
|
||||
className="inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm"
|
||||
aria-label="Subscribe for Updates"
|
||||
>
|
||||
<span className="ml-2">Subscribe</span>
|
||||
</button>
|
||||
<ShareRoadmapButton
|
||||
roadmapId={roadmapId!}
|
||||
description={description!}
|
||||
pageUrl={`https://roadmap.sh/r?id=${roadmapId}`}
|
||||
allowEmbed={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{$canManageCurrentRoadmap && (
|
||||
@@ -162,9 +161,9 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
|
||||
)}
|
||||
|
||||
<a
|
||||
href={`${import.meta.env.PUBLIC_EDITOR_APP_URL}/${
|
||||
$currentRoadmap?._id
|
||||
}`}
|
||||
href={`${
|
||||
import.meta.env.PUBLIC_EDITOR_APP_URL
|
||||
}/${$currentRoadmap?._id}`}
|
||||
target="_blank"
|
||||
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
|
||||
>
|
||||
@@ -183,7 +182,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
|
||||
<RoadmapActionButton
|
||||
onDelete={() => {
|
||||
const confirmation = window.confirm(
|
||||
'Are you sure you want to delete this roadmap?'
|
||||
'Are you sure you want to delete this roadmap?',
|
||||
);
|
||||
|
||||
if (!confirmation) {
|
||||
|
||||
@@ -12,7 +12,10 @@ export function SkeletonRoadmapHeader() {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between gap-2 sm:gap-0">
|
||||
<div className="h-7 w-[35.04px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-32" />
|
||||
<div className='flex gap-1 sm:gap-2'>
|
||||
<div className="h-7 w-[35.04px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-32" />
|
||||
<div className="h-7 w-[35.04px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[85px]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-7 w-[60.52px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[139.71px]" />
|
||||
<div className="h-7 w-[71.48px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[100.34px]" />
|
||||
|
||||
@@ -48,7 +48,7 @@ import Icon from './AstroIcon.astro';
|
||||
<span class='mx-2 text-gray-400'>by</span>
|
||||
<a
|
||||
class='font-regular rounded-md bg-blue-600 px-1.5 py-1 text-sm hover:bg-blue-700'
|
||||
href='https://twitter.com/intent/user?screen_name=kamrify'
|
||||
href='https://twitter.com/kamrify'
|
||||
target='_blank'
|
||||
>
|
||||
<span class='hidden sm:inline'>@kamrify</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import Loader from '../Loader.astro';
|
||||
import './FrameRenderer.css';
|
||||
import { ProgressNudge } from "./ProgressNudge";
|
||||
|
||||
export interface Props {
|
||||
resourceType: 'roadmap' | 'best-practice';
|
||||
@@ -27,4 +28,6 @@ const { resourceId, resourceType, dimensions = null } = Astro.props;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProgressNudge resourceId={resourceId} resourceType={resourceType} client:only="react" />
|
||||
|
||||
<script src='./renderer.ts'></script>
|
||||
|
||||
65
src/components/FrameRenderer/ProgressNudge.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { cn } from '../../lib/classname.ts';
|
||||
import { roadmapProgress, totalRoadmapNodes } from '../../stores/roadmap.ts';
|
||||
import { useStore } from '@nanostores/react';
|
||||
|
||||
type ProgressNudgeProps = {
|
||||
resourceType: 'roadmap' | 'best-practice';
|
||||
resourceId: string;
|
||||
};
|
||||
|
||||
export function ProgressNudge(props: ProgressNudgeProps) {
|
||||
const $totalRoadmapNodes = useStore(totalRoadmapNodes);
|
||||
const $roadmapProgress = useStore(roadmapProgress);
|
||||
|
||||
const done = $roadmapProgress?.done?.length || 0;
|
||||
|
||||
const hasProgress = done > 0;
|
||||
|
||||
if (!$totalRoadmapNodes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'fixed bottom-5 left-1/2 z-30 hidden -translate-x-1/2 transform animate-fade-slide-up overflow-hidden rounded-full bg-stone-900 px-4 py-2 text-center text-white shadow-2xl transition-all duration-300 sm:block'
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={cn('block', {
|
||||
hidden: hasProgress,
|
||||
})}
|
||||
>
|
||||
<span className="mr-2 text-sm font-semibold uppercase text-yellow-400">
|
||||
Tip
|
||||
</span>
|
||||
<span className="text-sm text-gray-200">
|
||||
Right-click on a topic to mark it as done.{' '}
|
||||
<button
|
||||
data-popup="progress-help"
|
||||
className="cursor-pointer font-semibold text-yellow-500 underline"
|
||||
>
|
||||
Learn more.
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className={cn('relative z-20 block text-sm', {
|
||||
hidden: !hasProgress,
|
||||
})}
|
||||
>
|
||||
<span className="relative -top-[0.45px] mr-2 text-xs font-medium uppercase text-yellow-400">
|
||||
Progress
|
||||
</span>
|
||||
<span>{done}</span> of <span>{$totalRoadmapNodes}</span> Done
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="absolute bottom-0 left-0 top-0 z-10 bg-stone-700"
|
||||
style={{
|
||||
width: `${(done / $totalRoadmapNodes) * 100}%`,
|
||||
}}
|
||||
></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,9 +7,13 @@ import {
|
||||
renderTopicProgress,
|
||||
updateResourceProgress,
|
||||
} from '../../lib/resource-progress';
|
||||
import type { ResourceProgressType, ResourceType } from '../../lib/resource-progress';
|
||||
import type {
|
||||
ResourceProgressType,
|
||||
ResourceType,
|
||||
} from '../../lib/resource-progress';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import { replaceChildren } from '../../lib/dom.ts';
|
||||
|
||||
export class Renderer {
|
||||
resourceId: string;
|
||||
@@ -88,12 +92,13 @@ export class Renderer {
|
||||
});
|
||||
})
|
||||
.then((svg) => {
|
||||
this.containerEl?.replaceChildren(svg);
|
||||
replaceChildren(this.containerEl!, svg);
|
||||
// this.containerEl?.replaceChildren(svg);
|
||||
})
|
||||
.then(() => {
|
||||
return renderResourceProgress(
|
||||
this.resourceType as ResourceType,
|
||||
this.resourceId
|
||||
this.resourceId,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -141,7 +146,7 @@ export class Renderer {
|
||||
this.jsonToSvg(
|
||||
this.resourceType === 'roadmap'
|
||||
? `/${this.resourceId}.json`
|
||||
: `/best-practices/${this.resourceId}.json`
|
||||
: `/best-practices/${this.resourceId}.json`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -181,7 +186,7 @@ export class Renderer {
|
||||
resourceType: this.resourceType as ResourceType,
|
||||
topicId,
|
||||
},
|
||||
newStatus
|
||||
newStatus,
|
||||
)
|
||||
.then(() => {
|
||||
renderTopicProgress(topicId, newStatus);
|
||||
@@ -213,9 +218,14 @@ export class Renderer {
|
||||
|
||||
const isCurrentStatusDone = targetGroup.classList.contains('done');
|
||||
const normalizedGroupId = groupId.replace(/^\d+-/, '');
|
||||
|
||||
if (normalizedGroupId.startsWith('ext_link:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateTopicStatus(
|
||||
normalizedGroupId,
|
||||
!isCurrentStatusDone ? 'done' : 'pending'
|
||||
!isCurrentStatusDone ? 'done' : 'pending',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -241,9 +251,12 @@ export class Renderer {
|
||||
action: `${this.resourceType} / ${this.resourceId}`,
|
||||
label: externalLink,
|
||||
});
|
||||
|
||||
window.open(`https://${externalLink}`);
|
||||
} else {
|
||||
window.location.href = `https://${externalLink}`;
|
||||
}
|
||||
|
||||
window.open(`https://${externalLink}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -263,7 +276,7 @@ export class Renderer {
|
||||
resourceType: this.resourceType,
|
||||
resourceId: this.resourceId,
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -278,7 +291,7 @@ export class Renderer {
|
||||
e.preventDefault();
|
||||
this.updateTopicStatus(
|
||||
normalizedGroupId,
|
||||
!isCurrentStatusLearning ? 'learning' : 'pending'
|
||||
!isCurrentStatusLearning ? 'learning' : 'pending',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -287,7 +300,7 @@ export class Renderer {
|
||||
e.preventDefault();
|
||||
this.updateTopicStatus(
|
||||
normalizedGroupId,
|
||||
!isCurrentStatusSkipped ? 'skipped' : 'pending'
|
||||
!isCurrentStatusSkipped ? 'skipped' : 'pending',
|
||||
);
|
||||
|
||||
return;
|
||||
@@ -300,7 +313,7 @@ export class Renderer {
|
||||
resourceId: this.resourceId,
|
||||
resourceType: this.resourceType,
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import UserPlusIcon from '../../icons/user-plus.svg';
|
||||
import CopyIcon from '../../icons/copy.svg';
|
||||
import { useCopyText } from '../../hooks/use-copy-text';
|
||||
import { CopyIcon, UserPlus2 } from 'lucide-react';
|
||||
|
||||
type EmptyFriendsProps = {
|
||||
befriendUrl: string;
|
||||
@@ -13,14 +12,12 @@ export function EmptyFriends(props: EmptyFriendsProps) {
|
||||
return (
|
||||
<div className="rounded-md">
|
||||
<div className="mx-auto flex flex-col items-center p-7 text-center">
|
||||
<img
|
||||
alt="no friends"
|
||||
src={UserPlusIcon.src}
|
||||
className="mb-2 h-[60px] w-[60px] opacity-10 sm:h-[120px] sm:w-[120px]"
|
||||
/>
|
||||
<UserPlus2 className="mb-2 h-[60px] w-[60px] opacity-10 sm:h-[120px] sm:w-[120px]" />
|
||||
|
||||
<h2 className="text-lg font-bold sm:text-xl">Invite your Friends</h2>
|
||||
<p className="mb-4 mt-1 max-w-[400px] text-sm leading-relaxed text-gray-500">
|
||||
Share the unique link below with your friends to track their skills and progress.
|
||||
Share the unique link below with your friends to track their skills
|
||||
and progress.
|
||||
</p>
|
||||
|
||||
<div className="flex w-full max-w-[352px] items-center justify-center gap-2 rounded-lg border-2 p-1 text-sm">
|
||||
@@ -44,7 +41,8 @@ export function EmptyFriends(props: EmptyFriendsProps) {
|
||||
copyText(befriendUrl);
|
||||
}}
|
||||
>
|
||||
<img src={CopyIcon.src} className="h-4 w-4" alt="Invite Friends" />
|
||||
<CopyIcon className="mr-1 h-4 w-4" />
|
||||
|
||||
{isCopied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,10 @@ import type { FriendshipStatus } from '../Befriend';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { EmptyFriends } from './EmptyFriends';
|
||||
import { FriendProgressItem } from './FriendProgressItem';
|
||||
import UserIcon from '../../icons/user.svg';
|
||||
import { UserProgressModal } from '../UserProgress/UserProgressModal';
|
||||
import { InviteFriendPopup } from './InviteFriendPopup';
|
||||
import { UserCustomProgressModal } from '../UserProgress/UserCustomProgressModal';
|
||||
import { UserIcon } from 'lucide-react';
|
||||
|
||||
type FriendResourceProgress = {
|
||||
updatedAt: string;
|
||||
@@ -64,7 +64,7 @@ export function FriendsPage() {
|
||||
|
||||
async function loadFriends() {
|
||||
const { response, error } = await httpGet<ListFriendsResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-list-friends`
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-list-friends`,
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -89,15 +89,15 @@ export function FriendsPage() {
|
||||
const befriendUrl = `${baseUrl}/befriend?u=${user?.id}`;
|
||||
|
||||
const selectedGroupingType = groupingTypes.find(
|
||||
(grouping) => grouping.value === selectedGrouping
|
||||
(grouping) => grouping.value === selectedGrouping,
|
||||
);
|
||||
|
||||
const filteredFriends = friends.filter((friend) =>
|
||||
selectedGroupingType?.statuses.includes(friend.status)
|
||||
const filteredFriends = friends.filter(
|
||||
(friend) => selectedGroupingType?.statuses.includes(friend.status),
|
||||
);
|
||||
|
||||
const receivedRequests = friends.filter(
|
||||
(friend) => friend.status === 'received'
|
||||
(friend) => friend.status === 'received',
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -203,11 +203,8 @@ export function FriendsPage() {
|
||||
|
||||
{filteredFriends.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<img
|
||||
src={UserIcon.src}
|
||||
alt="Empty Friends"
|
||||
className="mb-3 w-12 opacity-20"
|
||||
/>
|
||||
<UserIcon size={'60px'} className="mb-3 w-12 opacity-20" />
|
||||
|
||||
<h2 className="text-lg font-semibold">
|
||||
{selectedGrouping === 'active' && 'No friends yet'}
|
||||
{selectedGrouping === 'sent' && 'No requests sent'}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import CopyIcon from '../../icons/copy.svg';
|
||||
import { useCopyText } from '../../hooks/use-copy-text';
|
||||
import { CopyIcon } from 'lucide-react';
|
||||
|
||||
type InviteFriendPopupProps = {
|
||||
befriendUrl: string;
|
||||
@@ -54,11 +54,7 @@ export function InviteFriendPopup(props: InviteFriendPopupProps) {
|
||||
copyText(befriendUrl);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={CopyIcon.src}
|
||||
className="h-4 w-4"
|
||||
alt="Invite Friends"
|
||||
/>
|
||||
<CopyIcon className="mr-1 h-4 w-4" />
|
||||
{isCopied ? 'Copied' : 'Copy URL'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ import { AccountDropdown } from './AccountDropdown';
|
||||
>Best Practices</a
|
||||
>
|
||||
</li>
|
||||
<li class='hidden lg:inline'>
|
||||
<li class='hidden xl:inline'>
|
||||
<a href='/questions' class='text-gray-400 hover:text-white'>Questions</a
|
||||
>
|
||||
</li>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { httpGet, httpPatch, httpPost } from '../../lib/http';
|
||||
import { httpGet, httpPatch } from '../../lib/http';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import type { TeamMemberDocument } from '../TeamMembers/TeamMembersPage';
|
||||
import XIcon from '../../icons/close-dark.svg';
|
||||
import AcceptIcon from '../../icons/accept.svg';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { AcceptIcon } from '../ReactIcons/AcceptIcon.tsx';
|
||||
import { XIcon } from 'lucide-react';
|
||||
|
||||
interface NotificationList extends TeamMemberDocument {
|
||||
name: string;
|
||||
@@ -18,7 +18,7 @@ export function NotificationPage() {
|
||||
|
||||
const lostNotifications = async () => {
|
||||
const { error, response } = await httpGet<NotificationList[]>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-invitation-list`
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-invitation-list`,
|
||||
);
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
@@ -28,28 +28,37 @@ export function NotificationPage() {
|
||||
setNotifications(response);
|
||||
};
|
||||
|
||||
async function respondInvitation(status: 'accept' | 'reject', inviteId: string) {
|
||||
async function respondInvitation(
|
||||
status: 'accept' | 'reject',
|
||||
inviteId: string,
|
||||
) {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
const { response, error } = await httpPatch<{ teamId: string }>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-respond-invite/${inviteId}`, {
|
||||
status
|
||||
});
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-respond-invite/${inviteId}`,
|
||||
{
|
||||
status,
|
||||
},
|
||||
);
|
||||
if (error || !response) {
|
||||
setError(error?.message || 'Something went wrong')
|
||||
setIsLoading(false)
|
||||
setError(error?.message || 'Something went wrong');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === 'accept') {
|
||||
window.location.href = `/team/progress?t=${response.teamId}`;
|
||||
} else {
|
||||
window.dispatchEvent(new CustomEvent('refresh-notification', {
|
||||
detail: {
|
||||
count: notifications.length - 1
|
||||
}
|
||||
}));
|
||||
setNotifications(notifications.filter((notification) => notification._id !== inviteId));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('refresh-notification', {
|
||||
detail: {
|
||||
count: notifications.length - 1,
|
||||
},
|
||||
}),
|
||||
);
|
||||
setNotifications(
|
||||
notifications.filter((notification) => notification._id !== inviteId),
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
@@ -66,15 +75,20 @@ export function NotificationPage() {
|
||||
<h2 className="text-3xl font-bold sm:text-4xl">Notification</h2>
|
||||
<p className="mt-2 text-gray-400">Manage your notifications</p>
|
||||
</div>
|
||||
{
|
||||
notifications.length === 0 && (
|
||||
<div className="flex items-center justify-center mt-6">
|
||||
<p className="text-gray-400">
|
||||
No notifications, you can <a href="/team/new" className="text-blue-500 underline hover:no-underline">create a team</a> and invite your friends to join.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{notifications.length === 0 && (
|
||||
<div className="mt-6 flex items-center justify-center">
|
||||
<p className="text-gray-400">
|
||||
No notifications, you can{' '}
|
||||
<a
|
||||
href="/team/new"
|
||||
className="text-blue-500 underline hover:no-underline"
|
||||
>
|
||||
create a team
|
||||
</a>{' '}
|
||||
and invite your friends to join.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
{notifications.map((notification) => (
|
||||
<div className="flex items-center justify-between rounded-md border p-2">
|
||||
@@ -86,19 +100,21 @@ export function NotificationPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
className="inline-flex border p-1 rounded hover:bg-gray-50 disabled:opacity-75"
|
||||
className="inline-flex rounded border p-1 hover:bg-gray-50 disabled:opacity-75"
|
||||
onClick={() => respondInvitation('accept', notification?._id!)}
|
||||
>
|
||||
<img src={AcceptIcon.src} className="h-4 w-4" />
|
||||
<AcceptIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button type="button"
|
||||
<button
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
className="inline-flex border p-1 rounded hover:bg-gray-50 disabled:opacity-75"
|
||||
className="inline-flex rounded border p-1 hover:bg-gray-50 disabled:opacity-75"
|
||||
onClick={() => respondInvitation('reject', notification?._id!)}
|
||||
>
|
||||
<img alt={'Close'} src={XIcon.src} className="h-4 w-4" />
|
||||
<XIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import SpinnerIcon from '../icons/spinner.svg';
|
||||
import { pageProgressMessage } from '../stores/page';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Spinner } from './ReactIcons/Spinner';
|
||||
|
||||
export interface Props {
|
||||
initialMessage: string;
|
||||
@@ -30,10 +30,10 @@ export function PageProgress(props: Props) {
|
||||
{/* Tailwind based spinner for full page */}
|
||||
<div className="fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-white bg-opacity-75">
|
||||
<div className="flex items-center justify-center rounded-md border bg-white px-4 py-2 ">
|
||||
<img
|
||||
src={SpinnerIcon.src}
|
||||
alt="Loading"
|
||||
className="h-4 w-4 animate-spin fill-blue-600 text-gray-200 sm:h-4 sm:w-4"
|
||||
<Spinner
|
||||
className="h-4 w-4 sm:h-4 sm:w-4"
|
||||
outerFill="#e5e7eb"
|
||||
innerFill="#2563eb"
|
||||
/>
|
||||
<h1 className="ml-2">
|
||||
{message}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import CloseIcon from '../icons/close.svg';
|
||||
import { httpGet } from '../lib/http';
|
||||
import { sponsorHidden } from '../stores/page';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
export type PageSponsorType = {
|
||||
company: string;
|
||||
@@ -46,7 +46,7 @@ export function PageSponsor(props: PageSponsorProps) {
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-sponsor`,
|
||||
{
|
||||
href: window.location.pathname,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -101,7 +101,7 @@ export function PageSponsor(props: PageSponsorProps) {
|
||||
sponsorHidden.set(true);
|
||||
}}
|
||||
>
|
||||
<img alt="Close" className="h-4 w-4" src={CloseIcon.src} />
|
||||
<X className="h-4 w-4" />
|
||||
</span>
|
||||
<img
|
||||
src={imageUrl}
|
||||
|
||||
@@ -15,7 +15,7 @@ const { id, title, subtitle } = Astro.props;
|
||||
<div
|
||||
id={id}
|
||||
tabindex='-1'
|
||||
class='hidden bg-black/50 overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 h-full items-center justify-center popup'
|
||||
class='hidden bg-black/50 overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-[999] h-full items-center justify-center popup'
|
||||
>
|
||||
<div class='relative p-4 w-full max-w-md h-full md:h-auto'>
|
||||
<div class='relative bg-white rounded-lg shadow popup-body'>
|
||||
|
||||
24
src/components/ReactIcons/AcceptIcon.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
type AcceptIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function AcceptIcon(props: AcceptIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="#000"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
28
src/components/ReactIcons/BestPracticesIcon.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
type BestPracticesIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function BestPracticesIcon(props: BestPracticesIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<line x1="10" x2="21" y1="6" y2="6"></line>
|
||||
<line x1="10" x2="21" y1="12" y2="12"></line>
|
||||
<line x1="10" x2="21" y1="18" y2="18"></line>
|
||||
<polyline points="3 6 4 7 6 5"></polyline>
|
||||
<polyline points="3 12 4 13 6 11"></polyline>
|
||||
<polyline points="3 18 4 19 6 17"></polyline>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
29
src/components/ReactIcons/BuildingIcon.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
type BuildingIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function BuildingIcon(props: BuildingIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"></path>
|
||||
<path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"></path>
|
||||
<path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"></path>
|
||||
<path d="M10 6h4"></path>
|
||||
<path d="M10 10h4"></path>
|
||||
<path d="M10 14h4"></path>
|
||||
<path d="M10 18h4"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
28
src/components/ReactIcons/ClipboardIcon.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
type ClipboardIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function ClipboardIcon(props: ClipboardIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<rect width="8" height="4" x="8" y="2" rx="1" ry="1" />
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
|
||||
<path d="M12 11h4" />
|
||||
<path d="M12 16h4" />
|
||||
<path d="M8 11h.01" />
|
||||
<path d="M8 16h.01" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
28
src/components/ReactIcons/CogIcon.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
type CogIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function CogIcon(props: CogIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
26
src/components/ReactIcons/DropdownIcon.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { cn } from '../../lib/classname';
|
||||
|
||||
type DropdownIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function DropdownIcon(props: DropdownIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
className={cn('h-5 w-5', className)}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
48
src/components/ReactIcons/ErrorIcon2.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
type ErrorIcon2Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function ErrorIcon2(props: ErrorIcon2Props) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 48 48"
|
||||
className={className}
|
||||
>
|
||||
<linearGradient
|
||||
id="wRKXFJsqHCxLE9yyOYHkza"
|
||||
x1="9.858"
|
||||
x2="38.142"
|
||||
y1="9.858"
|
||||
y2="38.142"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="#f44f5a" />
|
||||
<stop offset=".443" stopColor="#ee3d4a" />
|
||||
<stop offset="1" stopColor="#e52030" />
|
||||
</linearGradient>
|
||||
<path
|
||||
fill="url(#wRKXFJsqHCxLE9yyOYHkza)"
|
||||
d="M44,24c0,11.045-8.955,20-20,20S4,35.045,4,24S12.955,4,24,4S44,12.955,44,24z"
|
||||
/>
|
||||
<path
|
||||
d="M33.192,28.95L28.243,24l4.95-4.95c0.781-0.781,0.781-2.047,0-2.828l-1.414-1.414 c-0.781-0.781-2.047-0.781-2.828,0L24,19.757l-4.95-4.95c-0.781-0.781-2.047-0.781-2.828,0l-1.414,1.414 c-0.781,0.781-0.781,2.047,0,2.828l4.95,4.95l-4.95,4.95c-0.781,0.781-0.781,2.047,0,2.828l1.414,1.414 c0.781,0.781,2.047,0.781,2.828,0l4.95-4.95l4.95,4.95c0.781,0.781,2.047,0.781,2.828,0l1.414-1.414 C33.973,30.997,33.973,29.731,33.192,28.95z"
|
||||
opacity=".05"
|
||||
/>
|
||||
<path
|
||||
d="M32.839,29.303L27.536,24l5.303-5.303c0.586-0.586,0.586-1.536,0-2.121l-1.414-1.414 c-0.586-0.586-1.536-0.586-2.121,0L24,20.464l-5.303-5.303c-0.586-0.586-1.536-0.586-2.121,0l-1.414,1.414 c-0.586,0.586-0.586,1.536,0,2.121L20.464,24l-5.303,5.303c-0.586,0.586-0.586,1.536,0,2.121l1.414,1.414 c0.586,0.586,1.536,0.586,2.121,0L24,27.536l5.303,5.303c0.586,0.586,1.536,0.586,2.121,0l1.414-1.414 C33.425,30.839,33.425,29.889,32.839,29.303z"
|
||||
opacity=".07"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M31.071,15.515l1.414,1.414c0.391,0.391,0.391,1.024,0,1.414L18.343,32.485 c-0.391,0.391-1.024,0.391-1.414,0l-1.414-1.414c-0.391-0.391-0.391-1.024,0-1.414l14.142-14.142 C30.047,15.124,30.681,15.124,31.071,15.515z"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M32.485,31.071l-1.414,1.414c-0.391,0.391-1.024,0.391-1.414,0L15.515,18.343 c-0.391-0.391-0.391-1.024,0-1.414l1.414-1.414c0.391-0.391,1.024-0.391,1.414,0l14.142,14.142 C32.876,30.047,32.876,30.681,32.485,31.071z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
20
src/components/ReactIcons/GitHubIcon.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
type GitHubIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function GitHubIcon(props: GitHubIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={className || ''}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 98 96"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362l-.08-9.127c-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126l-.08 13.526c0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
|
||||
fill={ className?.indexOf('text-') !== -1 ? 'currentColor' : '#24292f' }
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
32
src/components/ReactIcons/GoogleIcon.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
type GoogleIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function GoogleIcon(props: GoogleIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 90 92"
|
||||
fill="none"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M90 47.1c0-3.1-.3-6.3-.8-9.3H45.9v17.7h24.8c-1 5.7-4.3 10.7-9.2 13.9l14.8 11.5C85 72.8 90 61 90 47.1z"
|
||||
fill="#4280ef"
|
||||
/>
|
||||
<path
|
||||
d="M45.9 91.9c12.4 0 22.8-4.1 30.4-11.1L61.5 69.4c-4.1 2.8-9.4 4.4-15.6 4.4-12 0-22.1-8.1-25.8-18.9L4.9 66.6c7.8 15.5 23.6 25.3 41 25.3z"
|
||||
fill="#34a353"
|
||||
/>
|
||||
<path
|
||||
d="M20.1 54.8c-1.9-5.7-1.9-11.9 0-17.6L4.9 25.4c-6.5 13-6.5 28.3 0 41.2l15.2-11.8z"
|
||||
fill="#f6b704"
|
||||
/>
|
||||
<path
|
||||
d="M45.9 18.3c6.5-.1 12.9 2.4 17.6 6.9L76.6 12C68.3 4.2 57.3 0 45.9.1c-17.4 0-33.2 9.8-41 25.3l15.2 11.8c3.7-10.9 13.8-18.9 25.8-18.9z"
|
||||
fill="#e54335"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
26
src/components/ReactIcons/GroupIcon.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
type GroupIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function GroupIcon(props: GroupIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
23
src/components/ReactIcons/GuideIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
type GuideIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function GuideIcon(props: GuideIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9 6.75V15m6-6v8.25m.503 3.498l4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 00-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
23
src/components/ReactIcons/HomeIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
type HomeIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function HomeIcon(props: HomeIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
49
src/components/ReactIcons/LinkedInIcon.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
type LinkedInIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function LinkedInIcon(props: LinkedInIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="100"
|
||||
height="100"
|
||||
viewBox="0,0,256,256"
|
||||
>
|
||||
<g transform="translate(-26.66667,-26.66667) scale(1.20833,1.20833)">
|
||||
<g
|
||||
fill="none"
|
||||
fillRule="nonzero"
|
||||
stroke="none"
|
||||
strokeWidth="1"
|
||||
strokeLinecap="butt"
|
||||
strokeLinejoin="miter"
|
||||
strokeMiterlimit="10"
|
||||
strokeDasharray=""
|
||||
strokeDashoffset="0"
|
||||
fontFamily="none"
|
||||
fontWeight="none"
|
||||
fontSize="none"
|
||||
textAnchor="none"
|
||||
style={{ mixBlendMode: 'normal' }}
|
||||
>
|
||||
<g transform="scale(5.33333,5.33333)">
|
||||
<path
|
||||
d="M42,37c0,2.762 -2.238,5 -5,5h-26c-2.761,0 -5,-2.238 -5,-5v-26c0,-2.762 2.239,-5 5,-5h26c2.762,0 5,2.238 5,5z"
|
||||
fill="#0288d1"
|
||||
></path>
|
||||
<path
|
||||
d="M12,19h5v17h-5zM14.485,17h-0.028c-1.492,0 -2.457,-1.112 -2.457,-2.501c0,-1.419 0.995,-2.499 2.514,-2.499c1.521,0 2.458,1.08 2.486,2.499c0,1.388 -0.965,2.501 -2.515,2.501zM36,36h-5v-9.099c0,-2.198 -1.225,-3.698 -3.192,-3.698c-1.501,0 -2.313,1.012 -2.707,1.99c-0.144,0.35 -0.101,1.318 -0.101,1.807v9h-5v-17h5v2.616c0.721,-1.116 1.85,-2.616 4.738,-2.616c3.578,0 6.261,2.25 6.261,7.274l0.001,9.726z"
|
||||
fill="#ffffff"
|
||||
></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
21
src/components/ReactIcons/MoreVerticalIcon.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
type MoreVerticalIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function MoreVerticalIcon(props: MoreVerticalIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
clipRule="evenodd"
|
||||
fillRule="evenodd"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit="2"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path d="m12 16.495c1.242 0 2.25 1.008 2.25 2.25s-1.008 2.25-2.25 2.25-2.25-1.008-2.25-2.25 1.008-2.25 2.25-2.25zm0-6.75c1.242 0 2.25 1.008 2.25 2.25s-1.008 2.25-2.25 2.25-2.25-1.008-2.25-2.25 1.008-2.25 2.25-2.25zm0-6.75c1.242 0 2.25 1.008 2.25 2.25s-1.008 2.25-2.25 2.25-2.25-1.008-2.25-2.25 1.008-2.25 2.25-2.25z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
25
src/components/ReactIcons/RoadmapIcon.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
type RoadmapIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function RoadmapIcon(props: RoadmapIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M18 6H5a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h13l4-3.5L18 6Z"></path>
|
||||
<path d="M12 13v8"></path>
|
||||
<path d="M12 3v3"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
24
src/components/ReactIcons/TeamProgressIcon.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
type TeamProgressIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function TeamProgressIcon(props: TeamProgressIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M3 3v18h18" />
|
||||
<path d="m19 9-5 5-4-4-3 3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
23
src/components/ReactIcons/TwitterIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
type TwitterIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function TwitterIcon(props: TwitterIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="15"
|
||||
height="15"
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M8.9285 6.35221L14.5135 0H13.1905L8.339 5.5144L4.467 0H0L5.8565 8.33955L0 15H1.323L6.443 9.17535L10.533 15H15M1.8005 0.976187H3.833L13.1895 14.0718H11.1565"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
21
src/components/ReactIcons/UserIcon.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
type UserIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function UserIcon(props: UserIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
strokeWidth="0"
|
||||
viewBox="0 0 1024 1024"
|
||||
height="1em"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<path d="M858.5 763.6a374 374 0 0 0-80.6-119.5 375.63 375.63 0 0 0-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 0 0-80.6 119.5A371.7 371.7 0 0 0 136 901.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 0 0 8-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
26
src/components/ReactIcons/UsersIcon.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
type UsersIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
export function UsersIcon(props: UsersIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
78
src/components/ReactIcons/VerifyLetterIcon.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
type VerifyLetterIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function VerifyLetterIcon(props: VerifyLetterIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
fill="#f79219"
|
||||
d="M222.58,114.782c0-8.69-3.979-16.901-10.8-22.286l-69.526-54.889c-8.357-6.598-20.15-6.598-28.508,0 L44.22,92.496c-6.82,5.385-10.8,13.596-10.8,22.286v12.732H222.58V114.782z"
|
||||
/>
|
||||
<path
|
||||
fill="#ffa91a"
|
||||
d="M213.336,223.341H42.664c-5.105,0-9.244-4.138-9.244-9.244V113.116c0-5.105,4.138-9.244,9.244-9.244 h170.672c5.105,0,9.244,4.139,9.244,9.244v100.981C222.58,219.203,218.441,223.341,213.336,223.341z"
|
||||
/>
|
||||
<path
|
||||
fill="#f79219"
|
||||
d="M213.336,103.872h-0.756v100.225c0,5.105-4.138,9.244-9.244,9.244H33.42v0.756 c0,5.105,4.138,9.244,9.244,9.244h170.672c5.105,0,9.244-4.138,9.244-9.244V113.116 C222.58,108.011,218.441,103.872,213.336,103.872z"
|
||||
/>
|
||||
<path
|
||||
fill="#ef7816"
|
||||
d="M213.336,103.872H42.664c-4.488,0-8.229,3.199-9.067,7.441l79.417,62.697 c8.787,6.937,21.186,6.937,29.973,0l79.417-62.698C221.564,107.071,217.824,103.872,213.336,103.872z"
|
||||
/>
|
||||
<path
|
||||
fill="#f1f2f2"
|
||||
d="M203.33,73.49v52.88l-60.34,47.64c-8.789,6.939-21.191,6.939-29.98,0l-60.34-47.64V73.49 c0-4.418,3.582-8,8-8h134.66C199.748,65.49,203.33,69.072,203.33,73.49z"
|
||||
/>
|
||||
<g>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M58.67,125.46c-1.101,0-2-0.9-2-2V73.49c0-2.2,1.8-4,4-4h106.89c1.101,0,1.99,0.9,1.99,2s-0.89,2-1.99,2 H60.67v49.97C60.67,124.56,59.77,125.46,58.67,125.46z M175.55,73.49c-1.1,0-2-0.9-2-2s0.9-2,2-2c1.11,0,2,0.9,2,2 S176.66,73.49,175.55,73.49z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
fill="#e6e7e8"
|
||||
d="M195.33,65.49h-2v50.88l-60.34,47.64c-8.789,6.939-21.191,6.939-29.98,0l-50.34-39.745v2.105l60.34,47.64 c8.789,6.939,21.191,6.939,29.98,0l60.34-47.64V73.49C203.33,69.072,199.748,65.49,195.33,65.49z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
fill="#d1d3d4"
|
||||
d="M197.9,65.92c0.274,0.808,0.43,1.67,0.43,2.57v52.88l-60.34,47.64c-8.789,6.939-21.191,6.939-29.98,0 l-55.34-43.692v1.052l60.34,47.64c8.789,6.939,21.191,6.939,29.98,0l60.34-47.64V73.49 C203.33,69.972,201.056,66.991,197.9,65.92z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path
|
||||
fill="#d1d3d4"
|
||||
d="M109.036,99.997H80.422c-1.431,0-2.591-1.16-2.591-2.591v0c0-1.431,1.16-2.591,2.591-2.591h28.614 c1.431,0,2.591,1.16,2.591,2.591v0C111.627,98.836,110.467,99.997,109.036,99.997z"
|
||||
/>
|
||||
<path
|
||||
fill="#d1d3d4"
|
||||
d="M175.578,124.03H80.422c-1.431,0-2.591-1.16-2.591-2.591v0c0-1.431,1.16-2.591,2.591-2.591h95.156 c1.431,0,2.591,1.16,2.591,2.591v0C178.169,122.87,177.009,124.03,175.578,124.03z"
|
||||
/>
|
||||
<path
|
||||
fill="#d1d3d4"
|
||||
d="M175.578,138.881H80.422c-1.431,0-2.591-1.16-2.591-2.591l0,0c0-1.431,1.16-2.591,2.591-2.591h95.156 c1.431,0,2.591,1.16,2.591,2.591l0,0C178.169,137.721,177.009,138.881,175.578,138.881z"
|
||||
/>
|
||||
<polygon
|
||||
fill="#d1d3d4"
|
||||
points="156.425,163.403 99.575,163.403 106.139,168.585 149.861,168.585"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon
|
||||
fill="#d1d3d4"
|
||||
points="175.236,148.551 80.764,148.551 87.328,153.733 168.672,153.733"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
23
src/components/ReactIcons/VideoIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
type VideoIconProps = {
|
||||
className: string;
|
||||
};
|
||||
|
||||
export function VideoIcon(props: VideoIconProps) {
|
||||
const { className } = props;
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -59,7 +59,9 @@ const relatedQuestionDetails = await getQuestionGroupsByIds(relatedQuestions);
|
||||
|
||||
{
|
||||
relatedRoadmaps.length && (
|
||||
<div class='border-t bg-gray-100'>
|
||||
<div class:list={['border-t bg-gray-100', {
|
||||
'mt-8': !relatedQuestionDetails.length
|
||||
}]}>
|
||||
<div class='container'>
|
||||
<div class='relative -top-5 flex justify-between'>
|
||||
<span class='text-md flex items-center rounded-md border bg-white px-3 py-1 font-medium'>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { httpGet, httpPatch } from '../lib/http';
|
||||
import BuildingIcon from '../icons/building.svg';
|
||||
import ErrorIcon from '../icons/error.svg';
|
||||
import { pageProgressMessage } from '../stores/page';
|
||||
import type { TeamDocument } from './CreateTeam/CreateTeamForm';
|
||||
import type { AllowedRoles } from './CreateTeam/RoleDropdown';
|
||||
@@ -9,6 +7,8 @@ import type { AllowedMemberStatus } from './TeamDropdown/TeamDropdown';
|
||||
import { isLoggedIn } from '../lib/jwt';
|
||||
import { showLoginPopup } from '../lib/popup';
|
||||
import { getUrlParams } from '../lib/browser';
|
||||
import { ErrorIcon2 } from './ReactIcons/ErrorIcon2';
|
||||
import { BuildingIcon } from './ReactIcons/BuildingIcon';
|
||||
|
||||
type InvitationResponse = {
|
||||
team: TeamDocument;
|
||||
@@ -85,11 +85,7 @@ export function RespondInviteForm() {
|
||||
if (!invite) {
|
||||
return (
|
||||
<div className="container text-center">
|
||||
<img
|
||||
alt={'error'}
|
||||
src={ErrorIcon.src}
|
||||
className="mx-auto mb-4 mt-24 w-20 opacity-20"
|
||||
/>
|
||||
<ErrorIcon2 className="mx-auto mb-4 mt-24 w-20 opacity-20" />
|
||||
|
||||
<h2 className={'mb-1 text-2xl font-bold'}>Error</h2>
|
||||
<p className="mb-4 text-base leading-6 text-gray-600">
|
||||
@@ -110,11 +106,7 @@ export function RespondInviteForm() {
|
||||
|
||||
return (
|
||||
<div className="container text-center">
|
||||
<img
|
||||
alt={'join team'}
|
||||
src={BuildingIcon.src}
|
||||
className="mx-auto mb-4 mt-24 w-20 opacity-20"
|
||||
/>
|
||||
<BuildingIcon className="mx-auto mb-4 mt-24 w-20 opacity-20" />
|
||||
|
||||
<h2 className={'mb-1 text-2xl font-bold'}>Join Team</h2>
|
||||
<p className="mb-3 text-base leading-6 text-gray-600">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCopyText } from '../../hooks/use-copy-text';
|
||||
import CopyIcon from '../../icons/copy.svg';
|
||||
import { CopyIcon } from 'lucide-react';
|
||||
|
||||
type EditorProps = {
|
||||
title: string;
|
||||
@@ -20,11 +20,11 @@ export function Editor(props: EditorProps) {
|
||||
<button className="flex items-center" onClick={() => copyText(text)}>
|
||||
{isCopied && (
|
||||
<span className="mr-1 text-xs leading-none text-gray-700">
|
||||
Copied!
|
||||
Copied!
|
||||
</span>
|
||||
)}
|
||||
|
||||
<img src={CopyIcon.src} alt="Copy" className="inline-block h-4 w-4" />
|
||||
<CopyIcon className="inline-block h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useState } from 'react';
|
||||
|
||||
import { useCopyText } from '../../hooks/use-copy-text';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import CopyIcon from '../../icons/copy.svg';
|
||||
import { RoadmapSelect } from './RoadmapSelect';
|
||||
import { GitHubReadmeBanner } from './GitHubReadmeBanner';
|
||||
import { downloadImage } from '../../helper/download-image';
|
||||
import { SelectionButton } from './SelectionButton';
|
||||
import { StepCounter } from './StepCounter';
|
||||
import { Editor } from './Editor';
|
||||
import { CopyIcon } from 'lucide-react';
|
||||
|
||||
type StepLabelProps = {
|
||||
label: string;
|
||||
@@ -34,7 +34,7 @@ export function RoadCardPage() {
|
||||
}
|
||||
|
||||
const badgeUrl = new URL(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-badge/${version}/${user?.id}`
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-badge/${version}/${user?.id}`,
|
||||
);
|
||||
|
||||
badgeUrl.searchParams.set('variant', variant);
|
||||
@@ -44,7 +44,7 @@ export function RoadCardPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-start gap-4 mx-0 sm:-mx-10 px-0 sm:px-10 border-b pt-2 pb-4">
|
||||
<div className="mx-0 flex items-start gap-4 border-b px-0 pb-4 pt-2 sm:-mx-10 sm:px-10">
|
||||
<StepCounter step={1} />
|
||||
<div>
|
||||
<StepLabel label="Pick progress to show (Max. 4)" />
|
||||
@@ -58,7 +58,7 @@ export function RoadCardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4 mx-0 sm:-mx-10 px-0 sm:px-10 border-b py-4">
|
||||
<div className="mx-0 flex items-start gap-4 border-b px-0 py-4 sm:-mx-10 sm:px-10">
|
||||
<StepCounter step={2} />
|
||||
<div>
|
||||
<StepLabel label="Select Mode (Dark vs Light)" />
|
||||
@@ -85,7 +85,7 @@ export function RoadCardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4 mx-0 sm:-mx-10 px-0 sm:px-10 border-b py-4">
|
||||
<div className="mx-0 flex items-start gap-4 border-b px-0 py-4 sm:-mx-10 sm:px-10">
|
||||
<StepCounter step={3} />
|
||||
<div>
|
||||
<StepLabel label="Select Version" />
|
||||
@@ -111,7 +111,7 @@ export function RoadCardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4 mx-0 sm:-mx-10 px-0 sm:px-10 border-b py-4">
|
||||
<div className="mx-0 flex items-start gap-4 border-b px-0 py-4 sm:-mx-10 sm:px-10">
|
||||
<StepCounter step={4} />
|
||||
<div className="flex-grow">
|
||||
<StepLabel label="Share your #RoadCard with others" />
|
||||
@@ -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.src} className="mr-1" />
|
||||
<CopyIcon size={16} className="inline-block h-4 w-4 mr-1" />
|
||||
|
||||
{isCopied ? 'Copied!' : 'Copy Link'}
|
||||
</button>
|
||||
|
||||
@@ -8,7 +8,11 @@ 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';
|
||||
import { CreateVersion } from './CreateVersion/CreateVersion';
|
||||
import { type RoadmapFrontmatter } from '../lib/roadmap';
|
||||
import { ShareRoadmapButton } from './ShareRoadmapButton';
|
||||
import { Share2 } from 'lucide-react';
|
||||
import ShareIcons from './ShareIcons/ShareIcons.astro';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
@@ -20,6 +24,7 @@ export interface Props {
|
||||
hasSearch?: boolean;
|
||||
question?: RoadmapFrontmatter['question'];
|
||||
hasTopics?: boolean;
|
||||
isForkable?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -32,6 +37,7 @@ const {
|
||||
note,
|
||||
hasTopics = false,
|
||||
question,
|
||||
isForkable = false,
|
||||
} = Astro.props;
|
||||
|
||||
const isRoadmapReady = !isUpcoming;
|
||||
@@ -58,13 +64,21 @@ const hasTnsBanner = !!tnsBannerLink;
|
||||
]}
|
||||
>
|
||||
<div class='mb-3 mt-0 sm:mb-4'>
|
||||
{
|
||||
isForkable && (
|
||||
<div class='mb-2'>
|
||||
<CreateVersion client:load roadmapId={roadmapId} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<h1 class='mb-0.5 text-2xl font-bold sm:mb-2 sm:text-4xl'>
|
||||
{title}
|
||||
<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'
|
||||
className='relative ml-1.5 text-gray-500 !opacity-100 hover:text-gray-600 focus:outline-0 [&>svg]:h-4 [&>svg]:w-4 [&>svg]:stroke-gray-400 [&>svg]:stroke-[0.4] hover:[&>svg]:stroke-gray-600 sm:[&>svg]:h-4 sm:[&>svg]:w-4'
|
||||
client:only='react'
|
||||
/>
|
||||
</span>
|
||||
@@ -85,6 +99,12 @@ const hasTnsBanner = !!tnsBannerLink;
|
||||
←<span class='hidden sm:inline'> All Roadmaps</span>
|
||||
</a>
|
||||
|
||||
<ShareRoadmapButton
|
||||
description={description}
|
||||
pageUrl={`https://roadmap.sh/${roadmapId}`}
|
||||
client:idle
|
||||
/>
|
||||
|
||||
{isRoadmapReady && (
|
||||
<>
|
||||
<button
|
||||
@@ -109,16 +129,6 @@ const hasTnsBanner = !!tnsBannerLink;
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
data-guest-required
|
||||
data-popup='login-popup'
|
||||
class='inline-flex hidden items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm'
|
||||
aria-label='Subscribe for Updates'
|
||||
>
|
||||
<Icon icon='email' />
|
||||
<span class='ml-2'>Subscribe</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${page
|
||||
---
|
||||
|
||||
<div class='absolute left-[-18px] top-[110px] h-full hidden' id='page-share-icons'>
|
||||
<div class='flex sticky top-[100px] flex-col gap-1.5'>
|
||||
<a href={twitterUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
<div class='flex sticky top-[100px] flex-col gap-1.5 items-center'>
|
||||
<a href={twitterUrl} target='_blank' class='text-gray-500 hover:text-gray-700 mb-0.5'>
|
||||
<Icon icon='twitter' />
|
||||
</a>
|
||||
<a href={fbUrl} target='_blank' class='text-gray-500 hover:text-gray-700'>
|
||||
|
||||
@@ -45,6 +45,8 @@ export function ShareSuccess(props: ShareSuccessProps) {
|
||||
},
|
||||
];
|
||||
|
||||
const embedHtml = `<iframe src="${baseUrl}/r/embed?id=${roadmapId}" width="100%" height="500px" frameBorder="0"\n></iframe>`;
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col justify-center">
|
||||
<div className="mt-5 flex grow flex-col items-center justify-center gap-1.5">
|
||||
@@ -76,6 +78,23 @@ export function ShareSuccess(props: ShareSuccessProps) {
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-2 border-t pt-2">
|
||||
<p className="text-sm text-gray-400">
|
||||
You can also embed this roadmap on your website.
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
onClick={(e) => {
|
||||
e.currentTarget.select();
|
||||
copyText(embedHtml);
|
||||
}}
|
||||
readOnly={true}
|
||||
className="w-full resize-none rounded-md border bg-gray-50 p-2 text-sm"
|
||||
value={embedHtml}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{visibility === 'public' && (
|
||||
<>
|
||||
<div className="-mx-4 mt-4 flex items-center gap-1.5">
|
||||
|
||||
164
src/components/ShareRoadmapButton.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { Check, Code2, Copy, Facebook, Linkedin, Share2 } from 'lucide-react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useOutsideClick } from '../hooks/use-outside-click.ts';
|
||||
import { useCopyText } from '../hooks/use-copy-text.ts';
|
||||
import { cn } from '../lib/classname.ts';
|
||||
import { TwitterIcon } from './ReactIcons/TwitterIcon.tsx';
|
||||
import { EmbedRoadmapModal } from './CustomRoadmap/EmbedRoadmapModal.tsx';
|
||||
|
||||
type ShareRoadmapButtonProps = {
|
||||
roadmapId?: string;
|
||||
description: string;
|
||||
pageUrl: string;
|
||||
allowEmbed?: boolean;
|
||||
};
|
||||
|
||||
export function ShareRoadmapButton(props: ShareRoadmapButtonProps) {
|
||||
const { description, pageUrl, allowEmbed = false, roadmapId } = props;
|
||||
|
||||
const { isCopied, copyText } = useCopyText();
|
||||
|
||||
const [isEmbedModalOpen, setIsEmbedModalOpen] = useState(false);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
const twitterUrl = `https://twitter.com/intent/tweet?text=${description}&url=${pageUrl}`;
|
||||
const fbUrl = `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${pageUrl}`;
|
||||
const hnUrl = `https://news.ycombinator.com/submitlink?t=${description}&u=${pageUrl}`;
|
||||
const redditUrl = `https://www.reddit.com/submit?title=${description}&url=${pageUrl}`;
|
||||
const linkedinUrl = `https://www.linkedin.com/shareArticle?mini=true&url=${pageUrl}&title=${description}`;
|
||||
|
||||
useOutsideClick(containerRef, () => {
|
||||
setIsDropdownOpen(false);
|
||||
});
|
||||
|
||||
const embedHtml = `<iframe src="https://roadmap.sh/r/embed?id=${roadmapId}" width="100%" height="500px" frameBorder="0"\n></iframe>`;
|
||||
|
||||
return (
|
||||
<div className="relative" ref={containerRef}>
|
||||
{isEmbedModalOpen && (
|
||||
<EmbedRoadmapModal
|
||||
onClose={() => {
|
||||
setIsEmbedModalOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
||||
className={cn(
|
||||
'inline-flex h-full items-center justify-center rounded-md bg-yellow-400 px-3 py-1.5 text-xs font-medium hover:bg-yellow-500 sm:text-sm',
|
||||
{
|
||||
'bg-yellow-500': isDropdownOpen,
|
||||
'bg-green-400': isCopied,
|
||||
},
|
||||
)}
|
||||
aria-label="Share Roadmap"
|
||||
>
|
||||
{!isCopied && (
|
||||
<>
|
||||
<Share2 size="15px" />
|
||||
<span className="ml-2 hidden sm:inline">Share</span>
|
||||
</>
|
||||
)}
|
||||
{isCopied && (
|
||||
<>
|
||||
<Check size="15px" />
|
||||
<span className="ml-2 hidden sm:inline">Copied</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute left-0 z-[999] mt-1 w-48 rounded-md bg-slate-800 text-sm text-white shadow-lg ring-1 ring-black ring-opacity-5">
|
||||
<div className="flex flex-col px-1 py-1">
|
||||
<button
|
||||
onClick={() => {
|
||||
copyText(pageUrl);
|
||||
setIsDropdownOpen(false);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
<div className="flex w-[20px] items-center justify-center">
|
||||
<Copy size="15px" className="text-slate-400" />
|
||||
</div>
|
||||
Copy Link
|
||||
</button>
|
||||
{allowEmbed && roadmapId && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsDropdownOpen(false);
|
||||
setIsEmbedModalOpen(true);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
<div className="flex w-[20px] items-center justify-center">
|
||||
<Code2 size="15px" className="text-slate-400" />
|
||||
</div>
|
||||
Embed Roadmap
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
href={twitterUrl}
|
||||
target={'_blank'}
|
||||
className="mt-1 flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
<div className="flex w-[20px] items-center justify-center">
|
||||
<TwitterIcon className="h-[16px] text-slate-400" />
|
||||
</div>
|
||||
Twitter
|
||||
</a>
|
||||
<a
|
||||
href={fbUrl}
|
||||
target={'_blank'}
|
||||
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
<div className="flex w-[20px] items-center justify-center">
|
||||
<Facebook size="16px" className="text-slate-400" />
|
||||
</div>
|
||||
Facebook
|
||||
</a>
|
||||
<a
|
||||
href={hnUrl}
|
||||
target={'_blank'}
|
||||
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
<div className="flex w-[20px] items-center justify-center">
|
||||
<img
|
||||
alt={'hackernews logo'}
|
||||
src={'/images/hackernews.svg'}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
Hacker News
|
||||
</a>
|
||||
<a
|
||||
href={redditUrl}
|
||||
target={'_blank'}
|
||||
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
<div className="flex w-[20px] items-center justify-center">
|
||||
<img
|
||||
alt={'reddit logo'}
|
||||
src={'/images/reddit.svg'}
|
||||
className="h-5 w-5"
|
||||
/>
|
||||
</div>
|
||||
Reddit
|
||||
</a>
|
||||
<a
|
||||
href={linkedinUrl}
|
||||
target={'_blank'}
|
||||
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 text-sm text-slate-100 hover:bg-slate-700"
|
||||
>
|
||||
<div className="flex w-[20px] items-center justify-center">
|
||||
<Linkedin size="16px" className="text-slate-400" />
|
||||
</div>
|
||||
LinkedIn
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import ChevronDown from '../../icons/dropdown.svg';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
@@ -10,6 +9,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { useTeamId } from '../../hooks/use-team-id';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import type { ValidTeamType } from '../CreateTeam/Step0';
|
||||
import { DropdownIcon } from '../ReactIcons/DropdownIcon.tsx';
|
||||
|
||||
const allowedStatus = ['invited', 'joined', 'rejected'] as const;
|
||||
export type AllowedMemberStatus = (typeof allowedStatus)[number];
|
||||
@@ -44,7 +44,7 @@ export function TeamDropdown() {
|
||||
if (shouldShowTeamIndicator) {
|
||||
localStorage.setItem(
|
||||
'viewedTeamsCount',
|
||||
(viewedTeamsCountNumber + 1).toString()
|
||||
(viewedTeamsCountNumber + 1).toString(),
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
@@ -67,7 +67,7 @@ export function TeamDropdown() {
|
||||
|
||||
async function getAllTeams() {
|
||||
const { response, error } = await httpGet<TeamListResponse>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-teams`
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-user-teams`,
|
||||
);
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Something went wrong');
|
||||
@@ -140,7 +140,7 @@ export function TeamDropdown() {
|
||||
{isLoading && 'Loading ..'}
|
||||
</span>
|
||||
</div>
|
||||
<img alt={'show dropdown'} src={ChevronDown.src} className="h-4 w-4" />
|
||||
<DropdownIcon className={'h-4 w-4'} />
|
||||
</button>
|
||||
|
||||
{showDropdown && (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import type { TeamMemberDocument } from './TeamMembersPage';
|
||||
import MoreIcon from '../../icons/more-vertical.svg';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { MoreVerticalIcon } from '../ReactIcons/MoreVerticalIcon.tsx';
|
||||
|
||||
export function MemberActionDropdown({
|
||||
member,
|
||||
@@ -79,7 +79,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.src} className="h-4 w-4" />
|
||||
<MoreVerticalIcon className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import type { GroupByRoadmap, TeamMember } from './TeamProgressPage';
|
||||
import { getUrlParams } from '../../lib/browser';
|
||||
import ExternalLinkIcon from '../../icons/external-link.svg';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { LucideExternalLink } from 'lucide-react';
|
||||
|
||||
type GroupRoadmapItemProps = {
|
||||
roadmap: GroupByRoadmap;
|
||||
@@ -33,11 +33,7 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
|
||||
className="group mb-0.5 flex shrink-0 items-center justify-between text-base font-medium leading-none text-black"
|
||||
target={'_blank'}
|
||||
>
|
||||
<img
|
||||
alt={'link'}
|
||||
src={ExternalLinkIcon.src}
|
||||
className="ml-2 h-4 w-4 opacity-20 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
<LucideExternalLink className="h-4 w-4 opacity-20 transition-opacity group-hover:opacity-100" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,7 +54,7 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
|
||||
onClick={() => {
|
||||
onShowResourceProgress(
|
||||
member.member,
|
||||
member.progress?.resourceId!
|
||||
member.progress?.resourceId!,
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { Node } from 'reactflow';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { MemberProgressModalHeader } from './MemberProgressModalHeader';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
export type ProgressMapProps = {
|
||||
member: TeamMember;
|
||||
@@ -284,7 +285,7 @@ export function MemberCustomProgressModal(props: ProgressMapProps) {
|
||||
}`}
|
||||
onClick={onClose}
|
||||
>
|
||||
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -12,11 +12,12 @@ import {
|
||||
type ResourceType,
|
||||
updateResourceProgress,
|
||||
} from '../../lib/resource-progress';
|
||||
import CloseIcon from '../../icons/close.svg';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { MemberProgressModalHeader } from './MemberProgressModalHeader';
|
||||
import { replaceChildren } from '../../lib/dom.ts';
|
||||
import { XIcon } from 'lucide-react';
|
||||
|
||||
export type ProgressMapProps = {
|
||||
member: TeamMember;
|
||||
@@ -67,12 +68,12 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
teamId: string,
|
||||
memberId: string,
|
||||
resourceType: string,
|
||||
resourceId: string
|
||||
resourceId: string,
|
||||
) {
|
||||
const { error, response } = await httpGet<MemberProgressResponse>(
|
||||
`${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-get-member-resource-progress/${teamId}/${memberId}?resourceType=${resourceType}&resourceId=${resourceId}`
|
||||
}/v1-get-member-resource-progress/${teamId}/${memberId}?resourceType=${resourceType}&resourceId=${resourceId}`,
|
||||
);
|
||||
if (error || !response) {
|
||||
toast.error(error?.message || 'Failed to get member progress');
|
||||
@@ -91,7 +92,8 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
fontURL: '/fonts/balsamiq.woff2',
|
||||
});
|
||||
|
||||
containerEl.current?.replaceChildren(svg);
|
||||
replaceChildren(containerEl.current!, svg);
|
||||
// containerEl.current?.replaceChildren(svg);
|
||||
}
|
||||
|
||||
useKeydown('Escape', () => {
|
||||
@@ -158,14 +160,14 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
resourceType: resourceType as ResourceType,
|
||||
topicId,
|
||||
},
|
||||
newStatus
|
||||
newStatus,
|
||||
)
|
||||
.then(() => {
|
||||
renderTopicProgress(topicId, newStatus);
|
||||
getMemberProgress(teamId, member._id, resourceType, resourceId).then(
|
||||
(data) => {
|
||||
setMemberProgress(data);
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -225,7 +227,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
e.preventDefault();
|
||||
updateTopicStatus(
|
||||
topicId,
|
||||
!isCurrentStatusLearning ? 'learning' : 'pending'
|
||||
!isCurrentStatusLearning ? 'learning' : 'pending',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -234,7 +236,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
e.preventDefault();
|
||||
updateTopicStatus(
|
||||
topicId,
|
||||
!isCurrentStatusSkipped ? 'skipped' : 'pending'
|
||||
!isCurrentStatusSkipped ? 'skipped' : 'pending',
|
||||
);
|
||||
|
||||
return;
|
||||
@@ -296,7 +298,8 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
}`}
|
||||
onClick={onClose}
|
||||
>
|
||||
<img alt={'close'} src={CloseIcon.src} className="h-4 w-4" />
|
||||
<XIcon className="h-4 w-4" />
|
||||
|
||||
<span className="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import MoreIcon from '../../icons/more-vertical.svg';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import { Lock, MoreVertical, Shapes, Trash2 } from 'lucide-react';
|
||||
import { MoreVerticalIcon } from '../ReactIcons/MoreVerticalIcon.tsx';
|
||||
|
||||
type RoadmapActionDropdownProps = {
|
||||
onDelete?: () => void;
|
||||
@@ -26,7 +26,7 @@ export function RoadmapActionDropdown(props: RoadmapActionDropdownProps) {
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="hidden items-center opacity-60 transition-opacity hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-30 sm:flex"
|
||||
>
|
||||
<img alt="menu" src={MoreIcon.src} className="h-4 w-4" />
|
||||
<MoreVerticalIcon className={'h-4 w-4'} />
|
||||
</button>
|
||||
<button
|
||||
disabled={false}
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { TeamDocument } from '../CreateTeam/CreateTeamForm';
|
||||
import type { TeamResourceConfig } from '../CreateTeam/RoadmapSelector';
|
||||
import { httpGet, httpPut } from '../../lib/http';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import RoadmapIcon from '../../icons/roadmap.svg';
|
||||
import type { PageType } from '../CommandMenu/CommandMenu';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $canManageCurrentTeam } from '../../stores/team';
|
||||
@@ -28,6 +27,7 @@ import { RoadmapActionDropdown } from './RoadmapActionDropdown';
|
||||
import { UpdateTeamResourceModal } from '../CreateTeam/UpdateTeamResourceModal';
|
||||
import { ShareOptionsModal } from '../ShareOptions/ShareOptionsModal';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { RoadmapIcon } from '../ReactIcons/RoadmapIcon.tsx';
|
||||
|
||||
export function TeamRoadmaps() {
|
||||
const { t: teamId } = getUrlParams();
|
||||
@@ -73,7 +73,7 @@ export function TeamRoadmaps() {
|
||||
|
||||
async function loadTeam(teamIdToFetch: string) {
|
||||
const { response, error } = await httpGet<TeamDocument>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamIdToFetch}`
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-team/${teamIdToFetch}`,
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -87,7 +87,7 @@ export function TeamRoadmaps() {
|
||||
|
||||
async function loadTeamResourceConfig(teamId: string) {
|
||||
const { error, response } = await httpGet<TeamResourceConfig>(
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-resource-config/${teamId}`
|
||||
`${import.meta.env.PUBLIC_API_URL}/v1-get-team-resource-config/${teamId}`,
|
||||
);
|
||||
if (error || !Array.isArray(response)) {
|
||||
console.error(error);
|
||||
@@ -127,7 +127,7 @@ export function TeamRoadmaps() {
|
||||
{
|
||||
resourceId: roadmapId,
|
||||
resourceType: 'roadmap',
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -156,7 +156,7 @@ export function TeamRoadmaps() {
|
||||
resourceId: roadmapId,
|
||||
resourceType: 'roadmap',
|
||||
removed: [],
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -190,13 +190,13 @@ export function TeamRoadmaps() {
|
||||
}
|
||||
window.addEventListener(
|
||||
'custom-roadmap-created',
|
||||
handleCustomRoadmapCreated
|
||||
handleCustomRoadmapCreated,
|
||||
);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
'custom-roadmap-created',
|
||||
handleCustomRoadmapCreated
|
||||
handleCustomRoadmapCreated,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
@@ -252,13 +252,13 @@ export function TeamRoadmaps() {
|
||||
);
|
||||
|
||||
const placeholderRoadmaps = teamResources.filter(
|
||||
(c: TeamResourceConfig[0]) => c.isCustomResource && !c.topics
|
||||
(c: TeamResourceConfig[0]) => c.isCustomResource && !c.topics,
|
||||
);
|
||||
const customRoadmaps = teamResources.filter(
|
||||
(c: TeamResourceConfig[0]) => c.isCustomResource && c.topics
|
||||
(c: TeamResourceConfig[0]) => c.isCustomResource && c.topics,
|
||||
);
|
||||
const defaultRoadmaps = teamResources.filter(
|
||||
(c: TeamResourceConfig[0]) => !c.isCustomResource
|
||||
(c: TeamResourceConfig[0]) => !c.isCustomResource,
|
||||
);
|
||||
|
||||
const hasRoadmaps =
|
||||
@@ -272,11 +272,8 @@ export function TeamRoadmaps() {
|
||||
{addRoadmapModal}
|
||||
{createRoadmapModal}
|
||||
|
||||
<img
|
||||
alt="roadmap"
|
||||
src={RoadmapIcon.src}
|
||||
className="mb-4 h-24 w-24 opacity-10"
|
||||
/>
|
||||
<RoadmapIcon className="mb-4 h-24 w-24 opacity-10" />
|
||||
|
||||
<h3 className="mb-1 text-2xl font-bold text-gray-900">No roadmaps</h3>
|
||||
<p className="text-base text-gray-500">
|
||||
{canManageCurrentTeam
|
||||
@@ -380,11 +377,11 @@ export function TeamRoadmaps() {
|
||||
onDelete={() => {
|
||||
if (
|
||||
confirm(
|
||||
'Are you sure you want to remove this roadmap?'
|
||||
'Are you sure you want to remove this roadmap?',
|
||||
)
|
||||
) {
|
||||
onRemove(resourceConfig.resourceId).finally(
|
||||
() => {}
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
}}
|
||||
@@ -405,7 +402,7 @@ export function TeamRoadmaps() {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -433,7 +430,7 @@ export function TeamRoadmaps() {
|
||||
'grid grid-cols-1 p-2.5',
|
||||
canManageCurrentTeam
|
||||
? 'sm:grid-cols-[auto_172px]'
|
||||
: 'sm:grid-cols-[auto_110px]'
|
||||
: 'sm:grid-cols-[auto_110px]',
|
||||
)}
|
||||
key={resourceConfig.resourceId}
|
||||
>
|
||||
@@ -464,11 +461,11 @@ export function TeamRoadmaps() {
|
||||
onDelete={() => {
|
||||
if (
|
||||
confirm(
|
||||
'Are you sure you want to remove this roadmap?'
|
||||
'Are you sure you want to remove this roadmap?',
|
||||
)
|
||||
) {
|
||||
onRemove(resourceConfig.resourceId).finally(
|
||||
() => {}
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
}}
|
||||
@@ -557,11 +554,11 @@ export function TeamRoadmaps() {
|
||||
onDelete={() => {
|
||||
if (
|
||||
confirm(
|
||||
'Are you sure you want to remove this roadmap?'
|
||||
'Are you sure you want to remove this roadmap?',
|
||||
)
|
||||
) {
|
||||
onRemove(resourceConfig.resourceId).finally(
|
||||
() => {}
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { TeamDropdown } from './TeamDropdown/TeamDropdown';
|
||||
import ChevronDown from '../icons/dropdown.svg';
|
||||
import { useTeamId } from '../hooks/use-team-id';
|
||||
import TeamProgress from '../icons/team-progress.svg';
|
||||
import SettingsIcon from '../icons/cog.svg';
|
||||
import ChatIcon from '../icons/chat.svg';
|
||||
import MapIcon from '../icons/map.svg';
|
||||
import GroupIcon from '../icons/group.svg';
|
||||
import { useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $currentTeam } from '../stores/team';
|
||||
import { SubmitFeedbackPopup } from './Feedback/SubmitFeedbackPopup';
|
||||
import { ChevronDownIcon } from './ReactIcons/ChevronDownIcon.tsx';
|
||||
import { GroupIcon } from './ReactIcons/GroupIcon.tsx';
|
||||
import { TeamProgressIcon } from './ReactIcons/TeamProgressIcon.tsx';
|
||||
import { MapIcon, MessageCircle } from 'lucide-react';
|
||||
import { CogIcon } from './ReactIcons/CogIcon.tsx';
|
||||
|
||||
type TeamSidebarProps = {
|
||||
activePageId: string;
|
||||
@@ -29,26 +29,26 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
|
||||
title: 'Progress',
|
||||
href: `/team/progress?t=${teamId}`,
|
||||
id: 'progress',
|
||||
icon: TeamProgress.src,
|
||||
icon: TeamProgressIcon,
|
||||
},
|
||||
{
|
||||
title: 'Roadmaps',
|
||||
href: `/team/roadmaps?t=${teamId}`,
|
||||
id: 'roadmaps',
|
||||
icon: MapIcon.src,
|
||||
icon: MapIcon,
|
||||
hasWarning: currentTeam?.roadmaps?.length === 0,
|
||||
},
|
||||
{
|
||||
title: 'Members',
|
||||
href: `/team/members?t=${teamId}`,
|
||||
id: 'members',
|
||||
icon: GroupIcon.src,
|
||||
icon: GroupIcon,
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
href: `/team/settings?t=${teamId}`,
|
||||
id: 'settings',
|
||||
icon: SettingsIcon.src,
|
||||
icon: CogIcon,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -66,7 +66,7 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
|
||||
sidebarLinks.find((sidebarLink) => sidebarLink.id === activePageId)
|
||||
?.title
|
||||
}
|
||||
<img alt="menu" src={ChevronDown.src} className="h-4 w-4" />
|
||||
<ChevronDownIcon 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.src} className={`mr-2 h-4 w-4`} />
|
||||
<GroupIcon className="mr-2 h-4 w-4" />
|
||||
Personal Account / Teams
|
||||
</a>
|
||||
</li>
|
||||
@@ -95,11 +95,8 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
|
||||
isActive ? 'bg-slate-100' : ''
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
alt={'menu icon'}
|
||||
src={sidebarLink.icon}
|
||||
className="mr-2 h-4 w-4"
|
||||
/>
|
||||
{<sidebarLink.icon className="mr-2 h-4 w-4" />}
|
||||
|
||||
{sidebarLink.title}
|
||||
</a>
|
||||
</li>
|
||||
@@ -111,11 +108,7 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
|
||||
className={`flex w-full items-center rounded px-3 py-1.5 text-sm text-slate-900 hover:bg-slate-200`}
|
||||
onClick={() => setShowFeedbackPopup(true)}
|
||||
>
|
||||
<img
|
||||
alt={'menu icon'}
|
||||
src={ChatIcon.src}
|
||||
className="mr-2 h-4 w-4"
|
||||
/>
|
||||
<MessageCircle className="mr-2 h-4 w-4" />
|
||||
Send Feedback
|
||||
</button>
|
||||
</li>
|
||||
@@ -150,11 +143,8 @@ export function TeamSidebar({ activePageId, children }: TeamSidebarProps) {
|
||||
>
|
||||
<span className="flex flex-grow items-center justify-between">
|
||||
<span className="flex">
|
||||
<img
|
||||
alt="menu icon"
|
||||
src={sidebarLink.icon}
|
||||
className="relative top-[2px] mr-2 h-4 w-4"
|
||||
/>
|
||||
{<sidebarLink.icon className="mr-2 h-4 w-4" />}
|
||||
|
||||
{sidebarLink.title}
|
||||
</span>
|
||||
{sidebarLink.hasWarning && (
|
||||
@@ -174,7 +164,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.src} className="mr-2 h-4 w-4" />
|
||||
<MessageCircle className="mr-2 h-4 w-4" />
|
||||
Send Feedback
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import type { TeamDocument } from '../CreateTeam/CreateTeamForm';
|
||||
import type { TeamResourceConfig } from '../CreateTeam/RoadmapSelector';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import DropdownIcon from '../../icons/dropdown.svg';
|
||||
// import DropdownIcon from '../../icons/dropdown.svg';
|
||||
import {
|
||||
clearResourceProgress,
|
||||
refreshProgressCounters,
|
||||
@@ -15,6 +15,7 @@ import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { DropdownIcon } from '../ReactIcons/DropdownIcon';
|
||||
|
||||
type TeamVersionsProps = {
|
||||
resourceId: string;
|
||||
@@ -75,7 +76,7 @@ export function TeamVersions(props: TeamVersionsProps) {
|
||||
}/v1-get-team-versions?${new URLSearchParams({
|
||||
resourceId,
|
||||
resourceType,
|
||||
})}`
|
||||
})}`,
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -142,11 +143,7 @@ export function TeamVersions(props: TeamVersionsProps) {
|
||||
<span className="truncate">
|
||||
{selectedTeamVersion?.team.name || 'Team Versions'}
|
||||
</span>
|
||||
<img
|
||||
alt="Dropdown"
|
||||
src={DropdownIcon.src}
|
||||
className="h-3 w-3 sm:h-4 sm:w-4"
|
||||
/>
|
||||
<DropdownIcon className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</div>
|
||||
<div className="sm:hidden">
|
||||
{shouldShowAvatar ? (
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import CloseIcon from '../../icons/close.svg';
|
||||
import SpinnerIcon from '../../icons/spinner.svg';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { useLoadTopic } from '../../hooks/use-load-topic';
|
||||
@@ -17,19 +15,21 @@ import {
|
||||
} from '../../lib/resource-progress';
|
||||
import { pageProgressMessage, sponsorHidden } from '../../stores/page';
|
||||
import { TopicProgressButton } from './TopicProgressButton';
|
||||
import { ContributionForm } from './ContributionForm';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import type {
|
||||
AllowedLinkTypes,
|
||||
RoadmapContentDocument,
|
||||
} from '../CustomRoadmap/CustomRoadmap';
|
||||
import { markdownToHtml } from '../../lib/markdown';
|
||||
import { markdownToHtml, sanitizeMarkdown } from '../../lib/markdown';
|
||||
import { cn } from '../../lib/classname';
|
||||
import { Ban, FileText } from 'lucide-react';
|
||||
import { Ban, FileText, X } from 'lucide-react';
|
||||
import { getUrlParams } from '../../lib/browser';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
|
||||
|
||||
type TopicDetailProps = {
|
||||
isEmbed?: boolean;
|
||||
canSubmitContribution: boolean;
|
||||
};
|
||||
|
||||
@@ -43,9 +43,10 @@ const linkTypes: Record<AllowedLinkTypes, string> = {
|
||||
};
|
||||
|
||||
export function TopicDetail(props: TopicDetailProps) {
|
||||
const { canSubmitContribution } = props;
|
||||
const { canSubmitContribution, isEmbed = false } = props;
|
||||
|
||||
const [contributionAlertMessage, setContributionAlertMessage] = useState('');
|
||||
const [hasEnoughLinks, setHasEnoughLinks] = useState(false);
|
||||
const [contributionUrl, setContributionUrl] = useState('');
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isContributing, setIsContributing] = useState(false);
|
||||
@@ -67,12 +68,10 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
// Close the topic detail when user clicks outside the topic detail
|
||||
useOutsideClick(topicRef, () => {
|
||||
setIsActive(false);
|
||||
setIsContributing(false);
|
||||
});
|
||||
|
||||
useKeydown('Escape', () => {
|
||||
setIsActive(false);
|
||||
setIsContributing(false);
|
||||
});
|
||||
|
||||
// Toggle topic is available even if the component UI is not active
|
||||
@@ -121,7 +120,6 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
setIsActive(true);
|
||||
sponsorHidden.set(true);
|
||||
|
||||
setContributionAlertMessage('');
|
||||
setTopicId(topicId);
|
||||
setResourceType(resourceType);
|
||||
setResourceId(resourceId);
|
||||
@@ -159,20 +157,27 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
}
|
||||
let topicHtml = '';
|
||||
if (!isCustomResource) {
|
||||
// It's full HTML with page body, head etc.
|
||||
// We only need the inner HTML of the #main-content
|
||||
const node = new DOMParser().parseFromString(
|
||||
response as string,
|
||||
topicHtml = response as string;
|
||||
const topicDom = new DOMParser().parseFromString(
|
||||
topicHtml,
|
||||
'text/html',
|
||||
);
|
||||
topicHtml = node?.getElementById('main-content')?.outerHTML || '';
|
||||
|
||||
const links = topicDom.querySelectorAll('a');
|
||||
const urlElem: HTMLElement =
|
||||
topicDom.querySelector('[data-github-url]')!;
|
||||
const contributionUrl = urlElem?.dataset?.githubUrl || '';
|
||||
|
||||
setContributionUrl(contributionUrl);
|
||||
setHasEnoughLinks(links.length >= 3);
|
||||
} else {
|
||||
setLinks((response as RoadmapContentDocument)?.links || []);
|
||||
setTopicTitle((response as RoadmapContentDocument)?.title || '');
|
||||
topicHtml = markdownToHtml(
|
||||
(response as RoadmapContentDocument)?.description || '',
|
||||
false,
|
||||
|
||||
const sanitizedMarkdown = sanitizeMarkdown(
|
||||
(response as RoadmapContentDocument).description || '',
|
||||
);
|
||||
topicHtml = markdownToHtml(sanitizedMarkdown, false);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
@@ -184,6 +189,10 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) topicRef?.current?.focus();
|
||||
}, [isActive]);
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
@@ -194,46 +203,33 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
<div className={'relative z-50'}>
|
||||
<div
|
||||
ref={topicRef}
|
||||
className="fixed right-0 top-0 z-40 h-screen w-full overflow-y-auto bg-white p-4 sm:max-w-[600px] sm:p-6"
|
||||
tabIndex={0}
|
||||
className="fixed right-0 top-0 z-40 h-screen w-full overflow-y-auto bg-white p-4 focus:outline-0 sm:max-w-[600px] sm:p-6"
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="flex w-full justify-center">
|
||||
<img
|
||||
src={SpinnerIcon.src}
|
||||
alt="Loading"
|
||||
className="h-6 w-6 animate-spin fill-blue-600 text-gray-200 sm:h-12 sm:w-12"
|
||||
<Spinner
|
||||
outerFill="#d1d5db"
|
||||
className="h-6 w-6 sm:h-12 sm:w-12"
|
||||
innerFill="#2563eb"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && isContributing && (
|
||||
<ContributionForm
|
||||
resourceType={resourceType}
|
||||
resourceId={resourceId}
|
||||
topicId={topicId}
|
||||
onClose={(message?: string) => {
|
||||
if (message) {
|
||||
setContributionAlertMessage(message);
|
||||
}
|
||||
|
||||
setIsContributing(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isContributing && !isLoading && !error && (
|
||||
<>
|
||||
{/* Actions for the topic */}
|
||||
<div className="mb-2">
|
||||
<TopicProgressButton
|
||||
topicId={topicId}
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
onClose={() => {
|
||||
setIsActive(false);
|
||||
setIsContributing(false);
|
||||
}}
|
||||
/>
|
||||
{!isEmbed && (
|
||||
<TopicProgressButton
|
||||
topicId={topicId}
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
onClose={() => {
|
||||
setIsActive(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -241,10 +237,9 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
className="absolute right-2.5 top-2.5 inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900"
|
||||
onClick={() => {
|
||||
setIsActive(false);
|
||||
setIsContributing(false);
|
||||
}}
|
||||
>
|
||||
<img alt="Close" className="h-5 w-5" src={CloseIcon.src} />
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -294,29 +289,21 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
)}
|
||||
|
||||
{/* Contribution */}
|
||||
{canSubmitContribution && (
|
||||
{canSubmitContribution && !hasEnoughLinks && contributionUrl && (
|
||||
<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 us improve this introduction and submit a link to a good
|
||||
article, podcast, video, or any other resource that helped you
|
||||
understand this topic better.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (isGuest) {
|
||||
setIsActive(false);
|
||||
showLoginPopup();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsContributing(true);
|
||||
}}
|
||||
disabled={!!contributionAlertMessage}
|
||||
className="block w-full rounded-md bg-gray-800 p-2 text-sm text-white transition-colors hover:bg-black hover:text-white disabled:bg-green-200 disabled:text-black"
|
||||
<a
|
||||
href={contributionUrl}
|
||||
target={'_blank'}
|
||||
className="flex w-full items-center justify-center rounded-md bg-gray-800 p-2 text-sm text-white transition-colors hover:bg-black hover:text-white disabled:bg-green-200 disabled:text-black"
|
||||
>
|
||||
{contributionAlertMessage
|
||||
? contributionAlertMessage
|
||||
: 'Submit a Link'}
|
||||
</button>
|
||||
<GitHubIcon className="mr-2 inline-block h-4 w-4 text-white" />
|
||||
Edit this Content
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -334,7 +321,7 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
setIsContributing(false);
|
||||
}}
|
||||
>
|
||||
<img alt="Close" className="h-5 w-5" src={CloseIcon.src} />
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<Ban className="h-16 w-16 text-red-500" />
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||
import DownIcon from '../../icons/down.svg';
|
||||
import SpinnerIcon from '../../icons/spinner.svg';
|
||||
import { isLoggedIn } from '../../lib/jwt';
|
||||
import {
|
||||
getTopicStatus,
|
||||
@@ -10,9 +8,14 @@ import {
|
||||
renderTopicProgress,
|
||||
updateResourceProgress,
|
||||
} from '../../lib/resource-progress';
|
||||
import type { ResourceProgressType, ResourceType } from '../../lib/resource-progress';
|
||||
import type {
|
||||
ResourceProgressType,
|
||||
ResourceType,
|
||||
} from '../../lib/resource-progress';
|
||||
import { showLoginPopup } from '../../lib/popup';
|
||||
import { useToast } from '../../hooks/use-toast';
|
||||
import { Spinner } from '../ReactIcons/Spinner';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
type TopicProgressButtonProps = {
|
||||
topicId: string;
|
||||
@@ -27,7 +30,7 @@ const statusColors: Record<ResourceProgressType, string> = {
|
||||
learning: 'bg-yellow-500',
|
||||
pending: 'bg-gray-300',
|
||||
skipped: 'bg-black',
|
||||
removed: ''
|
||||
removed: '',
|
||||
};
|
||||
|
||||
export function TopicProgressButton(props: TopicProgressButtonProps) {
|
||||
@@ -71,7 +74,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
|
||||
|
||||
handleUpdateResourceProgress('done');
|
||||
},
|
||||
[progress]
|
||||
[progress],
|
||||
);
|
||||
|
||||
// Mark as learning
|
||||
@@ -85,7 +88,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
|
||||
|
||||
handleUpdateResourceProgress('learning');
|
||||
},
|
||||
[progress]
|
||||
[progress],
|
||||
);
|
||||
|
||||
// Mark as learning
|
||||
@@ -99,7 +102,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
|
||||
|
||||
handleUpdateResourceProgress('skipped');
|
||||
},
|
||||
[progress]
|
||||
[progress],
|
||||
);
|
||||
|
||||
// Mark as pending
|
||||
@@ -114,7 +117,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
|
||||
|
||||
handleUpdateResourceProgress('pending');
|
||||
},
|
||||
[progress]
|
||||
[progress],
|
||||
);
|
||||
|
||||
const handleUpdateResourceProgress = (progress: ResourceProgressType) => {
|
||||
@@ -131,7 +134,7 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
|
||||
resourceId,
|
||||
resourceType,
|
||||
},
|
||||
progress
|
||||
progress,
|
||||
)
|
||||
.then(() => {
|
||||
setProgress(progress);
|
||||
@@ -149,22 +152,22 @@ export function TopicProgressButton(props: TopicProgressButtonProps) {
|
||||
};
|
||||
|
||||
const allowMarkingSkipped = ['pending', 'learning', 'done'].includes(
|
||||
progress
|
||||
progress,
|
||||
);
|
||||
const allowMarkingDone = ['skipped', 'pending', 'learning'].includes(
|
||||
progress
|
||||
progress,
|
||||
);
|
||||
const allowMarkingLearning = ['done', 'skipped', 'pending'].includes(
|
||||
progress
|
||||
progress,
|
||||
);
|
||||
const allowMarkingPending = ['skipped', 'done', 'learning'].includes(
|
||||
progress
|
||||
progress,
|
||||
);
|
||||
|
||||
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.src} />
|
||||
<Spinner className="h-4 w-4" />
|
||||
<span className="ml-2">Updating Status..</span>
|
||||
</button>
|
||||
);
|
||||
@@ -188,7 +191,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.src} />
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{showChangeStatus && (
|
||||
|
||||
@@ -4,13 +4,13 @@ import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import type { ResourceType } from '../../lib/resource-progress';
|
||||
import { topicSelectorAll } from '../../lib/resource-progress';
|
||||
import CloseIcon from '../../icons/close.svg';
|
||||
import { deleteUrlParam, getUrlParams } from '../../lib/browser';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import type { GetRoadmapResponse } from '../CustomRoadmap/CustomRoadmap';
|
||||
import { ReadonlyEditor } from '../../../editor/readonly-editor';
|
||||
import { ProgressLoadingError } from './ProgressLoadingError';
|
||||
import { UserProgressModalHeader } from './UserProgressModalHeader';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
export type ProgressMapProps = {
|
||||
userId?: string;
|
||||
@@ -208,7 +208,7 @@ export function UserCustomProgressModal(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.src} className="h-4 w-4" />
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -6,11 +6,11 @@ import { useKeydown } from '../../hooks/use-keydown';
|
||||
import { httpGet } from '../../lib/http';
|
||||
import type { ResourceType } from '../../lib/resource-progress';
|
||||
import { topicSelectorAll } from '../../lib/resource-progress';
|
||||
import CloseIcon from '../../icons/close.svg';
|
||||
import { deleteUrlParam, getUrlParams } from '../../lib/browser';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { ProgressLoadingError } from './ProgressLoadingError';
|
||||
import { UserProgressModalHeader } from './UserProgressModalHeader';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
export type ProgressMapProps = {
|
||||
userId?: string;
|
||||
@@ -70,12 +70,12 @@ export function UserProgressModal(props: ProgressMapProps) {
|
||||
async function getUserProgress(
|
||||
userId: string,
|
||||
resourceType: string,
|
||||
resourceId: string
|
||||
resourceId: string,
|
||||
): Promise<UserProgressResponse | undefined> {
|
||||
const { error, response } = await httpGet<UserProgressResponse>(
|
||||
`${
|
||||
import.meta.env.PUBLIC_API_URL
|
||||
}/v1-get-user-progress/${userId}?resourceType=${resourceType}&resourceId=${resourceId}`
|
||||
}/v1-get-user-progress/${userId}?resourceType=${resourceType}&resourceId=${resourceId}`,
|
||||
);
|
||||
|
||||
if (error || !response) {
|
||||
@@ -86,7 +86,7 @@ export function UserProgressModal(props: ProgressMapProps) {
|
||||
}
|
||||
|
||||
async function getRoadmapSVG(
|
||||
jsonUrl: string
|
||||
jsonUrl: string,
|
||||
): Promise<SVGElement | undefined> {
|
||||
const { error, response: roadmapJson } = await httpGet(jsonUrl);
|
||||
if (error || !roadmapJson) {
|
||||
@@ -216,7 +216,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.src} className="h-4 w-4" />
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -67,7 +67,7 @@ Unlike browser cache which serves a single user, proxy caches may serve hundreds
|
||||
|
||||
A Reverse proxy cache or surrogate cache is implemented close to the origin servers in order to reduce the load on the server. Unlike proxy caches which are implemented by ISPs etc to reduce the bandwidth usage in a network, surrogates or reverse proxy caches are implemented near the origin servers by the server administrators to reduce the load on the server.
|
||||
|
||||

|
||||

|
||||
|
||||
Although you can control the reverse proxy caches (since it is implemented by you on your server) you can not avoid or control browser and proxy caches. And if your website is not configured to use these caches properly, it will still be cached using whatever defaults are set on these caches.
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ app.listen(3000, () => {
|
||||
});
|
||||
```
|
||||
|
||||
The important piece to note here is the `express-session` middleware registration which automatically handles the session initialization, cooking parsing and session data retrieval, and so on. In our example here, we are passing the following configuration options:
|
||||
The important piece to note here is the `express-session` middleware registration which automatically handles the session initialization, cookie parsing and session data retrieval, and so on. In our example here, we are passing the following configuration options:
|
||||
|
||||
- `secret`: This is used to sign the session ID cookie. Using a secret that cannot be guessed will reduce the ability to hijack a session.
|
||||
- `cookie`: Object containing the configuration for session id cookie.
|
||||
@@ -176,7 +176,7 @@ Next, we have to implement the functionality to process the login form submissio
|
||||
```javascript
|
||||
module.exports = function processLogin(req, res) {
|
||||
if (req.body.username !== 'admin' || req.body.password !== 'admin') {
|
||||
return res.send('Invalid username or password);
|
||||
return res.send('Invalid username or password');
|
||||
}
|
||||
|
||||
req.session.userid = req.body.username;
|
||||
|
||||