parent
3302c9ab3f
commit
b5395cd0c1
8 changed files with 882 additions and 910 deletions
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,33 @@ |
|||||||
|
import { type APIContext } from 'astro'; |
||||||
|
import { api } from './api.ts'; |
||||||
|
import type { RoadmapDocument } from '../components/CustomRoadmap/CreateRoadmap/CreateRoadmapModal.tsx'; |
||||||
|
|
||||||
|
export type ListShowcaseRoadmapResponse = { |
||||||
|
data: Pick< |
||||||
|
RoadmapDocument, |
||||||
|
| '_id' |
||||||
|
| 'title' |
||||||
|
| 'description' |
||||||
|
| 'slug' |
||||||
|
| 'creatorId' |
||||||
|
| 'visibility' |
||||||
|
| 'createdAt' |
||||||
|
| 'topicCount' |
||||||
|
>[]; |
||||||
|
totalCount: number; |
||||||
|
totalPages: number; |
||||||
|
currPage: number; |
||||||
|
perPage: number; |
||||||
|
}; |
||||||
|
|
||||||
|
export function roadmapApi(context: APIContext) { |
||||||
|
return { |
||||||
|
listShowcaseRoadmap: async function () { |
||||||
|
const searchParams = new URLSearchParams(context.url.searchParams); |
||||||
|
return api(context).get<ListShowcaseRoadmapResponse>( |
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-list-showcase-roadmap`, |
||||||
|
searchParams, |
||||||
|
); |
||||||
|
}, |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
import { Shapes } from 'lucide-react'; |
||||||
|
import type { ListShowcaseRoadmapResponse } from '../../api/roadmap'; |
||||||
|
import { Pagination } from '../Pagination/Pagination'; |
||||||
|
import { SearchRoadmap } from './SearchRoadmap'; |
||||||
|
import { EmptyDiscoverRoadmaps } from './EmptyDiscoverRoadmaps'; |
||||||
|
|
||||||
|
type DiscoverRoadmapsProps = { |
||||||
|
searchParams: string; |
||||||
|
roadmapsResponse: ListShowcaseRoadmapResponse; |
||||||
|
}; |
||||||
|
|
||||||
|
export function DiscoverRoadmaps(props: DiscoverRoadmapsProps) { |
||||||
|
const { roadmapsResponse, searchParams: defaultSearchparams } = props; |
||||||
|
|
||||||
|
const roadmaps = roadmapsResponse?.data || []; |
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(defaultSearchparams); |
||||||
|
const titleQuery = searchParams.get('q') || ''; |
||||||
|
|
||||||
|
return ( |
||||||
|
<section className="container mx-auto py-3 sm:py-6"> |
||||||
|
<SearchRoadmap |
||||||
|
total={roadmapsResponse?.totalCount || 0} |
||||||
|
value={titleQuery} |
||||||
|
/> |
||||||
|
|
||||||
|
{roadmaps.length === 0 && <EmptyDiscoverRoadmaps />} |
||||||
|
{roadmaps.length > 0 && ( |
||||||
|
<> |
||||||
|
<ul className="mb-4 grid grid-cols-1 items-stretch gap-2 sm:grid-cols-2 lg:grid-cols-3"> |
||||||
|
{roadmaps.map((roadmap) => { |
||||||
|
const roadmapLink = `/r/${roadmap.slug}`; |
||||||
|
return ( |
||||||
|
<li key={roadmap._id} className="h-full"> |
||||||
|
<a |
||||||
|
key={roadmap._id} |
||||||
|
href={roadmapLink} |
||||||
|
className="flex h-full flex-col rounded-md border transition-colors hover:bg-gray-100" |
||||||
|
target={'_blank'} |
||||||
|
> |
||||||
|
<div className="grow"> |
||||||
|
<h2 className="mt-2.5 px-2.5 text-base font-medium leading-tight"> |
||||||
|
{roadmap.title} |
||||||
|
</h2> |
||||||
|
<p className="my-2.5 px-2.5 text-sm text-gray-500"> |
||||||
|
{roadmap.description} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-2 px-2.5 py-2"> |
||||||
|
<span className="flex items-center gap-1.5 text-xs text-gray-400"> |
||||||
|
<Shapes size={15} className="inline-block" /> |
||||||
|
{Intl.NumberFormat('en-US', { |
||||||
|
notation: 'compact', |
||||||
|
}).format(roadmap.topicCount)}{' '} |
||||||
|
topics |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
); |
||||||
|
})} |
||||||
|
</ul> |
||||||
|
|
||||||
|
<Pagination |
||||||
|
currPage={roadmapsResponse?.currPage || 1} |
||||||
|
totalPages={roadmapsResponse?.totalPages || 1} |
||||||
|
perPage={roadmapsResponse?.perPage || 0} |
||||||
|
totalCount={roadmapsResponse?.totalCount || 0} |
||||||
|
onPageChange={(page) => { |
||||||
|
const newSearchParams = new URLSearchParams(); |
||||||
|
if (titleQuery) { |
||||||
|
newSearchParams.set('q', titleQuery); |
||||||
|
} |
||||||
|
|
||||||
|
newSearchParams.set('currPage', page.toString()); |
||||||
|
window.location.href = `/discover?${newSearchParams.toString()}`; |
||||||
|
}} |
||||||
|
/> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</section> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
import { Map, Wand2 } from 'lucide-react'; |
||||||
|
import { useState } from 'react'; |
||||||
|
import { CreateRoadmapModal } from '../CustomRoadmap/CreateRoadmap/CreateRoadmapModal'; |
||||||
|
|
||||||
|
export function EmptyDiscoverRoadmaps() { |
||||||
|
const [isCreatingRoadmap, setIsCreatingRoadmap] = useState(false); |
||||||
|
|
||||||
|
const creatingRoadmapModal = isCreatingRoadmap && ( |
||||||
|
<CreateRoadmapModal |
||||||
|
onClose={() => setIsCreatingRoadmap(false)} |
||||||
|
onCreated={(roadmap) => { |
||||||
|
window.location.href = `${ |
||||||
|
import.meta.env.PUBLIC_EDITOR_APP_URL |
||||||
|
}/${roadmap?._id}`;
|
||||||
|
}} |
||||||
|
/> |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
{creatingRoadmapModal} |
||||||
|
|
||||||
|
<div className="flex min-h-[250px] flex-col items-center justify-center rounded-xl border px-5 py-3 sm:px-0 sm:py-20"> |
||||||
|
<Wand2 className="mb-4 h-8 w-8 opacity-10 sm:h-14 sm:w-14" /> |
||||||
|
<h2 className="mb-1 text-lg font-semibold sm:text-xl"> |
||||||
|
No Roadmaps Found |
||||||
|
</h2> |
||||||
|
<p className="mb-3 text-balance text-center text-xs text-gray-800 sm:text-sm"> |
||||||
|
Try searching for something else or create a new roadmap. |
||||||
|
</p> |
||||||
|
<div className="flex flex-col items-center gap-1 sm:flex-row sm:gap-1.5"> |
||||||
|
<button |
||||||
|
className="flex w-full items-center gap-1.5 rounded-md bg-gray-900 px-3 py-1.5 text-xs text-white sm:w-auto sm:text-sm" |
||||||
|
type="button" |
||||||
|
onClick={() => { |
||||||
|
setIsCreatingRoadmap(true); |
||||||
|
}} |
||||||
|
> |
||||||
|
<Wand2 className="h-4 w-4" /> |
||||||
|
Create your Roadmap |
||||||
|
</button> |
||||||
|
<a |
||||||
|
href="/roadmaps" |
||||||
|
className="flex w-full items-center gap-1.5 rounded-md bg-yellow-400 px-3 py-1.5 text-xs text-black hover:bg-yellow-500 sm:w-auto sm:text-sm" |
||||||
|
> |
||||||
|
<Map className="h-4 w-4" /> |
||||||
|
Visit Official Roadmaps |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
import { Search } from 'lucide-react'; |
||||||
|
|
||||||
|
type SearchRoadmapProps = { |
||||||
|
value: string; |
||||||
|
total: number; |
||||||
|
}; |
||||||
|
|
||||||
|
export function SearchRoadmap(props: SearchRoadmapProps) { |
||||||
|
const { total, value: defaultValue } = props; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="relative mb-3 flex w-full items-center gap-3"> |
||||||
|
<form |
||||||
|
className="relative flex w-full max-w-[310px] items-center" |
||||||
|
action="/discover" |
||||||
|
> |
||||||
|
<label |
||||||
|
className="absolute left-3 flex h-full items-center text-gray-500" |
||||||
|
htmlFor="search" |
||||||
|
> |
||||||
|
<Search className="h-4 w-4" /> |
||||||
|
</label> |
||||||
|
<input |
||||||
|
id="q" |
||||||
|
name="q" |
||||||
|
type="text" |
||||||
|
minLength={3} |
||||||
|
placeholder="Type 3 or more characters to search..." |
||||||
|
className="w-full rounded-md border border-gray-200 px-3 py-2 pl-9 text-sm transition-colors focus:border-black focus:outline-none" |
||||||
|
defaultValue={defaultValue} |
||||||
|
/> |
||||||
|
</form> |
||||||
|
{total > 0 && ( |
||||||
|
<p className="hidden flex-shrink-0 text-sm text-gray-500 sm:block"> |
||||||
|
{Intl.NumberFormat('en-US', { |
||||||
|
notation: 'compact', |
||||||
|
}).format(total)}{' '} |
||||||
|
results found |
||||||
|
</p> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
--- |
||||||
|
import { roadmapApi } from '../api/roadmap'; |
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro'; |
||||||
|
import { DiscoverRoadmaps } from '../components/DiscoverRoadmaps/DiscoverRoadmaps'; |
||||||
|
|
||||||
|
export const prerender = false; |
||||||
|
|
||||||
|
const roadmapApiClient = roadmapApi(Astro); |
||||||
|
|
||||||
|
const { error, response: roadmaps } = |
||||||
|
await roadmapApiClient.listShowcaseRoadmap(); |
||||||
|
console.log('-'.repeat(20)); |
||||||
|
console.log(error); |
||||||
|
console.log('-'.repeat(20)); |
||||||
|
|
||||||
|
const searchParams = Astro.url.searchParams.toString(); |
||||||
|
--- |
||||||
|
|
||||||
|
<BaseLayout title='Discover Custom Roadmaps'> |
||||||
|
{ |
||||||
|
roadmaps && ( |
||||||
|
<DiscoverRoadmaps |
||||||
|
roadmapsResponse={roadmaps} |
||||||
|
searchParams={searchParams} |
||||||
|
client:load |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
</BaseLayout> |
Loading…
Reference in new issue