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