diff --git a/apps/client/app/api/collections/index.ts b/apps/client/app/api/collections/index.ts index b90c9375..ad2f3006 100644 --- a/apps/client/app/api/collections/index.ts +++ b/apps/client/app/api/collections/index.ts @@ -218,6 +218,55 @@ export const useGetCollectionBySlug = ({ retry: retryQuery, }) +export const getCollectionById = async ( + id: string, + query: FetchMultipleDataInputParams, + apiConfig?: ApiConfigForServerConfig, +) => { + try { + const removeAllNullableValues = getQueryParams(query) + const params = new URLSearchParams(removeAllNullableValues) + const response = await fetchClient>( + `/v1/collections/${id}?${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 useGetCollectionById = ({ + id, + query, + retryQuery, +}: { + id?: string + query: FetchMultipleDataInputParams + retryQuery?: boolean +}) => + useQuery({ + queryKey: [QUERY_KEYS.COLLECTIONS, id, 'id'], + queryFn: () => getCollectionById(safeString(id), query), + enabled: Boolean(id), + retry: retryQuery, + }) + export const getCollectionContentsBySlug = async ( slug: string, query: FetchMultipleDataInputParams, diff --git a/apps/client/app/api/explore/index.ts b/apps/client/app/api/explore/index.ts new file mode 100644 index 00000000..4ded3fa4 --- /dev/null +++ b/apps/client/app/api/explore/index.ts @@ -0,0 +1,30 @@ +import { getQueryParams } from '@/lib/get-param.ts' +import { fetchClient } from '@/lib/transport/index.ts' + +export const getExploreSections = async ( + props: FetchMultipleDataInputParams, + apiConfig: ApiConfigForServerConfig, +) => { + try { + const removeAllNullableValues = + getQueryParams(props) + const params = new URLSearchParams(removeAllNullableValues) + const response = await fetchClient< + ApiResponse> + >(`/v1/explore?${params.toString()}`, { + ...(apiConfig ? apiConfig : {}), + }) + + return response.parsedBody.data + } catch (error: unknown) { + // Error from server. + if (error instanceof Response) { + const response = await error.json() + throw new Error(response.errorMessage) + } + + if (error instanceof Error) { + throw error + } + } +} diff --git a/apps/client/app/components/Content/index.tsx b/apps/client/app/components/Content/index.tsx index 3d97b01c..2c4aa2ac 100644 --- a/apps/client/app/components/Content/index.tsx +++ b/apps/client/app/components/Content/index.tsx @@ -8,13 +8,16 @@ import { HeartIcon as HeartIconSolid, XCircleIcon, } from '@heroicons/react/24/solid' -import { Link } from '@remix-run/react' +import { Link, useNavigate } from '@remix-run/react' import { Image } from 'remix-image' import { Button } from '../button/index.tsx' import { PhotographerCreatorCard } from '../creator-card/index.tsx' import { FlyoutContainer } from '../flyout/flyout-container.tsx' import { LikeButton } from '@/components/like-button.tsx' import { blurDataURL, PAGES } from '@/constants/index.ts' +import { useValidateImage } from '@/hooks/use-validate-image.tsx' +import { getNameInitials } from '@/lib/misc.ts' +import { safeString } from '@/lib/strings.ts' import { useAuth } from '@/providers/auth/index.tsx' interface Props { @@ -25,6 +28,7 @@ interface Props { export const Content = ({ content, showCreator = true }: Props) => { const { currentUser } = useAuth() + const navigate = useNavigate() const isContentMine = content.createdById === currentUser?.id return ( @@ -145,12 +149,19 @@ export const Content = ({ content, showCreator = true }: Props) => { /> } > -
- {content.createdBy.name} +
{ + e.preventDefault() + navigate( + PAGES.CREATOR.PHOTOS.replace( + ':username', + safeString(content?.createdBy?.username), + ), + ) + }} + > + {content.createdBy.name} @@ -190,3 +201,28 @@ export const Content = ({ content, showCreator = true }: Props) => { ) } + +interface CreatedByCardProps { + createdBy: BasicCreator +} + +const CreatedByCard = ({ createdBy }: CreatedByCardProps) => { + const isProfilePhotoValid = useValidateImage(safeString(createdBy?.photo)) + const initials = getNameInitials(safeString(createdBy?.name)) + + return ( +
+ {isProfilePhotoValid && createdBy?.photo ? ( + {createdBy.name} + ) : ( + + {initials} + + )} +
+ ) +} diff --git a/apps/client/app/constants/index.ts b/apps/client/app/constants/index.ts index 2185f862..abe4be51 100644 --- a/apps/client/app/constants/index.ts +++ b/apps/client/app/constants/index.ts @@ -18,6 +18,7 @@ export const QUERY_KEYS = { COLLECTIONS: 'collections', CONTENT_LIKES: 'content-likes', CREATORS: 'creators', + EXPLORE: 'explore', } as const export const MFONI_PACKAGES: Array = ['FREE', 'BASIC', 'ADVANCED'] @@ -67,10 +68,10 @@ export const PAGES = { PACKAGE_AND_BILLINGS: '/account/package-and-billings', }, PHOTO: '/photos/:slug', - TAGS: '/tags', - TAG: '/tags/:tag', - COLLECTIONS: '/collections', - COLLECTION: '/collections/:collection', + TAGS: '/explore/tags', + TAG: '/explore/tags/:tag', + COLLECTIONS: '/explore/collections', + COLLECTION: '/explore/collections/:collection', SEARCH: { CREATORS: '/search/creators/:query', PHOTOS: '/search/photos/:query', diff --git a/apps/client/app/modules/account/home/components/cover.tsx b/apps/client/app/modules/account/home/components/cover.tsx index eb28c9ba..78233ada 100644 --- a/apps/client/app/modules/account/home/components/cover.tsx +++ b/apps/client/app/modules/account/home/components/cover.tsx @@ -13,15 +13,10 @@ import { MFONI_PACKAGES_DETAILED } from '@/constants/index.ts' import { useValidateImage } from '@/hooks/use-validate-image.tsx' import { classNames } from '@/lib/classNames.ts' import { isBrowser } from '@/lib/is-browser.ts' +import { getNameInitials } from '@/lib/misc.ts' import { safeString } from '@/lib/strings.ts' import { useAuth } from '@/providers/auth/index.tsx' -const getNameInitials = (name: string) => - name - .split(' ') - .map((n) => n[0]) - .join('') - interface Props { application: CreatorApplication } diff --git a/apps/client/app/modules/collections/all/index.tsx b/apps/client/app/modules/explore/collections/all/index.tsx similarity index 98% rename from apps/client/app/modules/collections/all/index.tsx rename to apps/client/app/modules/explore/collections/all/index.tsx index c6562941..5cc3545e 100644 --- a/apps/client/app/modules/collections/all/index.tsx +++ b/apps/client/app/modules/explore/collections/all/index.tsx @@ -14,7 +14,7 @@ export function CollectionsModule() { const { data, isPending, isError } = useGetCollections({ pagination: { page: 0, per: 50 }, filters: { visibility: 'PUBLIC' }, - populate: ['collection.createdBy'], + populate: ['collection.createdBy', 'content'], }) let content = <> diff --git a/apps/client/app/modules/collections/single/components/add-image-contents-modal.tsx b/apps/client/app/modules/explore/collections/single/components/add-image-contents-modal.tsx similarity index 100% rename from apps/client/app/modules/collections/single/components/add-image-contents-modal.tsx rename to apps/client/app/modules/explore/collections/single/components/add-image-contents-modal.tsx diff --git a/apps/client/app/modules/collections/single/components/edit-collection-modal/index.tsx b/apps/client/app/modules/explore/collections/single/components/edit-collection-modal/index.tsx similarity index 100% rename from apps/client/app/modules/collections/single/components/edit-collection-modal/index.tsx rename to apps/client/app/modules/explore/collections/single/components/edit-collection-modal/index.tsx index 2a99cca1..800013a4 100644 --- a/apps/client/app/modules/collections/single/components/edit-collection-modal/index.tsx +++ b/apps/client/app/modules/explore/collections/single/components/edit-collection-modal/index.tsx @@ -1,6 +1,6 @@ +import { useState } from 'react' import { Button } from '@/components/button/index.tsx' import { Modal } from '@/components/modal/index.tsx' -import { useState } from 'react' interface Props { isOpened: boolean diff --git a/apps/client/app/modules/collections/single/components/remove-image-content-dialog.tsx b/apps/client/app/modules/explore/collections/single/components/remove-image-content-dialog.tsx similarity index 100% rename from apps/client/app/modules/collections/single/components/remove-image-content-dialog.tsx rename to apps/client/app/modules/explore/collections/single/components/remove-image-content-dialog.tsx diff --git a/apps/client/app/modules/collections/single/components/status-button.tsx b/apps/client/app/modules/explore/collections/single/components/status-button.tsx similarity index 100% rename from apps/client/app/modules/collections/single/components/status-button.tsx rename to apps/client/app/modules/explore/collections/single/components/status-button.tsx diff --git a/apps/client/app/modules/collections/single/index.tsx b/apps/client/app/modules/explore/collections/single/index.tsx similarity index 99% rename from apps/client/app/modules/collections/single/index.tsx rename to apps/client/app/modules/explore/collections/single/index.tsx index 474aa0f2..86b8db59 100644 --- a/apps/client/app/modules/collections/single/index.tsx +++ b/apps/client/app/modules/explore/collections/single/index.tsx @@ -9,6 +9,7 @@ import { useLoaderData, useNavigate, useParams } from '@remix-run/react' import dayjs from 'dayjs' import { useState } from 'react' import { AddImageContentsModal } from './components/add-image-contents-modal.tsx' +import { EditCollectionTitleModal } from './components/edit-collection-modal/index.tsx' import { RemoveImageContentModal } from './components/remove-image-content-dialog.tsx' import { StatusButton } from './components/status-button.tsx' import { useGetCollectionContentsBySlug } from '@/api/collections/index.ts' @@ -26,8 +27,7 @@ import { PAGES } from '@/constants/index.ts' import { useDisclosure } from '@/hooks/use-disclosure.tsx' import { safeString } from '@/lib/strings.ts' import { useAuth } from '@/providers/auth/index.tsx' -import { type loader } from '@/routes/collections.$collection.ts' -import { EditCollectionTitleModal } from './components/edit-collection-modal/index.tsx' +import { type loader } from '@/routes/explore.collections.$collection.ts' export function CollectionModule() { const { collection } = useLoaderData() diff --git a/apps/client/app/modules/explore/components/section/collection-section.tsx b/apps/client/app/modules/explore/components/section/collection-section.tsx index 5c978f8e..edb19a82 100644 --- a/apps/client/app/modules/explore/components/section/collection-section.tsx +++ b/apps/client/app/modules/explore/components/section/collection-section.tsx @@ -1,18 +1,35 @@ import { Link } from '@remix-run/react' +import { useGetCollectionById } from '@/api/collections/index.ts' +import { CollectionCard } from '@/components/CollectionCard/index.tsx' interface Props { - data: Collection + collectionId: string } -export function CollectionSection({}: Props) { - return <>Tag +export function CollectionSection({ collectionId }: Props) { + const { isPending, data, isError } = useGetCollectionById({ + id: collectionId, + query: { + populate: ['content'], + }, + }) + + if (isPending) return + if (!data || isError) return null + + return ( +
+ +
+ ) } export function CollectionShimmer() { return ( -
-
+
+
+
) } diff --git a/apps/client/app/modules/explore/components/section/content-section.tsx b/apps/client/app/modules/explore/components/section/content-section.tsx index 0e0468a9..279e436e 100644 --- a/apps/client/app/modules/explore/components/section/content-section.tsx +++ b/apps/client/app/modules/explore/components/section/content-section.tsx @@ -1,11 +1,16 @@ import { Link } from '@remix-run/react' +import { Content } from '@/components/Content/index.tsx' interface Props { data: Content } -export function ContentSection({}: Props) { - return <>Content +export function ContentSection({ data }: Props) { + return ( +
+ +
+ ) } export function ContentShimmer() { diff --git a/apps/client/app/modules/explore/components/section/creator-section.tsx b/apps/client/app/modules/explore/components/section/creator-section.tsx index c757732d..62840d35 100644 --- a/apps/client/app/modules/explore/components/section/creator-section.tsx +++ b/apps/client/app/modules/explore/components/section/creator-section.tsx @@ -1,9 +1,45 @@ +import { Link } from '@remix-run/react' +import { Image } from 'remix-image' +import { PAGES } from '@/constants/index.ts' +import { useValidateImage } from '@/hooks/use-validate-image.tsx' +import { getNameInitials } from '@/lib/misc.ts' +import { safeString } from '@/lib/strings.ts' + interface Props { data: EnhancedCreator } -export function CreatorSection({}: Props) { - return <>Tag +export function CreatorSection({ data }: Props) { + const isProfilePhotoValid = useValidateImage(safeString(data.photo)) + const initials = getNameInitials(safeString(data.name)) + + return ( + +
+ {isProfilePhotoValid && data.photo ? ( + {data.name} + ) : ( + + + {initials} + + + )} +
+

+ {data.name} +

+

+ {data.username} +

+
+
+ + ) } export function CreatorShimmer() { diff --git a/apps/client/app/modules/explore/components/section/index.tsx b/apps/client/app/modules/explore/components/section/index.tsx index 244d8797..acbe488c 100644 --- a/apps/client/app/modules/explore/components/section/index.tsx +++ b/apps/client/app/modules/explore/components/section/index.tsx @@ -1,24 +1,18 @@ +import { ChevronRightIcon } from '@heroicons/react/24/solid' import { Link } from '@remix-run/react' +import { useQuery } from '@tanstack/react-query' import { CollectionSection, CollectionShimmer } from './collection-section.tsx' import { ContentSection, ContentShimmer } from './content-section.tsx' import { CreatorSection, CreatorShimmer } from './creator-section.tsx' import { TagSection, TagShimmer } from './tag-section.tsx' +import { QUERY_KEYS } from '@/constants/index.ts' +import { fetchClient } from '@/lib/transport/index.ts' -interface Props { - type: 'TAG' | 'CONTENT' | 'COLLECTION' | 'CREATOR' - title?: string - seeMoreLink?: string - contents: Array - isLoading?: boolean - count?: number +interface Props { + section: ExploreSection } -const sections = { - TAG: TagSection, - CONTENT: ContentSection, - COLLECTION: CollectionSection, - CREATOR: CreatorSection, -} + const loadingSections = { TAG: TagShimmer, @@ -27,48 +21,96 @@ const loadingSections = { CREATOR: CreatorShimmer, } -export function ExploreSection< - T extends Tag | Content | Collection | EnhancedCreator, ->({ - type, - title, - seeMoreLink, - contents, - isLoading = false, - count = 0, -}: Props) { +export function ExploreSection({ section }: Props) { + const { isLoading, data, isError } = useQuery({ + queryKey: [QUERY_KEYS.EXPLORE, section.id], + queryFn: async () => { + const response = await fetchClient< + ApiResponse> + >(section.endpoint) + return response?.parsedBody?.data + }, + }) + + let content: Array = [] + + if (isLoading) { + content = [...new Array(7)].map((_, index) => { + const Section = loadingSections[section.type] + return
+ }) + } + + if (isError || (data && !data?.total)) return null + + if (data?.total) { + content = data?.rows.map((content, index) => { + + switch (section.type) { + case 'TAG': + const tag = (content as CollectionContent).tag + if (tag) { + return + } + return null + case 'CONTENT': + const contentData = (content as CollectionContent).content + if (contentData) { + return + } + + return null + case 'COLLECTION': + const childCollectionId = (content as CollectionContent) + .childCollectionId + if (childCollectionId) { + return ( + + ) + } + return null + case 'CREATOR': + const isGettingDataFromCollectionsEndpoint = + section.endpoint.includes('collections') + if (isGettingDataFromCollectionsEndpoint) { + const creator = (content as CollectionContent).creator + if (creator) { + return + } + } + + const isGettingDataFromFollowings = + section.endpoint.includes('followings') + if (isGettingDataFromFollowings && content) { + return ( + + ) + } + + return null + default: + return null + } + }) + } + return (
-
-

- {title} - {count ? ( - {count} - ) : undefined} -

- {seeMoreLink ? ( +
+

{section.title}

+ {section?.seeMorePathname ? ( - See more + See More ) : null}
- {isLoading - ? [...new Array(7)].map((_, index) => { - const Section = loadingSections[type] - return
- }) - : contents.map((content, index) => { - const Section = sections[type] - - // fix type issue - return
- })} + {content}
) diff --git a/apps/client/app/modules/explore/components/section/tag-section.tsx b/apps/client/app/modules/explore/components/section/tag-section.tsx index a336573b..497388d0 100644 --- a/apps/client/app/modules/explore/components/section/tag-section.tsx +++ b/apps/client/app/modules/explore/components/section/tag-section.tsx @@ -14,9 +14,7 @@ export function TagSection({ data }: Props) {
-

- Tag akjbcjas fjas fhas fhas fhas fhas fh ashfj asjhf asj kofi -

+

{data.name}

created on {dayjs().format('L')}

diff --git a/apps/client/app/modules/explore/index.tsx b/apps/client/app/modules/explore/index.tsx index c68d025b..93e7c04c 100644 --- a/apps/client/app/modules/explore/index.tsx +++ b/apps/client/app/modules/explore/index.tsx @@ -1,8 +1,33 @@ +import { useLoaderData } from '@remix-run/react' +import { useMemo, useRef } from 'react' import { ExploreSection } from './components/section/index.tsx' +import { EmptyState } from '@/components/empty-state/index.tsx' import { Footer } from '@/components/footer/index.tsx' import { Header } from '@/components/layout/index.ts' +import { useAuth } from '@/providers/auth/index.tsx' +import { type loader } from '@/routes/explore._index.ts' export const ExploreModule = () => { + const { isLoggedIn } = useAuth() + const { exploreSections } = useLoaderData() + const containerRef = useRef(null) + + const contents = useMemo(() => { + return exploreSections?.rows?.map((section) => { + if (section.ensureAuth && !isLoggedIn) return null + + // TODO: bring this back after the backend is ready. + if (section.endpoint.includes('/followings')) return null + + return ( + + ) + }) + }, [exploreSections?.rows, isLoggedIn]) + return (
@@ -17,55 +42,14 @@ export const ExploreModule = () => {
-
- {/* for featured tags */} - - isLoading - type="TAG" - title="Featured Tags" - contents={[...new Array(30)]} - /> + {containerRef.current?.children?.length === 0 ? ( +
+ +
+ ) : null} - - isLoading - type="COLLECTION" - title="Featured Collections" - seeMoreLink="/collections" - contents={[]} - /> - - isLoading - type="CREATOR" - title="Featured creators" - contents={[]} - /> - - isLoading - type="CONTENT" - title="Featured Photos" - seeMoreLink="/collections/featured_contents" - contents={[]} - /> - - isLoading - type="COLLECTION" - title="Trending Collections" - seeMoreLink="/collections" - contents={[]} - /> - - isLoading - type="TAG" - title="Popular tags" - seeMoreLink="/tags" - contents={[...new Array(30)]} - /> - - isLoading - type="CREATOR" - title="Your Followings" - contents={[]} - /> +
+ {contents}