Refactor UI for ai

fix/ai-roadmap
Kamran Ahmed 9 months ago
parent 058f5c3203
commit 00fa41773c
  1. 116
      src/components/ExploreAIRoadmap/ExploreAIRoadmap.tsx
  2. 53
      src/components/GenerateRoadmap/AIRoadmapAlert.tsx
  3. 94
      src/components/GenerateRoadmap/GenerateRoadmap.tsx
  4. 28
      src/components/GenerateRoadmap/RoadmapSearch.tsx
  5. 2
      src/components/GenerateRoadmap/RoadmapTopicDetail.tsx
  6. 4
      src/lib/date.ts

@ -1,19 +1,14 @@
import { useEffect, useState, useCallback } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { getRelativeTimeString } from '../../lib/date'; import { getRelativeTimeString } from '../../lib/date';
import { import { Eye, Loader2, RefreshCcw } from 'lucide-react';
BadgeCheck, import { AIRoadmapAlert } from '../GenerateRoadmap/AIRoadmapAlert.tsx';
CalendarCheck,
Eye,
Loader2,
RefreshCcw,
Sparkles,
} from 'lucide-react';
export interface AIRoadmapDocument { export interface AIRoadmapDocument {
_id?: string; _id?: string;
topic: string; term: string;
title: string;
data: string; data: string;
viewCount: number; viewCount: number;
createdAt: Date; createdAt: Date;
@ -37,7 +32,7 @@ export function ExploreAIRoadmap() {
const [currPage, setCurrPage] = useState(1); const [currPage, setCurrPage] = useState(1);
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const loadAIRoadamps = useCallback( const loadAIRoadmaps = useCallback(
async (currPage: number) => { async (currPage: number) => {
const { response, error } = await httpGet<ExploreRoadmapsResponse>( const { response, error } = await httpGet<ExploreRoadmapsResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-list-ai-roadmaps`, `${import.meta.env.PUBLIC_API_URL}/v1-list-ai-roadmaps`,
@ -67,7 +62,7 @@ export function ExploreAIRoadmap() {
); );
useEffect(() => { useEffect(() => {
loadAIRoadamps(currPage).finally(() => { loadAIRoadmaps(currPage).finally(() => {
setIsLoading(false); setIsLoading(false);
}); });
}, []); }, []);
@ -75,99 +70,64 @@ export function ExploreAIRoadmap() {
const hasMorePages = currPage < totalPages; const hasMorePages = currPage < totalPages;
return ( return (
<section className="container mx-auto"> <section className="container mx-auto py-3 sm:py-6">
<div className="border-b pb-8 pt-10"> <div className="mb-6">
<h2 className="text-base font-semibold text-gray-800 sm:text-lg"> <AIRoadmapAlert isListing />
Explore AI Generated Roadmaps
</h2>
<p className="mb-2.5 mt-2 text-balance text-sm text-gray-800 sm:mb-1.5 sm:mt-1 sm:text-base">
These roadmaps are generated by AI based on the data and the topic
community has provided. You can also create your own roadmap and share
it with the community.
</p>
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center">
<a
href="/roadmaps"
className="inline-flex items-center gap-1.5 text-sm font-semibold text-gray-700 underline-offset-2 hover:underline"
>
<BadgeCheck className="h-4 w-4 stroke-[2.5]" />
Visit Official Roadmaps
</a>
<span className="hidden font-black text-gray-700 sm:block">
&middot;
</span>
<a
href="/ai"
className="inline-flex items-center gap-1.5 text-sm font-semibold text-blue-600 underline-offset-2 hover:underline"
>
<Sparkles className="h-4 w-4 stroke-[2.5]" />
Generate Your Own Roadmap
</a>
</div>
</div> </div>
{isLoading ? ( {isLoading ? (
<ul className="mt-8 grid grid-cols-3 gap-2"> <ul className="grid grid-cols-3 gap-2">
{new Array(6).fill(0).map((_, index) => ( {new Array(21).fill(0).map((_, index) => (
<li <li
key={index} key={index}
className="animate-pulse rounded-md border bg-gray-100" className="h-[75px] animate-pulse rounded-md border bg-gray-100"
> ></li>
<div className="h-10 w-full"></div>
<div className="flex items-center justify-between gap-2 border-t px-2.5 py-2">
<span className="flex items-center gap-1.5 text-sm text-gray-600">
<Eye size={15} className="inline-block" />
<span>{index} views</span>
</span>
<span className="flex items-center gap-1.5 text-sm text-gray-600">
<CalendarCheck size={15} className="inline-block" />
<span>{index}h ago</span>
</span>
</div>
</li>
))} ))}
</ul> </ul>
) : ( ) : (
<div className="mt-8"> <div>
{roadmaps?.length === 0 ? ( {roadmaps?.length === 0 ? (
<div className="text-center text-gray-800">No roadmaps found</div> <div className="text-center text-gray-800">No roadmaps found</div>
) : ( ) : (
<>
<ul className="grid grid-cols-3 gap-2"> <ul className="grid grid-cols-3 gap-2">
{roadmaps.map((roadmap) => { {roadmaps.map((roadmap) => {
const roadmapLink = `/ai?id=${roadmap._id}`; const roadmapLink = `/ai?id=${roadmap._id}`;
return ( return (
<li key={roadmap._id}> <a
<a href={roadmapLink} className="block rounded-md border"> key={roadmap._id}
<h2 href={roadmapLink}
className="truncate px-2.5 py-2.5 text-xl font-medium leading-none tracking-wide" className="flex flex-col rounded-md border transition-colors hover:bg-gray-100"
title={roadmap.topic}
> >
{roadmap.topic} <h2 className="flex-grow px-2.5 py-2.5 text-base font-medium leading-tight">
{roadmap.title}
</h2> </h2>
<div className="flex items-center justify-between gap-2 border-t px-2.5 py-2"> <div className="flex items-center justify-between gap-2 px-2.5 py-2">
<span className="flex items-center gap-1.5 text-sm text-gray-600"> <span className="flex items-center gap-1.5 text-xs text-gray-400">
<Eye size={15} className="inline-block" /> <Eye size={15} className="inline-block" />
<span>{roadmap?.viewCount || 0} views</span> {Intl.NumberFormat('en-US', {
notation: 'compact',
}).format(roadmap.viewCount)}{' '}
views
</span> </span>
<span className="flex items-center gap-1.5 text-sm text-gray-600"> <span className="flex items-center gap-1.5 text-xs text-gray-400">
<CalendarCheck size={15} className="inline-block" />
<span>
{getRelativeTimeString(String(roadmap?.createdAt))} {getRelativeTimeString(String(roadmap?.createdAt))}
</span> </span>
</span>
</div> </div>
</a> </a>
</li>
); );
})} })}
</ul>
{hasMorePages && ( {hasMorePages && (
<li> <div className='my-5 flex items-center justify-center'>
<button <button
onClick={async () => { onClick={() => {
setIsLoadingMore(true); setIsLoadingMore(true);
await loadAIRoadamps(currPage + 1); loadAIRoadmaps(currPage + 1).finally(() => {
setIsLoadingMore(false); setIsLoadingMore(false);
});
}} }}
className="flex h-full min-h-[79px] w-full items-center justify-center gap-1.5 rounded-md border bg-gray-100 font-medium text-gray-900 hover:bg-gray-100/80 disabled:cursor-not-allowed disabled:opacity-50" className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-black transition-colors rounded-full shadow-xl focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
disabled={isLoadingMore} disabled={isLoadingMore}
> >
{isLoadingMore ? ( {isLoadingMore ? (
@ -177,9 +137,9 @@ export function ExploreAIRoadmap() {
)} )}
Load More Load More
</button> </button>
</li> </div>
)} )}
</ul> </>
)} )}
</div> </div>
)} )}

@ -0,0 +1,53 @@
import { BadgeCheck, Telescope, Wand } from 'lucide-react';
type AIRoadmapAlertProps = {
isListing?: boolean;
};
export function AIRoadmapAlert(props: AIRoadmapAlertProps) {
const { isListing = false } = props;
return (
<div className="mb-3 w-full rounded-xl bg-yellow-100 px-4 py-3 text-yellow-800">
<h2 className="flex items-center text-base font-semibold text-yellow-800 sm:text-lg">
AI Generated Roadmap{isListing ? 's' : ''}{' '}
<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">
Beta
</span>
</h2>
<p className="mb-2 mt-1">
{isListing
? 'These are AI generated roadmaps and are not verified by'
: 'This is an AI generated roadmap and is not verified by'}{' '}
<span className={'font-semibold'}>roadmap.sh</span>. We are currently in
beta and working hard to improve the quality of the generated roadmaps.
</p>
<p className="mb-1.5 mt-2 flex flex-col gap-2 text-sm sm:flex-row">
{isListing ? (
<a
href="/ai"
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"
>
<Wand size={15} />
Create your own Roadmap with AI
</a>
) : (
<a
href="/ai/explore"
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"
>
<Telescope size={15} />
Explore other AI Roadmaps
</a>
)}
<a
href="/roadmaps"
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"
>
<BadgeCheck size={15} />
Visit Official Roadmaps
</a>
</p>
</div>
);
}

@ -15,15 +15,7 @@ import { readAIRoadmapStream } from '../../helper/read-stream';
import { isLoggedIn, removeAuthToken, visitAIRoadmap } from '../../lib/jwt'; import { isLoggedIn, removeAuthToken, visitAIRoadmap } from '../../lib/jwt';
import { RoadmapSearch } from './RoadmapSearch.tsx'; import { RoadmapSearch } from './RoadmapSearch.tsx';
import { Spinner } from '../ReactIcons/Spinner.tsx'; import { Spinner } from '../ReactIcons/Spinner.tsx';
import { import { Ban, Download, PenSquare, Save, Wand } from 'lucide-react';
BadgeCheck,
Ban,
Download,
PenSquare,
Save,
Telescope,
Wand,
} from 'lucide-react';
import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx'; import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx';
import { httpGet, httpPost } from '../../lib/http.ts'; import { httpGet, httpPost } from '../../lib/http.ts';
import { pageProgressMessage } from '../../stores/page.ts'; import { pageProgressMessage } from '../../stores/page.ts';
@ -36,6 +28,7 @@ import { downloadGeneratedRoadmapImage } from '../../helper/download-image.ts';
import { showLoginPopup } from '../../lib/popup.ts'; import { showLoginPopup } from '../../lib/popup.ts';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx'; import { RoadmapTopicDetail } from './RoadmapTopicDetail.tsx';
import { AIRoadmapAlert } from './AIRoadmapAlert.tsx';
export type GetAIRoadmapLimitResponse = { export type GetAIRoadmapLimitResponse = {
used: number; used: number;
@ -77,7 +70,8 @@ export const allowedClickableNodeTypes = [
type GetAIRoadmapResponse = { type GetAIRoadmapResponse = {
id: string; id: string;
topic: string; term: string;
title: string;
data: string; data: string;
}; };
@ -89,7 +83,7 @@ export function GenerateRoadmap() {
const [hasSubmitted, setHasSubmitted] = useState<boolean>(false); const [hasSubmitted, setHasSubmitted] = useState<boolean>(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [roadmapTopic, setRoadmapTopic] = useState(''); const [roadmapTerm, setRoadmapTerm] = useState('');
const [generatedRoadmapContent, setGeneratedRoadmapContent] = useState(''); const [generatedRoadmapContent, setGeneratedRoadmapContent] = useState('');
const [currentRoadmap, setCurrentRoadmap] = const [currentRoadmap, setCurrentRoadmap] =
useState<GetAIRoadmapResponse | null>(null); useState<GetAIRoadmapResponse | null>(null);
@ -110,7 +104,7 @@ export function GenerateRoadmap() {
} }
}; };
const loadTopic = async (topic: string) => { const loadTermRoadmap = async (term: string) => {
setIsLoading(true); setIsLoading(true);
setHasSubmitted(true); setHasSubmitted(true);
@ -131,7 +125,7 @@ export function GenerateRoadmap() {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
credentials: 'include', credentials: 'include',
body: JSON.stringify({ topic: topic }), body: JSON.stringify({ term }),
}, },
); );
@ -167,7 +161,8 @@ export function GenerateRoadmap() {
result = result.replace(ROADMAP_ID_REGEX, ''); result = result.replace(ROADMAP_ID_REGEX, '');
setCurrentRoadmap({ setCurrentRoadmap({
id: roadmapId, id: roadmapId,
topic: roadmapTopic, term: roadmapTerm,
title: term,
data: result, data: result,
}); });
} }
@ -186,15 +181,15 @@ export function GenerateRoadmap() {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
if (!roadmapTopic) { if (!roadmapTerm) {
return; return;
} }
if (roadmapTopic === currentRoadmap?.topic) { if (roadmapTerm === currentRoadmap?.topic) {
return; return;
} }
loadTopic(roadmapTopic); loadTermRoadmap(roadmapTerm).finally(() => null);
}; };
const saveAIRoadmap = async () => { const saveAIRoadmap = async () => {
@ -212,7 +207,7 @@ export function GenerateRoadmap() {
}>( }>(
`${import.meta.env.PUBLIC_API_URL}/v1-save-ai-roadmap/${currentRoadmap?.id}`, `${import.meta.env.PUBLIC_API_URL}/v1-save-ai-roadmap/${currentRoadmap?.id}`,
{ {
title: roadmapTopic, title: roadmapTerm,
nodes: nodes.map((node) => ({ nodes: nodes.map((node) => ({
...node, ...node,
@ -252,7 +247,7 @@ export function GenerateRoadmap() {
} }
try { try {
await downloadGeneratedRoadmapImage(roadmapTopic, node); await downloadGeneratedRoadmapImage(roadmapTerm, node);
pageProgressMessage.set(''); pageProgressMessage.set('');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -291,15 +286,17 @@ export function GenerateRoadmap() {
return; return;
} }
const { topic, data } = response; const { term, title, data } = response;
await renderRoadmap(data); await renderRoadmap(data);
setCurrentRoadmap({ setCurrentRoadmap({
id: roadmapId, id: roadmapId,
topic, title: title,
term: term,
data, data,
}); });
setRoadmapTopic(topic);
setRoadmapTerm(title);
setGeneratedRoadmapContent(data); setGeneratedRoadmapContent(data);
visitAIRoadmap(roadmapId); visitAIRoadmap(roadmapId);
}; };
@ -360,14 +357,14 @@ export function GenerateRoadmap() {
if (!hasSubmitted) { if (!hasSubmitted) {
return ( return (
<RoadmapSearch <RoadmapSearch
roadmapTopic={roadmapTopic} roadmapTerm={roadmapTerm}
setRoadmapTopic={setRoadmapTopic} setRoadmapTerm={setRoadmapTerm}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
limit={roadmapLimit} limit={roadmapLimit}
limitUsed={roadmapLimitUsed} limitUsed={roadmapLimitUsed}
onLoadTopic={(topic: string) => { onLoadTerm={(term: string) => {
setRoadmapTopic(topic); setRoadmapTerm(term);
loadTopic(topic).finally(() => {}); loadTermRoadmap(term).finally(() => {});
}} }}
/> />
); );
@ -406,37 +403,8 @@ export function GenerateRoadmap() {
</span> </span>
)} )}
{!isLoading && ( {!isLoading && (
<div className="flex max-w-[750px] flex-grow flex-col items-center px-5"> <div className="container flex flex-grow flex-col items-center">
<div className="mb-3 w-full rounded-md bg-yellow-100 px-3 py-2 text-yellow-800"> <AIRoadmapAlert />
<h2 className="flex items-center text-base font-semibold text-yellow-800 sm:text-lg">
AI Generated Roadmap{' '}
<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">
Beta
</span>
</h2>
<p className="mb-2 mt-1">
This is an AI generated roadmap and is not verified by{' '}
<span className={'font-semibold'}>roadmap.sh</span>. We are
currently in beta and working hard to improve the quality of
the generated roadmaps.
</p>
<p className="mb-1.5 mt-2 flex gap-2 text-sm">
<a
href="/ai/explore"
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"
>
<Telescope size={15} />
Explore other AI Roadmaps
</a>
<a
href="/roadmaps"
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"
>
<BadgeCheck size={15} />
Visit Official Roadmaps
</a>
</p>
</div>
<div className="mt-2 flex w-full flex-col items-start justify-between gap-2 text-sm sm:flex-row sm:items-center sm:gap-0"> <div className="mt-2 flex w-full flex-col items-start justify-between gap-2 text-sm sm:flex-row sm:items-center sm:gap-0">
<span> <span>
<span <span
@ -474,9 +442,9 @@ export function GenerateRoadmap() {
autoFocus autoFocus
placeholder="e.g. Try searching for Ansible or DevOps" placeholder="e.g. Try searching for Ansible or DevOps"
className="flex-grow rounded-md border border-gray-400 px-3 py-2 transition-colors focus:border-black focus:outline-none" className="flex-grow rounded-md border border-gray-400 px-3 py-2 transition-colors focus:border-black focus:outline-none"
value={roadmapTopic} value={roadmapTerm}
onInput={(e) => onInput={(e) =>
setRoadmapTopic((e.target as HTMLInputElement).value) setRoadmapTerm((e.target as HTMLInputElement).value)
} }
/> />
<button <button
@ -487,9 +455,9 @@ export function GenerateRoadmap() {
)} )}
disabled={ disabled={
!roadmapLimit || !roadmapLimit ||
!roadmapTopic || !roadmapTerm ||
roadmapLimitUsed >= roadmapLimit || roadmapLimitUsed >= roadmapLimit ||
roadmapTopic === currentRoadmap?.topic roadmapTerm === currentRoadmap?.term
} }
> >
{roadmapLimit > 0 && canGenerateMore && ( {roadmapLimit > 0 && canGenerateMore && (
@ -520,7 +488,7 @@ export function GenerateRoadmap() {
</button> </button>
{roadmapId && ( {roadmapId && (
<ShareRoadmapButton <ShareRoadmapButton
description={`Check out ${roadmapTopic} roadmap I generated on roadmap.sh`} description={`Check out ${roadmapTerm} roadmap I generated on roadmap.sh`}
pageUrl={pageUrl} pageUrl={pageUrl}
/> />
)} )}

@ -11,27 +11,27 @@ import { showLoginPopup } from '../../lib/popup';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
type RoadmapSearchProps = { type RoadmapSearchProps = {
roadmapTopic: string; roadmapTerm: string;
setRoadmapTopic: (topic: string) => void; setRoadmapTerm: (topic: string) => void;
handleSubmit: (e: FormEvent<HTMLFormElement>) => void; handleSubmit: (e: FormEvent<HTMLFormElement>) => void;
onLoadTopic: (topic: string) => void; onLoadTerm: (topic: string) => void;
limit: number; limit: number;
limitUsed: number; limitUsed: number;
}; };
export function RoadmapSearch(props: RoadmapSearchProps) { export function RoadmapSearch(props: RoadmapSearchProps) {
const { const {
roadmapTopic, roadmapTerm,
setRoadmapTopic, setRoadmapTerm,
handleSubmit, handleSubmit,
limit = 0, limit = 0,
limitUsed = 0, limitUsed = 0,
onLoadTopic, onLoadTerm,
} = props; } = props;
const canGenerateMore = limitUsed < limit; const canGenerateMore = limitUsed < limit;
const randomTopics = ['Linux', 'Prometheus', 'gRPC']; const randomTerms = ['OAuth', 'APIs', 'UX Design', 'gRPC'];
return ( return (
<div className="flex flex-grow flex-col items-center justify-center px-4 py-6 sm:px-6"> <div className="flex flex-grow flex-col items-center justify-center px-4 py-6 sm:px-6">
@ -65,9 +65,9 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
type="text" type="text"
placeholder="Enter a topic to generate a roadmap for" placeholder="Enter a topic to generate a roadmap for"
className="w-full rounded-md border border-gray-400 px-3 py-2.5 transition-colors focus:border-black focus:outline-none" className="w-full rounded-md border border-gray-400 px-3 py-2.5 transition-colors focus:border-black focus:outline-none"
value={roadmapTopic} value={roadmapTerm}
onInput={(e) => onInput={(e) =>
setRoadmapTopic((e.target as HTMLInputElement).value) setRoadmapTerm((e.target as HTMLInputElement).value)
} }
/> />
<button <button
@ -75,7 +75,7 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
'flex min-w-[154px] flex-shrink-0 items-center justify-center gap-2 rounded-md bg-black px-4 py-2 text-white', 'flex min-w-[154px] flex-shrink-0 items-center justify-center gap-2 rounded-md bg-black px-4 py-2 text-white',
'disabled:cursor-not-allowed disabled:opacity-50', 'disabled:cursor-not-allowed disabled:opacity-50',
)} )}
disabled={!limit || !roadmapTopic || limitUsed >= limit} disabled={!limit || !roadmapTerm || limitUsed >= limit}
> >
{(!limit || canGenerateMore) && ( {(!limit || canGenerateMore) && (
<> <>
@ -93,17 +93,17 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
</button> </button>
</form> </form>
<div className="flex flex-row items-center justify-center gap-2"> <div className="flex flex-row items-center justify-center gap-2">
{randomTopics.map((topic) => ( {randomTerms.map((term) => (
<button <button
key={topic} key={term}
disabled={!limit || !canGenerateMore} disabled={!limit || !canGenerateMore}
type="button" type="button"
onClick={() => { onClick={() => {
onLoadTopic(topic); onLoadTerm(term);
}} }}
className="flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-sm transition-colors hover:border-black hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50" className="flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-sm transition-colors hover:border-black hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
> >
{topic} <ArrowUpRight size={17} /> {term} <ArrowUpRight size={17} />
</button> </button>
))} ))}
<a <a

@ -54,7 +54,7 @@ export function RoadmapTopicDetail(props: RoadmapTopicDetailProps) {
} }
const response = await fetch( const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-genereate-ai-roadmap-content/${roadmapId}`, `${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-roadmap-content/${roadmapId}`,
{ {
method: 'POST', method: 'POST',
headers: { headers: {

@ -26,5 +26,9 @@ export function getRelativeTimeString(date: string): string {
relativeTime = rtf.format(-diffInDays, 'day'); relativeTime = rtf.format(-diffInDays, 'day');
} }
if (relativeTime === 'this minute') {
return 'just now';
}
return relativeTime; return relativeTime;
} }

Loading…
Cancel
Save