Compare commits

..

3 Commits

Author SHA1 Message Date
Arik Chakma
749648e94b refactor: resource meta 2025-02-11 18:33:32 +06:00
Arik Chakma
8d3a3aba35 fix: remove question check 2025-02-11 18:30:46 +06:00
Arik Chakma
fbf3e6577a fix: member progress 2025-02-11 18:29:18 +06:00
144 changed files with 8500 additions and 7385 deletions

View File

@@ -3,6 +3,6 @@
"enabled": false
},
"_variables": {
"lastUpdateCheck": 1739229597159
"lastUpdateCheck": 1738019390029
}
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1,519 +0,0 @@
{
"PcYnYAAkKMbzoiCnBfjqO": {
"title": "JavaScript Fundamentals",
"description": "",
"links": []
},
"q9oQTt_NqhdWvJfA5XH1V": {
"title": "Basic Command-line Knowledge",
"description": "",
"links": []
},
"9iSdASlRxyod9YwZ2IUry": {
"title": "HTTP and Web Protocols",
"description": "",
"links": []
},
"C08pIguX1N45Iw0kh0Fvu": {
"title": "Git Basics",
"description": "",
"links": []
},
"dSBYTGGkol3MAXyg7G7_J": {
"title": "Node.js and NPM",
"description": "",
"links": []
},
"zR84MFLL6y0dygz9hXXPA": {
"title": "TypeScript Fundamentals",
"description": "",
"links": []
},
"Isl5anwDvb1MacA-JH4ej": {
"title": "Understand Serverless Architecture",
"description": "",
"links": []
},
"TB6vGzDgGZ9yAd9MGR7vw": {
"title": "Workers Runtime Environment",
"description": "",
"links": []
},
"aGWLomYHGkIfn7GFc0_Yl": {
"title": "Edge Computing Fundamentals",
"description": "",
"links": []
},
"HNEXPU6r_T7UYvwLv2wnt": {
"title": "Request/Response Handling",
"description": "",
"links": []
},
"_2UnRlbUplHvs5-Stj4O4": {
"title": "Fetch API and Runtime APIs",
"description": "",
"links": []
},
"i6L9FI6fBDXr0XtMrc_uR": {
"title": "Workers Lifecycle",
"description": "",
"links": []
},
"WZSi9inWPhqZQvDN-C8BV": {
"title": "Service Bindings",
"description": "",
"links": []
},
"uBjcQ9PJUfwzu5N_2CNjN": {
"title": "Caching Strategies",
"description": "",
"links": []
},
"aStbAF4zraqhJ-N3RH4ha": {
"title": "Middleware Patterns",
"description": "",
"links": []
},
"9ef2VPCru8lCmRxxGe-Eo": {
"title": "Bindings",
"description": "",
"links": []
},
"-8MsWNvuqwQCbLpOx_kov": {
"title": "Hono",
"description": "Hono is a small, simple and ultrafast web framework built on web standards. It works on any JavaScript runtime: Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Netlify, AWS Lambda, Lambda@Edge, and Node.js. Hono is more known for supporting a lot more than the basics.\n\nUse-cases\n---------\n\nHono is a simple web application framework similar to the well known javascript framework Express, without a frontend. But it runs on CDN Edges and allows you to construct larger applications when combined with middleware.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Hono JS Examples",
"url": "https://github.com/honojs/examples",
"type": "opensource"
},
{
"title": "Official Documentation",
"url": "https://hono.dev/docs/",
"type": "article"
},
{
"title": "Hono.js: A Small Framework with Big Potential",
"url": "https://medium.com/@appvintechnologies/hono-js-a-small-framework-with-big-potential-15a093fc5c07",
"type": "article"
},
{
"title": "Quick Start with Hono: Simple Setup Guide",
"url": "https://dev.to/koshirok096/quick-start-with-hono-simple-setup-guide-bite-sized-article-lhe",
"type": "article"
},
{
"title": "Learn with me",
"url": "https://www.youtube.com/watch?v=gY-TK33G6kQ",
"type": "video"
}
]
},
"15jl6CSCkqnh_eFfysLDM": {
"title": "Itty Router",
"description": "Itty Router is a lightweight router with the motto \"less is more\" that supports Cloudflare workers and pages. While other libraries may suffer from feature creep/bloat to please a wider audience, Itty Router painfully consider every single byte added to itty. Our router options range from ~450 bytes to ~970 bytes for a batteries-included version with built-in defaults, error handling, formatting, etc. On top of that, the following concepts aim to keep YOUR code tiny (and readable) as well.\n\nSimple Projects ideas\n---------------------\n\nItty Router is a lightweight router system that supports typescript. You can create easy and good routers for Cloudflare workers or pages. With a simple project like a URL shortener, you can use Itty Router and Cloudflare KV.\n\nOther project ideas can be found:\n\n* Webhook Relay\n * Transform webhook data or API data towards another API so you can transform the data as you like.\n* Micro URL Monitoring\n * Monitor any URL and give back responses on the specific endpoint.\n* Single-Use Download Links (Watch out for costs from Cloudflare!)\n * Generate links that expire after one download, ideal for file sharing.\n\nVisit the following resources to learn more:",
"links": [
{
"title": "Official Documentation",
"url": "https://itty.dev/itty-router/",
"type": "article"
}
]
},
"Tzx93tvoGrc9_fKQqkorN": {
"title": "Wrangler",
"description": "",
"links": []
},
"uoaOrypiMkyoikXvTHeVS": {
"title": "DevTools Integration",
"description": "",
"links": []
},
"8Y6TIYoWIXrxtmzDVdS0b": {
"title": "CI/CD Pipelines",
"description": "",
"links": []
},
"zSwio18XdBfqwSneAx_AP": {
"title": "Any Frontend Framework",
"description": "",
"links": []
},
"o4sBgniPmLqwej6TlIPcl": {
"title": "Miniflare",
"description": "",
"links": []
},
"1dGFfQauOgHP7T4ReMpCU": {
"title": "Workers KV",
"description": "",
"links": []
},
"EBTHbXOOZiqrcYJvKhcWV": {
"title": "Key-value Operations",
"description": "",
"links": []
},
"sQlRIYLnZcugATgpogJmw": {
"title": "Metadata Handling",
"description": "",
"links": []
},
"i64-aCpZHygq76fBU6eXD": {
"title": "Bulk Operations",
"description": "",
"links": []
},
"OgW-iIrJZ5-sOWKnFpIZd": {
"title": "Caching Patterns",
"description": "",
"links": []
},
"gxLUlXGuaY5Q-0xmBgQwz": {
"title": "R2 Storage",
"description": "",
"links": []
},
"K9iW2H6riKwddWmpWJFJw": {
"title": "Object Storage",
"description": "",
"links": []
},
"BPahk1qH9Hk11tsE2hw3A": {
"title": "Large File Handling",
"description": "",
"links": []
},
"3jU5753Uza2aS-gZo7w4k": {
"title": "Asset Management",
"description": "",
"links": []
},
"UNE6XK4su5r2jcxhY7hOG": {
"title": "Bucket Operations",
"description": "",
"links": []
},
"YvgmmF9sWfURgijFV7E31": {
"title": "Bucket Lifecycle",
"description": "",
"links": []
},
"mKN0Ta3zSk7PCm_uHYKFN": {
"title": "D1",
"description": "",
"links": []
},
"PnhP47woPJb_JnLpMMiTw": {
"title": "Schema Management",
"description": "",
"links": []
},
"M8rSSVFUHixgWZRfaBPHb": {
"title": "Migrations",
"description": "",
"links": []
},
"65xDESm6jbHWkVO4NgHqx": {
"title": "Query Optimization",
"description": "",
"links": []
},
"MpWO1sroeF106SEMU1V1a": {
"title": "Drizzle",
"description": "",
"links": []
},
"h3MHLZZwkYqqb5PSfMhpB": {
"title": "Prisma",
"description": "",
"links": []
},
"zyRgTtlng6idboSgL9YTt": {
"title": "Queues",
"description": "",
"links": []
},
"EFA8m0EdhygxcBWzwmbnT": {
"title": "Message Processing",
"description": "",
"links": []
},
"qgvDGyLjc6lMmVPjHozFM": {
"title": "Background Jobs",
"description": "",
"links": []
},
"MInAsLLJtIq6WQDSj5yGH": {
"title": "Rate Limiting",
"description": "",
"links": []
},
"Grl59SjY31Q3sgf9uX-xf": {
"title": "Dead Letter Queues",
"description": "",
"links": []
},
"G-xBbtaniYFRE9Dgs18px": {
"title": "Durable Objects",
"description": "",
"links": []
},
"EQjhRlM7zpANNWkypScIl": {
"title": "State Management",
"description": "",
"links": []
},
"RYm0oBFCxm-S-aCwZ21p6": {
"title": "Coordination",
"description": "",
"links": []
},
"36w4Q73XkCwo5Cva0XsF8": {
"title": "Persistence",
"description": "",
"links": []
},
"rxxibrJUo1rQ3XCuUIP59": {
"title": "Transactional Operations",
"description": "",
"links": []
},
"rAl7zXcODiqIpS__3qf1A": {
"title": "Workflows",
"description": "",
"links": []
},
"a0S0_JLwLLNGLUAHrqG4P": {
"title": "Workers AI",
"description": "",
"links": []
},
"zMwmoCUp9429_aXU-Bz4H": {
"title": "Text Generation",
"description": "",
"links": []
},
"S7laV14zsx31O0Tsj2SRL": {
"title": "Image Processing",
"description": "",
"links": []
},
"HJbJ8OxjJzznYwLlIOSO2": {
"title": "Speech Rcognition",
"description": "",
"links": []
},
"QxPoNHsL-Pj_z3aU6qEP4": {
"title": "AI Model Integration",
"description": "",
"links": []
},
"NWGVtH1vxQuO4lly0Omuy": {
"title": "Vectorize",
"description": "",
"links": []
},
"UIWaR1ZdjSm0UAS69Kz_5": {
"title": "Vector Embeddings",
"description": "",
"links": []
},
"pg3GtykCegK411DYDN8sN": {
"title": "Similarity Search",
"description": "",
"links": []
},
"Ep9_oV_YnkbH1gHM-n3gO": {
"title": "AI-powered Search",
"description": "",
"links": []
},
"LoT3NtpNj9uAgQRV-MD_E": {
"title": "Stream",
"description": "",
"links": []
},
"zQp7XfDKWJgMf2LexRJhN": {
"title": "Video Delivery",
"description": "",
"links": []
},
"RiQSPAV9uRFgwQFJckTFV": {
"title": "Live streaming",
"description": "",
"links": []
},
"3B6Z7F0D3Sf8ZBlV3kkGx": {
"title": "Video Processing",
"description": "",
"links": []
},
"8bOWuopxHtBWUSFaVT54P": {
"title": "Images",
"description": "",
"links": []
},
"vHQdMgaL2EEr2o_eJmOuV": {
"title": "Calls",
"description": "",
"links": []
},
"aKEH4ZxI6J1nwjp_AgH5r": {
"title": "Logging and Monitoring",
"description": "",
"links": []
},
"z-1Ye5hcNdr9r6Gwdw7mv": {
"title": "Email Workers",
"description": "",
"links": []
},
"-lsYPD6JueIV94RybGH_Y": {
"title": "Routing",
"description": "",
"links": []
},
"6bNUqx5f_w5NuDL25BABN": {
"title": "Processing",
"description": "",
"links": []
},
"kdIfqTCcOSvV4KDpjr7nu": {
"title": "Filtering",
"description": "",
"links": []
},
"vu8yJsS1WccsdcEVUqwNd": {
"title": "AI Gateway",
"description": "",
"links": []
},
"qkFRW_tJB8_1IYpYskQ5M": {
"title": "Browser Rendering",
"description": "",
"links": []
},
"76xovsBrKOnlRBVjsqNq1": {
"title": "Security & Performance",
"description": "",
"links": []
},
"8IF7jftushwZrn7JXpC_v": {
"title": "Workers Security Model",
"description": "",
"links": []
},
"uNinrB9wm5ahjGXu5fc0g": {
"title": "Isolates Architecture",
"description": "",
"links": []
},
"KWix4jeNUKJ07Iu95Mqj_": {
"title": "Web Security Headers",
"description": "",
"links": []
},
"JP5U6c2fZjtkU-Xzwtapx": {
"title": "Rate Limiting",
"description": "",
"links": []
},
"ui3pUfsGMxv4WRzHkgbF0": {
"title": "Cache API",
"description": "",
"links": []
},
"INiqdtppBmCthOEXuHb-V": {
"title": "HTML Rewriting",
"description": "",
"links": []
},
"sXBxaQtwJ-luGVXdqVXk1": {
"title": "Edge SSL/TLS",
"description": "",
"links": []
},
"So-cKAVfbgsw2zzFREu7Q": {
"title": "Bot Management",
"description": "",
"links": []
},
"wvurOKbemF4Tt2WZcmqDL": {
"title": "Integration & Workflows",
"description": "",
"links": []
},
"SaHqm7T4FFVrsgyfImo66": {
"title": "Pages Functions",
"description": "",
"links": []
},
"JfpVexcbuWCx_R3EjFmbo": {
"title": "Service Bindings",
"description": "",
"links": []
},
"jYAUIKozuhsNK5LbkeAJ6": {
"title": "Inter Worker Communication",
"description": "",
"links": []
},
"4g5w6IAdzefdlRTxbRbdS": {
"title": "External API Integration",
"description": "",
"links": []
},
"uOUjI6CPrhZIlz6mRCtOW": {
"title": "Webhook Handling",
"description": "",
"links": []
},
"Z9Yywlf7rXFBtxTq5B2Y5": {
"title": "Event-driven Architectures",
"description": "",
"links": []
},
"gsCRhwwjXuyueaYHSPOVZ": {
"title": "Development Tools",
"description": "",
"links": []
},
"n0vIbHmUZHrF4WjEhYdb8": {
"title": "Wrangler",
"description": "",
"links": []
},
"vZHBp4S6WaS5sa5rfUOk-": {
"title": "Miniflare",
"description": "",
"links": []
},
"G6YQZUQh_x8Qxm1oBseLQ": {
"title": "DevTools",
"description": "",
"links": []
},
"jyWxaMx7_nojt5HsyAv7K": {
"title": "Testing Frameworks",
"description": "",
"links": []
},
"Cy2T8978yUAPGol-yzxv_": {
"title": "Monitoring Tools",
"description": "",
"links": []
},
"TmQC7fTL6b9EsBDYibv4g": {
"title": "Debugging Techniques",
"description": "",
"links": []
},
"8WZpSKBHCeYfTEL9tBNKr": {
"title": "Tunnels",
"description": "",
"links": []
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 506 KiB

View File

@@ -43,7 +43,6 @@ Here is the list of available roadmaps with more being actively worked upon.
- [AI and Data Scientist Roadmap](https://roadmap.sh/ai-data-scientist)
- [AI Engineer Roadmap](https://roadmap.sh/ai-engineer)
- [AWS Roadmap](https://roadmap.sh/aws)
- [Cloudflare Roadmap](https://roadmap.sh/cloudflare)
- [Linux Roadmap](https://roadmap.sh/linux)
- [Terraform Roadmap](https://roadmap.sh/terraform)
- [Data Analyst Roadmap](https://roadmap.sh/data-analyst)

View File

@@ -5,7 +5,7 @@ import {
COURSE_PURCHASE_PARAM,
setAuthToken,
} from '../../lib/jwt';
import { cn } from '../../lib/classname.ts';
import { cn } from '../../../editor/utils/classname.ts';
import { httpGet } from '../../lib/http';
import { Spinner } from '../ReactIcons/Spinner.tsx';
import { CHECKOUT_AFTER_LOGIN_KEY } from './CourseLoginPopup.tsx';

View File

@@ -1,27 +1,20 @@
import { useStore } from '@nanostores/react';
import { useEffect, useState } from 'react';
import { cn } from '../../../editor/utils/classname';
import { useParams } from '../../hooks/use-params';
import { useToast } from '../../hooks/use-toast';
import { httpGet } from '../../lib/http';
import { getUser } from '../../lib/jwt';
import { useToast } from '../../hooks/use-toast';
import { useStore } from '@nanostores/react';
import { $teamList } from '../../stores/team';
import type { TeamListResponse } from '../TeamDropdown/TeamDropdown';
import { DashboardTabButton } from './DashboardTabButton';
import { DashboardTab } from './DashboardTab';
import { PersonalDashboard, type BuiltInRoadmap } from './PersonalDashboard';
import { TeamDashboard } from './TeamDashboard';
import type { QuestionGroupType } from '../../lib/question-group';
import type { GuideFileType } from '../../lib/guide';
import type { VideoFileType } from '../../lib/video';
import { getUser } from '../../lib/jwt';
import { useParams } from '../../hooks/use-params';
type DashboardPageProps = {
builtInRoleRoadmaps?: BuiltInRoadmap[];
builtInSkillRoadmaps?: BuiltInRoadmap[];
builtInBestPractices?: BuiltInRoadmap[];
isTeamPage?: boolean;
questionGroups?: QuestionGroupType[];
guides?: GuideFileType[];
videos?: VideoFileType[];
};
export function DashboardPage(props: DashboardPageProps) {
@@ -30,9 +23,6 @@ export function DashboardPage(props: DashboardPageProps) {
builtInBestPractices,
builtInSkillRoadmaps,
isTeamPage = false,
questionGroups,
guides,
videos,
} = props;
const currentUser = getUser();
@@ -76,79 +66,78 @@ export function DashboardPage(props: DashboardPageProps) {
: '/images/default-avatar.png';
return (
<>
<div
className={cn('bg-slate-900', {
'striped-loader-slate': isLoading,
})}
>
<div className="bg-slate-800/30 py-5 min-h-[70px]">
<div className="container flex flex-wrap items-center gap-1.5">
{!isLoading && (
<>
<DashboardTabButton
label="Personal"
isActive={!selectedTeamId && !isTeamPage}
href="/dashboard"
avatar={userAvatar}
/>
{teamList.map((team) => {
const { avatar } = team;
const avatarUrl = avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
: '/images/default-avatar.png';
return (
<DashboardTabButton
key={team._id}
label={team.name}
isActive={team._id === selectedTeamId}
{...(team.status === 'invited'
? {
href: `/respond-invite?i=${team.memberId}`,
}
: {
href: `/team?t=${team._id}`,
})}
avatar={avatarUrl}
/>
);
})}
<DashboardTabButton
label="+ Create Team"
isActive={false}
href="/team/new"
className="border border-dashed border-slate-700 bg-transparent px-3 text-[13px] text-sm text-gray-500 hover:border-solid hover:border-slate-700 hover:text-gray-400"
/>
</>
)}
</div>
</div>
</div>
<div className="min-h-screen bg-gray-50 pb-20 pt-8">
<div className="container">
<div className="mb-6 flex flex-wrap items-center gap-1.5 sm:mb-8">
<DashboardTab
label="Personal"
isActive={!selectedTeamId && !isTeamPage}
href="/dashboard"
avatar={userAvatar}
/>
{isLoading && (
<>
<DashboardTabSkeleton />
<DashboardTabSkeleton />
</>
)}
{!isLoading && (
<>
{teamList.map((team) => {
const { avatar } = team;
const avatarUrl = avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
: '/images/default-avatar.png';
return (
<DashboardTab
key={team._id}
label={team.name}
isActive={team._id === selectedTeamId}
{...(team.status === 'invited'
? {
href: `/respond-invite?i=${team.memberId}`,
}
: {
href: `/team?t=${team._id}`,
})}
avatar={avatarUrl}
/>
);
})}
<DashboardTab
label="+ Create Team"
isActive={false}
href="/team/new"
className="border border-dashed border-gray-300 bg-transparent px-3 text-[13px] text-sm text-gray-500 hover:border-gray-600 hover:text-black"
/>
</>
)}
</div>
<div className="">
{!selectedTeamId && !isTeamPage && (
<div className="bg-slate-900">
<PersonalDashboard
builtInRoleRoadmaps={builtInRoleRoadmaps}
builtInSkillRoadmaps={builtInSkillRoadmaps}
builtInBestPractices={builtInBestPractices}
questionGroups={questionGroups}
guides={guides}
videos={videos}
/>
</div>
<PersonalDashboard
builtInRoleRoadmaps={builtInRoleRoadmaps}
builtInSkillRoadmaps={builtInSkillRoadmaps}
builtInBestPractices={builtInBestPractices}
/>
)}
{(selectedTeamId || isTeamPage) && (
<div className="container">
<TeamDashboard
builtInRoleRoadmaps={builtInRoleRoadmaps!}
builtInSkillRoadmaps={builtInSkillRoadmaps!}
teamId={selectedTeamId!}
/>
</div>
<TeamDashboard
builtInRoleRoadmaps={builtInRoleRoadmaps!}
builtInSkillRoadmaps={builtInSkillRoadmaps!}
teamId={selectedTeamId!}
/>
)}
</div>
</>
</div>
);
}
function DashboardTabSkeleton() {
return (
<div className="h-[30px] w-[114px] animate-pulse rounded-md border bg-white"></div>
);
}

View File

@@ -11,7 +11,7 @@ type DashboardTabProps = {
icon?: ReactNode;
};
export function DashboardTabButton(props: DashboardTabProps) {
export function DashboardTab(props: DashboardTabProps) {
const { isActive, onClick, label, className, href, avatar, icon } = props;
const Slot = href ? 'a' : 'button';
@@ -20,10 +20,8 @@ export function DashboardTabButton(props: DashboardTabProps) {
<Slot
onClick={onClick}
className={cn(
'flex h-[30px] shrink-0 items-center gap-1 rounded-md border border-slate-700 bg-slate-800 p-1.5 pl-2 pr-3 text-sm leading-none text-gray-400 transition-colors hover:bg-slate-700',
isActive
? 'border-slate-200 bg-slate-200 text-gray-900 hover:bg-slate-200'
: '',
'flex h-[30px] shrink-0 items-center gap-1 rounded-md border bg-white p-1.5 px-2 text-sm leading-none text-gray-600',
isActive ? 'border-gray-500 bg-gray-200 text-gray-900' : '',
className,
)}
{...(href ? { href } : {})}
@@ -32,7 +30,7 @@ export function DashboardTabButton(props: DashboardTabProps) {
<img
src={avatar}
alt="avatar"
className="mr-0.5 h-4 w-4 rounded-full object-cover"
className="h-4 w-4 mr-0.5 rounded-full object-cover"
/>
)}
{icon}

View File

@@ -1,47 +1,23 @@
import { useStore } from '@nanostores/react';
import {
ChartColumn,
CheckSquare,
FolderGit2,
SquarePen,
Zap,
type LucideIcon
} from 'lucide-react';
import { useEffect, useState } from 'react';
import type { AllowedProfileVisibility } from '../../api/user.ts';
import { useToast } from '../../hooks/use-toast';
import { cn } from '../../lib/classname.ts';
import type { GuideFileType } from '../../lib/guide';
import { type JSXElementConstructor, useEffect, useState } from 'react';
import { httpGet } from '../../lib/http';
import type { QuestionGroupType } from '../../lib/question-group';
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
import type { VideoFileType } from '../../lib/video';
import { $accountStreak, type StreakResponse } from '../../stores/streak';
import type { PageType } from '../CommandMenu/CommandMenu';
import { FeaturedGuideList } from '../FeaturedGuides/FeaturedGuideList';
import { FeaturedVideoList } from '../FeaturedVideos/FeaturedVideoList';
import {
FavoriteRoadmaps,
type AIRoadmapType,
} from '../HeroSection/FavoriteRoadmaps.tsx';
import { HeroRoadmap } from '../HeroSection/HeroRoadmap.tsx';
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
import type { UserProgress } from '../TeamProgress/TeamProgressPage';
const projectGroups = [
{
title: 'Frontend',
id: 'frontend',
},
{
title: 'Backend',
id: 'backend',
},
{
title: 'DevOps',
id: 'devops',
},
];
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions';
import type { PageType } from '../CommandMenu/CommandMenu';
import { useToast } from '../../hooks/use-toast';
import { getCurrentPeriod } from '../../lib/date';
import { ListDashboardCustomProgress } from './ListDashboardCustomProgress';
import { RecommendedRoadmaps } from './RecommendedRoadmaps';
import { ProgressStack } from './ProgressStack';
import { useStore } from '@nanostores/react';
import { $accountStreak, type StreakResponse } from '../../stores/streak';
import { CheckEmoji } from '../ReactIcons/CheckEmoji.tsx';
import { ConstructionEmoji } from '../ReactIcons/ConstructionEmoji.tsx';
import { BookEmoji } from '../ReactIcons/BookEmoji.tsx';
import { DashboardAiRoadmaps } from './DashboardAiRoadmaps.tsx';
import type { AllowedProfileVisibility } from '../../api/user.ts';
import { PencilIcon, type LucideIcon } from 'lucide-react';
import { cn } from '../../lib/classname.ts';
import type { AllowedRoadmapRenderer } from '../../lib/roadmap.ts';
type UserDashboardResponse = {
name: string;
@@ -52,7 +28,11 @@ type UserDashboardResponse = {
profileVisibility: AllowedProfileVisibility;
progresses: UserProgress[];
projects: ProjectStatusDocument[];
aiRoadmaps: AIRoadmapType[];
aiRoadmaps: {
id: string;
title: string;
slug: string;
}[];
topicDoneToday: number;
};
@@ -62,7 +42,6 @@ export type BuiltInRoadmap = {
title: string;
description: string;
isFavorite?: boolean;
isNew?: boolean;
relatedRoadmapIds?: string[];
renderer?: AllowedRoadmapRenderer;
metadata?: Record<string, any>;
@@ -72,162 +51,16 @@ type PersonalDashboardProps = {
builtInRoleRoadmaps?: BuiltInRoadmap[];
builtInSkillRoadmaps?: BuiltInRoadmap[];
builtInBestPractices?: BuiltInRoadmap[];
questionGroups?: QuestionGroupType[];
guides?: GuideFileType[];
videos?: VideoFileType[];
};
type DashboardStatItemProps = {
icon: LucideIcon;
iconClassName: string;
value: number;
label: string;
isLoading: boolean;
};
function DashboardStatItem(props: DashboardStatItemProps) {
const { icon: Icon, iconClassName, value, label, isLoading } = props;
return (
<div
className={cn(
'flex items-center gap-1.5 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3',
{
'striped-loader-slate striped-loader-slate-fast text-transparent':
isLoading,
},
)}
>
<Icon
size={16}
className={cn(iconClassName, { 'text-transparent': isLoading })}
/>
<span>
<span className="tabular-nums">{value}</span> {label}
</span>
</div>
);
}
type ProfileButtonProps = {
isLoading: boolean;
name?: string;
username?: string;
avatar?: string;
};
function PersonalProfileButton(props: ProfileButtonProps) {
const { isLoading, name, username, avatar } = props;
if (isLoading || !username) {
return (
<a
href="/account/update-profile"
className={cn(
'flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3 font-medium outline-slate-700 hover:bg-slate-800 hover:outline-slate-400',
{
'striped-loader-slate striped-loader-slate-fast text-transparent':
isLoading,
'bg-blue-500/10 text-blue-500 hover:bg-blue-500/20': !isLoading,
},
)}
>
<CheckSquare className="h-4 w-4" strokeWidth={2.5} />
Set up your profile
</a>
);
}
return (
<div className="flex gap-1.5">
<a
href={`/u/${username}`}
className="flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3 text-slate-300 transition-colors hover:bg-slate-800/70"
>
<img
src={avatar}
alt={name || 'Profile'}
className="h-5 w-5 rounded-full ring-1 ring-slate-700"
/>
<span className="font-medium">Visit Profile</span>
</a>
<a
href="/account/update-profile"
className="flex items-center gap-2 rounded-lg bg-slate-800/50 py-2 pl-3 pr-3 text-slate-400 transition-colors hover:bg-slate-800/70 hover:text-slate-300"
title="Edit Profile"
>
<SquarePen className="h-4 w-4" />
</a>
</div>
);
}
type DashboardStatsProps = {
profile: ProfileButtonProps;
accountStreak?: StreakResponse;
topicsDoneToday?: number;
finishedProjectsCount?: number;
isLoading: boolean;
};
function DashboardStats(props: DashboardStatsProps) {
const {
accountStreak,
topicsDoneToday = 0,
finishedProjectsCount = 0,
isLoading,
profile,
} = props;
return (
<div className="container mb-3 flex flex-col gap-4 pb-2 pt-6 text-sm text-slate-400 sm:flex-row sm:items-center sm:justify-between">
<div className="flex w-full flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<PersonalProfileButton
isLoading={isLoading}
name={profile.name}
username={profile.username}
avatar={profile.avatar}
/>
<div className="hidden flex-wrap items-center gap-2 md:flex">
<DashboardStatItem
icon={Zap}
iconClassName="text-yellow-500"
value={accountStreak?.count || 0}
label="day streak"
isLoading={isLoading}
/>
<DashboardStatItem
icon={ChartColumn}
iconClassName="text-green-500"
value={topicsDoneToday}
label="learnt today"
isLoading={isLoading}
/>
<DashboardStatItem
icon={FolderGit2}
iconClassName="text-blue-500"
value={finishedProjectsCount}
label="projects finished"
isLoading={isLoading}
/>
</div>
</div>
</div>
);
}
export function PersonalDashboard(props: PersonalDashboardProps) {
const {
builtInRoleRoadmaps = [],
builtInBestPractices = [],
builtInSkillRoadmaps = [],
questionGroups = [],
guides = [],
videos = [],
} = props;
const toast = useToast();
const [isLoading, setIsLoading] = useState(true);
const [personalDashboardDetails, setPersonalDashboardDetails] =
useState<UserDashboardResponse>();
@@ -305,9 +138,7 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
return () => window.removeEventListener('refresh-favorites', loadProgress);
}, []);
const learningRoadmapsToShow: UserProgress[] = (
personalDashboardDetails?.progresses || []
)
const learningRoadmapsToShow = (personalDashboardDetails?.progresses || [])
.filter((progress) => !progress.isCustomResource)
.sort((a, b) => {
const updatedAtA = new Date(a.updatedAt);
@@ -325,10 +156,7 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
});
const aiGeneratedRoadmaps = personalDashboardDetails?.aiRoadmaps || [];
const customRoadmaps: UserProgress[] = (
personalDashboardDetails?.progresses || []
)
const customRoadmaps = (personalDashboardDetails?.progresses || [])
.filter((progress) => progress.isCustomResource)
.sort((a, b) => {
const updatedAtA = new Date(a.updatedAt);
@@ -341,6 +169,43 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
: '/images/default-avatar.png';
const allRoadmapsAndBestPractices = [
...builtInRoleRoadmaps,
...builtInSkillRoadmaps,
...builtInBestPractices,
];
const relatedRoadmapIds = allRoadmapsAndBestPractices
// take the ones that user is learning
.filter((roadmap) =>
learningRoadmapsToShow?.some(
(learningRoadmap) => learningRoadmap.resourceId === roadmap.id,
),
)
.flatMap((roadmap) => roadmap.relatedRoadmapIds)
// remove the ones that user is already learning or has bookmarked
.filter(
(roadmapId) =>
!learningRoadmapsToShow.some((lr) => lr.resourceId === roadmapId),
);
const recommendedRoadmapIds = new Set(
relatedRoadmapIds.length === 0
? [
'frontend',
'backend',
'devops',
'ai-data-scientist',
'full-stack',
'api-design',
]
: relatedRoadmapIds,
);
const recommendedRoadmaps = allRoadmapsAndBestPractices.filter((roadmap) =>
recommendedRoadmapIds.has(roadmap.id),
);
const enrichedProjects = personalDashboardDetails?.projects
.map((project) => {
const projectDetail = projectDetails.find(
@@ -367,200 +232,165 @@ export function PersonalDashboard(props: PersonalDashboardProps) {
const { username } = personalDashboardDetails || {};
return (
<div>
<DashboardStats
profile={{
name,
username,
avatar: avatarLink,
isLoading,
}}
isLoading={isLoading}
accountStreak={accountStreak}
topicsDoneToday={personalDashboardDetails?.topicDoneToday}
finishedProjectsCount={
enrichedProjects?.filter((p) => p.submittedAt && p.repositoryUrl)
.length
}
/>
<section>
{isLoading ? (
<div className="h-7 w-1/4 animate-pulse rounded-lg bg-gray-200"></div>
) : (
<div className="flex flex-col items-start justify-between gap-1 sm:flex-row sm:items-center">
<h2 className="text-lg font-medium">
Hi {name}, good {getCurrentPeriod()}!
</h2>
<a
href="/home"
className="rounded-full bg-gray-200 px-2.5 py-1 text-xs font-medium text-gray-700 hover:bg-gray-300 hover:text-black"
>
Visit Homepage
</a>
</div>
)}
<FavoriteRoadmaps
progress={learningRoadmapsToShow}
customRoadmaps={customRoadmaps}
aiRoadmaps={aiGeneratedRoadmaps}
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-4">
{isLoading ? (
<>
<DashboardCardSkeleton />
<DashboardCardSkeleton />
<DashboardCardSkeleton />
<DashboardCardSkeleton />
</>
) : (
<>
<DashboardCard
imgUrl={avatarLink}
title={name!}
description={
username ? 'View your profile' : 'Setup your profile'
}
href={username ? `/u/${username}` : '/account/update-profile'}
{...(username && {
externalLinkIcon: PencilIcon,
externalLinkHref: '/account/update-profile',
externalLinkText: 'Edit',
})}
className={
!username
? 'border-dashed border-gray-500 bg-gray-100 hover:border-gray-500 hover:bg-gray-200'
: ''
}
/>
<DashboardCard
icon={BookEmoji}
title="Visit Roadmaps"
description="Learn new skills"
href="/roadmaps"
/>
<DashboardCard
icon={ConstructionEmoji}
title="Build Projects"
description="Practice what you learn"
href="/projects"
/>
<DashboardCard
icon={CheckEmoji}
title="Best Practices"
description="Do things the right way"
href="/best-practices"
/>
</>
)}
</div>
<ProgressStack
progresses={learningRoadmapsToShow}
projects={enrichedProjects || []}
isLoading={isLoading}
accountStreak={accountStreak}
topicDoneToday={personalDashboardDetails?.topicDoneToday || 0}
/>
<div className="bg-gradient-to-b from-slate-900 to-black pb-12">
<div className="relative mt-6 border-t border-t-[#1e293c] pt-12">
<div className="container">
<h2
id="role-based-roadmaps"
className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2"
>
Role Based Roadmaps
</h2>
<ListDashboardCustomProgress
progresses={customRoadmaps}
isLoading={isLoading}
/>
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
{builtInRoleRoadmaps.map((roadmap) => {
const roadmapProgress = learningRoadmapsToShow.find(
(lr) => lr.resourceId === roadmap.id,
);
<DashboardAiRoadmaps
roadmaps={aiGeneratedRoadmaps}
isLoading={isLoading}
/>
const percentageDone =
(((roadmapProgress?.skipped || 0) +
(roadmapProgress?.done || 0)) /
(roadmapProgress?.total || 1)) *
100;
<RecommendedRoadmaps
roadmaps={recommendedRoadmaps}
isLoading={isLoading}
/>
</section>
);
}
return (
<HeroRoadmap
key={roadmap.id}
resourceId={roadmap.id}
resourceType="roadmap"
resourceTitle={roadmap.title}
isFavorite={roadmap.isFavorite}
percentageDone={percentageDone}
isNew={roadmap.isNew}
url={`/${roadmap.id}`}
/>
);
})}
</div>
type DashboardCardProps = {
icon?: JSXElementConstructor<any>;
imgUrl?: string;
title: string;
description: string;
href: string;
externalLinkIcon?: LucideIcon;
externalLinkText?: string;
externalLinkHref?: string;
className?: string;
};
function DashboardCard(props: DashboardCardProps) {
const {
icon: Icon,
imgUrl,
title,
description,
href,
externalLinkHref,
externalLinkIcon: ExternalLinkIcon,
externalLinkText,
className,
} = props;
return (
<div className={cn('relative overflow-hidden', className)}>
<a
href={href}
className="flex flex-col rounded-lg border border-gray-300 bg-white hover:border-gray-400 hover:bg-gray-50"
>
{Icon && (
<div className="px-4 pb-3 pt-4">
<Icon className="size-6" />
</div>
</div>
)}
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
<div className="container">
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
Skill Based Roadmaps
</h2>
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
{builtInSkillRoadmaps.map((roadmap) => {
const roadmapProgress = learningRoadmapsToShow.find(
(lr) => lr.resourceId === roadmap.id,
);
const percentageDone =
(((roadmapProgress?.skipped || 0) +
(roadmapProgress?.done || 0)) /
(roadmapProgress?.total || 1)) *
100;
return (
<HeroRoadmap
key={roadmap.id}
resourceId={roadmap.id}
resourceType="roadmap"
resourceTitle={roadmap.title}
isFavorite={roadmap.isFavorite}
percentageDone={percentageDone}
isNew={roadmap.isNew}
url={`/${roadmap.id}`}
/>
);
})}
</div>
{imgUrl && (
<div className="px-4 pb-1.5 pt-3.5">
<img src={imgUrl} alt={title} className="size-8 rounded-full" />
</div>
)}
<div className="flex grow flex-col justify-center gap-0.5 p-4">
<h3 className="truncate font-medium text-black">{title}</h3>
<p className="text-xs text-black">{description}</p>
</div>
</a>
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
<div className="container">
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
Project Ideas
</h2>
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
{projectGroups.map((projectGroup) => {
return (
<HeroRoadmap
percentageDone={0}
key={projectGroup.id}
resourceId={projectGroup.id}
resourceType="roadmap"
resourceTitle={projectGroup.title}
url={`/${projectGroup.id}/projects`}
allowFavorite={false}
/>
);
})}
</div>
</div>
</div>
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
<div className="container">
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
Best Practices
</h2>
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
{builtInBestPractices.map((roadmap) => {
const roadmapProgress = learningRoadmapsToShow.find(
(lr) => lr.resourceId === roadmap.id,
);
const percentageDone =
(((roadmapProgress?.skipped || 0) +
(roadmapProgress?.done || 0)) /
(roadmapProgress?.total || 1)) *
100;
return (
<HeroRoadmap
key={roadmap.id}
resourceId={roadmap.id}
resourceType="best-practice"
resourceTitle={roadmap.title}
isFavorite={roadmap.isFavorite}
percentageDone={percentageDone}
isNew={roadmap.isNew}
url={`/best-practices/${roadmap.id}`}
/>
);
})}
</div>
</div>
</div>
<div className="relative mt-12 border-t border-t-[#1e293c] pt-12">
<div className="container">
<h2 className="text-md font-regular absolute -top-[17px] left-4 flex rounded-lg border border-[#1e293c] bg-slate-900 px-3 py-1 text-slate-400 sm:left-1/2 sm:-translate-x-1/2">
Questions
</h2>
<div className="grid grid-cols-1 gap-3 px-2 sm:grid-cols-2 sm:px-0 lg:grid-cols-3">
{questionGroups.map((questionGroup) => {
return (
<HeroRoadmap
percentageDone={0}
key={questionGroup.id}
resourceId={questionGroup.id}
resourceType="roadmap"
resourceTitle={questionGroup.frontmatter.briefTitle}
url={`/questions/${questionGroup.id}`}
allowFavorite={false}
isNew={questionGroup.frontmatter.isNew}
/>
);
})}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-5 bg-gray-50 px-4 py-5 sm:gap-16 sm:px-0 sm:py-16">
<FeaturedGuideList
heading="Guides"
guides={guides}
questions={questionGroups
.filter((questionGroup) => questionGroup.frontmatter.authorId)
.slice(0, 7)}
/>
<FeaturedVideoList heading="Videos" videos={videos} />
</div>
{externalLinkHref && (
<a
href={externalLinkHref}
className="absolute right-1 top-1 flex items-center gap-1.5 rounded-md bg-gray-200 p-1 px-2 text-xs text-gray-600 hover:bg-gray-300 hover:text-black"
>
{ExternalLinkIcon && <ExternalLinkIcon className="size-3" />}
{externalLinkText}
</a>
)}
</div>
);
}
function DashboardCardSkeleton() {
return (
<div className="h-[128px] animate-pulse rounded-lg border border-gray-300 bg-white"></div>
);
}

View File

@@ -0,0 +1,47 @@
---
import type { GuideFileType } from '../lib/guide';
import GuideListItem from './GuideListItem.astro';
import type { QuestionGroupType } from '../lib/question-group';
export interface Props {
heading: string;
guides: GuideFileType[];
questions: QuestionGroupType[];
}
const { heading, guides, questions = [] } = Astro.props;
const sortedGuides: (QuestionGroupType | GuideFileType)[] = [
...guides,
...questions,
].sort((a, b) => {
const aDate = new Date(a.frontmatter.date as string);
const bDate = new Date(b.frontmatter.date as string);
return bDate.getTime() - aDate.getTime();
});
---
<div class='container'>
<h2 class='block text-2xl font-bold sm:text-3xl'>{heading}</h2>
<div class='mt-3 sm:my-5'>
{sortedGuides.map((guide) => <GuideListItem guide={guide} />)}
</div>
<a
href='/guides'
class='hidden rounded-full bg-gradient-to-r from-slate-600 to-black px-3 py-2 text-xs font-medium text-white transition-colors hover:from-blue-600 hover:to-blue-800 sm:inline'
>
View All Guides &rarr;
</a>
<div class='mt-3 block sm:hidden'>
<a
href='/guides'
class='font-regular block rounded-md border border-black p-2 text-center text-sm text-black hover:bg-black hover:text-gray-50'
>
View All Guides &nbsp;&rarr;
</a>
</div>
</div>

View File

@@ -1,51 +0,0 @@
import type { GuideFileType } from '../../lib/guide';
import type { QuestionGroupType } from '../../lib/question-group';
import { GuideListItem } from './GuideListItem';
export interface FeaturedGuidesProps {
heading: string;
guides: GuideFileType[];
questions: QuestionGroupType[];
}
export function FeaturedGuideList(props: FeaturedGuidesProps) {
const { heading, guides, questions = [] } = props;
const sortedGuides: (QuestionGroupType | GuideFileType)[] = [
...guides,
...questions,
].sort((a, b) => {
const aDate = new Date(a.frontmatter.date as string);
const bDate = new Date(b.frontmatter.date as string);
return bDate.getTime() - aDate.getTime();
});
return (
<div className="container">
<h2 className="block text-2xl font-bold sm:text-3xl">{heading}</h2>
<div className="mt-3 sm:my-5">
{sortedGuides.map((guide) => (
<GuideListItem key={guide.id} guide={guide} />
))}
</div>
<a
href="/guides"
className="hidden rounded-full bg-gradient-to-r from-slate-600 to-black px-3 py-2 text-xs font-medium text-white transition-colors hover:from-blue-600 hover:to-blue-800 sm:inline"
>
View All Guides &rarr;
</a>
<div className="mt-3 block sm:hidden">
<a
href="/guides"
className="font-regular block rounded-md border border-black p-2 text-center text-sm text-black hover:bg-black hover:text-gray-50"
>
View All Guides &nbsp;&rarr;
</a>
</div>
</div>
);
}

View File

@@ -1,57 +0,0 @@
import type { GuideFileType, GuideFrontmatter } from '../../lib/guide';
import { type QuestionGroupType } from '../../lib/question-group';
export interface GuideListItemProps {
guide: GuideFileType | QuestionGroupType;
}
function isQuestionGroupType(
guide: GuideFileType | QuestionGroupType,
): guide is QuestionGroupType {
return (guide as QuestionGroupType).questions !== undefined;
}
export function GuideListItem(props: GuideListItemProps) {
const { guide } = props;
const { frontmatter, id } = guide;
let pageUrl = '';
let guideType = '';
if (isQuestionGroupType(guide)) {
pageUrl = `/questions/${id}`;
guideType = 'Questions';
} else {
const excludedBySlug = (frontmatter as GuideFrontmatter).excludedBySlug;
pageUrl = excludedBySlug ? excludedBySlug : `/guides/${id}`;
guideType = (frontmatter as GuideFrontmatter).type;
}
return (
<a
className="text-md group block flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600"
href={pageUrl}
>
<span className="text-sm transition-transform group-hover:translate-x-2 md:text-base">
{frontmatter.title}
{frontmatter.isNew && (
<span className="ml-2.5 rounded-sm bg-green-300 px-1.5 py-0.5 text-xs font-medium uppercase text-green-900">
New
<span className="hidden sm:inline">
&nbsp;&middot;&nbsp;
{new Date(frontmatter.date || '').toLocaleString('default', {
month: 'long',
})}
</span>
</span>
)}
</span>
<span className="hidden text-xs capitalize text-gray-500 sm:block">
{guideType}
</span>
<span className="block text-xs text-gray-400 sm:hidden"> &raquo;</span>
</a>
);
}

View File

@@ -0,0 +1,35 @@
---
import type { VideoFileType } from '../lib/video';
import VideoListItem from './VideoListItem.astro';
export interface Props {
heading: string;
videos: VideoFileType[];
}
const { heading, videos } = Astro.props;
---
<div class='container'>
<h2 class='text-2xl sm:text-3xl font-bold block'>{heading}</h2>
<div class='mt-3 sm:my-5'>
{videos.map((video) => <VideoListItem video={video} />)}
</div>
<a
href='/videos'
class='hidden sm:inline transition-colors py-2 px-3 text-xs font-medium rounded-full bg-gradient-to-r from-slate-600 to-black hover:from-blue-600 hover:to-blue-800 text-white'
>
View All Videos &rarr;
</a>
<div class='block sm:hidden mt-3'>
<a
href='/videos'
class='text-sm font-regular block p-2 border border-black text-black rounded-md text-center hover:bg-black hover:text-gray-50'
>
View All Videos &nbsp;&rarr;
</a>
</div>
</div>

View File

@@ -1,39 +0,0 @@
import type { VideoFileType } from '../../lib/video';
import { VideoListItem } from './VideoListItem';
export interface FeaturedVideoListProps {
heading: string;
videos: VideoFileType[];
}
export function FeaturedVideoList(props: FeaturedVideoListProps) {
const { heading, videos } = props;
return (
<div className="container">
<h2 className="block text-2xl font-bold sm:text-3xl">{heading}</h2>
<div className="mt-3 sm:my-5">
{videos.map((video) => (
<VideoListItem key={video.id} video={video} />
))}
</div>
<a
href="/videos"
className="hidden rounded-full bg-gradient-to-r from-slate-600 to-black px-3 py-2 text-xs font-medium text-white transition-colors hover:from-blue-600 hover:to-blue-800 sm:inline"
>
View All Videos &rarr;
</a>
<div className="mt-3 block sm:hidden">
<a
href="/videos"
className="font-regular block rounded-md border border-black p-2 text-center text-sm text-black hover:bg-black hover:text-gray-50"
>
View All Videos &nbsp;&rarr;
</a>
</div>
</div>
);
}

View File

@@ -1,38 +0,0 @@
import type { VideoFileType } from '../../lib/video';
export interface VideoListItemProps {
video: VideoFileType;
}
export function VideoListItem(props: VideoListItemProps) {
const { video } = props;
const { frontmatter, id } = video;
return (
<a
className="block no-underline py-2 group text-md items-center text-gray-600 hover:text-blue-600 flex justify-between border-b"
href={`/videos/${id}`}
>
<span className="group-hover:translate-x-2 transition-transform">
{frontmatter.title}
{frontmatter.isNew && (
<span className="bg-green-300 text-green-900 text-xs font-medium px-1.5 py-0.5 rounded-sm uppercase ml-1.5">
New
<span className="hidden sm:inline">
&middot;
{new Date(frontmatter.date).toLocaleString('default', {
month: 'long',
})}
</span>
</span>
)}
</span>
<span className="capitalize text-gray-500 text-xs hidden sm:block">
{frontmatter.duration}
</span>
<span className="text-gray-400 text-xs block sm:hidden"> &raquo;</span>
</a>
);
}

View File

@@ -0,0 +1,61 @@
---
import type { GuideFileType, GuideFrontmatter } from '../lib/guide';
import { type QuestionGroupType } from '../lib/question-group';
export interface Props {
guide: GuideFileType | QuestionGroupType;
}
function isQuestionGroupType(
guide: GuideFileType | QuestionGroupType,
): guide is QuestionGroupType {
return (guide as QuestionGroupType).questions !== undefined;
}
const { guide } = Astro.props;
const { frontmatter, id } = guide;
let pageUrl = '';
let guideType = '';
if (isQuestionGroupType(guide)) {
pageUrl = `/questions/${id}`;
guideType = 'Questions';
} else {
const excludedBySlug = (frontmatter as GuideFrontmatter).excludedBySlug;
pageUrl = excludedBySlug ? excludedBySlug : `/guides/${id}`;
guideType = (frontmatter as GuideFrontmatter).type;
}
---
<a
class:list={[
'text-md group block flex items-center justify-between border-b py-2 text-gray-600 no-underline hover:text-blue-600',
]}
href={pageUrl}
>
<span
class='text-sm transition-transform group-hover:translate-x-2 md:text-base'
>
{frontmatter.title}
{
frontmatter.isNew && (
<span class='ml-1.5 rounded-sm bg-green-300 px-1.5 py-0.5 text-xs font-medium uppercase text-green-900'>
New
<span class='hidden sm:inline'>
&middot;
{new Date(frontmatter.date || '').toLocaleString('default', {
month: 'long',
})}
</span>
</span>
)
}
</span>
<span class='hidden text-xs capitalize text-gray-500 sm:block'>
{guideType}
</span>
<span class='block text-xs text-gray-400 sm:hidden'> &raquo;</span>
</a>

View File

@@ -1,229 +1,164 @@
import {
FolderKanban,
MapIcon,
Plus,
Sparkle,
Eye,
EyeOff,
Square,
SquareCheckBig,
} from 'lucide-react';
import { useState } from 'react';
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions.tsx';
import { CheckIcon } from '../ReactIcons/CheckIcon.tsx';
import type { UserProgress } from '../TeamProgress/TeamProgressPage.tsx';
import { HeroProject } from './HeroProject';
import { HeroRoadmap } from './HeroRoadmap';
import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton.tsx';
import { HeroItemsGroup } from './HeroItemsGroup';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx';
import { useEffect, useState } from 'react';
import { EmptyProgress } from './EmptyProgress';
import { httpGet } from '../../lib/http';
import { HeroRoadmaps, type HeroTeamRoadmaps } from './HeroRoadmaps';
import { isLoggedIn } from '../../lib/jwt';
import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx';
export type AIRoadmapType = {
id: string;
title: string;
slug: string;
};
export type UserProgressResponse = {
resourceId: string;
resourceType: 'roadmap' | 'best-practice';
resourceTitle: string;
isFavorite: boolean;
done: number;
learning: number;
skipped: number;
total: number;
updatedAt: Date;
isCustomResource: boolean;
roadmapSlug?: string;
team?: {
name: string;
id: string;
role: AllowedMemberRoles;
};
}[];
type FavoriteRoadmapsProps = {
progress: UserProgress[];
projects: (ProjectStatusDocument & {
title: string;
})[];
customRoadmaps: UserProgress[];
aiRoadmaps: AIRoadmapType[];
isLoading: boolean;
};
function renderProgress(progressList: UserProgressResponse) {
progressList.forEach((progress) => {
const href =
progress.resourceType === 'best-practice'
? `/best-practices/${progress.resourceId}`
: `/${progress.resourceId}`;
const element = document.querySelector(`a[href="${href}"]`);
if (!element) {
return;
}
export function FavoriteRoadmaps(props: FavoriteRoadmapsProps) {
const { progress, isLoading, customRoadmaps, aiRoadmaps, projects } = props;
const [showCompleted, setShowCompleted] = useState(false);
const [isCreatingCustomRoadmap, setIsCreatingCustomRoadmap] = useState(false);
window.dispatchEvent(
new CustomEvent('mark-favorite', {
detail: {
resourceId: progress.resourceId,
resourceType: progress.resourceType,
isFavorite: progress.isFavorite,
},
}),
);
const completedProjects = projects.filter(
(project) => project.submittedAt && project.repositoryUrl,
);
const inProgressProjects = projects.filter(
(project) => !project.submittedAt || !project.repositoryUrl,
const totalDone = progress.done + progress.skipped;
const percentageDone = (totalDone / progress.total) * 100;
const progressBar: HTMLElement | null =
element.querySelector('[data-progress]');
if (progressBar) {
progressBar.style.width = `${percentageDone}%`;
}
});
}
type ProgressResponse = UserProgressResponse;
export function FavoriteRoadmaps() {
const isAuthenticated = isLoggedIn();
if (!isAuthenticated) {
return null;
}
const [isPreparing, setIsPreparing] = useState(true);
const [isLoading, setIsLoading] = useState(true);
const [progress, setProgress] = useState<ProgressResponse>([]);
const [containerOpacity, setContainerOpacity] = useState(0);
function showProgressContainer() {
const heroEl = document.getElementById('hero-text')!;
if (!heroEl) {
return;
}
heroEl.classList.add('opacity-0');
setTimeout(() => {
heroEl.parentElement?.removeChild(heroEl);
setIsPreparing(false);
setTimeout(() => {
setContainerOpacity(100);
}, 50);
}, 0);
}
async function loadProgress() {
setIsLoading(true);
const { response: progressList, error } = await httpGet<ProgressResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-hero-roadmaps`,
);
if (error || !progressList) {
return;
}
setProgress(progressList);
setIsLoading(false);
showProgressContainer();
// render progress on featured items
renderProgress(progressList);
}
useEffect(() => {
loadProgress().finally(() => {
setIsLoading(false);
});
}, []);
useEffect(() => {
window.addEventListener('refresh-favorites', loadProgress);
return () => window.removeEventListener('refresh-favorites', loadProgress);
}, []);
if (isPreparing) {
return null;
}
const hasProgress = progress?.length > 0;
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] = [];
}
const projectsToShow = [
...inProgressProjects,
...(showCompleted ? completedProjects : []),
];
acc[currTeam.name].push(curr);
return acc;
}, {});
return (
<div className="flex flex-col">
{isCreatingCustomRoadmap && (
<CreateRoadmapModal
onClose={() => {
setIsCreatingCustomRoadmap(false);
}}
/>
)}
<HeroItemsGroup
icon={<CheckIcon additionalClasses="mr-1.5 h-[14px] w-[14px]" />}
isLoading={isLoading}
title="Your progress and bookmarks"
isEmpty={!isLoading && progress.length === 0}
emptyTitle={
<>
No bookmars found
<a
href="#role-based-roadmaps"
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
>
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
Bookmark a roadmap
</a>
</>
}
<div
className={`transition-opacity duration-500 opacity-${containerOpacity}`}
>
<div
className={`flex min-h-[192px] bg-gradient-to-b sm:min-h-[280px] ${
hasProgress && `border-t border-t-[#1e293c]`
}`}
>
{progress.map((resource) => (
<HeroRoadmap
key={`${resource.resourceType}-${resource.resourceId}`}
resourceId={resource.resourceId}
resourceType={resource.resourceType}
resourceTitle={resource.resourceTitle}
isFavorite={resource.isFavorite}
percentageDone={
((resource.skipped + resource.done) / resource.total) * 100
}
url={
resource.resourceType === 'roadmap'
? `/${resource.resourceId}`
: `/best-practices/${resource.resourceId}`
}
/>
))}
</HeroItemsGroup>
<HeroItemsGroup
icon={<MapIcon className="mr-1.5 h-[14px] w-[14px]" />}
isLoading={isLoading}
title="Your custom roadmaps"
isEmpty={!isLoading && customRoadmaps.length === 0}
emptyTitle={
<>
No custom roadmaps found
<button
onClick={() => {
setIsCreatingCustomRoadmap(true);
}}
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
>
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
Create custom roadmap
</button>
</>
}
>
{customRoadmaps.map((customRoadmap) => (
<HeroRoadmap
key={customRoadmap.resourceId}
resourceId={customRoadmap.resourceId}
resourceType={'roadmap'}
resourceTitle={customRoadmap.resourceTitle}
percentageDone={
((customRoadmap.skipped + customRoadmap.done) /
customRoadmap.total) *
100
}
url={`/r/${customRoadmap?.roadmapSlug}`}
allowFavorite={false}
/>
))}
<CreateRoadmapButton />
</HeroItemsGroup>
<HeroItemsGroup
icon={<Sparkle className="mr-1.5 h-[14px] w-[14px]" />}
isLoading={isLoading}
title="Your AI roadmaps"
isEmpty={!isLoading && aiRoadmaps.length === 0}
emptyTitle={
<>
No AI roadmaps found
<a
href="/ai"
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
>
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
Generate AI roadmap
</a>
</>
}
>
{aiRoadmaps.map((aiRoadmap) => (
<HeroRoadmap
key={aiRoadmap.id}
resourceId={aiRoadmap.id}
resourceType={'roadmap'}
resourceTitle={aiRoadmap.title}
url={`/ai/${aiRoadmap.slug}`}
percentageDone={0}
allowFavorite={false}
isTrackable={false}
/>
))}
<a
href="/ai"
className={
'flex h-full w-full items-center justify-center gap-1 overflow-hidden rounded-md border border-dashed border-gray-800 p-3 text-sm text-gray-400 hover:border-gray-600 hover:bg-gray-900 hover:text-gray-300'
}
>
<Plus size={16} />
Generate New
</a>
</HeroItemsGroup>
<HeroItemsGroup
icon={<FolderKanban className="mr-1.5 h-[14px] w-[14px]" />}
isLoading={isLoading}
title="Your active projects"
isEmpty={!isLoading && projectsToShow.length === 0}
emptyTitle={
<>
No active projects found
<a
href="/projects"
className="ml-1.5 inline-flex items-center gap-1 font-medium text-blue-500 underline-offset-2 hover:underline"
>
<SquareCheckBig className="size-3.5" strokeWidth={2.5} />
Start a new project
</a>
</>
}
rightContent={
completedProjects.length > 0 && (
<button
onClick={() => setShowCompleted(!showCompleted)}
className="hidden items-center gap-2 rounded-md text-xs text-slate-400 hover:text-slate-300 sm:flex"
>
{showCompleted ? (
<EyeOff className="h-3.5 w-3.5" />
) : (
<Eye className="h-3.5 w-3.5" />
)}
{completedProjects.length} Completed
</button>
)
}
className="border-b-0"
>
{projectsToShow.map((project) => (
<HeroProject key={project._id} project={project} />
))}
<a
href="/projects"
className="flex min-h-[80px] items-center justify-center gap-2 rounded-md border border-dashed border-slate-800 p-4 text-sm text-slate-400 hover:border-slate-600 hover:bg-slate-900/50 hover:text-slate-300"
>
<Plus size={16} />
Start a new project
</a>
</HeroItemsGroup>
<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

@@ -1,78 +0,0 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { cn } from '../../lib/classname';
import { HeroTitle } from './HeroTitle';
type HeroItemsGroupProps = {
icon: any;
isLoading?: boolean;
isEmpty?: boolean;
emptyTitle?: ReactNode;
title: string | ReactNode;
rightContent?: ReactNode;
children?: ReactNode;
className?: string;
};
export function HeroItemsGroup(props: HeroItemsGroupProps) {
const {
icon,
isLoading = false,
isEmpty = false,
emptyTitle,
title,
rightContent,
children,
className,
} = props;
const storageKey = `hero-group-${title}-collapsed`;
const [isCollapsed, setIsCollapsed] = useState(true);
function isCollapsedByStorage() {
const stored = localStorage.getItem(storageKey);
return stored === 'true';
}
useEffect(() => {
setIsCollapsed(isCollapsedByStorage());
}, [isLoading]);
const isLoadingOrCollapsedOrEmpty = isLoading || isCollapsed || isEmpty;
return (
<div
className={cn(
'border-b border-gray-800/50',
{
'py-4': !isLoadingOrCollapsedOrEmpty,
'py-4 ': isLoadingOrCollapsedOrEmpty,
'opacity-50 transition-opacity hover:opacity-100':
isCollapsed && !isLoading,
},
className,
)}
>
<div className="container">
<HeroTitle
icon={icon}
isLoading={isLoading}
isEmpty={isEmpty}
emptyTitle={emptyTitle}
title={title}
rightContent={rightContent}
isCollapsed={isCollapsed}
onToggleCollapse={() => {
setIsCollapsed(!isCollapsed);
localStorage.setItem(storageKey, (!isCollapsed).toString());
}}
/>
{!isLoadingOrCollapsedOrEmpty && (
<div className="mt-4 grid grid-cols-1 gap-2.5 sm:grid-cols-2 md:grid-cols-3">
{children}
</div>
)}
</div>
</div>
);
}

View File

@@ -1,52 +0,0 @@
import { ThumbsUp } from 'lucide-react';
import { cn } from '../../lib/classname.ts';
import { getRelativeTimeString } from '../../lib/date';
import type { ProjectStatusDocument } from '../Projects/ListProjectSolutions.tsx';
type HeroProjectProps = {
project: ProjectStatusDocument & {
title: string;
};
};
export function HeroProject({ project }: HeroProjectProps) {
return (
<a
href={`/projects/${project.projectId}`}
className="group relative flex flex-col justify-between gap-2 rounded-md border border-slate-800 bg-slate-900 p-3.5 hover:border-slate-600"
>
<div className="relative z-10 flex items-start justify-between gap-2">
<h3 className="truncate font-medium text-slate-300 group-hover:text-slate-100">
{project.title}
</h3>
<span
className={cn(
'absolute -right-2 -top-2 flex flex-shrink-0 items-center gap-1 rounded-full text-xs uppercase tracking-wide',
{
'text-green-600/50': project.submittedAt && project.repositoryUrl,
'text-yellow-600': !project.submittedAt || !project.repositoryUrl,
},
)}
>
{project.submittedAt && project.repositoryUrl ? 'Done' : ''}
</span>
</div>
<div className="relative z-10 flex items-center gap-2 text-xs text-slate-400">
{project.submittedAt && project.repositoryUrl && (
<span className="flex items-center gap-1">
<ThumbsUp className="h-3 w-3" />
{project.upvotes}
</span>
)}
{project.startedAt && (
<span>Started {getRelativeTimeString(project.startedAt)}</span>
)}
</div>
<div className="absolute inset-0 rounded-md bg-gradient-to-br from-slate-800/50 via-transparent to-transparent" />
{project.submittedAt && project.repositoryUrl && (
<div className="absolute inset-0 rounded-md bg-gradient-to-br from-green-950/20 via-transparent to-transparent" />
)}
</a>
);
}

View File

@@ -1,74 +0,0 @@
import { cn } from '../../lib/classname.ts';
import type { ResourceType } from '../../lib/resource-progress.ts';
import { MarkFavorite } from '../FeaturedItems/MarkFavorite.tsx';
type ProgressRoadmapProps = {
url: string;
percentageDone: number;
allowFavorite?: boolean;
resourceId: string;
resourceType: ResourceType;
resourceTitle: string;
isFavorite?: boolean;
isTrackable?: boolean;
isNew?: boolean;
};
export function HeroRoadmap(props: ProgressRoadmapProps) {
const {
url,
percentageDone,
resourceType,
resourceId,
resourceTitle,
isFavorite,
allowFavorite = true,
isTrackable = true,
isNew = false,
} = props;
return (
<a
href={url}
className={cn(
'relative flex flex-col overflow-hidden rounded-md border p-3 text-sm text-slate-400 hover:text-slate-300',
{
'border-slate-800 bg-slate-900 hover:border-slate-600': isTrackable,
'border-slate-700/50 bg-slate-800/50 hover:border-slate-600/70':
!isTrackable,
},
)}
>
<span title={resourceTitle} className="relative z-20 truncate">
{resourceTitle}
</span>
{isTrackable && (
<span
className="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]"
style={{ width: `${percentageDone}%` }}
></span>
)}
{allowFavorite && (
<MarkFavorite
resourceId={resourceId}
resourceType={resourceType}
favorite={isFavorite}
/>
)}
{isNew && (
<span className="absolute bottom-1.5 right-2 flex items-center rounded-br rounded-tl text-xs font-medium text-purple-300">
<span className="mr-1.5 flex h-2 w-2">
<span className="absolute inline-flex h-2 w-2 animate-ping rounded-full bg-purple-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-purple-500" />
</span>
New
</span>
)}
</a>
);
}

View File

@@ -0,0 +1,264 @@
import type { UserProgressResponse } from './FavoriteRoadmaps';
import { CheckIcon } from '../ReactIcons/CheckIcon';
import { MarkFavorite } from '../FeaturedItems/MarkFavorite';
import { Spinner } from '../ReactIcons/Spinner';
import type { ResourceType } from '../../lib/resource-progress';
import { MapIcon, Users2 } from 'lucide-react';
import { CreateRoadmapButton } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapButton';
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal';
import { type ReactNode, useState } from 'react';
import { FeatureAnnouncement } from '../FeatureAnnouncement.tsx';
type ProgressRoadmapProps = {
url: string;
percentageDone: number;
allowFavorite?: boolean;
resourceId: string;
resourceType: ResourceType;
resourceTitle: string;
isFavorite?: boolean;
};
function HeroRoadmap(props: ProgressRoadmapProps) {
const {
url,
percentageDone,
resourceType,
resourceId,
resourceTitle,
isFavorite,
allowFavorite = true,
} = props;
return (
<a
href={url}
className="relative flex flex-col overflow-hidden rounded-md border border-slate-800 bg-slate-900 p-3 text-sm text-slate-400 hover:border-slate-600 hover:text-slate-300"
>
<span className="relative z-20">{resourceTitle}</span>
<span
className="absolute bottom-0 left-0 top-0 z-10 bg-[#172a3a]"
style={{ width: `${percentageDone}%` }}
></span>
{allowFavorite && (
<MarkFavorite
resourceId={resourceId}
resourceType={resourceType}
favorite={isFavorite}
/>
)}
</a>
);
}
type ProgressTitleProps = {
icon: any;
isLoading?: boolean;
title: string | ReactNode;
};
export function HeroTitle(props: ProgressTitleProps) {
const { isLoading = false, title, icon } = props;
return (
<p className="mb-4 flex items-center text-sm text-gray-400">
{!isLoading && icon}
{isLoading && (
<span className="mr-1.5">
<Spinner />
</span>
)}
{title}
</p>
);
}
export type HeroTeamRoadmaps = Record<string, UserProgressResponse>;
type ProgressListProps = {
progress: UserProgressResponse;
customRoadmaps: UserProgressResponse;
teamRoadmaps?: HeroTeamRoadmaps;
isLoading?: boolean;
};
export function HeroRoadmaps(props: ProgressListProps) {
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">
<FeatureAnnouncement />
</p>
{isCreatingRoadmap && (
<CreateRoadmapModal
teamId={creatingRoadmapTeamId}
onClose={() => {
setIsCreatingRoadmap(false);
setCreatingRoadmapTeamId(undefined);
}}
/>
)}
{
<HeroTitle
icon={
(<CheckIcon additionalClasses="mr-1.5 h-[14px] w-[14px]" />) as any
}
isLoading={isLoading}
title="Your progress and favorite roadmaps."
/>
}
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
{progress.map((resource) => (
<HeroRoadmap
key={`${resource.resourceType}-${resource.resourceId}`}
resourceId={resource.resourceId}
resourceType={resource.resourceType}
resourceTitle={resource.resourceTitle}
isFavorite={resource.isFavorite}
percentageDone={
((resource.skipped + resource.done) / resource.total) * 100
}
url={
resource.resourceType === 'roadmap'
? `/${resource.resourceId}`
: `/best-practices/${resource.resourceId}`
}
/>
))}
</div>
<div className="mt-5">
{
<HeroTitle
icon={<MapIcon className="mr-1.5 h-[14px] w-[14px]" />}
title="Your custom roadmaps"
/>
}
{customRoadmaps.length === 0 && (
<p className="rounded-md border border-dashed border-gray-800 p-2 text-sm text-gray-600">
You haven't created any custom roadmaps yet.{' '}
<button
className="text-gray-500 underline underline-offset-2 hover:text-gray-400"
onClick={() => setIsCreatingRoadmap(true)}
>
Create one!
</button>
</p>
)}
{customRoadmaps.length > 0 && (
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
{customRoadmaps.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/${customRoadmap?.roadmapSlug}`}
allowFavorite={false}
/>
);
})}
<CreateRoadmapButton />
</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/activity?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/${customRoadmap?.roadmapSlug}`}
allowFavorite={false}
/>
);
})}
{canManageTeam && (
<CreateRoadmapButton
teamId={currentTeam?.id}
text="Create Team Roadmap"
/>
)}
</div>
)}
</div>
);
})}
</div>
);
}

View File

@@ -1,71 +0,0 @@
import type { ReactNode } from 'react';
import { Spinner } from '../ReactIcons/Spinner.tsx';
import { ChevronDown, ChevronsDownUp, ChevronsUpDown } from 'lucide-react';
import { cn } from '../../lib/classname.ts';
type HeroTitleProps = {
icon: any;
isLoading?: boolean;
title: string | ReactNode;
rightContent?: ReactNode;
isCollapsed?: boolean;
onToggleCollapse?: () => void;
isEmpty?: boolean;
emptyTitle?: ReactNode;
};
export function HeroTitle(props: HeroTitleProps) {
const {
isLoading = false,
title,
icon,
rightContent,
isCollapsed = false,
onToggleCollapse,
isEmpty = false,
emptyTitle,
} = props;
return (
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<p className="flex items-center gap-0.5 text-sm text-gray-400">
{!isLoading && icon}
{isLoading && (
<span className="mr-1.5">
<Spinner />
</span>
)}
{!isEmpty ? title : emptyTitle || title}
</p>
</div>
<div className="flex items-center gap-2">
{!isCollapsed && rightContent}
{!isLoading && !isEmpty && (
<button
onClick={onToggleCollapse}
className={cn(
'ml-2 inline-flex items-center gap-1 rounded-md bg-slate-800 py-0.5 pl-1 pr-1.5 text-xs uppercase tracking-wider text-slate-400 hover:bg-slate-700',
{
'bg-slate-800 text-slate-500 hover:bg-slate-800 hover:text-slate-400':
!isCollapsed,
},
)}
>
{isCollapsed && (
<>
<ChevronsUpDown className="h-3.5 w-3.5" /> Expand
</>
)}
{!isCollapsed && (
<>
<ChevronsDownUp className="h-3.5 w-3.5" /> Collapse
</>
)}
</button>
)}
</div>
</div>
);
}

View File

@@ -1,8 +1,8 @@
import { httpGet } from '../../lib/http';
import { useEffect, useState } from 'react';
import { pageProgressMessage } from '../../stores/page';
import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps';
import { SelectionButton } from './SelectionButton';
import type { UserProgressResponse } from '../Roadmaps/RoadmapsPage';
type RoadmapSelectProps = {
selectedRoadmaps: string[];

View File

@@ -10,27 +10,8 @@ import {
} from '../../lib/browser.ts';
import { RoadmapCard } from './RoadmapCard.tsx';
import { httpGet } from '../../lib/http.ts';
import type { UserProgressResponse } from '../HeroSection/FavoriteRoadmaps.tsx';
import { isLoggedIn } from '../../lib/jwt.ts';
import type { AllowedMemberRoles } from '../ShareOptions/ShareTeamMemberList.tsx';
export type UserProgressResponse = {
resourceId: string;
resourceType: 'roadmap' | 'best-practice';
resourceTitle: string;
isFavorite: boolean;
done: number;
learning: number;
skipped: number;
total: number;
updatedAt: Date;
isCustomResource: boolean;
roadmapSlug?: string;
team?: {
name: string;
id: string;
role: AllowedMemberRoles;
};
}[];
const groupNames = [
'Absolute Beginners',
@@ -257,12 +238,6 @@ const groups: GroupType[] = [
type: 'skill',
otherGroups: ['Web Development'],
},
{
title: 'Cloudflare',
link: '/cloudflare',
type: 'skill',
otherGroups: ['Web Development'],
},
{
title: 'Linux',
link: '/linux',

View File

@@ -1,163 +0,0 @@
import { ChevronDownIcon, StarIcon, User2Icon } from 'lucide-react';
import { useState } from 'react';
import { cn } from '../../../editor/utils/classname';
import { markdownToHtml } from '../../lib/markdown';
type Review = {
name: string;
role: string;
rating: number;
text: string;
avatarUrl?: string;
};
export function ReviewsSection() {
const [isExpanded, setIsExpanded] = useState(false);
const reviews: Review[] = [
{
name: 'Tomáš Janků',
role: 'Software Engineer',
rating: 5,
text: "The course and it's interactivity is excellent and I'd honestly say it's **one of the best** on the SQL theme I've seen out there.",
avatarUrl: 'https://github.com/jankudev.png',
},
{
name: 'Gourav Khunger',
role: 'Software Engineer',
rating: 5,
text: 'This course was **absolutely brilliant!** The integrated database environment to practice what I learned was the best part.',
avatarUrl: 'https://github.com/gouravkhunger.png',
},
{
name: 'Meabed',
role: 'CTO',
rating: 5,
text: 'Kamran has **clearly put a lot of thought** into this course. The content, structure and exercises were all great.',
avatarUrl: 'https://github.com/meabed.png',
},
{
name: 'Mohsin Aheer',
role: 'Sr. Software Engineer',
rating: 5,
text: 'I already knew SQL but this course **taught me a bunch of new things.** Practical examples and challenges were great. Highly recommended!',
avatarUrl: 'https://github.com/aheermohsinse.png',
},
{
name: 'Reeve Tee',
role: 'Software Engineer',
rating: 5,
text: 'I found the course **highly comprehensive and incredibly valuable**. I would love to see more courses like this!',
avatarUrl: '',
},
{
name: 'Zeeshan',
role: 'Sr. Software Engineer',
rating: 5,
text: 'Loved the teaching style and the way the course was structured. The **AI tutor was a great help** when I got stuck.',
avatarUrl: 'https://github.com/ziishaned.png',
},
{
name: 'Adnan Ahmed',
role: 'Engineering Manager',
rating: 5,
text: 'Having the integrated IDE made a huge difference. Being able to immediately practice what I learned was **invaluable**.',
avatarUrl: 'https://github.com/idnan.png',
},
{
name: 'Kalvin Chakma',
role: 'Jr. Software Engineer',
rating: 5,
text: "Best SQL course I've taken. The progression from basic to advanced concepts is **well thought out**, and the challenges are **excellent**.",
avatarUrl: 'https://github.com/kalvin-chakma.png',
},
{
name: 'Faisal Ahsan',
role: 'Software Engineer',
rating: 5,
text: 'The course and the learning experience was great. What I really liked was the **no-fluff explanations** and practical examples.',
avatarUrl: 'https://github.com/faisalahsan.png',
},
];
return (
<div className="relative max-w-5xl">
<div
className={cn('rounded-2xl pb-0 pt-24', {
'pb-8': isExpanded,
})}
>
<div
className={cn(
'relative grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3',
isExpanded ? '' : 'max-h-[400px] overflow-hidden',
)}
>
{reviews.map((review, index) => (
<div
key={index}
className="review-testimonial flex-shrink-0 break-inside-avoid-column rounded-xl bg-zinc-800/30 p-6 backdrop-blur [&_strong]:font-normal [&_strong]:text-yellow-300/70"
>
<div className="flex items-center gap-4">
{review.avatarUrl && (
<img
src={review.avatarUrl}
alt={review.name}
className="h-12 w-12 rounded-full object-cover"
/>
)}
{!review.avatarUrl && (
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-zinc-800">
<User2Icon className="h-6 w-6 text-zinc-400" />
</div>
)}
<div>
<h3 className="font-semibold text-zinc-100">{review.name}</h3>
<p className="text-sm text-zinc-400">{review.role}</p>
</div>
</div>
<div className="mt-2 flex">
{Array.from({ length: review.rating }).map((_, i) => (
<StarIcon
key={i}
className="h-4 w-4 fill-yellow-500 text-yellow-500"
/>
))}
</div>
<p
className="mt-4 text-zinc-300"
dangerouslySetInnerHTML={{
__html: markdownToHtml(review.text),
}}
/>
</div>
))}
<div
className={cn(
'absolute bottom-0 left-0 right-0 h-40 bg-gradient-to-t from-[#121212] via-[#121212]/80 to-transparent',
isExpanded ? 'opacity-0' : 'opacity-100',
)}
/>
</div>
</div>
<div
className={cn('absolute left-1/2 top-full -translate-x-1/2', {
'-translate-y-1/2': !isExpanded,
})}
>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center gap-2 rounded-full bg-zinc-800 px-6 py-2 text-sm font-medium text-zinc-300 transition-all hover:bg-zinc-700 hover:text-zinc-100"
>
{isExpanded ? 'Show Less' : 'Show More Reviews'}
<ChevronDownIcon
className={`h-4 w-4 transition-transform ${
isExpanded ? 'rotate-180' : ''
}`}
/>
</button>
</div>
</div>
);
}

View File

@@ -14,7 +14,6 @@ import {
LayersIcon,
TableIcon,
WrenchIcon,
StarIcon,
} from 'lucide-react';
import { ChapterRow } from './ChapterRow';
import { CourseFeature } from './CourseFeature';
@@ -28,7 +27,6 @@ import { AccountButton } from './AccountButton';
import { RoadmapLogoIcon } from '../ReactIcons/RoadmapLogo';
import { PlatformDemo } from './PlatformDemo';
import { AuthorQuoteMessage } from './AuthorQuoteMessage';
import { ReviewsSection } from './ReviewsSection';
type ChapterData = {
icon: React.ReactNode;
title: string;
@@ -256,9 +254,8 @@ export function SQLCoursePage() {
</a>
<AccountButton />
</div>
<div className="relative mt-7 max-w-4xl text-left md:mt-20 md:text-center">
<div className="relative mt-7 max-w-3xl text-left md:mt-20 md:text-center">
<Spotlight className="left-[-170px] top-[-200px]" fill="#EAB308" />
<div className="inline-block rounded-full bg-yellow-500/10 px-4 py-1.5 text-base text-yellow-500 md:px-6 md:py-2 md:text-lg">
<span className="hidden sm:block">
Complete Course to Master Practical SQL
@@ -302,8 +299,6 @@ export function SQLCoursePage() {
</div>
</div>
<ReviewsSection />
<AuthorQuoteMessage />
<PlatformDemo />

View File

@@ -1,8 +1,7 @@
import '../FrameRenderer/FrameRenderer.css';
import '../EditorRoadmap/EditorRoadmapRenderer.css';
import { useEffect, useRef, useState } from 'react';
import { wireframeJSONToSVG } from 'roadmap-renderer';
import { Spinner } from '../ReactIcons/Spinner';
import '../FrameRenderer/FrameRenderer.css';
import { useOutsideClick } from '../../hooks/use-outside-click';
import { useKeydown } from '../../hooks/use-keydown';
import type { TeamMember } from './TeamProgressPage';
@@ -60,7 +59,6 @@ export function MemberProgressModal(props: ProgressMapProps) {
useState<MemberProgressResponse>();
const [isLoading, setIsLoading] = useState(true);
const toast = useToast();
const [renderer, setRenderer] = useState<PageType['renderer']>('balsamiq');
let resourceJsonUrl = import.meta.env.DEV
? 'http://localhost:3000'
@@ -100,7 +98,6 @@ export function MemberProgressModal(props: ProgressMapProps) {
}
const renderer = page.renderer || 'balsamiq';
setRenderer(renderer);
const res = await fetch(jsonUrl, {});
const json = await res.json();
@@ -278,7 +275,7 @@ export function MemberProgressModal(props: ProgressMapProps) {
return (
<div className="fixed left-0 right-0 top-0 z-[100] h-full items-center justify-center overflow-y-auto overflow-x-hidden overscroll-contain bg-black/50">
<div
id={renderer === 'editor' ? undefined : 'customized-roadmap'}
id={'customized-roadmap'}
className="relative mx-auto h-full w-full max-w-4xl p-4 md:h-auto"
>
<div

View File

@@ -1,15 +1,16 @@
import { useStore } from '@nanostores/react';
import { useEffect, useState } from 'react';
import { useAuth } from '../../hooks/use-auth';
import { useToast } from '../../hooks/use-toast';
import { getUrlParams, setUrlParams } from '../../lib/browser';
import { httpGet } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page';
import { MemberProgressItem } from './MemberProgressItem';
import { useToast } from '../../hooks/use-toast';
import { useStore } from '@nanostores/react';
import { $currentTeam } from '../../stores/team';
import { GroupRoadmapItem } from './GroupRoadmapItem';
import { MemberCustomProgressModal } from './MemberCustomProgressModal';
import { MemberProgressItem } from './MemberProgressItem';
import { getUrlParams, setUrlParams } from '../../lib/browser';
import { useAuth } from '../../hooks/use-auth';
import { MemberProgressModal } from './MemberProgressModal';
import { MemberCustomProgressModal } from './MemberCustomProgressModal';
import { canManageCurrentRoadmap } from '../../stores/roadmap.ts';
export type UserProgress = {
resourceTitle: string;

View File

@@ -0,0 +1,40 @@
---
import type { VideoFileType } from '../lib/video';
export interface Props {
video: VideoFileType;
}
const { video } = Astro.props;
const { frontmatter, id } = video;
---
<a
class:list={[
'block no-underline py-2 group text-md items-center text-gray-600 hover:text-blue-600 flex justify-between border-b',
]}
href={`/videos/${id}`}
>
<span class='group-hover:translate-x-2 transition-transform'>
{frontmatter.title}
{
frontmatter.isNew && (
<span class='bg-green-300 text-green-900 text-xs font-medium px-1.5 py-0.5 rounded-sm uppercase ml-1.5'>
New
<span class='hidden sm:inline'>
&middot;
{new Date(frontmatter.date).toLocaleString('default', {
month: 'long',
})}
</span>
</span>
)
}
</span>
<span class='capitalize text-gray-500 text-xs hidden sm:block'>
{frontmatter.duration}
</span>
<span class='text-gray-400 text-xs block sm:hidden'> &raquo;</span>
</a>

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,6 @@ Machine learning is a field of artificial intelligence that uses statistical tec
Learn more from the following resources:
- [@article@Advantages and Disadvantages of AI](https://medium.com/@laners.org/advantages-and-disadvantages-of-artificial-intelligence-cd6e42819b20)
- [@article@Reinforcement Learning 101](https://medium.com/towards-data-science/reinforcement-learning-101-e24b50e1d292)
- [@article@Understanding AUC-ROC Curve](https://medium.com/towards-data-science/understanding-auc-roc-curve-68b2303cc9c5)
- [@article@Advantages and Disadvantages of AI](https://towardsdatascience.com/advantages-and-disadvantages-of-artificial-intelligence-182a5ef6588c)
- [@article@Reinforcement Learning 101](https://towardsdatascience.com/reinforcement-learning-101-e24b50e1d292)
- [@article@Understanding AUC-ROC Curve](https://towardsdatascience.com/understanding-auc-roc-curve-68b2303cc9c5)

File diff suppressed because it is too large Load Diff

View File

@@ -1,64 +0,0 @@
---
jsonUrl: '/jsons/roadmaps/cloudflare.json'
pdfUrl: '/pdfs/roadmaps/cloudflare.pdf'
order: 21
briefTitle: 'Cloudflare'
briefDescription: 'Learn to deploy your applications on Cloudflare'
title: 'Cloudflare'
description: 'Learn to deploy your applications on Cloudflare'
isNew: true
hasTopics: true
renderer: editor
dimensions:
width: 968
height: 2700
schema:
headline: 'Cloudflare Roadmap'
description: 'Learn how to use Cloudflare with this interactive step by step guide in 2025. 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/cloudflare.png'
datePublished: '2025-02-12'
dateModified: '2025-02-12'
seo:
title: 'Cloudflare Roadmap - roadmap.sh'
description: 'Step by step guide to learn Cloudflare in 2025. 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:
- 'cloudflare tutorial'
- 'step by step guide for cloudflare'
- 'cloudflare for beginners'
- 'how to learn cloudflare'
- 'use cloudflare in production'
- 'cloudflare roadmap 2024'
- 'guide to learning cloudflare'
- 'cloudflare roadmap'
- 'cloudflare learning path'
- 'cloudflare learning roadmap'
- 'container roadmap'
- 'cloudflare'
- 'cloudflare learning guide'
- 'cloudflare skills'
- 'cloudflare for development'
- 'cloudflare for development skills'
- 'cloudflare for development skills test'
- 'cloudflare learning guide'
- 'become a cloudflare expert'
- 'cloudflare career path'
- 'learn cloudflare for development'
- 'what is cloudflare'
- 'cloudflare quiz'
- 'cloudflare interview questions'
relatedRoadmaps:
- 'devops'
- 'backend'
- 'full-stack'
- 'javascript'
- 'nodejs'
- 'aws'
- 'linux'
sitemap:
priority: 1
changefreq: 'monthly'
tags:
- 'roadmap'
- 'main-sitemap'
- 'skill-roadmap'
---

View File

@@ -1 +0,0 @@
# Basic Command-line Knowledge

View File

@@ -1,13 +0,0 @@
# Hono
Hono is a small, simple and ultrafast web framework built on web standards. It works on any JavaScript runtime: Cloudflare Workers, Fastly Compute, Deno, Bun, Vercel, Netlify, AWS Lambda, Lambda@Edge, and Node.js. Hono is more known for supporting a lot more than the basics.
## Use-cases
Hono is a simple web application framework similar to the well known javascript framework Express, without a frontend. But it runs on CDN Edges and allows you to construct larger applications when combined with middleware.
Visit the following resources to learn more:
- [@official@Official Documentation](https://hono.dev/docs/)
- [@article@Hono.js: A Small Framework with Big Potential](https://medium.com/@appvintechnologies/hono-js-a-small-framework-with-big-potential-15a093fc5c07)
- [@article@Quick Start with Hono: Simple Setup Guide](https://dev.to/koshirok096/quick-start-with-hono-simple-setup-guide-bite-sized-article-lhe)
- [@opensource@Hono JS Examples](https://github.com/honojs/examples)
- [@video@Learn with me](https://www.youtube.com/watch?v=gY-TK33G6kQ)

View File

@@ -1,19 +0,0 @@
# Itty Router
Itty Router is a lightweight router with the motto "less is more" that supports Cloudflare workers and pages. While other libraries may suffer from feature creep/bloat to please a wider audience, Itty Router painfully consider every single byte added to itty. Our router options range from ~450 bytes to ~970 bytes for a batteries-included version with built-in defaults, error handling, formatting, etc. On top of that, the following concepts aim to keep YOUR code tiny (and readable) as well.
## Simple Projects ideas
Itty Router is a lightweight router system that supports typescript. You can create easy and good routers for Cloudflare workers or pages.
With a simple project like a URL shortener, you can use Itty Router and Cloudflare KV.
Other project ideas can be found:
- Webhook Relay
- Transform webhook data or API data towards another API so you can transform the data as you like.
- Micro URL Monitoring
- Monitor any URL and give back responses on the specific endpoint.
- Single-Use Download Links (Watch out for costs from Cloudflare!)
- Generate links that expire after one download, ideal for file sharing.
Visit the following resources to learn more:
- [@official@Official Documentation](https://itty.dev/itty-router/)

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