Compare commits
74 Commits
roadmap/de
...
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 |
@@ -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/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/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,7 +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)
|
||||
- [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)
|
||||
@@ -53,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)
|
||||
@@ -67,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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]" />
|
||||
|
||||
@@ -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,10 +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";
|
||||
import { replaceChildren } from '../../lib/dom.ts';
|
||||
|
||||
export class Renderer {
|
||||
resourceId: string;
|
||||
@@ -95,7 +98,7 @@ export class Renderer {
|
||||
.then(() => {
|
||||
return renderResourceProgress(
|
||||
this.resourceType as ResourceType,
|
||||
this.resourceId
|
||||
this.resourceId,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -143,7 +146,7 @@ export class Renderer {
|
||||
this.jsonToSvg(
|
||||
this.resourceType === 'roadmap'
|
||||
? `/${this.resourceId}.json`
|
||||
: `/best-practices/${this.resourceId}.json`
|
||||
: `/best-practices/${this.resourceId}.json`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -183,7 +186,7 @@ export class Renderer {
|
||||
resourceType: this.resourceType as ResourceType,
|
||||
topicId,
|
||||
},
|
||||
newStatus
|
||||
newStatus,
|
||||
)
|
||||
.then(() => {
|
||||
renderTopicProgress(topicId, newStatus);
|
||||
@@ -215,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',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -243,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;
|
||||
}
|
||||
|
||||
@@ -265,7 +276,7 @@ export class Renderer {
|
||||
resourceType: this.resourceType,
|
||||
resourceId: this.resourceId,
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -280,7 +291,7 @@ export class Renderer {
|
||||
e.preventDefault();
|
||||
this.updateTopicStatus(
|
||||
normalizedGroupId,
|
||||
!isCurrentStatusLearning ? 'learning' : 'pending'
|
||||
!isCurrentStatusLearning ? 'learning' : 'pending',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -289,7 +300,7 @@ export class Renderer {
|
||||
e.preventDefault();
|
||||
this.updateTopicStatus(
|
||||
normalizedGroupId,
|
||||
!isCurrentStatusSkipped ? 'skipped' : 'pending'
|
||||
!isCurrentStatusSkipped ? 'skipped' : 'pending',
|
||||
);
|
||||
|
||||
return;
|
||||
@@ -302,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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -10,6 +10,9 @@ import { MarkFavorite } from './FeaturedItems/MarkFavorite';
|
||||
import { TeamVersions } from './TeamVersions/TeamVersions';
|
||||
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;
|
||||
@@ -96,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
|
||||
@@ -120,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,12 +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 { replaceChildren } from '../../lib/dom.ts';
|
||||
import { XIcon } from 'lucide-react';
|
||||
|
||||
export type ProgressMapProps = {
|
||||
member: TeamMember;
|
||||
@@ -68,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');
|
||||
@@ -160,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) => {
|
||||
@@ -227,7 +227,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
e.preventDefault();
|
||||
updateTopicStatus(
|
||||
topicId,
|
||||
!isCurrentStatusLearning ? 'learning' : 'pending'
|
||||
!isCurrentStatusLearning ? 'learning' : 'pending',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -236,7 +236,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
|
||||
e.preventDefault();
|
||||
updateTopicStatus(
|
||||
topicId,
|
||||
!isCurrentStatusSkipped ? 'skipped' : 'pending'
|
||||
!isCurrentStatusSkipped ? 'skipped' : 'pending',
|
||||
);
|
||||
|
||||
return;
|
||||
@@ -298,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 { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import CloseIcon from '../../icons/close.svg';
|
||||
import SpinnerIcon from '../../icons/spinner.svg';
|
||||
|
||||
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);
|
||||
@@ -203,42 +208,28 @@ export function TopicDetail(props: TopicDetailProps) {
|
||||
>
|
||||
{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"
|
||||
@@ -246,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>
|
||||
|
||||
@@ -299,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>
|
||||
)}
|
||||
</>
|
||||
@@ -339,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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# MLOps
|
||||
|
||||
- [Machine Learning Engineering for Production (MLOps) Specialization](https://imp.i384100.net/nLA5mx)
|
||||
- [Full Stack Deep Learning](https://fullstackdeeplearning.com/course/2022/)
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
# App Shortcuts
|
||||
|
||||
Sure, I can provide information about "app shortcuts" feature in Android.
|
||||
|
||||
### App Shortcuts
|
||||
|
||||
App shortcuts in Android are designed to provide quick and convenient routes to specific actions or functions within your app from the device home screen. To use them, long-press an app's icon and a pop-up menu will appear with the available shortcuts. Depending on the app, you might be able to send a message, make a booking, navigate home, or perform some other specific task without having to first open the app and navigate to the desired function. These shortcuts can also be moved and placed individually on the home screen for even faster access.
|
||||
App shortcuts in Android are designed to provide quick and convenient routes to specific actions or functions within your app from the device home screen. To use them, long-press an app's icon and a pop-up menu will appear with the available shortcuts. Depending on the app, you might be able to send a message, make a booking, navigate home, or perform some other specific task without having to first open the app and navigate to the desired function. These shortcuts can also be moved and placed individually on the home screen for even faster access.
|
||||
|
||||
7623
src/data/roadmaps/aws/aws.json
Normal file
58
src/data/roadmaps/aws/aws.md
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
jsonUrl: '/jsons/roadmaps/aws.json'
|
||||
pdfUrl: '/pdfs/roadmaps/aws.pdf'
|
||||
order: 14
|
||||
briefTitle: 'AWS'
|
||||
briefDescription: 'Step by step guide to learning AWS in 2023'
|
||||
title: 'AWS Roadmap'
|
||||
description: 'Step by step guide to learning AWS in 2023'
|
||||
isNew: true
|
||||
hasTopics: true
|
||||
dimensions:
|
||||
width: 968
|
||||
height: 2183.87
|
||||
schema:
|
||||
headline: 'AWS Roadmap'
|
||||
description: 'Learn how to use AWS with this interactive step by step guide in 2023. We also have resources and short descriptions attached to the roadmap items so you can get everything you want to learn in one place.'
|
||||
imageUrl: 'https://roadmap.sh/roadmaps/aws.png'
|
||||
datePublished: '2023-11-29'
|
||||
dateModified: '2023-11-29'
|
||||
seo:
|
||||
title: 'AWS Roadmap - roadmap.sh'
|
||||
description: 'Step by step guide to learn AWS in 2023. We also have resources and short descriptions attached to the roadmap items so you can get everything you want to learn in one place.'
|
||||
keywords:
|
||||
- 'aws tutorial'
|
||||
- 'step by step guide for aws'
|
||||
- 'aws for beginners'
|
||||
- 'how to learn aws'
|
||||
- 'use aws in production'
|
||||
- 'aws roadmap 2023'
|
||||
- 'guide to learning aws'
|
||||
- 'aws roadmap'
|
||||
- 'aws learning path'
|
||||
- 'aws learning roadmap'
|
||||
- 'container roadmap'
|
||||
- 'aws'
|
||||
- 'aws learning guide'
|
||||
- 'aws skills'
|
||||
- 'aws for development'
|
||||
- 'aws for development skills'
|
||||
- 'aws for development skills test'
|
||||
- 'aws learning guide'
|
||||
- 'become a aws expert'
|
||||
- 'aws career path'
|
||||
- 'learn aws for development'
|
||||
- 'what is aws'
|
||||
- 'aws quiz'
|
||||
- 'aws interview questions'
|
||||
relatedRoadmaps:
|
||||
- 'devops'
|
||||
- 'backend'
|
||||
sitemap:
|
||||
priority: 1
|
||||
changefreq: 'monthly'
|
||||
tags:
|
||||
- 'roadmap'
|
||||
- 'main-sitemap'
|
||||
- 'skill-roadmap'
|
||||
---
|
||||
@@ -0,0 +1,3 @@
|
||||
# What is Cloud Computing?
|
||||
|
||||
Cloud Computing refers to the delivery of computing services over the internet instead of using local servers. It offers reliable, scalable, and inexpensive cloud computing services which include data storage, databases, applications, analytics, machine learning, and even setting up virtual servers. The biggest names providing cloud computing services are Amazon Web Services (AWS), Microsoft Azure, Google Cloud, and others. The main selling point is that you only pay for the services you use, helping you manage your expenses more effectively.
|
||||
@@ -0,0 +1,3 @@
|
||||
# IaaS vs PaaS vs SaaS
|
||||
|
||||
"IaaS, PaaS, and SaaS are three types of cloud service models. **IaaS** or Infrastructure as a Service provides users with a resource-based service via virtualization technology, offering computing infrastructure, physical or (more often) virtual machines and other resources. **PaaS** or Platform as a Service provides runtime environments for developing, testing, and managing applications, it is utilized for software development and offers a platform to developers to build applications and services over the internet. **SaaS** or Software as a Service provides on-demand software accessed via the internet, it delivers a complete software solution which you purchase on a pay-as-you-go basis from a cloud service provider."
|
||||
@@ -0,0 +1,3 @@
|
||||
# Public vs Private vs Hybrid Cloud
|
||||
|
||||
Amazon Web Services (AWS) offers various cloud-based deployment models to cater to varying business needs, including Public, Private, and Hybrid clouds. A **Public Cloud** is a model where the service provider offers resources, such as applications and storage, available to the general public over the internet. Resources may be free, or sold on a pay-per-usage model. On the other hand, a **Private Cloud** is a type of cloud computing that delivers similar advantages to public cloud, including scalability and self-service, but through a proprietary architecture dedicated to a single organization. Unlike public clouds, which deliver services to multiple organizations, a private cloud is dedicated to the needs and goals of a single entity. Lastly, a **Hybrid Cloud** is a solution that combines a private cloud with one or more public cloud services, with proprietary software enabling communication between each distinct service.
|
||||