init starter package
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"use client"
|
||||
|
||||
import { clx } from "@medusajs/ui"
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
|
||||
export function Pagination({
|
||||
page,
|
||||
totalPages,
|
||||
'data-testid': dataTestid
|
||||
}: {
|
||||
page: number
|
||||
totalPages: number
|
||||
'data-testid'?: string
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
// Helper function to generate an array of numbers within a range
|
||||
const arrayRange = (start: number, stop: number) =>
|
||||
Array.from({ length: stop - start + 1 }, (_, index) => start + index)
|
||||
|
||||
// Function to handle page changes
|
||||
const handlePageChange = (newPage: number) => {
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.set("page", newPage.toString())
|
||||
router.push(`${pathname}?${params.toString()}`)
|
||||
}
|
||||
|
||||
// Function to render a page button
|
||||
const renderPageButton = (
|
||||
p: number,
|
||||
label: string | number,
|
||||
isCurrent: boolean
|
||||
) => (
|
||||
<button
|
||||
key={p}
|
||||
className={clx("txt-xlarge-plus text-ui-fg-muted", {
|
||||
"text-ui-fg-base hover:text-ui-fg-subtle": isCurrent,
|
||||
})}
|
||||
disabled={isCurrent}
|
||||
onClick={() => handlePageChange(p)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
|
||||
// Function to render ellipsis
|
||||
const renderEllipsis = (key: string) => (
|
||||
<span
|
||||
key={key}
|
||||
className="txt-xlarge-plus text-ui-fg-muted items-center cursor-default"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
)
|
||||
|
||||
// Function to render page buttons based on the current page and total pages
|
||||
const renderPageButtons = () => {
|
||||
const buttons = []
|
||||
|
||||
if (totalPages <= 7) {
|
||||
// Show all pages
|
||||
buttons.push(
|
||||
...arrayRange(1, totalPages).map((p) =>
|
||||
renderPageButton(p, p, p === page)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// Handle different cases for displaying pages and ellipses
|
||||
if (page <= 4) {
|
||||
// Show 1, 2, 3, 4, 5, ..., lastpage
|
||||
buttons.push(
|
||||
...arrayRange(1, 5).map((p) => renderPageButton(p, p, p === page))
|
||||
)
|
||||
buttons.push(renderEllipsis("ellipsis1"))
|
||||
buttons.push(
|
||||
renderPageButton(totalPages, totalPages, totalPages === page)
|
||||
)
|
||||
} else if (page >= totalPages - 3) {
|
||||
// Show 1, ..., lastpage - 4, lastpage - 3, lastpage - 2, lastpage - 1, lastpage
|
||||
buttons.push(renderPageButton(1, 1, 1 === page))
|
||||
buttons.push(renderEllipsis("ellipsis2"))
|
||||
buttons.push(
|
||||
...arrayRange(totalPages - 4, totalPages).map((p) =>
|
||||
renderPageButton(p, p, p === page)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// Show 1, ..., page - 1, page, page + 1, ..., lastpage
|
||||
buttons.push(renderPageButton(1, 1, 1 === page))
|
||||
buttons.push(renderEllipsis("ellipsis3"))
|
||||
buttons.push(
|
||||
...arrayRange(page - 1, page + 1).map((p) =>
|
||||
renderPageButton(p, p, p === page)
|
||||
)
|
||||
)
|
||||
buttons.push(renderEllipsis("ellipsis4"))
|
||||
buttons.push(
|
||||
renderPageButton(totalPages, totalPages, totalPages === page)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return buttons
|
||||
}
|
||||
|
||||
// Render the component
|
||||
return (
|
||||
<div className="flex justify-center w-full mt-12">
|
||||
<div className="flex gap-3 items-end" data-testid={dataTestid}>{renderPageButtons()}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { useCallback } from "react"
|
||||
|
||||
import SortProducts, { SortOptions } from "./sort-products"
|
||||
|
||||
type RefinementListProps = {
|
||||
sortBy: SortOptions
|
||||
search?: boolean
|
||||
'data-testid'?: string
|
||||
}
|
||||
|
||||
const RefinementList = ({ sortBy, 'data-testid': dataTestId }: RefinementListProps) => {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const createQueryString = useCallback(
|
||||
(name: string, value: string) => {
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.set(name, value)
|
||||
|
||||
return params.toString()
|
||||
},
|
||||
[searchParams]
|
||||
)
|
||||
|
||||
const setQueryParams = (name: string, value: string) => {
|
||||
const query = createQueryString(name, value)
|
||||
router.push(`${pathname}?${query}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex small:flex-col gap-12 py-4 mb-8 small:px-0 pl-6 small:min-w-[250px] small:ml-[1.675rem]">
|
||||
<SortProducts sortBy={sortBy} setQueryParams={setQueryParams} data-testid={dataTestId} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RefinementList
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import FilterRadioGroup from "@modules/common/components/filter-radio-group"
|
||||
|
||||
export type SortOptions = "price_asc" | "price_desc" | "created_at"
|
||||
|
||||
type SortProductsProps = {
|
||||
sortBy: SortOptions
|
||||
setQueryParams: (name: string, value: SortOptions) => void
|
||||
"data-testid"?: string
|
||||
}
|
||||
|
||||
const sortOptions = [
|
||||
{
|
||||
value: "created_at",
|
||||
label: "Latest Arrivals",
|
||||
},
|
||||
{
|
||||
value: "price_asc",
|
||||
label: "Price: Low -> High",
|
||||
},
|
||||
{
|
||||
value: "price_desc",
|
||||
label: "Price: High -> Low",
|
||||
},
|
||||
]
|
||||
|
||||
const SortProducts = ({
|
||||
"data-testid": dataTestId,
|
||||
sortBy,
|
||||
setQueryParams,
|
||||
}: SortProductsProps) => {
|
||||
const handleChange = (value: SortOptions) => {
|
||||
setQueryParams("sortBy", value)
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterRadioGroup
|
||||
title="Sort by"
|
||||
items={sortOptions}
|
||||
value={sortBy}
|
||||
handleChange={handleChange}
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SortProducts
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import SkeletonProductGrid from "@modules/skeletons/templates/skeleton-product-grid"
|
||||
import RefinementList from "@modules/store/components/refinement-list"
|
||||
import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
|
||||
|
||||
import PaginatedProducts from "./paginated-products"
|
||||
|
||||
const StoreTemplate = ({
|
||||
sortBy,
|
||||
page,
|
||||
countryCode,
|
||||
}: {
|
||||
sortBy?: SortOptions
|
||||
page?: string
|
||||
countryCode: string
|
||||
}) => {
|
||||
const pageNumber = page ? parseInt(page) : 1
|
||||
const sort = sortBy || "created_at"
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col small:flex-row small:items-start py-6 content-container"
|
||||
data-testid="category-container"
|
||||
>
|
||||
<RefinementList sortBy={sort} />
|
||||
<div className="w-full">
|
||||
<div className="mb-8 text-2xl-semi">
|
||||
<h1 data-testid="store-page-title">All products</h1>
|
||||
</div>
|
||||
<Suspense fallback={<SkeletonProductGrid />}>
|
||||
<PaginatedProducts
|
||||
sortBy={sort}
|
||||
page={pageNumber}
|
||||
countryCode={countryCode}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StoreTemplate
|
||||
@@ -0,0 +1,92 @@
|
||||
import { listProductsWithSort } from "@lib/data/products"
|
||||
import { getRegion } from "@lib/data/regions"
|
||||
import ProductPreview from "@modules/products/components/product-preview"
|
||||
import { Pagination } from "@modules/store/components/pagination"
|
||||
import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
|
||||
|
||||
const PRODUCT_LIMIT = 12
|
||||
|
||||
type PaginatedProductsParams = {
|
||||
limit: number
|
||||
collection_id?: string[]
|
||||
category_id?: string[]
|
||||
id?: string[]
|
||||
order?: string
|
||||
}
|
||||
|
||||
export default async function PaginatedProducts({
|
||||
sortBy,
|
||||
page,
|
||||
collectionId,
|
||||
categoryId,
|
||||
productsIds,
|
||||
countryCode,
|
||||
}: {
|
||||
sortBy?: SortOptions
|
||||
page: number
|
||||
collectionId?: string
|
||||
categoryId?: string
|
||||
productsIds?: string[]
|
||||
countryCode: string
|
||||
}) {
|
||||
const queryParams: PaginatedProductsParams = {
|
||||
limit: 12,
|
||||
}
|
||||
|
||||
if (collectionId) {
|
||||
queryParams["collection_id"] = [collectionId]
|
||||
}
|
||||
|
||||
if (categoryId) {
|
||||
queryParams["category_id"] = [categoryId]
|
||||
}
|
||||
|
||||
if (productsIds) {
|
||||
queryParams["id"] = productsIds
|
||||
}
|
||||
|
||||
if (sortBy === "created_at") {
|
||||
queryParams["order"] = "created_at"
|
||||
}
|
||||
|
||||
const region = await getRegion(countryCode)
|
||||
|
||||
if (!region) {
|
||||
return null
|
||||
}
|
||||
|
||||
let {
|
||||
response: { products, count },
|
||||
} = await listProductsWithSort({
|
||||
page,
|
||||
queryParams,
|
||||
sortBy,
|
||||
countryCode,
|
||||
})
|
||||
|
||||
const totalPages = Math.ceil(count / PRODUCT_LIMIT)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul
|
||||
className="grid grid-cols-2 w-full small:grid-cols-3 medium:grid-cols-4 gap-x-6 gap-y-8"
|
||||
data-testid="products-list"
|
||||
>
|
||||
{products.map((p) => {
|
||||
return (
|
||||
<li key={p.id}>
|
||||
<ProductPreview product={p} region={region} />
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
data-testid="product-pagination"
|
||||
page={page}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user