Skip to content

Commit

Permalink
feat: Contents Page (#162)
Browse files Browse the repository at this point in the history
* chore: added /explore/contents page

* chore: added button on home page to redirect us to all contents page
  • Loading branch information
Bendomey authored Jan 7, 2025
1 parent 31eeb5f commit 176619c
Show file tree
Hide file tree
Showing 16 changed files with 730 additions and 198 deletions.
44 changes: 44 additions & 0 deletions apps/client/app/api/contents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,47 @@ export const useGetRelatedContents = ({
enabled: Boolean(contentId),
retry: retryQuery,
})

export const getContents = async (
query: FetchMultipleDataInputParams<FetchContentFilter>,
apiConfig?: ApiConfigForServerConfig,
) => {
try {
const removeAllNullableValues = getQueryParams<FetchContentFilter>(query)
const params = new URLSearchParams(removeAllNullableValues)
const response = await fetchClient<
ApiResponse<FetchMultipleDataResponse<Content>>
>(`/v1/contents?${params.toString()}`, {
...(apiConfig ? apiConfig : {}),
})

if (!response.parsedBody.status && response.parsedBody.errorMessage) {
throw new Error(response.parsedBody.errorMessage)
}

return response.parsedBody.data
} catch (error: unknown) {
if (error instanceof Error) {
throw error
}

// Error from server.
if (error instanceof Response) {
const response = await error.json()
throw new Error(response.errorMessage)
}
}
}

export const useGetContents = ({
query,
retryQuery,
}: {
query: FetchMultipleDataInputParams<FetchContentFilter>
retryQuery?: boolean
}) =>
useQuery({
queryKey: [QUERY_KEYS.CONTENTS, query],
queryFn: () => getContents(query),
retry: retryQuery,
})
72 changes: 46 additions & 26 deletions apps/client/app/components/Content/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@heroicons/react/24/outline'
import {
HeartIcon as HeartIconSolid,
StarIcon,
XCircleIcon,
} from '@heroicons/react/24/solid'
import { Link, useNavigate } from '@remix-run/react'
Expand Down Expand Up @@ -44,8 +45,8 @@ export const Content = ({ content, showCreator = true }: Props) => {
alt={content.title}
/>
<div className="group absolute top-0 h-full w-full rounded-lg hover:bg-black/50">
<div className="flex h-full w-full flex-col justify-between p-2">
<div className="flex flex-row items-center justify-between p-2">
<div className="flex h-full w-full flex-col justify-between p-4">
<div className="flex flex-row items-center justify-between">
<div className="flex items-center gap-1">
{content.amount > 0 ? (
<FlyoutContainer
Expand Down Expand Up @@ -112,30 +113,49 @@ export const Content = ({ content, showCreator = true }: Props) => {
</FlyoutContainer>
) : null}
</div>
<div className="hidden group-hover:block">
{content.status !== 'DONE' ? null : (
<LikeButton content={content}>
{({ isDisabled, isLiked, onClick }) => (
<Button
title={isLiked ? 'Remove Like' : 'Like Image'}
onClick={(e) => {
e.preventDefault()
onClick()
}}
variant="outlined"
size="sm"
className="px-2"
disabled={isDisabled}
>
{isLiked ? (
<HeartIconSolid className="h-6 w-6 text-blue-700" />
) : (
<HeartIconOutline className="h-6 w-6 text-zinc-700" />
)}
</Button>
)}
</LikeButton>
)}

<div className="flex items-center gap-4">
<div className="hidden group-hover:block">
{content.status !== 'DONE' ? null : (
<LikeButton content={content}>
{({ isDisabled, isLiked, onClick }) => (
<Button
title={isLiked ? 'Remove Like' : 'Like Image'}
onClick={(e) => {
e.preventDefault()
onClick()
}}
variant="outlined"
size="sm"
className="px-2"
disabled={isDisabled}
>
{isLiked ? (
<HeartIconSolid className="h-6 w-6 text-blue-700" />
) : (
<HeartIconOutline className="h-6 w-6 text-zinc-700" />
)}
</Button>
)}
</LikeButton>
)}
</div>
{content.isFeatured ? (
<div>
<FlyoutContainer
intendedPosition="y"
FlyoutContent={
<div className="z-50 flex w-48 flex-col items-center justify-center rounded-2xl bg-black px-3 py-4 shadow-xl">
<h3 className="text-center text-sm font-bold text-white">
This image is featured
</h3>
</div>
}
>
<StarIcon className="h-7 w-7 text-white" />
</FlyoutContainer>
</div>
) : null}
</div>
</div>
<div className="hidden flex-row items-center justify-between gap-2.5 group-hover:flex">
Expand Down
1 change: 1 addition & 0 deletions apps/client/app/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const PAGES = {
TAG: '/explore/tags/:tag',
COLLECTIONS: '/explore/collections',
COLLECTION: '/explore/collections/:collection',
CONTENTS: '/explore/contents',
SEARCH: {
CREATORS: '/search/creators/:query',
PHOTOS: '/search/photos/:query',
Expand Down
73 changes: 73 additions & 0 deletions apps/client/app/modules/explore/contents/components/filter-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { AdjustmentsHorizontalIcon } from '@heroicons/react/24/outline'
import { useSearchParams } from '@remix-run/react'
import { useMemo } from 'react'
import { Button } from '@/components/button/index.tsx'
import { useDisclosure } from '@/hooks/use-disclosure.tsx'
import { FiltersDialog } from '@/modules/search/components/filters-dialog/index.tsx'
import { FilterByLicense } from '@/modules/search/components/license-select/index.tsx'
import { FilterByOrientation } from '@/modules/search/components/orientation-select/index.tsx'

export function FilterBar() {
const filterModalState = useDisclosure()
const [searchParams, setSearchParams] = useSearchParams()

const showClearButton =
searchParams.get('license') || searchParams.get('orientation')

const clearFilters = () => {
searchParams.delete('license')
searchParams.delete('orientation')
setSearchParams(searchParams)
}

const filterSize = useMemo(() => {
let size = 0
if (searchParams.has('license')) {
size++
}
if (searchParams.has('orientation')) {
size++
}

return size
}, [searchParams])

return (
<div>
<div className="my-10 flex items-end justify-between">
<div>
<div className="mb-3">
<h1 className="text-4xl font-black">All Contents</h1>
</div>
<p className="text-zinc-500 md:w-2/3">
mfoni offers millions of free, high-quality pictures. All pictures
are free to download and use under the mfoni license.
</p>
</div>
<div className="hidden flex-row items-center gap-3 lg:flex">
{showClearButton ? (
<button onClick={clearFilters} className="text-xs text-gray-500">
Clear
</button>
) : null}
<FilterByLicense />
<FilterByOrientation />
</div>
</div>
<div className="mb-5 flex justify-end lg:hidden">
<Button
onClick={filterModalState.onOpen}
size="sm"
color="secondaryGhost"
>
<AdjustmentsHorizontalIcon className="mr-2 h-auto w-5" /> Filters{' '}
{filterSize > 0 ? `(${searchParams.size})` : ''}
</Button>
</div>
<FiltersDialog
isOpened={filterModalState.isOpened}
onClose={filterModalState.onClose}
/>
</div>
)
}
115 changes: 115 additions & 0 deletions apps/client/app/modules/explore/contents/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { ArrowPathIcon } from '@heroicons/react/24/outline'
import { useSearchParams } from '@remix-run/react'
import { useMemo } from 'react'
import { FilterBar } from './components/filter-bar.tsx'
import { useGetContents } from '@/api/contents/index.ts'
import { FadeIn, FadeInStagger } from '@/components/animation/FadeIn.tsx'
import { Button } from '@/components/button/index.tsx'
import { Content } from '@/components/Content/index.tsx'
import { EmptyState } from '@/components/empty-state/index.tsx'
import { ErrorState } from '@/components/error-state/index.tsx'
import { Footer } from '@/components/footer/index.tsx'
import { Header } from '@/components/layout/index.ts'
import { Loader } from '@/components/loader/index.tsx'

export const ContentsModule = () => {
const [searchParams] = useSearchParams()

const validateLicense = useMemo(() => {
const base = searchParams.get('license') ?? 'ALL'

if (['ALL', 'FREE', 'PREMIUM'].includes(base)) {
return base
}

return 'ALL'
}, [searchParams])

const validateOrientation = useMemo(() => {
const base = searchParams.get('orientation') ?? 'ALL'

if (['ALL', 'LANDSCAPE', 'PORTRAIT', 'SQUARE'].includes(base)) {
return base
}

return 'ALL'
}, [searchParams])

const { data, isPending, isError, refetch } = useGetContents({
query: {
pagination: { page: 0, per: 50 },
filters: {
license: validateLicense,
orientation: validateOrientation,
},
populate: ['content.createdBy'],
},
})

let content = <></>

if (isPending) {
content = (
<div className="flex h-[50vh] flex-1 items-center justify-center">
<Loader />
</div>
)
}

if (isError) {
content = (
<div className="flex h-[50vh] flex-1 items-center justify-center">
<ErrorState
message="An error occurred fetching contents."
title="Something happened."
>
<Button isLink onClick={() => refetch()} variant="outlined">
<ArrowPathIcon
aria-hidden="true"
className="-ml-0.5 mr-1.5 size-5"
/>
Reload
</Button>
</ErrorState>
</div>
)
}

if (data && !data?.total) {
content = (
<div className="flex h-[50vh] flex-1 items-center justify-center">
<EmptyState
message={`There are no contents found`}
title="No content found"
/>
</div>
)
}

if (data?.total) {
content = (
<FadeInStagger faster>
<div className="columns-1 gap-2 sm:columns-2 sm:gap-4 md:columns-3 lg:columns-4 [&>img:not(:first-child)]:mt-8">
{data.rows.map((content) => (
<div className="mb-5" key={content.id}>
<FadeIn>
<Content content={content} />
</FadeIn>
</div>
))}
</div>
</FadeInStagger>
)
}

return (
<div>
<Header isHeroSearchInVisible={false} />
<div className="max-w-8xl mx-auto my-10 items-center px-3 sm:px-3 md:px-8">
<FilterBar />
{content}
</div>
<Footer />
</div>
)
}
1 change: 1 addition & 0 deletions apps/client/app/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export * from './explore/tags/all/index.tsx'
export * from './explore/tags/single/index.tsx'
export * from './explore/collections/all/index.tsx'
export * from './explore/collections/single/index.tsx'
export * from './explore/contents/index.tsx'
6 changes: 6 additions & 0 deletions apps/client/app/modules/landing-page/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { FadeIn, FadeInStagger } from '@/components/animation/FadeIn.tsx'
import { Content } from '@/components/Content/index.tsx'
import { Footer } from '@/components/footer/index.tsx'
import { Header } from '@/components/layout/index.ts'
import { Button } from '@/components/button/index.tsx'
import { PAGES } from '@/constants/index.ts'

export const imageUrls = [
'https://flowbite.s3.amazonaws.com/docs/gallery/masonry/image.jpg',
Expand Down Expand Up @@ -43,6 +45,10 @@ export const LandingPageModule = () => {
</div>
))}
</div>

<div className='flex justify-center'>
<Button isLink href={PAGES.CONTENTS} variant='solid' color='secondaryGhost' size='xl'>See More Contents</Button>
</div>
</FadeInStagger>
<Pricing />
</div>
Expand Down
20 changes: 10 additions & 10 deletions apps/client/app/modules/photo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
ShieldCheckIcon,
HeartIcon as HeartIconOutline,
} from '@heroicons/react/24/outline'
import { HeartIcon as HeartIconSolid, PencilIcon } from '@heroicons/react/24/solid'
import {
HeartIcon as HeartIconSolid,
PencilIcon,
} from '@heroicons/react/24/solid'
import { Link, useLoaderData } from '@remix-run/react'
import dayjs from 'dayjs'
import { Fragment } from 'react'
Expand Down Expand Up @@ -173,15 +176,12 @@ export const PhotoModule = () => {
<h1 className="text-gray-500">Likes</h1>
<p className="font-semibold">{content.meta.likes}</p>
</div>
{
content.isFeatured ? (
<div className="text-sm">
<h1 className="text-gray-500">Is featured</h1>
<p className="font-semibold">Yes</p>
</div>
) : null
}

{content.isFeatured ? (
<div className="text-sm">
<h1 className="text-gray-500">Is featured</h1>
<p className="font-semibold">Yes</p>
</div>
) : null}
</div>
<div className="flex flex-row items-center gap-2">
<ShareButton
Expand Down
Loading

0 comments on commit 176619c

Please sign in to comment.