Skip to content

Commit

Permalink
Add interactivity between pagination and list
Browse files Browse the repository at this point in the history
fixes #49
  • Loading branch information
franthormel committed Aug 6, 2024
1 parent 2c5726f commit 94a5de9
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 86 deletions.
45 changes: 45 additions & 0 deletions app/listings/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use server";

import prisma from "@/lib/db";
import { LISTINGS_PER_PAGE } from "./constants";
import { countListingsToSkip } from "./function";
import { Listing } from "./types";

export async function fetchListingsByPage(currentPage: number) {
const dbListings = await prisma.listing.findMany({
skip: countListingsToSkip(currentPage, LISTINGS_PER_PAGE),
take: LISTINGS_PER_PAGE,
include: {
address: true,
prices: true,
},
});
const processedListings = dbListings.map((dbListing) => {
const dbListingPrice = dbListing.prices
.filter((dbListingPrice) => dbListingPrice.isCurrent)
.at(0)!
.value.toNumber();
const dbListingAddress = dbListing.address!;
const listing: Listing = {
id: dbListing.id,
imageUrls: dbListing.imageUrls,
beds: dbListing.beds,
baths: dbListing.baths,
area: dbListing.area.toNumber(),
createdDate: dbListing.createdDate,
address: {
addressLine: dbListingAddress.addressLine,
city: dbListingAddress.city,
state: dbListingAddress.state,
longitude: dbListingAddress.longitude.toNumber(),
latitude: dbListingAddress.latitude.toNumber(),
},
price: {
value: dbListingPrice,
},
};
return listing;
});

return processedListings;
}
2 changes: 1 addition & 1 deletion app/listings/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const LISTINGS_PER_PAGE = 32;
export const LISTINGS_PER_PAGE = 5;
export const STARTING_PAGE = 1;
20 changes: 18 additions & 2 deletions app/listings/content.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
"use client"

import { useContext, useEffect, useState } from "react"
import { fetchListingsByPage } from "./actions"
import ListingsList from "./list"
import { ListingsListTop } from "./list-top"
import { ListingsMap } from "./map"
import ListingsPagination from "./pagination"
import { ListingsContext } from "./provider"
import { Listing } from "./types"

export interface ListingsContentProps {
listings: Listing[]
pages: number
}

