From 12e7941f6cd71bd8ac83b5e8110adaca65e4b8d9 Mon Sep 17 00:00:00 2001 From: RizqiSyahrendra Date: Tue, 22 Apr 2025 00:35:53 +0700 Subject: [PATCH 01/10] feat: recent blog post related fetching --- src/app/(main)/blog/[slug]/page.tsx | 69 ++++++----------------- src/components/blogs/CardBlog.tsx | 49 ++++++++++++++-- src/components/blogs/ListOfBlog.tsx | 2 +- src/components/blogs/ListOfRecentBlog.tsx | 48 ++++++++++++++++ src/payload.config.ts | 2 - src/schema/blog.ts | 2 +- src/schema/services/blog.ts | 4 ++ src/services/hooks/blog.ts | 33 ++++++++++- src/services/payload/blog.ts | 10 ++-- src/services/rest/blog.ts | 42 +++++++++++++- 10 files changed, 193 insertions(+), 68 deletions(-) create mode 100644 src/components/blogs/ListOfRecentBlog.tsx diff --git a/src/app/(main)/blog/[slug]/page.tsx b/src/app/(main)/blog/[slug]/page.tsx index 204a1f0..8287882 100644 --- a/src/app/(main)/blog/[slug]/page.tsx +++ b/src/app/(main)/blog/[slug]/page.tsx @@ -1,3 +1,4 @@ +import ListOfRecentBlog from "@/components/blogs/ListOfRecentBlog"; import HeroImage from "@/components/HeroImage"; import { fetchBlogDetail } from "@/services/payload/blog"; import { getDefaultMetadata } from "@/utils/metadata"; @@ -110,65 +111,31 @@ export default async function BlogDetail(props: { params: Promise<{ slug: string Share this post
  • - +
  • - +
  • - +
  • -
    -
    -
    Recent Posts
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    + + diff --git a/src/components/blogs/CardBlog.tsx b/src/components/blogs/CardBlog.tsx index 52e7afa..0d41600 100644 --- a/src/components/blogs/CardBlog.tsx +++ b/src/components/blogs/CardBlog.tsx @@ -3,12 +3,47 @@ import Image from "next/image"; import Link from "next/link"; type CardBlogProps = { + colorPreset?: 1 | 2; + isDescriptionVisible?: boolean; data: BlogData; }; -export default function CardBlog({ data }: CardBlogProps) { +export default function CardBlog({ data, colorPreset = 1, isDescriptionVisible = true }: CardBlogProps) { const linkDetail = `/blog/${data.slug}`; + if (colorPreset === 2) { + return ( +
    +
    +
    + + {data.img?.alt + +
    +
    +
    +

    + {data.title} +

    +
    + {isDescriptionVisible && !!data?.description && ( + <> +
    +
    +

    {data.description}

    +
    + + )} +
    + + {data.posted_at} +
    +
    +
    +
    + ); + } + return (
    @@ -23,10 +58,14 @@ export default function CardBlog({ data }: CardBlogProps) { {data.title}
    -
    -
    -

    {data.description}

    -
    + {isDescriptionVisible && !!data?.description && ( + <> +
    +
    +

    {data.description}

    +
    + + )}
    {data.posted_at} diff --git a/src/components/blogs/ListOfBlog.tsx b/src/components/blogs/ListOfBlog.tsx index 12a2da8..010f516 100644 --- a/src/components/blogs/ListOfBlog.tsx +++ b/src/components/blogs/ListOfBlog.tsx @@ -36,7 +36,7 @@ export default function ListOfBlog({ searchKeyword }: ListOfBlogProps) {
    {blogQuery.isFetching && } - {blogQuery.hasNext && ( + {!blogQuery.isFetching && blogQuery.hasNext && ( diff --git a/src/components/blogs/ListOfRecentBlog.tsx b/src/components/blogs/ListOfRecentBlog.tsx new file mode 100644 index 0000000..591c1e5 --- /dev/null +++ b/src/components/blogs/ListOfRecentBlog.tsx @@ -0,0 +1,48 @@ +"use client"; + +import Loader from "@/components/loaders/Loader"; +import { useRecentBlogQuery } from "@/services/hooks/blog"; +import { useEffect } from "react"; +import CardBlog from "./CardBlog"; + +type ListOfRecentBlogProps = { + currentBlogId?: number; +}; + +export default function ListOfRecentBlog({ currentBlogId }: ListOfRecentBlogProps) { + const recentBlogQuery = useRecentBlogQuery(); + + useEffect(() => { + if (!!currentBlogId) { + recentBlogQuery._fetch({ + currentBlogId, + }); + } + }, [currentBlogId]); + + if (recentBlogQuery.isFetching) { + return ( +
    + +
    + ); + } + + if (recentBlogQuery.data.length <= 0) return <>; + + return ( + <> +
    +
    +
    Recent Posts
    +
    +
    +
    + {recentBlogQuery.data.map((blog) => ( + + ))} +
    +
    + + ); +} diff --git a/src/payload.config.ts b/src/payload.config.ts index c53d1fc..83618ec 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -18,8 +18,6 @@ const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); export default buildConfig({ - cors: [process.env.SITE_URL || ""], - csrf: [process.env.SITE_URL || ""], admin: { user: Users.slug, importMap: { diff --git a/src/schema/blog.ts b/src/schema/blog.ts index 6349fc2..ea26548 100644 --- a/src/schema/blog.ts +++ b/src/schema/blog.ts @@ -1,7 +1,7 @@ export type BlogData = { slug?: string | null; title: string; - description: string; + description?: string; img?: { url: string; alt?: string }; posted_at: string; }; diff --git a/src/schema/services/blog.ts b/src/schema/services/blog.ts index ab91539..16e2b5d 100644 --- a/src/schema/services/blog.ts +++ b/src/schema/services/blog.ts @@ -4,3 +4,7 @@ export type FetchBlogParams = { categoryId?: number; tagId?: number; }; + +export type FetchRecentBlogParams = { + currentBlogId: number; +}; diff --git a/src/services/hooks/blog.ts b/src/services/hooks/blog.ts index ba9f31e..dc8a757 100644 --- a/src/services/hooks/blog.ts +++ b/src/services/hooks/blog.ts @@ -1,7 +1,7 @@ import { BlogData } from "@/schema/blog"; -import { FetchBlogParams } from "@/schema/services/blog"; +import { FetchBlogParams, FetchRecentBlogParams } from "@/schema/services/blog"; import { useState } from "react"; -import { fetchBlogREST } from "../rest/blog"; +import { fetchBlogREST, fetchRecentBlogREST } from "../rest/blog"; export function useBlogQuery() { const [data, setData] = useState([]); @@ -14,7 +14,13 @@ export function useBlogQuery() { setFetching(false); if (Array.isArray(res?.formattedData)) { - setData(res.formattedData); + if (!!params.page && params.page > 1) { + setData((currentData) => { + return [...currentData, ...res.formattedData]; + }); + } else { + setData(res.formattedData); + } } setHasNext(res?.hasNextPage ?? false); } @@ -26,3 +32,24 @@ export function useBlogQuery() { hasNext, }; } + +export function useRecentBlogQuery() { + const [data, setData] = useState([]); + const [isFetching, setFetching] = useState(false); + + async function _fetch(params: FetchRecentBlogParams) { + setFetching(true); + const res = await fetchRecentBlogREST(params); + setFetching(false); + + if (Array.isArray(res?.formattedData)) { + setData(res.formattedData); + } + } + + return { + _fetch, + data, + isFetching, + }; +} diff --git a/src/services/payload/blog.ts b/src/services/payload/blog.ts index 4e57278..de75adb 100644 --- a/src/services/payload/blog.ts +++ b/src/services/payload/blog.ts @@ -37,11 +37,13 @@ export async function fetchBlog({ page, search = "", categoryId, tagId }: FetchB where: queryCondition, }); - const formattedData = blogDataQuery.docs.map((item) => { + const formattedData: BlogData[] = blogDataQuery.docs.map((item) => { return { - ...item, - imgFormatted: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined, - createdAtFormatted: formatDate(item.createdAt), + slug: item.slug, + title: item.title, + description: sanitizeBlogContentIntoStringPreview(item.content), + img: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined, + posted_at: formatDate(item.createdAt), }; }); diff --git a/src/services/rest/blog.ts b/src/services/rest/blog.ts index 1ea4527..19d5b76 100644 --- a/src/services/rest/blog.ts +++ b/src/services/rest/blog.ts @@ -1,6 +1,6 @@ import { Blog } from "@/payload-types"; import { BlogData } from "@/schema/blog"; -import { FetchBlogParams } from "@/schema/services/blog"; +import { FetchBlogParams, FetchRecentBlogParams } from "@/schema/services/blog"; import { formatDate } from "@/utils/datetime"; import { sanitizeBlogContentIntoStringPreview } from "@/utils/sanitize"; import { PaginatedDocs, Where } from "payload"; @@ -59,3 +59,43 @@ export async function fetchBlogREST({ page, search = "", categoryId, tagId }: Fe return null; } } + +export async function fetchRecentBlogREST({ currentBlogId }: FetchRecentBlogParams) { + const queryCondition: Where = { + _status: { equals: "published" }, + id: { + not_equals: currentBlogId, + }, + }; + + const queryParams = stringify( + { + pagination: true, + limit: 2, + where: queryCondition, + }, + { addQueryPrefix: true } + ); + + const blogRequest = await fetch(`/api/blogs${queryParams}`); + + if (blogRequest.ok) { + const resData = (await blogRequest.json()) as PaginatedDocs; + const formattedData: BlogData[] = resData.docs.map((item) => { + return { + slug: item.slug, + title: item.title, + description: sanitizeBlogContentIntoStringPreview(item.content), + img: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined, + posted_at: formatDate(item.createdAt), + }; + }); + + return { + ...resData, + formattedData, + }; + } else { + return null; + } +} From f574fdc66d227e5e74a9545ebde294385a88be66 Mon Sep 17 00:00:00 2001 From: RizqiSyahrendra Date: Tue, 22 Apr 2025 18:03:54 +0700 Subject: [PATCH 02/10] fix: property list and detail UI slicing --- public/css/style.css | 6 +- src/app/(main)/globals.css | 1 + .../(main)/listings-for-rent/[slug]/page.tsx | 594 ++++++++++++++++++ src/app/(main)/listings-for-rent/page.tsx | 292 +++++++++ src/components/CardProperty.tsx | 63 ++ src/components/Pagination.tsx | 120 ++++ src/components/layouts/Header.tsx | 2 +- src/schema/property.ts | 16 + src/utils/general.ts | 19 + 9 files changed, 1109 insertions(+), 4 deletions(-) create mode 100644 src/app/(main)/listings-for-rent/[slug]/page.tsx create mode 100644 src/app/(main)/listings-for-rent/page.tsx create mode 100644 src/components/CardProperty.tsx create mode 100644 src/components/Pagination.tsx create mode 100644 src/schema/property.ts diff --git a/public/css/style.css b/public/css/style.css index 9b91adc..b223537 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -588,9 +588,9 @@ a:hover { color: #967244; } -a[href*='tel'], a[href*='mailto'] { +/* a[href*='tel'], a[href*='mailto'] { white-space: nowrap; -} +} */ .link-default, .link-default:active, .link-default:focus { color: #424445; @@ -1177,7 +1177,7 @@ a.privacy-link { padding: 12px 11px; color: #9cc1ff; letter-spacing: 0; - background-color: #31323c; + background-color: var(--color-colorContactForm); } .block-callboard a, .block-callboard a:focus, .block-callboard a:active { diff --git a/src/app/(main)/globals.css b/src/app/(main)/globals.css index 08bd10e..29b5ab8 100644 --- a/src/app/(main)/globals.css +++ b/src/app/(main)/globals.css @@ -34,6 +34,7 @@ --color-colorText1: var(--color-colorExt10); --color-colorText2: var(--color-colorExt20); --color-colorLoaderBackground: var(--color-colorExt20); + --color-colorPriceTag: var(--color-colorExt30); } @layer components { diff --git a/src/app/(main)/listings-for-rent/[slug]/page.tsx b/src/app/(main)/listings-for-rent/[slug]/page.tsx new file mode 100644 index 0000000..a6dada7 --- /dev/null +++ b/src/app/(main)/listings-for-rent/[slug]/page.tsx @@ -0,0 +1,594 @@ +import CardProperty from "@/components/CardProperty"; +import HeroImage from "@/components/HeroImage"; +import { CardPropertyData } from "@/schema/property"; +import { formatCurrency } from "@/utils/general"; + +const similarPropertiesData: CardPropertyData[] = [ + { + id: 1, + title: "401 Biscayne Boulevard, Miami", + slug: "401-biscayne-boulevard", + images: [ + { url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }, + { url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }, + { url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }, + ], + price: 5000, + propertyType: "rent", + posted_at: "", + area: 480, + bathrooms_count: 2, + bedrooms_count: 2, + is_available: true, + }, + { + id: 2, + title: "402 Biscayne Boulevard, Miami", + slug: "402-biscayne-boulevard", + images: [{ url: "/images/featured-properties-01-480x287.jpg", alt: "biscayne boulevard" }], + price: 5000, + propertyType: "rent", + posted_at: "", + area: 480, + bathrooms_count: 2, + bedrooms_count: 2, + is_available: true, + }, +]; + +export default function ListingsForRentDetail() { + return ( + <> + + +
    +
    +
    +
    +
    +
    $5000\mo
    + + +
    +
    +
    +
    +
      +
    • + + 2 Bathrooms +
    • +
    • + + 2 Bedrooms +
    • +
    • + + 480 Sq Ft +
    • +
    +
    + +
    +
    +

    + Choose this property if you are looking for a modern house near the ocean shore. With 2 bathrooms and 2 + bedrooms as well as a single garage, it is a perfect option for a small family. +

    +

    + This home has been completely renovated within the past year and features amazing views and sunsets of + the local lake, solid wood cabinets (and loads of them), granite counters with colored glass backsplash, + sliding glass doors across the entire family room allowing beautiful views of the lake etc. Its + affordable price serves as a great bonus for a family looking for an opportunity to save money on Miami + MyHome. +

    + +
    +
    + +
    +
    +
    +
    +
    Address:
    +
    Biscayne Blvd
    +
    +
    +
    State/County:
    +
    Florida
    +
    +
    +
    City:
    +
    Miami
    +
    +
    +
    Zip:
    +
    8322
    +
    +
    +
    Country:
    +
    United States
    +
    +
    +
    Area:
    +
    Lake Worth
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
      +
    • 2 Stories
    • +
    • Basketball Court
    • +
    • Lawn
    • +
    • Gym
    • +
    • Fireplace
    • +
    • Sprinklers
    • +
    • Private Space
    • +
    • Balcony
    • +
    • Laundry
    • +
    • Ocean View
    • +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    Base Rent
    +
    {formatCurrency(2700)}
    +
    +
    +
    +
    +
    Smart Home
    +
    {formatCurrency(20)}
    +
    +
    +
    +
    +
    Utility Service
    +
    {formatCurrency(5)}
    +
    +
    +
    +
    +
    Est. total monthly*
    +
    {formatCurrency(2900)}
    +
    +
    +
    +
    +
    +
    + +
    +

    Property Map

    +
    +
    +