Skip to content

Commit 183fd28

Browse files
Merge pull request #126 from kamranahmedse/master
Create a new pull request by comparing changes across two branches
2 parents 1bdb3d7 + f838b5d commit 183fd28

File tree

17 files changed

+1402
-236
lines changed

17 files changed

+1402
-236
lines changed

src/components/AIAnnouncement.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
type AIAnnouncementProps = {};
2+
3+
export function AIAnnouncement(props: AIAnnouncementProps) {
4+
return (
5+
<a
6+
className="rounded-md border border-dashed border-purple-600 px-3 py-1.5 text-purple-400 transition-colors hover:border-purple-400 hover:text-purple-200"
7+
href="/ai"
8+
>
9+
<span className="relative -top-[1px] mr-1 text-xs font-semibold uppercase text-white">
10+
New
11+
</span>{' '}
12+
<span className={'hidden sm:inline'}>Generate visual roadmaps with AI</span>
13+
<span className={'inline text-sm sm:hidden'}>AI Roadmap Generator!</span>
14+
</a>
15+
);
16+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { useCallback, useEffect, useState } from 'react';
2+
import { useToast } from '../../hooks/use-toast';
3+
import { httpGet } from '../../lib/http';
4+
import { getRelativeTimeString } from '../../lib/date';
5+
import { Eye, Loader2, RefreshCcw } from 'lucide-react';
6+
import { AIRoadmapAlert } from '../GenerateRoadmap/AIRoadmapAlert.tsx';
7+
8+
export interface AIRoadmapDocument {
9+
_id?: string;
10+
term: string;
11+
title: string;
12+
data: string;
13+
viewCount: number;
14+
createdAt: Date;
15+
updatedAt: Date;
16+
}
17+
18+
type ExploreRoadmapsResponse = {
19+
data: AIRoadmapDocument[];
20+
totalCount: number;
21+
totalPages: number;
22+
currPage: number;
23+
perPage: number;
24+
};
25+
26+
export function ExploreAIRoadmap() {
27+
const toast = useToast();
28+
29+
const [isLoading, setIsLoading] = useState(true);
30+
const [isLoadingMore, setIsLoadingMore] = useState(false);
31+
const [roadmaps, setRoadmaps] = useState<AIRoadmapDocument[]>([]);
32+
const [currPage, setCurrPage] = useState(1);
33+
const [totalPages, setTotalPages] = useState(1);
34+
35+
const loadAIRoadmaps = useCallback(
36+
async (currPage: number) => {
37+
const { response, error } = await httpGet<ExploreRoadmapsResponse>(
38+
`${import.meta.env.PUBLIC_API_URL}/v1-list-ai-roadmaps`,
39+
{
40+
currPage,
41+
},
42+
);
43+
44+
if (error || !response) {
45+
toast.error(error?.message || 'Something went wrong');
46+
return;
47+
}
48+
49+
const newRoadmaps = [...roadmaps, ...response.data];
50+
if (
51+
JSON.stringify(roadmaps) === JSON.stringify(response.data) ||
52+
JSON.stringify(roadmaps) === JSON.stringify(newRoadmaps)
53+
) {
54+
return;
55+
}
56+
57+
setRoadmaps(newRoadmaps);
58+
setCurrPage(response.currPage);
59+
setTotalPages(response.totalPages);
60+
},
61+
[currPage, roadmaps],
62+
);
63+
64+
useEffect(() => {
65+
loadAIRoadmaps(currPage).finally(() => {
66+
setIsLoading(false);
67+
});
68+
}, []);
69+
70+
const hasMorePages = currPage < totalPages;
71+
72+
return (
73+
<section className="container mx-auto py-3 sm:py-6">
74+
<div className="mb-6">
75+
<AIRoadmapAlert isListing />
76+
</div>
77+
78+
{isLoading ? (
79+
<ul className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
80+
{new Array(21).fill(0).map((_, index) => (
81+
<li
82+
key={index}
83+
className="h-[75px] animate-pulse rounded-md border bg-gray-100"
84+
></li>
85+
))}
86+
</ul>
87+
) : (
88+
<div>
89+
{roadmaps?.length === 0 ? (
90+
<div className="text-center text-gray-800">No roadmaps found</div>
91+
) : (
92+
<>
93+
<ul className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
94+
{roadmaps.map((roadmap) => {
95+
const roadmapLink = `/ai?id=${roadmap._id}`;
96+
return (
97+
<a
98+
key={roadmap._id}
99+
href={roadmapLink}
100+
className="flex flex-col rounded-md border transition-colors hover:bg-gray-100"
101+
target={'_blank'}
102+
>
103+
<h2 className="flex-grow px-2.5 py-2.5 text-base font-medium leading-tight">
104+
{roadmap.title}
105+
</h2>
106+
<div className="flex items-center justify-between gap-2 px-2.5 py-2">
107+
<span className="flex items-center gap-1.5 text-xs text-gray-400">
108+
<Eye size={15} className="inline-block" />
109+
{Intl.NumberFormat('en-US', {
110+
notation: 'compact',
111+
}).format(roadmap.viewCount)}{' '}
112+
views
113+
</span>
114+
<span className="flex items-center gap-1.5 text-xs text-gray-400">
115+
{getRelativeTimeString(String(roadmap?.createdAt))}
116+
</span>
117+
</div>
118+
</a>
119+
);
120+
})}
121+
</ul>
122+
{hasMorePages && (
123+
<div className="my-5 flex items-center justify-center">
124+
<button
125+
onClick={() => {
126+
setIsLoadingMore(true);
127+
loadAIRoadmaps(currPage + 1).finally(() => {
128+
setIsLoadingMore(false);
129+
});
130+
}}
131+
className="inline-flex items-center gap-1.5 rounded-full bg-black px-3 py-1.5 text-sm font-medium text-white shadow-xl transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
132+
disabled={isLoadingMore}
133+
>
134+
{isLoadingMore ? (
135+
<Loader2 className="h-4 w-4 animate-spin stroke-[2.5]" />
136+
) : (
137+
<RefreshCcw className="h-4 w-4 stroke-[2.5]" />
138+
)}
139+
Load More
140+
</button>
141+
</div>
142+
)}
143+
</>
144+
)}
145+
</div>
146+
)}
147+
</section>
148+
);
149+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { BadgeCheck, Telescope, Wand } from 'lucide-react';
2+
3+
type AIRoadmapAlertProps = {
4+
isListing?: boolean;
5+
};
6+
7+
export function AIRoadmapAlert(props: AIRoadmapAlertProps) {
8+
const { isListing = false } = props;
9+
10+
return (
11+
<div className="mb-3 w-full rounded-xl bg-yellow-100 px-4 py-3 text-yellow-800">
12+
<h2 className="flex items-center text-base font-semibold text-yellow-800 sm:text-lg">
13+
AI Generated Roadmap{isListing ? 's' : ''}{' '}
14+
<span className="ml-1.5 rounded-md border border-yellow-500 bg-yellow-200 px-1.5 text-xs uppercase tracking-wide text-yellow-800">
15+
Beta
16+
</span>
17+
</h2>
18+
<p className="mb-2 mt-1">
19+
{isListing
20+
? 'These are AI generated roadmaps and are not verified by'
21+
: 'This is an AI generated roadmap and is not verified by'}{' '}
22+
<span className={'font-semibold'}>roadmap.sh</span>. We are currently in
23+
beta and working hard to improve the quality of the generated roadmaps.
24+
</p>
25+
<p className="mb-1.5 mt-2 flex flex-col gap-2 text-sm sm:flex-row">
26+
{isListing ? (
27+
<a
28+
href="/ai"
29+
className="flex items-center gap-1.5 rounded-md border border-yellow-600 px-2 py-1 text-yellow-700 transition-colors hover:bg-yellow-300 hover:text-yellow-800"
30+
>
31+
<Wand size={15} />
32+
Create your own Roadmap with AI
33+
</a>
34+
) : (
35+
<a
36+
href="/ai/explore"
37+
className="flex items-center gap-1.5 rounded-md border border-yellow-600 px-2 py-1 text-yellow-700 transition-colors hover:bg-yellow-300 hover:text-yellow-800"
38+
>
39+
<Telescope size={15} />
40+
Explore other AI Roadmaps
41+
</a>
42+
)}
43+
<a
44+
href="/roadmaps"
45+
className="flex items-center gap-1.5 rounded-md border border-yellow-600 bg-yellow-200 px-2 py-1 text-yellow-800 transition-colors hover:bg-yellow-300"
46+
>
47+
<BadgeCheck size={15} />
48+
Visit Official Roadmaps
49+
</a>
50+
</p>
51+
</div>
52+
);
53+
}

0 commit comments

Comments
 (0)