Compare commits

...

31 Commits

Author SHA1 Message Date
Arik Chakma
8407dd7448 Minor fix 2023-10-09 19:41:25 +06:00
Arik Chakma
d659752ad2 Implement social share options 2023-10-09 19:30:38 +06:00
Kamran Ahmed
6f9fe361ae Change style of custom roadmap page 2023-10-09 09:07:59 +01:00
Arik Chakma
036b34c6f3 Implement Custom Roadmap minor features (#4565)
* Remove roadmap type

* Add Edit Roadmap button

* Add Edit Roadmap permission

* Add Edit and Share roadmap button

* Remove Margin

* Implement Discoverable Checkbox

* Add Loading State for buttons
2023-10-09 08:44:30 +01:00
Kamran Ahmed
93c2043f23 Fix warning in hero roadmap 2023-10-08 18:38:02 +01:00
Saleh Hashemi
d2da3c8621 update checkout version to v4 (#4559) 2023-10-07 22:15:01 +01:00
Kamran Ahmed
4aa8f15c07 Add email icon in footer 2023-10-07 15:31:24 +01:00
Arik Chakma
ceb4c3b95d Remove invited members from sharing settings (#4555)
* Fix team member list

* Minor change
2023-10-06 19:00:48 +01:00
Kamran Ahmed
7ec5e30b51 Hero roadmap section updates 2023-10-06 19:00:08 +01:00
Kamran Ahmed
e5e0a7c8c5 Add teams banner 2023-10-04 20:32:28 +01:00
Kamran Ahmed
90f3ffe270 Add banner for teams 2023-10-04 16:13:41 +01:00
Kamran Ahmed
ce47a7433e Teams button in navigation 2023-10-04 15:44:34 +01:00
Selva Muthu Kumaran
21b8358683 roadmap-aspnet-change-tracker-api.md (#4546)
aspnet-change-tracker-api URL fixed
fixes : #4544
2023-10-04 20:40:45 +06:00
Kamran Ahmed
e1751b105f Add team page 2023-10-04 15:28:46 +01:00
Kamran Ahmed
e43bea7c40 Setup redirects on the teams page 2023-10-04 15:22:20 +01:00
Kamran Ahmed
5fa669aec2 Update team page 2023-10-04 15:06:59 +01:00
Kamran Ahmed
4b8f868b2b Add roadmaps and friends to account dropdown 2023-10-04 10:34:29 +01:00
Kamran Ahmed
a0743a8272 Fix sharing options button 2023-10-04 10:30:28 +01:00
Arik Chakma
2cae13c090 Add Members while Transferring Roadmap (#4534)
* Add members while Transferring Roadmap

* Implement Responsive in Roadmaps page
2023-10-04 10:15:56 +01:00
Kamran Ahmed
0bf287f1d6 Add features to pricing 2023-10-04 10:12:08 +01:00
Kamran Ahmed
d7d819b4b3 Add teams introduction page 2023-10-03 21:07:53 +01:00
Kamran Ahmed
29cff6a6f8 Update badge 2023-10-02 17:34:51 +01:00
Kamran Ahmed
044df81b7a Creator details on roadmap page 2023-10-02 17:03:20 +01:00
Arik Chakma
3151ee5021 Add Creator Details (#4530)
* Add Creator details

* Add Skeleton Loading
2023-10-02 16:37:30 +01:00
Kamran Ahmed
e6ce9f40ee Update roadmap contribution template 2023-10-02 15:50:42 +01:00
Kamran Ahmed
3b5e3c44f9 Update label for roadmap creation 2023-10-02 15:10:48 +01:00
Kamran Ahmed
c286e0a6f8 Increase max team member count 2023-10-01 03:24:16 +01:00
linxiaowang
3bebe0c1de fix(typo): fix typo in 101-instanceof-operator.md (#4514) 2023-10-01 03:00:55 +06:00
Sherkhan Azimov
9845fe624a separate articles in 107-domain-name-system.md (#4517)
Transfer an article to a new line
2023-10-01 02:59:02 +06:00
Nicky Lim
4b2b2ebe8c Fix typo cpp 104 index (#4520) 2023-09-30 21:40:03 +06:00
Arik Chakma
82c2aaacc3 Fix Roadmap Share Link (#4522) 2023-09-30 14:48:04 +01:00
63 changed files with 1110 additions and 256 deletions

View File

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

View File

@@ -12,7 +12,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v1

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

View File

@@ -9,8 +9,8 @@
<a href="https://roadmap.sh/best-practices">
<img src="https://img.shields.io/badge/%E2%9C%A8-Best%20Practices-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="best practices" />
</a>
<a href="https://youtube.com/theroadmap?sub_confirmation=1">
<img src="https://img.shields.io/badge/%E2%9C%A8-Videos-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="videos" />
<a href="https://roadmap.sh/questions">
<img src="https://img.shields.io/badge/%E2%9C%A8-Questions-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="videos" />
</a>
<a href="https://www.youtube.com/channel/UCA0H2KIWgWTwpTFjSxp0now?sub_confirmation=1">
<img src="https://img.shields.io/badge/%E2%9C%A8-YouTube%20Channel-0a0a0a.svg?style=flat&colorA=0a0a0a" alt="roadmaps" />

View File

@@ -56,6 +56,12 @@ export function GitHubButton(props: GitHubButtonProps) {
}
}
const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) {
localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl;
}
localStorage.removeItem(GITHUB_REDIRECT_AT);
localStorage.removeItem(GITHUB_LAST_PAGE);
Cookies.set(TOKEN_COOKIE_NAME, response.token, {

View File

@@ -55,6 +55,12 @@ export function GoogleButton(props: GoogleButtonProps) {
}
}
const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) {
localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl;
}
localStorage.removeItem(GOOGLE_REDIRECT_AT);
localStorage.removeItem(GOOGLE_LAST_PAGE);
Cookies.set(TOKEN_COOKIE_NAME, response.token, {
@@ -86,10 +92,11 @@ export function GoogleButton(props: GoogleButtonProps) {
// For non authentication pages, we want to redirect back to the page
// the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath =
['/respond-invite', '/befriend'].includes(window.location.pathname)
? window.location.pathname + window.location.search
: window.location.pathname;
const pagePath = ['/respond-invite', '/befriend'].includes(
window.location.pathname
)
? window.location.pathname + window.location.search
: window.location.pathname;
localStorage.setItem(GOOGLE_REDIRECT_AT, Date.now().toString());
localStorage.setItem(GOOGLE_LAST_PAGE, pagePath);

View File

@@ -55,6 +55,12 @@ export function LinkedInButton(props: LinkedInButtonProps) {
}
}
const authRedirectUrl = localStorage.getItem('authRedirect');
if (authRedirectUrl) {
localStorage.removeItem('authRedirect');
redirectUrl = authRedirectUrl;
}
localStorage.removeItem(LINKEDIN_REDIRECT_AT);
localStorage.removeItem(LINKEDIN_LAST_PAGE);
Cookies.set(TOKEN_COOKIE_NAME, response.token, {

View File

@@ -73,7 +73,10 @@ function handleAuthenticated() {
// If the user is on a guest route, redirect them to the home page
if (guestRoutes.includes(window.location.pathname)) {
window.location.href = '/';
const authRedirect = window.localStorage.getItem('authRedirect') || '/';
window.localStorage.removeItem('authRedirect');
window.location.href = authRedirect;
}
}

View File

@@ -40,6 +40,14 @@ const defaultPages: PageType[] = [
icon: GroupIcon.src,
isProtected: true,
},
{
id: 'friends',
url: '/account/friends',
title: 'Friends',
group: 'Pages',
icon: GroupIcon.src,
isProtected: true,
},
{
id: 'roadmaps',
url: '/roadmaps',
@@ -47,6 +55,14 @@ const defaultPages: PageType[] = [
group: 'Pages',
icon: RoadmapIcon.src,
},
{
id: 'account-roadmaps',
url: '/account/roadmaps',
title: 'Custom Roadmaps',
group: 'Pages',
icon: RoadmapIcon.src,
isProtected: true,
},
{
id: 'best-practices',
url: '/best-practices',

View File

@@ -15,6 +15,7 @@ import { useToast } from '../../hooks/use-toast';
export type TeamResourceConfig = {
isCustomResource: boolean;
title: string;
description?: string;
visibility?: AllowedRoadmapVisibility;
resourceId: string;
resourceType: string;

View File

@@ -2,20 +2,17 @@ import { Plus } from 'lucide-react';
import { isLoggedIn } from '../../../lib/jwt';
import { showLoginPopup } from '../../../lib/popup';
import { cn } from '../../../lib/classname';
import {
type AllowedCustomRoadmapType,
type AllowedRoadmapVisibility,
CreateRoadmapModal,
} from './CreateRoadmapModal';
import { CreateRoadmapModal } from './CreateRoadmapModal';
import { useState } from 'react';
type CreateRoadmapButtonProps = {
className?: string;
type?: AllowedCustomRoadmapType;
text?: string;
teamId?: string;
};
export function CreateRoadmapButton(props: CreateRoadmapButtonProps) {
const { className, type } = props;
const { teamId, className, text = 'Create your own Roadmap' } = props;
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
@@ -31,7 +28,7 @@ export function CreateRoadmapButton(props: CreateRoadmapButtonProps) {
<>
{isCreatingRoadmap && (
<CreateRoadmapModal
type={type}
teamId={teamId}
onClose={() => {
setIsCreatingRoadmap(false);
}}
@@ -46,7 +43,7 @@ export function CreateRoadmapButton(props: CreateRoadmapButtonProps) {
onClick={toggleCreateRoadmapHandler}
>
<Plus size={16} />
Create a new roadmap
{text}
</button>
</>
);

View File

@@ -10,7 +10,6 @@ import { Modal } from '../../Modal';
import { useToast } from '../../../hooks/use-toast';
import { httpPost } from '../../../lib/http';
import { cn } from '../../../lib/classname';
import { allowedVisibilityLabels } from '../ShareRoadmapModal';
export const allowedRoadmapVisibility = [
'me',
@@ -46,12 +45,11 @@ interface CreateRoadmapModalProps {
onClose: () => void;
onCreated?: (roadmap: RoadmapDocument) => void;
teamId?: string;
type?: AllowedCustomRoadmapType;
visibility?: AllowedRoadmapVisibility;
}
export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
const { onClose, onCreated, teamId, type: defaultType = 'role' } = props;
const { onClose, onCreated, teamId } = props;
const titleRef = useRef<HTMLInputElement>(null);
const toast = useToast();
@@ -59,7 +57,6 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
const [isLoading, setIsLoading] = useState(false);
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [type, setType] = useState<AllowedCustomRoadmapType>(defaultType);
const isInvalidDescription = description?.trim().length > 80;
async function handleSubmit(
@@ -71,7 +68,7 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
return;
}
if (title.trim() === '' || isInvalidDescription || !type) {
if (title.trim() === '' || isInvalidDescription) {
toast.error('Please fill all the fields');
return;
}
@@ -82,7 +79,6 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
{
title,
description,
type,
...(teamId && {
teamId,
}),
@@ -114,7 +110,6 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
setTitle('');
setDescription('');
setType('role');
setIsLoading(false);
}
@@ -182,33 +177,6 @@ export function CreateRoadmapModal(props: CreateRoadmapModalProps) {
</div>
</div>
<div className="mt-4">
<label
htmlFor="type"
className="block text-xs uppercase text-gray-400"
>
Type
</label>
<div className="mt-1">
<select
id="type"
name="type"
required
className="block w-full rounded-md border border-gray-300 px-2.5 py-2 outline-none focus:border-black sm:text-sm"
value={type}
onChange={(e) =>
setType(e.target.value as AllowedCustomRoadmapType)
}
>
{allowedCustomRoadmapType.map((type) => (
<option key={type} value={type}>
{type.charAt(0).toUpperCase() + type.slice(1)} Based Roadmap
</option>
))}
</select>
</div>
</div>
<div
className={cn('mt-4 flex justify-between gap-2', teamId && 'mt-8')}
>

View File

@@ -40,6 +40,18 @@ export interface RoadmapContentDocument {
}[];
}
export type CreatorType = {
id: string;
name: string;
avatar: string;
};
export type GetRoadmapResponse = RoadmapDocument & {
canManage: boolean;
creator?: CreatorType;
team?: CreatorType;
};
export function hideRoadmapLoader() {
const loaderEl = document.querySelector(
'[data-roadmap-loader]'
@@ -53,7 +65,7 @@ export function CustomRoadmap() {
const { id, secret } = getUrlParams() as { id: string; secret: string };
const [isLoading, setIsLoading] = useState(true);
const [roadmap, setRoadmap] = useState<RoadmapDocument | null>(null);
const [roadmap, setRoadmap] = useState<GetRoadmapResponse | null>(null);
const [error, setError] = useState<AppError | FetchError | undefined>();
async function getRoadmap() {
@@ -66,7 +78,7 @@ export function CustomRoadmap() {
roadmapUrl.searchParams.set('secret', secret);
}
const { response, error } = await httpGet<RoadmapDocument>(
const { response, error } = await httpGet<GetRoadmapResponse>(
roadmapUrl.toString()
);

View File

@@ -1,11 +1,29 @@
import { CircleSlash } from 'lucide-react';
import {CircleSlash, PenSquare, Shapes} from 'lucide-react';
type EmptyRoadmapProps = {
roadmapId: string;
canManage: boolean;
};
export function EmptyRoadmap(props: EmptyRoadmapProps) {
const { roadmapId, canManage } = props;
const editUrl = `${import.meta.env.PUBLIC_EDITOR_APP_URL}/${roadmapId}`;
export function EmptyRoadmap() {
return (
<div className="flex h-full items-center justify-center">
<div className="flex flex-col items-center">
<CircleSlash className="mx-auto h-20 w-20 text-gray-400" />
<h3 className="mt-4">This roadmap is currently empty.</h3>
<h3 className="mt-2">This roadmap is currently empty.</h3>
{canManage && (
<a
href={editUrl}
className="mt-4 rounded-md bg-gray-500 px-4 py-2 font-medium text-white hover:bg-gray-600 flex items-center"
>
<Shapes className="inline-block mr-2 h-4 w-4" />
Edit Roadmap
</a>
)}
</div>
</div>
);

View File

@@ -60,6 +60,7 @@ export function PersonalRoadmapList(props: PersonalRoadmapListType) {
const shareSettingsModal = selectedRoadmap && (
<ShareOptionsModal
description={selectedRoadmap.description}
visibility={selectedRoadmap.visibility}
sharedFriendIds={selectedRoadmap.sharedFriendIds}
sharedTeamMemberIds={selectedRoadmap.sharedTeamMemberIds}

View File

@@ -24,6 +24,7 @@ export function ResourceProgressStats(props: ResourceProgressStatsProps) {
<>
{isSharing && $canManageCurrentRoadmap && $currentRoadmap && (
<ShareOptionsModal
description={$currentRoadmap?.description}
visibility={$currentRoadmap?.visibility}
teamId={$currentRoadmap?.teamId}
roadmapId={$currentRoadmap?._id!}

View File

@@ -8,6 +8,7 @@ import { httpDelete, httpPut } from '../../lib/http';
import { type TeamResourceConfig } from '../CreateTeam/RoadmapSelector';
import { useToast } from '../../hooks/use-toast';
import { RoadmapActionButton } from './RoadmapActionButton';
import { Lock, Shapes } from 'lucide-react';
type RoadmapHeaderProps = {};
@@ -15,7 +16,13 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
const $canManageCurrentRoadmap = useStore(canManageCurrentRoadmap);
const $currentRoadmap = useStore(currentRoadmap);
const { title, description, _id: roadmapId } = useStore(currentRoadmap) || {};
const {
title,
description,
_id: roadmapId,
creator,
team,
} = useStore(currentRoadmap) || {};
const [isSharing, setIsSharing] = useState(false);
const toast = useToast();
@@ -54,10 +61,37 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
}
}
const avatarUrl = creator?.avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${creator?.avatar}`
: '/images/default-avatar.png';
return (
<div className="border-b">
<div className="container relative py-5 sm:py-12">
<div className="mb-3 mt-0 sm:mb-4">
{creator?.name && (
<div className="-mb-1 flex items-center gap-1.5 text-sm text-gray-500">
<img
alt={creator.name}
src={avatarUrl}
className="h-5 w-5 rounded-full"
/>
<span>
Created by&nbsp;
<span className="font-semibold text-gray-900">
{creator?.name}
</span>
{team && (
<>
&nbsp;in&nbsp;
<span className="font-semibold text-gray-900">
{team?.name}
</span>
</>
)}
</span>
</div>
)}
<div className="mb-3 mt-4 sm:mb-4">
<h1 className="text-2xl font-bold sm:mb-2 sm:text-4xl">{title}</h1>
<p className="mt-0.5 text-sm text-gray-500 sm:text-lg">
{description}
@@ -87,6 +121,7 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
<div className="flex items-center gap-2">
{isSharing && $currentRoadmap && (
<ShareOptionsModal
description={$currentRoadmap?.description}
visibility={$currentRoadmap?.visibility}
teamId={$currentRoadmap?.teamId}
roadmapId={$currentRoadmap?._id!}
@@ -104,6 +139,25 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
/>
)}
<a
href={`${import.meta.env.PUBLIC_EDITOR_APP_URL}/${
$currentRoadmap?._id
}`}
target="_blank"
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
>
<Shapes className="mr-1.5 h-4 w-4 stroke-[2.5]" />
<span className="hidden sm:inline-block">Edit Roadmap</span>
<span className="sm:hidden">Edit</span>
</a>
<button
onClick={() => setIsSharing(true)}
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white py-1.5 pl-2 pr-2 text-xs font-medium text-black hover:border-gray-300 hover:bg-gray-300 sm:px-3 sm:text-sm"
>
<Lock className="mr-1.5 h-4 w-4 stroke-[2.5]" />
Sharing
</button>
<RoadmapActionButton
onDelete={() => {
const confirmation = window.confirm(
@@ -116,16 +170,6 @@ export function RoadmapHeader(props: RoadmapHeaderProps) {
deleteResource().finally(() => null);
}}
onCustomize={() => {
const editorLink = `${
import.meta.env.PUBLIC_EDITOR_APP_URL
}/${$currentRoadmap?._id}`;
window.open(editorLink, '_blank');
}}
onUpdateSharing={() => {
setIsSharing(true);
}}
/>
</div>
)}

View File

@@ -86,13 +86,13 @@ export function RoadmapListPage() {
<CreateRoadmapModal onClose={() => setIsCreatingRoadmap(false)} />
)}
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="mb-6 flex flex-col justify-between gap-2 sm:flex-row sm:items-center sm:gap-0">
<div className="flex grow items-center gap-2">
{tabTypes.map((tab) => {
return (
<button
key={tab.value}
className={`relative flex items-center justify-center rounded-md border p-1 px-3 text-sm ${
className={`relative flex w-full items-center justify-center whitespace-nowrap rounded-md border p-1 px-3 text-sm sm:w-auto ${
activeTab === tab.value ? ' border-gray-400 bg-gray-200 ' : ''
} w-full sm:w-auto`}
onClick={() => setActiveTab(tab.value)}

View File

@@ -12,8 +12,6 @@ import { pageProgressMessage } from '../../stores/page';
import { useToast } from '../../hooks/use-toast';
import type { RoadmapDocument } from './CreateRoadmap/CreateRoadmapModal';
import { EmptyRoadmap } from './EmptyRoadmap';
import { isLoggedIn } from '../../lib/jwt';
import { httpPost } from '../../lib/http';
type RoadmapRendererProps = {
roadmap: RoadmapDocument;
@@ -170,7 +168,9 @@ export function RoadmapRenderer(props: RoadmapRendererProps) {
});
}}
/>
{hideRenderer && <EmptyRoadmap />}
{hideRenderer && (
<EmptyRoadmap roadmapId={roadmapId} canManage={roadmap.canManage} />
)}
</div>
</div>
);

View File

@@ -2,14 +2,22 @@ export function SkeletonRoadmapHeader() {
return (
<div className="border-b">
<div className="container relative py-5 sm:py-12">
<div className="mb-3 mt-0 sm:mb-4">
<div className="flex items-center gap-1.5">
<div className="h-4 w-4 animate-pulse rounded-full bg-gray-300" />
<div className="h-5 w-5/12 animate-pulse rounded-md bg-gray-200" />
</div>
<div className="mb-3 mt-4 sm:mb-4">
<div className="h-8 w-1/2 animate-pulse rounded-md bg-gray-300 sm:mb-2 sm:h-10" />
<div className="mt-0.5 h-5 w-1/3 animate-pulse rounded-md bg-gray-200 sm:h-7" />
</div>
<div className="flex justify-between gap-2 sm:gap-0">
<div className="h-7 w-[35.04px] sm:w-32 animate-pulse rounded-md bg-gray-300 sm:h-8" />
<div className="h-7 w-[32px] sm:w-[89.73px] animate-pulse rounded-md bg-gray-300 sm:h-8" />
<div className="h-7 w-[35.04px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-32" />
<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]" />
<div className="h-7 w-[32px] animate-pulse rounded-md bg-gray-300 sm:h-8 sm:w-[89.73px]" />
</div>
</div>
<div className="mb-0 mt-4 rounded-md border-0 sm:-mb-[65px] sm:mt-7 sm:border">

View File

@@ -42,13 +42,7 @@ const {
{
showCreateRoadmap && (
<li>
<CreateRoadmapButton
client:load
className='min-h-[54px]'
type={
heading.toLowerCase().indexOf('role') > -1 ? 'role' : 'skill'
}
/>
<CreateRoadmapButton client:load className='min-h-[54px]' />
</li>
)
}

View File

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

View File

@@ -1,23 +1,30 @@
import { CheckIcon } from '../ReactIcons/CheckIcon';
import { TeamAnnouncement } from '../TeamAnnouncement';
type EmptyProgressProps = {
title?: string;
message?: string;
title?: string;
message?: string;
};
export function EmptyProgress(props: EmptyProgressProps) {
const {
title = 'Start learning ..',
message = 'Your progress and favorite roadmaps will show up here.',
} = props;
const {
title = 'Start learning ..',
message = 'Your progress and favorite roadmaps will show up here.',
} = props;
return (
<div className="relative flex min-h-full flex-col items-start sm:items-center justify-center py-6">
<h2 className={'mb-1 flex items-center text-lg sm:text-2xl text-gray-200'}>
<CheckIcon additionalClasses='mr-2 top-[0.5px] w-[16px] h-[16px] sm:w-[20px] sm:h-[20px]' />
Start learning ..
</h2>
<p className={'text-gray-400 text-sm sm:text-base'}>{message}</p>
</div>
);
return (
<div className="relative flex min-h-full flex-col items-start justify-center py-6 sm:items-center">
<h2
className={'mb-1.5 flex items-center text-lg text-gray-200 sm:text-2xl'}
>
<CheckIcon additionalClasses="mr-2 top-[0.5px] w-[16px] h-[16px] sm:w-[20px] sm:h-[20px]" />
{title}
</h2>
<p className={'text-sm text-gray-400 sm:text-base'}>{message}</p>
<p className="mt-5">
<TeamAnnouncement />
</p>
</div>
);
}

View File

@@ -1,8 +1,9 @@
import { useEffect, useState } from 'react';
import { EmptyProgress } from './EmptyProgress';
import { httpGet } from '../../lib/http';
import { HeroRoadmaps } from './HeroRoadmaps';
import { HeroRoadmaps, type HeroTeamRoadmaps } from './HeroRoadmaps';
import { isLoggedIn } from '../../lib/jwt';
import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx';
export type UserProgressResponse = {
resourceId: string;
@@ -15,6 +16,11 @@ export type UserProgressResponse = {
total: number;
updatedAt: Date;
isCustomResource: boolean;
team?: {
name: string;
id: string;
role: AllowedMemberRoles;
};
}[];
function renderProgress(progressList: UserProgressResponse) {
@@ -114,25 +120,43 @@ export function FavoriteRoadmaps() {
}
const hasProgress = progress?.length > 0;
const customRoadmaps = progress?.filter((p) => p.isCustomResource);
const customRoadmaps = progress?.filter(
(p) => p.isCustomResource && !p.team?.name
);
const defaultRoadmaps = progress?.filter((p) => !p.isCustomResource);
const teamRoadmaps: HeroTeamRoadmaps = progress
?.filter((p) => p.isCustomResource && p.team?.name)
.reduce((acc: HeroTeamRoadmaps, curr) => {
const currTeam = curr.team!;
if (!acc[currTeam.name]) {
acc[currTeam.name] = [];
}
acc[currTeam.name].push(curr);
return acc;
}, {});
return (
<div
className={`flex min-h-[192px] bg-gradient-to-b transition-opacity duration-500 sm:min-h-[280px] opacity-${containerOpacity} ${
hasProgress && `border-t border-t-[#1e293c]`
}`}
className={`transition-opacity duration-500 opacity-${containerOpacity}`}
>
<div className="container min-h-full">
{!isLoading && progress?.length == 0 && <EmptyProgress />}
{hasProgress && (
<HeroRoadmaps
showCustomRoadmaps={true}
customRoadmaps={customRoadmaps}
progress={defaultRoadmaps}
isLoading={isLoading}
/>
)}
<div
className={`flex min-h-[192px] bg-gradient-to-b sm:min-h-[280px] ${
hasProgress && `border-t border-t-[#1e293c]`
}`}
>
<div className="container min-h-full">
{!isLoading && progress?.length == 0 && <EmptyProgress />}
{hasProgress && (
<HeroRoadmaps
teamRoadmaps={teamRoadmaps}
customRoadmaps={customRoadmaps}
progress={defaultRoadmaps}
isLoading={isLoading}
/>
)}
</div>
</div>
</div>
);

View File

@@ -3,10 +3,11 @@ import { CheckIcon } from '../ReactIcons/CheckIcon';
import { MarkFavorite } from '../FeaturedItems/MarkFavorite';
import { Spinner } from '../ReactIcons/Spinner';
import type { ResourceType } from '../../lib/resource-progress';
import { MapIcon } from 'lucide-react';
import { MapIcon, Users2 } from 'lucide-react';
import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { useState } from 'react';
import { type ReactNode, useState } from 'react';
import { TeamAnnouncement } from '../TeamAnnouncement';
type ProgressRoadmapProps = {
url: string;
@@ -55,7 +56,7 @@ function HeroRoadmap(props: ProgressRoadmapProps) {
type ProgressTitleProps = {
icon: any;
isLoading?: boolean;
title: string;
title: string | ReactNode;
};
export function HeroTitle(props: ProgressTitleProps) {
@@ -73,22 +74,39 @@ export function HeroTitle(props: ProgressTitleProps) {
</p>
);
}
export type HeroTeamRoadmaps = Record<string, UserProgressResponse>;
type ProgressListProps = {
progress: UserProgressResponse;
customRoadmaps: UserProgressResponse;
teamRoadmaps?: HeroTeamRoadmaps;
isLoading?: boolean;
};
export function HeroRoadmaps(props: ProgressListProps) {
const { progress, isLoading = false, customRoadmaps } = props;
const {
teamRoadmaps = {},
progress,
isLoading = false,
customRoadmaps,
} = props;
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false);
const [creatingRoadmapTeamId, setCreatingRoadmapTeamId] = useState<string>();
return (
<div className="relative pb-12 pt-4 sm:pt-7">
<p className="mb-7 mt-2 text-sm">
<TeamAnnouncement />
</p>
{isCreatingRoadmap && (
<CreateRoadmapModal onClose={() => setIsCreatingRoadmap(false)} />
<CreateRoadmapModal
teamId={creatingRoadmapTeamId}
onClose={() => {
setIsCreatingRoadmap(false);
setCreatingRoadmapTeamId(undefined);
}}
/>
)}
{
<HeroTitle
@@ -164,6 +182,83 @@ export function HeroRoadmaps(props: ProgressListProps) {
</div>
)}
</div>
{Object.keys(teamRoadmaps).map((teamName) => {
const currentTeam: UserProgressResponse[0]['team'] =
teamRoadmaps?.[teamName]?.[0]?.team;
const roadmapsList = teamRoadmaps[teamName].filter(
(roadmap) => !!roadmap.resourceTitle
);
const canManageTeam = ['admin', 'manager'].includes(currentTeam?.role!);
return (
<div className="mt-5" key={teamName}>
{
<HeroTitle
icon={<Users2 className="mr-1.5 h-[14px] w-[14px]" />}
title={
<>
Team{' '}
<a
className="mx-1 font-medium underline underline-offset-2 transition-colors hover:text-gray-300"
href={`/team/progress?t=${currentTeam?.id}`}
>
{teamName}
</a>
Roadmaps
</>
}
/>
}
{roadmapsList.length === 0 && (
<p className="rounded-md border border-dashed border-gray-800 p-2 text-sm text-gray-600">
Team does not have any roadmaps yet.{' '}
{canManageTeam && (
<button
className="text-gray-500 underline underline-offset-2 hover:text-gray-400"
onClick={() => {
setCreatingRoadmapTeamId(currentTeam?.id);
setIsCreatingRoadmap(true);
}}
>
Create one!
</button>
)}
</p>
)}
{roadmapsList.length > 0 && (
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
{roadmapsList.map((customRoadmap) => {
return (
<HeroRoadmap
key={customRoadmap.resourceId}
resourceId={customRoadmap.resourceId}
resourceType={'roadmap'}
resourceTitle={customRoadmap.resourceTitle}
percentageDone={
((customRoadmap.skipped + customRoadmap.done) /
customRoadmap.total) *
100
}
url={`/r?id=${customRoadmap.resourceId}`}
allowFavorite={false}
/>
);
})}
{canManageTeam && (
<CreateRoadmapButton
teamId={currentTeam?.id}
text="Create Team Roadmap"
/>
)}
</div>
)}
</div>
);
})}
</div>
);
}

View File

@@ -1,12 +1,19 @@
---
import { FavoriteRoadmaps } from './FavoriteRoadmaps';
import {TeamAnnouncement} from "../TeamAnnouncement";
---
<div class='relative min-h-auto min-h-[192px] sm:min-h-[281px] border-b border-b-[#1e293c]'>
<div
class='min-h-auto relative min-h-[192px] border-b border-b-[#1e293c] sm:min-h-[281px] transition-all'
>
<div
class='container px-6 py-6 pb-14 text-left sm:px-0 sm:py-20 sm:text-center transition-opacity duration-300'
class='container px-5 py-6 pb-14 text-left transition-opacity duration-300 sm:px-0 sm:py-20 sm:text-center'
id='hero-text'
>
<p class='-mt-4 sm:-mt-10 mb-7'>
<TeamAnnouncement />
</p>
<h1
class='mb-2 bg-gradient-to-b from-amber-50 to-purple-500 bg-clip-text text-2xl font-bold text-transparent sm:mb-4 sm:text-5xl'
>
@@ -24,5 +31,5 @@ import { FavoriteRoadmaps } from './FavoriteRoadmaps';
their career.
</p>
</div>
<FavoriteRoadmaps client:only="react" />
<FavoriteRoadmaps client:only='react' />
</div>

View File

@@ -26,6 +26,14 @@ export function AccountDropdownList(props: AccountDropdownListProps) {
Friends
</a>
</li>
<li className="px-1">
<a
href="/account/roadmaps"
className="block rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"
>
Roadmaps
</a>
</li>
<li className="px-1">
<button
className="group flex w-full items-center justify-between rounded pl-4 pr-2 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700"

View File

@@ -2,7 +2,6 @@
import Icon from '../AstroIcon.astro';
import { AccountDropdown } from './AccountDropdown';
---
<div class='bg-slate-900 py-5 text-white sm:py-8'>
<nav class='container flex items-center justify-between'>
<a
@@ -27,8 +26,25 @@ import { AccountDropdown } from './AccountDropdown';
<a href='/questions' class='text-gray-400 hover:text-white'>Questions</a
>
</li>
<li class='hidden lg:inline'>
<a href='/guides' class='text-gray-400 hover:text-white'>Guides</a>
<li>
<a href='/teams' class='group relative text-blue-300 hover:text-white'>
Teams
<span
class='ml-0.5 hidden rounded-sm border-black bg-blue-300 px-1 py-0.5 text-xs font-semibold uppercase text-black group-hover:bg-white md:inline'
>
New
</span>
<span class='inline md:hidden absolute -right-[11px] top-0'>
<span class='relative flex h-2 w-2'>
<span
class='absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75'
></span>
<span class='relative inline-flex h-2 w-2 rounded-full bg-sky-500'
></span>
</span>
</span>
</a>
</li>
<li>
<kbd
@@ -45,7 +61,7 @@ import { AccountDropdown } from './AccountDropdown';
<a href='/login' class='text-gray-400 hover:text-white'>Login</a>
</li>
<li>
<AccountDropdown client:only="react" />
<AccountDropdown client:only='react' />
<a
data-guest-required

View File

@@ -1,64 +0,0 @@
import { CheckCircle, Copy } from 'lucide-react';
import { useCopyText } from '../../hooks/use-copy-text';
import { cn } from '../../lib/classname';
type CopyRoadmapLinkProps = {
roadmapId: string;
onClose: () => void;
};
export function CopyRoadmapLink(props: CopyRoadmapLinkProps) {
const { roadmapId, onClose } = props;
const shareLink = `${
import.meta.env.PUBLIC_ROADMAP_WEB_URL
}/r?id=${roadmapId}`;
const { copyText, isCopied } = useCopyText();
return (
<div className="flex grow flex-col justify-center">
<div className="mt-5 flex grow flex-col items-center justify-center gap-1.5">
<CheckCircle className="h-14 w-14 text-green-500" />
<h3 className="text-xl font-medium">Sharing Settings Updated</h3>
</div>
<input
type="text"
className="mt-6 w-full rounded-md border bg-gray-50 p-2 px-2.5 text-gray-700 focus:outline-none"
value={shareLink}
readOnly
onClick={(e) => {
e.currentTarget.select();
copyText(shareLink);
}}
/>
<p className="mt-1 text-sm text-gray-400">
You can share the above link with anyone who has access
</p>
<div className="mt-4 flex flex-col items-center justify-end gap-2">
<button
className={cn(
'flex w-full items-center justify-center gap-1.5 rounded bg-black px-4 py-2.5 text-sm font-medium text-white hover:opacity-80',
isCopied && 'bg-green-300 text-green-800'
)}
disabled={isCopied}
onClick={() => {
copyText(shareLink);
}}
>
<Copy className="h-3.5 w-3.5 stroke-[2.5]" />
{isCopied ? 'Copied' : 'Copy'}
</button>
<button
className={cn(
'flex w-full items-center justify-center gap-1.5 rounded border border-black px-4 py-2 text-sm font-medium hover:bg-gray-100'
)}
onClick={onClose}
>
Close
</button>
</div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { type ReactNode, useCallback, useState } from 'react';
import { type ReactNode, useCallback, useState, useMemo } from 'react';
import { Globe2, Loader2, Lock } from 'lucide-react';
import { type ListFriendsResponse, ShareFriendList } from './ShareFriendList';
import { TransferToTeamList } from './TransferToTeamList';
@@ -7,7 +7,7 @@ import {
ShareTeamMemberList,
type TeamMemberList,
} from './ShareTeamMemberList';
import { CopyRoadmapLink } from './CopyRoadmapLink';
import { ShareSuccess } from './ShareSuccess';
import { useToast } from '../../hooks/use-toast';
import type { AllowedRoadmapVisibility } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { httpPatch } from '../../lib/http';
@@ -28,6 +28,7 @@ type ShareOptionsModalProps = {
sharedTeamMemberIds?: string[];
teamId?: string;
roadmapId?: string;
description?: string;
onShareSettingsUpdate: OnShareSettingsUpdate;
};
@@ -41,6 +42,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
sharedFriendIds: defaultSharedFriendIds = [],
teamId,
onShareSettingsUpdate,
description,
} = props;
const toast = useToast();
@@ -49,9 +51,13 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
const [isSettingsUpdated, setIsSettingsUpdated] = useState(false);
const [friends, setFriends] = useState<ListFriendsResponse>([]);
const [teams, setTeams] = useState<UserTeamItem[]>([]);
const [members, setMembers] = useState<TeamMemberList[]>([]);
// Using global team members loading state to avoid glitchy UI when switching between teams
const [isTeamMembersLoading, setIsTeamMembersLoading] = useState(false);
const membersCache = useMemo(() => new Map<string, TeamMemberList[]>(), []);
const [visibility, setVisibility] = useState(defaultVisibility);
const [isDiscoverable, setIsDiscoverable] = useState(false);
const [sharedTeamMemberIds, setSharedTeamMemberIds] = useState<string[]>(
defaultSharedMemberIds
);
@@ -104,6 +110,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
visibility,
sharedFriendIds,
sharedTeamMemberIds,
isDiscoverable,
}
);
@@ -118,7 +125,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
};
const handleTransferToTeam = useCallback(
async (teamId: string) => {
async (teamId: string, sharedTeamMemberIds: string[]) => {
if (!roadmapId) {
return;
}
@@ -128,6 +135,8 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
`${import.meta.env.PUBLIC_API_URL}/v1-transfer-roadmap/${roadmapId}`,
{
teamId,
sharedTeamMemberIds,
isDiscoverable,
}
);
@@ -149,7 +158,12 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
wrapperClassName="max-w-lg"
bodyClassName="p-4 flex flex-col"
>
<CopyRoadmapLink roadmapId={roadmapId!} onClose={onClose} />
<ShareSuccess
visibility={visibility}
roadmapId={roadmapId!}
description={description}
onClose={onClose}
/>
</Modal>
);
}
@@ -163,7 +177,7 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
onClose();
}}
wrapperClassName="max-w-3xl"
bodyClassName="p-4 flex flex-col min-h-[400px]"
bodyClassName="p-4 flex flex-col min-h-[440px]"
>
<div className="mb-4">
<h3 className="mb-1 text-xl font-semibold">Update Sharing Settings</h3>
@@ -225,14 +239,6 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
setSharedFriendIds={setSharedFriendIds}
/>
)}
{canTransferRoadmap && (
<TransferToTeamList
teams={teams}
setTeams={setTeams}
selectedTeamId={selectedTeamId}
setSelectedTeamId={setSelectedTeamId}
/>
)}
{/* For Team Roadmap */}
{visibility === 'team' && teamId && (
@@ -240,12 +246,57 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
teamId={teamId}
sharedTeamMemberIds={sharedTeamMemberIds}
setSharedTeamMemberIds={setSharedTeamMemberIds}
members={members}
setMembers={setMembers}
membersCache={membersCache}
isTeamMembersLoading={isTeamMembersLoading}
setIsTeamMembersLoading={setIsTeamMembersLoading}
/>
)}
{canTransferRoadmap && (
<>
<TransferToTeamList
teams={teams}
setTeams={setTeams}
selectedTeamId={selectedTeamId}
setSelectedTeamId={setSelectedTeamId}
isTeamMembersLoading={isTeamMembersLoading}
setIsTeamMembersLoading={setIsTeamMembersLoading}
onTeamChange={() => {
setSharedTeamMemberIds([]);
}}
/>
{selectedTeamId && (
<>
<hr className="-mx-4 my-4" />
<div className="mb-4">
<ShareTeamMemberList
title="Select who can access this roadmap. You can change this later."
teamId={selectedTeamId!}
sharedTeamMemberIds={sharedTeamMemberIds}
setSharedTeamMemberIds={setSharedTeamMemberIds}
membersCache={membersCache}
isTeamMembersLoading={isTeamMembersLoading}
setIsTeamMembersLoading={setIsTeamMembersLoading}
/>
</div>
</>
)}
</>
)}
</div>
{visibility !== 'me' && (
<>
<hr className="-mx-4 my-4" />
<div className="mb-2">
<DiscoveryCheckbox
isDiscoverable={isDiscoverable}
setIsDiscoverable={setIsDiscoverable}
/>
</div>
</>
)}
<div className="mt-2 flex items-center justify-between gap-1.5">
<button
className="flex items-center justify-center gap-1.5 rounded-md border px-3.5 py-1.5 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-75"
@@ -255,17 +306,23 @@ export function ShareOptionsModal(props: ShareOptionsModalProps) {
Close
</button>
{canTransferRoadmap ? (
{canTransferRoadmap && (
<UpdateAction
disabled={isUpdateDisabled || isLoading}
disabled={
isUpdateDisabled || isLoading || sharedTeamMemberIds.length === 0
}
onClick={() => {
handleTransferToTeam(selectedTeamId!).then(() => null);
handleTransferToTeam(selectedTeamId!, sharedTeamMemberIds).then(
() => null
);
}}
>
{isLoading && <Loader2 className="h-4 w-4 animate-spin" />}
Transfer
</UpdateAction>
) : (
)}
{!canTransferRoadmap && (
<UpdateAction
disabled={isUpdateDisabled || isLoading}
onClick={() => {
@@ -309,3 +366,25 @@ function UpdateAction(props: {
</button>
);
}
type DiscoveryCheckboxProps = {
isDiscoverable: boolean;
setIsDiscoverable: (isDiscoverable: boolean) => void;
};
function DiscoveryCheckbox(props: DiscoveryCheckboxProps) {
const { isDiscoverable, setIsDiscoverable } = props;
return (
<label className="group flex items-center gap-1.5">
<input
type="checkbox"
checked={isDiscoverable}
onChange={(e) => setIsDiscoverable(e.target.checked)}
/>
<span className="text-sm text-gray-500 group-hover:text-gray-700">
Include on discovery page (when launched)
</span>
</label>
);
}

View File

@@ -0,0 +1,115 @@
import { CheckCircle, Copy, Facebook, Linkedin, Twitter } from 'lucide-react';
import { useCopyText } from '../../hooks/use-copy-text';
import { cn } from '../../lib/classname';
import type { AllowedRoadmapVisibility } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
type ShareSuccessProps = {
roadmapId: string;
onClose: () => void;
visibility: AllowedRoadmapVisibility;
description?: string;
};
export function ShareSuccess(props: ShareSuccessProps) {
const { roadmapId, onClose, description, visibility } = props;
const baseUrl = import.meta.env.DEV
? 'http://localhost:3000'
: 'https://roadmap.sh';
const shareLink = `${baseUrl}/r?id=${roadmapId}`;
const { copyText, isCopied } = useCopyText();
const socialShareLinks = [
{
title: 'Twitter',
href: `https://twitter.com/intent/tweet?text=${description}&url=${shareLink}`,
icon: Twitter,
},
{
title: 'Facebook',
href: `https://www.facebook.com/sharer/sharer.php?quote=${description}&u=${shareLink}`,
icon: Facebook,
},
{
title: 'Linkedin',
href: `https://www.linkedin.com/sharing/share-offsite/?url=${shareLink}`,
icon: Linkedin,
},
];
return (
<div className="flex grow flex-col justify-center">
<div className="mt-5 flex grow flex-col items-center justify-center gap-1.5">
<CheckCircle className="h-14 w-14 text-green-500" />
<h3 className="text-xl font-medium">Sharing Settings Updated</h3>
</div>
<input
type="text"
className="mt-6 w-full rounded-md border bg-gray-50 p-2 px-2.5 text-gray-700 focus:outline-none"
value={shareLink}
readOnly
onClick={(e) => {
e.currentTarget.select();
copyText(shareLink);
}}
/>
<p className="mt-1 text-sm text-gray-400">
You can share the above link with anyone who has access
</p>
{visibility === 'public' && (
<>
<div className="-mx-4 mt-4 flex items-center gap-1.5">
<span className="h-px grow bg-gray-300" />
<span className="text-sm uppercase text-gray-600">Or</span>
<span className="h-px grow bg-gray-300" />
</div>
<div className="mt-4 flex items-center gap-2">
<span className="text-sm text-gray-600">Share on</span>
<ul className="flex items-center gap-1.5">
{socialShareLinks.map((socialShareLink) => (
<li key={socialShareLink.title}>
<a
href={socialShareLink.href}
target="_blank"
rel="noopener noreferrer"
className="flex h-8 w-8 items-center justify-center gap-1.5 rounded-md border bg-gray-50 text-sm text-gray-700 hover:bg-gray-100 focus:outline-none"
>
<socialShareLink.icon className="h-4 w-4" />
</a>
</li>
))}
</ul>
</div>
</>
)}
<div className="mt-4 flex flex-col items-center justify-end gap-2">
<button
className={cn(
'flex w-full items-center justify-center gap-1.5 rounded bg-black px-4 py-2.5 text-sm font-medium text-white hover:opacity-80',
isCopied && 'bg-green-300 text-green-800'
)}
disabled={isCopied}
onClick={() => {
copyText(shareLink);
}}
>
<Copy className="h-3.5 w-3.5 stroke-[2.5]" />
{isCopied ? 'Copied' : 'Copy'}
</button>
<button
className={cn(
'flex w-full items-center justify-center gap-1.5 rounded border border-black px-4 py-2 text-sm font-medium hover:bg-gray-100'
)}
onClick={onClose}
>
Close
</button>
</div>
</div>
);
}

View File

@@ -33,27 +33,31 @@ export interface TeamMemberList extends TeamMemberDocument {
type ShareTeamMemberListProps = {
teamId: string;
setMembers: (members: TeamMemberList[]) => void;
members: TeamMemberList[];
title?: string;
sharedTeamMemberIds: string[];
setSharedTeamMemberIds: (sharedTeamMemberIds: string[]) => void;
membersCache: Map<string, TeamMemberList[]>;
isTeamMembersLoading: boolean;
setIsTeamMembersLoading: (isLoading: boolean) => void;
};
export function ShareTeamMemberList(props: ShareTeamMemberListProps) {
const {
setMembers,
members,
teamId,
title = 'Select Members',
sharedTeamMemberIds,
setSharedTeamMemberIds,
teamId,
membersCache,
isTeamMembersLoading: isLoading,
setIsTeamMembersLoading: setIsLoading,
} = props;
const toast = useToast();
const [isLoading, setIsLoading] = useState(true);
async function loadTeamMembers() {
if (members?.length > 0) {
if (membersCache.has(teamId)) {
return;
}
@@ -67,21 +71,23 @@ export function ShareTeamMemberList(props: ShareTeamMemberListProps) {
return;
}
setMembers(response);
const joinedMembers =
response?.filter((member) => member.status === 'joined') || [];
membersCache.set(teamId, joinedMembers);
}
useEffect(() => {
loadTeamMembers().finally(() => {
setIsLoading(false);
});
}, []);
}, [teamId]);
const loadingMembers = isLoading && (
<ul className="mt-2 grid grid-cols-3 gap-2.5">
{[...Array(3)].map((_, idx) => (
<li
key={idx}
className="flex min-h-[62px] animate-pulse items-center gap-2 rounded-md border p-2"
className="flex min-h-[66px] animate-pulse items-center gap-2 rounded-md border p-2"
>
<div className="h-8 w-8 shrink-0 rounded-full bg-gray-200" />
<div className="inline-grid w-full">
@@ -93,11 +99,13 @@ export function ShareTeamMemberList(props: ShareTeamMemberListProps) {
</ul>
);
const members = membersCache.get(teamId) || [];
return (
<>
{(members.length > 0 || isLoading) && (
<div className="flex items-center justify-between gap-2">
<p className="text-sm">Select Members</p>
<p className="text-sm">{title}</p>
<label className="flex items-center gap-2">
<input

View File

@@ -11,10 +11,22 @@ type TransferToTeamListProps = {
selectedTeamId: string | null;
setSelectedTeamId: (teamId: string | null) => void;
isTeamMembersLoading: boolean;
setIsTeamMembersLoading: (isLoading: boolean) => void;
onTeamChange: (teamId: string | null) => void;
};
export function TransferToTeamList(props: TransferToTeamListProps) {
const { teams, setTeams, selectedTeamId, setSelectedTeamId } = props;
const {
teams,
setTeams,
selectedTeamId,
setSelectedTeamId,
isTeamMembersLoading,
setIsTeamMembersLoading,
onTeamChange,
} = props;
const toast = useToast();
@@ -73,11 +85,17 @@ export function TransferToTeamList(props: TransferToTeamListProps) {
<li key={team._id}>
<button
className={cn(
'relative flex w-full items-center gap-2.5 rounded-lg border p-2.5',
'relative flex w-full items-center gap-2.5 rounded-lg border p-2.5 disabled:cursor-not-allowed disabled:opacity-70',
isSelected && 'border-gray-500 bg-gray-100 text-black'
)}
disabled={isTeamMembersLoading}
onClick={() => {
setSelectedTeamId(team._id);
if (isSelected) {
setSelectedTeamId(null);
} else {
setSelectedTeamId(team._id);
}
onTeamChange(team._id);
}}
>
<img

View File

@@ -0,0 +1,16 @@
type TeamAnnouncementProps = {};
export function TeamAnnouncement(props: TeamAnnouncementProps) {
return (
<a
className="rounded-md border border-dashed border-purple-700 px-3 py-1.5 text-purple-400 transition-colors hover:border-gray-700 hover:text-white"
href="/teams"
>
<span className="relative -top-[0.5px] mr-1 text-xs font-semibold uppercase text-white">
New
</span>{' '}
<span className={'hidden sm:inline'}>Announcing roadmaps for teams. <span className='font-semibold'>Learn more!</span></span>
<span className={'inline sm:hidden'}>Announcing roadmaps for teams!</span>
</a>
);
}

View File

@@ -0,0 +1,164 @@
import { useEffect, useState } from 'react';
import { cn } from '../../lib/classname.ts';
import { isLoggedIn } from '../../lib/jwt.ts';
import { fireTeamCreationClick } from './TeamHeroBanner.tsx';
const demoItems = [
{
title: 'Roadmap Editor',
description:
'<span class="font-semibold">Powerful editor</span> to create custom roadmaps and other trackable documents',
image: '/images/team-promo/roadmap-editor.png',
},
{
title: 'Invite Members',
description:
'Invite your <span class="font-semibold">team members and assign roles</span>',
image: '/images/team-promo/invite-members.png',
},
{
title: 'Track Progress',
description:
'You and your team can <span class="font-semibold">track progress</span> on the roadmaps',
image: '/images/team-promo/update-progress.png',
},
{
title: 'Team Dashboard',
description:
'Keep an eye on the team progress through <span class="font-semibold">team dashboards</span>',
image: '/images/team-promo/team-dashboard.png',
},
{
title: 'Roadmaps and Documents',
description:
'Create as many <span class="font-semibold">roadmaps or trackable documents</span> as you want',
image: '/images/team-promo/many-roadmaps.png',
},
{
title: 'Community Roadmaps',
description:
'Create custom roadmaps or customize <span class="font-semibold">community roadmaps</span> to fit your needs',
image: '/images/team-promo/our-roadmaps.png',
},
{
title: 'Sharing Settings',
description:
'Share a roadmap or trackable document with <span class="font-semibold">everyone or specific people</span>',
image: '/images/team-promo/sharing-settings.png',
},
{
title: 'More Coming Soon!',
description:
'<span class="font-semibold">We have a lot more coming soon!</span>',
},
];
export function TeamDemo() {
const [hasViewed, setHasViewed] = useState<number[]>([0]);
const [activeItem, setActiveItem] = useState(demoItems[0]);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>();
useEffect(() => {
setIsAuthenticated(isLoggedIn());
}, []);
return (
<div className="hidden border-t py-12 sm:block">
<div className="container">
<h2 className="mb-2 text-3xl font-bold">See how it works</h2>
<p>Here is a sneak peek of what you can do today (more coming soon!)</p>
<div className="relative mt-7 flex flex-row items-center gap-2.5">
{demoItems.map((item, counter) => {
const isActive = item === activeItem;
const hasAlreadyViewed = hasViewed.includes(counter);
if (!isActive) {
return (
<span key={item.title} className="relative flex items-center">
<span
onClick={() => {
setHasViewed([...hasViewed, counter]);
setActiveItem(item);
}}
className={cn('z-50 cursor-pointer rounded-full p-[6px]', {
'bg-black': item === activeItem,
'bg-gray-300 hover:bg-gray-400': item !== activeItem,
})}
/>
{!hasAlreadyViewed && (
<span className="pointer-events-none absolute inline-flex h-full w-full animate-ping rounded-full bg-gray-400 opacity-75"></span>
)}
</span>
);
}
return (
<span
key={item.title}
className=" rounded-full bg-black px-3 py-0.5 text-sm text-white"
>
{activeItem.title}
</span>
);
})}
</div>
<div className="mt-4 overflow-hidden rounded-xl border border-gray-300">
<div className="p-3">
<p
className="text-base text-black"
dangerouslySetInnerHTML={{ __html: activeItem.description }}
/>
</div>
{activeItem.image && (
<img
className="rounded-b-xl border-t"
src={activeItem.image}
alt=""
/>
)}
{!activeItem.image && (
<div className="bg-gray-50 py-4 pl-3">
<p className="mb-3">
Register your team now and help us shape the future of teams in
roadmap.sh!
</p>
<div className="flex flex-row items-center gap-2">
<a
onClick={() => {
fireTeamCreationClick();
if (isAuthenticated) {
return;
}
localStorage.setItem('authRedirect', '/team/new');
}}
href={isAuthenticated ? '/team/new' : '/signup'}
className="inline-flex items-center justify-center rounded-lg border border-transparent bg-purple-600 px-5 py-2 text-base font-medium text-white hover:bg-purple-700"
>
Create your Team
</a>
{!isAuthenticated && (
<span className="ml-1 text-base">
or &nbsp;
<a
onClick={() => {
fireTeamCreationClick();
localStorage.setItem('authRedirect', '/team/new');
}}
href="/login"
className="text-purple-600 underline hover:text-purple-700"
>
Login to your account
</a>
</span>
)}
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,96 @@
import { CheckCircle, CheckCircle2, CheckIcon } from 'lucide-react';
import { isLoggedIn } from '../../lib/jwt.ts';
import { useEffect, useState } from 'react';
const featureList = [
'Create custom roadmaps for your team',
"Plan, track and document your team's skills and growth",
'Invite your team members',
"Get insights on your team's skills and growth",
];
export function fireTeamCreationClick() {
window.fireEvent({
category: 'FeatureClick',
action: `Pages / Teams`,
label: 'Create your Team',
});
}
export function TeamHeroBanner() {
const [isAuthenticated, setIsAuthenticated] = useState<boolean>();
useEffect(() => {
setIsAuthenticated(isLoggedIn());
}, []);
return (
<div className="bg-white py-8 lg:py-12">
<div className="container">
<div className="flex flex-row items-center justify-start text-left lg:justify-between">
<div className="flex flex-grow flex-col">
<h1 className="mb-0.5 text-2xl font-bold sm:mb-2.5 sm:text-4xl lg:mb-4 lg:text-5xl">
Roadmaps for Teams
</h1>
<p className="mb-4 text-base leading-normal text-gray-600 sm:mb-0 sm:leading-none lg:text-lg">
Train, plan and track your team's skills and career growth.
</p>
<ul className="mb-4 mt-0 hidden text-sm leading-7 sm:mb-4 sm:mt-4 sm:flex sm:flex-col lg:mb-6 lg:mt-6 lg:text-base lg:leading-8">
{featureList.map((feature, index) => (
<li key={feature}>
<CheckCircle className="hidden h-6 w-6 text-green-500 sm:inline-block" />
<span className="ml-0 sm:ml-2">{feature}</span>
</li>
))}
</ul>
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center">
<a
onClick={() => {
fireTeamCreationClick();
}}
href={isAuthenticated ? '/team/new' : '/signup'}
className="flex w-full items-center justify-center rounded-lg border border-transparent bg-purple-600 px-5 py-2 text-sm font-medium text-white hover:bg-blue-700 sm:w-auto sm:text-base"
>
Create your Team
</a>
{!isAuthenticated && (
<>
<span className="ml-1 hidden text-base sm:inline">
or &nbsp;
<a
href="/login"
onClick={() => {
fireTeamCreationClick();
localStorage.setItem('authRedirect', '/team/new');
}}
className="text-purple-600 underline hover:text-purple-700"
>
Login to your account
</a>
</span>
<a
href="/login"
onClick={() => {
fireTeamCreationClick();
localStorage.setItem('authRedirect', '/team/new');
}}
className="flex w-full items-center justify-center rounded-lg border border-purple-600 px-5 py-2 text-base text-sm font-medium text-purple-600 hover:bg-blue-700 sm:hidden sm:text-base"
>
Login to your account
</a>
</>
)}
</div>
</div>
<img
alt={'team roadmaps'}
className="hidden h-64 md:block lg:h-80"
src="/images/team-promo/hero-img.png"
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,106 @@
import { Check, CheckCircle, Copy, Sparkles } from 'lucide-react';
import { useCopyText } from '../../hooks/use-copy-text.ts';
import { cn } from '../../lib/classname.ts';
import { isLoggedIn } from '../../lib/jwt.ts';
import { fireTeamCreationClick } from './TeamHeroBanner.tsx';
import { useEffect, useState } from 'react';
export function TeamPricing() {
const { isCopied, copyText } = useCopyText();
const teamEmail = 'teams@roadmap.sh';
const [isAuthenticated, setIsAuthenticated] = useState<boolean>();
useEffect(() => {
setIsAuthenticated(isLoggedIn());
}, []);
return (
<div className="border-t py-4 sm:py-8 md:py-12">
<div className="container">
<h2 className="mb-1 text-xl font-bold sm:mb-1.5 sm:text-2xl md:mb-2 md:text-3xl">
Beta Pricing
</h2>
<p className="mb-4 text-base text-gray-600 sm:mb-8 sm:text-lg">
We are currently in public beta and are offering free access to all
features.
</p>
<div className="flex flex-col gap-6 sm:flex-row sm:gap-4">
<div className="relative flex flex-col items-center justify-center gap-2 overflow-hidden rounded-xl border border-purple-500">
<div className="px-8 pb-2 pt-5 text-center sm:pt-4">
<h3 className="mb-1 text-2xl font-bold">Free</h3>
<p className="text-sm text-gray-500">No credit card required</p>
<p className="flex items-start justify-center gap-1 py-6 text-3xl">
<span className="text-base text-gray-600">$</span>
<span className="text-5xl font-bold">0</span>
</p>
<a
onClick={() => {
fireTeamCreationClick();
if (isAuthenticated) {
return;
}
localStorage.setItem('redirect', '/team/new');
}}
href={isAuthenticated ? '/team/new' : '/signup'}
className="block rounded-md bg-purple-600 px-6 py-2 text-center text-sm font-medium leading-6 text-white shadow transition hover:bg-gray-700 hover:shadow-lg focus:outline-none"
>
{isAuthenticated ? 'Create your Team' : 'Sign up for free'}
</a>
</div>
<div className="flex w-full flex-col gap-1 border-t px-8 py-5 text-center sm:py-3">
<p className="font-semibold text-black">All the features</p>
<p className="text-gray-600">Roles and Permissions</p>
<p className="text-gray-600">Custom Roadmaps</p>
<p className="text-gray-600">Sharing Options</p>
<p className="text-gray-600">Progress Tracking</p>
<p className="text-gray-600">Team Insights</p>
<p className="text-gray-600">Onboarding support</p>
</div>
</div>
<div className="flex flex-grow flex-col items-center justify-center rounded-md border border-gray-300 py-8">
<img
alt={'waving hand'}
src={'/images/team-promo/contact.png'}
className="mb-3 h-40"
/>
<p className="mb-2 font-medium text-gray-500">
Questions? We are here to help!
</p>
<p className="text-gray-600">
<button
onClick={() => {
copyText(teamEmail);
}}
className={cn(
'relative flex items-center justify-between gap-3 overflow-hidden rounded-md border border-black bg-white px-4 py-2 text-black hover:bg-gray-100'
)}
>
{teamEmail}
<Copy
className="relative top-[1px] ml-2 inline-block text-black transition-opacity"
size={16}
/>
<span
className={cn(
'absolute bottom-0 left-0 right-0 flex items-center justify-center bg-black text-white transition-all',
{
'top-full': !isCopied,
'top-0': isCopied,
}
)}
>
Email copied!
</span>
</button>
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,61 @@
const toolsList = [
{
imageUrl: '/images/team-promo/growth-plans.png',
title: 'Growth plans',
description: 'Prepare shared or individual growth plans for members.',
},
{
imageUrl: '/images/team-promo/progress-tracking.png',
title: 'Progress tracking',
description: 'Track the and compare the progress of team members.',
},
{
imageUrl: '/images/team-promo/onboarding.png',
title: 'Onboarding',
description: 'Prepare onboarding plans for new team members.',
},
{
imageUrl: '/images/team-promo/team-insights.png',
title: 'Team insights',
description: 'Get insights about your team skills, progress and more.',
},
{
imageUrl: '/images/team-promo/skill-gap.png',
title: 'Skill gap analysis',
description: 'Understand the skills of your team and identify gaps.',
},
{
imageUrl: '/images/team-promo/documentation.png',
title: 'Documentation',
description: 'Create and share visual team documentation.',
},
];
export function TeamTools() {
return (
<div className="py-4 sm:py-8 md:py-12 border-t">
<div className="container">
<h2 className="mb-1 sm:mb-1.5 md:mb-2 text-xl sm:text-2xl md:text-3xl font-bold">Track and guide your teams knowledge</h2>
<p className='text-sm md:text-base'>
Individual and team level growth plans, progress tracking, skill gap analysis, team insights and more.
</p>
<div className="mt-3 sm:mt-5 md:mt-8 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 sm:gap-4">
{toolsList.map((tool) => {
return (
<div className="rounded-md sm:rounded-xl border p-2 sm:p-5 text-left sm:text-center md:text-left">
<img
alt={tool.title}
src={tool.imageUrl}
className="mb-5 h-48 hidden sm:block mx-auto md:mx-0"
/>
<h3 className="mb-0.5 sm:mb-2 text-lg sm:text-2xl font-bold">{tool.title}</h3>
<p className='text-sm sm:text-base'>{tool.description}</p>
</div>
);
})}
</div>
</div>
</div>
);
}

View File

@@ -44,6 +44,8 @@ export interface TeamMemberItem extends TeamMemberDocument {
hasProgress: boolean;
}
const MAX_MEMBER_COUNT = 100;
export function TeamMembersPage() {
const { t: teamId } = getUrlParams();
@@ -307,7 +309,7 @@ export function TeamMembersPage() {
{canManageCurrentTeam && (
<div className="mt-4">
<button
disabled={teamMembers.length >= 25}
disabled={teamMembers.length >= MAX_MEMBER_COUNT}
onClick={() => setIsInvitingMember(true)}
className="block w-full rounded-md border border-dashed border-gray-300 py-2 text-sm transition-colors hover:border-gray-600 hover:bg-gray-50 focus:outline-0"
>
@@ -316,7 +318,7 @@ export function TeamMembersPage() {
</div>
)}
{teamMembers.length >= 25 && canManageCurrentTeam && (
{teamMembers.length >= MAX_MEMBER_COUNT && canManageCurrentTeam && (
<p className="mt-2 rounded-lg bg-red-100 p-2 text-red-700">
You have reached the maximum number of members in a team. Please reach
out to us if you need more.

View File

@@ -311,6 +311,7 @@ export function TeamRoadmaps() {
const shareSettingsModal = selectedResource && (
<ShareOptionsModal
description={selectedResource.description!}
visibility={selectedResource.visibility!}
sharedTeamMemberIds={selectedResource.sharedTeamMemberIds!}
sharedFriendIds={selectedResource.sharedFriendIds!}

View File

@@ -9,5 +9,4 @@ For more resources, visit the following links:
- [Change Tracking in EF Core](https://learn.microsoft.com/en-us/ef/core/change-tracking/)
- [Intro to Change Tracking](https://www.oreilly.com/library/view/programming-entity-framework/9781449331825/ch05.html)
- [ChangeTracker in Entity Framework Core](https://www.entityframeworktutorial.net/efcore/changetracker-in-ef-core.aspxs)
- [ChangeTracker in Entity Framework Core](https://www.entityframeworktutorial.net/efcore/changetracker-in-ef-core.aspx)
- [ChangeTracker in Entity Framework Core](https://www.entityframeworktutorial.net/efcore/changetracker-in-ef-core.aspx)

View File

@@ -85,7 +85,7 @@ int& numRef = num;
User-defined data types are types that are defined by the programmer, such as structures, classes, and unions.
## Structures (struct)
Structures are used to store different data tyes under a single variable and accessibility of member variables and methods are public.
Structures are used to store different data types under a single variable and accessibility of member variables and methods are public.
Example:
```cpp

View File

@@ -21,4 +21,4 @@ Services such as [CloudFlare](https://www.cloudflare.com/dns/) and [Route53](htt
To learn more, visit the following links:
- [Getting started with Domain Name System](https://github.com/donnemartin/system-design-primer#domain-name-system)
[What is DNS?](https://www.cloudflare.com/learning/dns/what-is-dns/)
- [What is DNS?](https://www.cloudflare.com/learning/dns/what-is-dns/)

View File

@@ -1,4 +1,4 @@
# instanceOf operator
# instanceof operator
The `instanceof` operator is a way to narrow down the type of a variable. It is used to check if an object is an instance of a class.

1
src/icons/letter.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--ic" width="35" height="35" viewBox="0 0 24 24"><path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5l-8-5V6l8 5l8-5v2z"></path></svg>

After

Width:  |  Height:  |  Size: 338 B

14
src/pages/teams.astro Normal file
View File

@@ -0,0 +1,14 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { TeamHeroBanner } from '../components/TeamMarketing/TeamHeroBanner';
import { TeamTools } from '../components/TeamMarketing/TeamTools';
import { TeamDemo } from '../components/TeamMarketing/TeamDemo';
import { TeamPricing } from '../components/TeamMarketing/TeamPricing';
---
<BaseLayout title='Roadmaps for teams' permalink={'/teams'}>
<TeamHeroBanner client:load />
<TeamTools />
<TeamDemo client:load />
<TeamPricing client:load />
</BaseLayout>

View File

@@ -1,7 +1,7 @@
import { atom, computed } from 'nanostores';
import { type RoadmapDocument } from '../components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import type { GetRoadmapResponse } from '../components/CustomRoadmap/CustomRoadmap';
export const currentRoadmap = atom<RoadmapDocument | undefined>(undefined);
export const currentRoadmap = atom<GetRoadmapResponse | undefined>(undefined);
export const isCurrentRoadmapPersonal = computed(
currentRoadmap,
(roadmap) => !roadmap?.teamId