export default function ListingsContent(props: ListingsContentProps) {
const [listings, setListings] = useState<Listing[]>([]);
const context = useContext(ListingsContext);
const currentPage = context.pagination.currentPage.value;

useEffect(() => {
async function updateListings() {
const newListings = await fetchListingsByPage(currentPage);
setListings(newListings);
}
updateListings();
}, [currentPage])

return (
<div className="flex h-[36rem]">
<ListingsMap />
<div className="flex basis-1/2 flex-col">
<ListingsListTop />
<ListingsList listings={props.listings} />
<ListingsList listings={listings} />
<ListingsPagination pages={props.pages} />
</div>
</div>
Expand Down
6 changes: 4 additions & 2 deletions app/listings/filter-beds-baths.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ export default function ListingsFiltersBedsBaths() {
return (
<div className="flex flex-col gap-5">
<ButtonsSegmentedLabelled label="Beds"
values={bedsBathsOptions} />
values={bedsBathsOptions}
activeIndex={0} />
<ButtonsSegmentedLabelled label="Baths"
values={bedsBathsOptions} />
values={bedsBathsOptions}
activeIndex={0} />
<ListingsFilterButtons />
</div>
);
Expand Down
9 changes: 3 additions & 6 deletions app/listings/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@

import CardListing from "@/components/card-listing";
import { CURRENCY_FORMATTER } from "@/lib/formatter/currency";
import { useState } from "react";
import { Listing } from "./types";

export interface ListingsListInterface {
export interface ListingsListProps {
listings: Listing[]
}

export default function ListingsList(props: ListingsListInterface) {
const [listings, setListings] = useState<Listing[]>(props.listings);

export default function ListingsList(props: ListingsListProps) {
return (
<div className="flex basis-full flex-col items-center overflow-x-auto p-4">
<div className="grid-cols-auto grid gap-4 xl:grid-cols-2 xl:gap-6">
{listings.map((listing) => {
{props.listings.map((listing) => {
const priceFormatted = CURRENCY_FORMATTER.format(listing.price.value)
return (
<CardListing
Expand Down
45 changes: 3 additions & 42 deletions app/listings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,19 @@
import prisma from "@/lib/db";
import { LISTINGS_PER_PAGE, STARTING_PAGE } from "./constants";
import { LISTINGS_PER_PAGE } from "./constants";
import ListingsContent from "./content";
import { countListingsToSkip } from "./function";
import ListingsProvider from "./provider";
import { ListingsSearchFilters } from "./search-filters";
import { Listing } from "./types";

export default async function Listings() {
// These are just the initial values.
// The ones that will be 'filtered' are different.
const dbListings = await prisma.listing.findMany({
skip: countListingsToSkip(STARTING_PAGE, LISTINGS_PER_PAGE),
take: LISTINGS_PER_PAGE,
include: {
address: true,
prices: true,
}
});

// FUTURE: Might need to move this data 'transformer' into another function
// since it might be used in server actions by client components down the tree
const processedListings: Listing[] = dbListings.map((dbListing) => {
const dbListingPrice = dbListing.prices.filter((dbListingPrice) => dbListingPrice.isCurrent).at(0)!.value.toNumber();
const dbListingAddress = dbListing.address!;
const listing: Listing = {
id: dbListing.id,
imageUrls: dbListing.imageUrls,
beds: dbListing.beds,
baths: dbListing.baths,
area: dbListing.area.toNumber(),
createdDate: dbListing.createdDate,
address: {
addressLine: dbListingAddress.addressLine,
city: dbListingAddress.city,
state: dbListingAddress.state,
longitude: dbListingAddress.longitude.toNumber(),
latitude: dbListingAddress.latitude.toNumber(),
},
price: {
value: dbListingPrice
}
}
return listing
})


// TODO: Move down and the count must be equal to the number of matched listings based on filter values
const listingsCount = await prisma.listing.count()
const pages = Math.ceil(listingsCount / LISTINGS_PER_PAGE);

return (
<ListingsProvider>
<div className="flex flex-col gap-y-8">
<ListingsSearchFilters />
<ListingsContent listings={processedListings} pages={pages} />
<ListingsContent pages={pages} />
</div>
</ListingsProvider>
);
Expand Down
8 changes: 7 additions & 1 deletion app/listings/pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ export default function ListingsPagination(props: ListingsPaginationProps) {

return (
<div className="flex basis-20 items-center justify-center border-y-[1px] border-gray-200">
<Pagination pages={props.pages} currentPage={context.pagination.currentPage} />
<Pagination
pages={props.pages}
currentPage={context.pagination.currentPage.value}
changeToPreviousPage={context.pagination.changeToPreviousPage}
changeCurrentPage={context.pagination.currentPage.change}
changeToNextPage={context.pagination.changeToNextPage}
/>
</div>
);
}
45 changes: 36 additions & 9 deletions app/listings/provider.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,52 @@
"use client"

import { createContext } from "react"
import { createContext, useState } from "react"
import { STARTING_PAGE } from "./constants"

export interface ListingsContextInterface {
interface ListingsContextInterface {
pagination: {
currentPage: number
changeToPreviousPage: () => void,
changeToNextPage: () => void,
currentPage: {
value: number,
change: (page: number) => void
}
}
}

const INITIAL_STATE: ListingsContextInterface = {
export const ListingsContext = createContext<ListingsContextInterface>({
pagination: {
currentPage: STARTING_PAGE
changeToPreviousPage: () => { },
changeToNextPage: () => { },
currentPage: {
value: STARTING_PAGE,
change: (page) => { }
},
}
}

export const ListingsContext = createContext<ListingsContextInterface>(INITIAL_STATE);
});

export default function ListingsProvider({ children }: { children: React.ReactNode }) {
const [currentPage, setCurrentPage] = useState<number>(STARTING_PAGE);

const stateValue: ListingsContextInterface = {
pagination: {
changeToPreviousPage: () => {
setCurrentPage(currentPage - 1)
},
changeToNextPage: () => {
setCurrentPage(currentPage + 1)
},
currentPage: {
value: currentPage,
change: (page: number) => {
setCurrentPage(page)
},
}
}
}

return (
<ListingsContext.Provider value={INITIAL_STATE}>
<ListingsContext.Provider value={stateValue}>
{children}
</ListingsContext.Provider>
)
Expand Down
2 changes: 1 addition & 1 deletion components/buttons-segmented/labelled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ export default function ButtonsSegmentedLabelled(props: ButtonsSegmentedLabelled
</div>
</div>
);
}
}
12 changes: 6 additions & 6 deletions components/pagination/function.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
/**
*
* Show the previous button only if the current page is not the first page
* Enable the previous button only if the current page is not the first page
*
* @param currentPage Current page
* @returns `true` if the previous button should be shown, otherwise false.
* @returns `true` if the previous button should be enabled, otherwise false.
*/
export function checkShowPreviousButton(currentPage: number) {
export function checkPreviousButton(currentPage: number) {
return currentPage > 1;
}

/**
* Show the next button only if the current page is not the last page
* Enable the next button only if the current page is not the last page
*
* @param pages Total number of pages
* @param currentPage Current page
* @returns `true` if the next button should be shown, otherwise false.
* @returns `true` if the next button should be enabled, otherwise false.
*/
export function checkShowNextButton(pages: number, currentPage: number) {
export function checkNextButton(pages: number, currentPage: number) {
return currentPage < pages;
}
45 changes: 33 additions & 12 deletions components/pagination/index.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,64 @@
import { checkShowNextButton, checkShowPreviousButton } from "./function"
import { checkNextButton, checkPreviousButton } from "./function"
import PaginationItemNav from "./item-nav"
import PaginationItemNumber from "./item-number"

export interface PaginationProps {
pages: number
currentPage: number
changeToPreviousPage?: () => void
changeCurrentPage?: (page: number) => void
changeToNextPage?: () => void
}

export default function Pagination(props: PaginationProps) {
const pageNumbers = Array.from({ length: props.pages }, (elem, i) => i + 1)
const showPreviousButton = checkShowPreviousButton(props.currentPage);
const showNextButton = checkShowNextButton(props.pages, props.currentPage);
const previousButtonEnabled = checkPreviousButton(props.currentPage);
const nextButtonEnabled = checkNextButton(props.pages, props.currentPage);

return (
<div className="flex gap-3">
{/* Previous (hidden if the current page is the first page) */}
{showPreviousButton &&
<div className={`${previousButtonEnabled ? 'visible' : 'invisible'}`}
onClick={(e) => {
if (props.changeToPreviousPage && previousButtonEnabled) {
props.changeToPreviousPage()
}
}}>
<PaginationItemNav>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" width="24px" viewBox="0 -960 960 960">
<path d="M560-240 320-480l240-240 56 56-184 184 184 184-56 56Z" />
</svg>
</PaginationItemNav>
}
</div>
{pageNumbers.map((pageNumber) => {
const isCurrentPage = props.currentPage === pageNumber
return <PaginationItemNumber
page={pageNumber}
key={pageNumber}
isCurrentPage={isCurrentPage}
/>
return (
<div key={pageNumber}
onClick={(e) => {
if (props.changeCurrentPage) {
props.changeCurrentPage(pageNumber);
}
}}>
<PaginationItemNumber
page={pageNumber}
isCurrentPage={isCurrentPage}
/>
</div>
)
})}
{/* Next (hidden if current page is the last page) */}
{showNextButton &&
<div className={`${nextButtonEnabled ? 'visible' : 'invisible'}`}
onClick={(e) => {
if (props.changeToNextPage && nextButtonEnabled) {
props.changeToNextPage()
}
}}>
<PaginationItemNav>
<svg xmlns="http://www.w3.org/2000/svg" height="24px" width="24px" viewBox="0 -960 960 960">
<path d="M504-480 320-664l56-56 240 240-240 240-56-56 184-184Z" />
</svg>
</PaginationItemNav>
}
</div>
</div>
)
}
Loading

0 comments on commit 94a5de9

Please sign in to comment.