Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/auction demo #293

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
49 changes: 42 additions & 7 deletions src/app/[countryCode]/(main)/products/[handle]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@ import { Metadata } from "next"
import { notFound } from "next/navigation"

import {
getCustomer,
getProductByHandle,
getProductsList,
getRegion,
listAuctions,
listRegions,
retrievePricedProductById,
} from "@lib/data"
import { Region } from "@medusajs/medusa"
import ProductTemplate from "@modules/products/templates"
import AuctionsActions from "@modules/products/components/auction-actions"
import ImageGallery from "@modules/products/components/image-gallery"
import ProductTabs from "@modules/products/components/product-tabs"
import RelatedProducts from "@modules/products/components/related-products"
import ProductInfo from "@modules/products/templates/product-info"
import SkeletonRelatedProducts from "@modules/skeletons/templates/skeleton-related-products"
import { Suspense } from "react"

type Props = {
params: { countryCode: string; handle: string }
Expand Down Expand Up @@ -84,6 +92,10 @@ const getPricedProductByHandle = async (handle: string, region: Region) => {
}

export default async function ProductPage({ params }: Props) {
const { product } = await getProductByHandle(params.handle).then(
(product) => product
)

const region = await getRegion(params.countryCode)

if (!region) {
Expand All @@ -92,15 +104,38 @@ export default async function ProductPage({ params }: Props) {

const pricedProduct = await getPricedProductByHandle(params.handle, region)

if (!pricedProduct) {
if (!pricedProduct || !pricedProduct.id) {
notFound()
}

const customer = await getCustomer()

const {
auctions: { [0]: auction },
} = await listAuctions(pricedProduct.id)

return (
<ProductTemplate
product={pricedProduct}
region={region}
countryCode={params.countryCode}
/>
<>
<div className="content-container flex flex-col small:flex-row small:items-start py-6 relative">
<div className="flex flex-col small:sticky small:top-48 small:py-0 small:max-w-[300px] w-full py-8 gap-y-6">
<ProductInfo product={product} />
<ProductTabs product={product} />
</div>
<div className="block w-full relative">
<ImageGallery images={product?.images || []} />
</div>
<AuctionsActions
product={product}
region={region}
auction={auction}
customer={customer}
/>
</div>
<div className="content-container my-16 small:my-32">
<Suspense fallback={<SkeletonRelatedProducts />}>
<RelatedProducts product={product} countryCode={params.countryCode} />
</Suspense>
</div>
</>
)
}
38 changes: 37 additions & 1 deletion src/lib/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import { cache } from "react"
import sortProducts from "@lib/util/sort-products"
import transformProductPreview from "@lib/util/transform-product-preview"
import { SortOptions } from "@modules/store/components/refinement-list/sort-products"
import { ProductCategoryWithChildren, ProductPreviewType } from "types/global"
import {
Auction,
ProductCategoryWithChildren,
ProductPreviewType,
} from "types/global"

import { medusaClient } from "@lib/config"
import medusaError from "@lib/util/medusa-error"
Expand Down Expand Up @@ -51,6 +55,38 @@ const getMedusaHeaders = (tags: string[] = []) => {
return headers
}

// Auction actions
export async function listAuctions(
productId: string
): Promise<{ auctions: Auction[] }> {
const headers = getMedusaHeaders(["auctions"])

return fetch(
`http://localhost:9000/store/auctions?product_id=${productId}&status=active`,
{
headers,
}
)
.then((response) => response.json())
.catch((err) => medusaError(err))
}

export async function createBid(
auctionId: string,
amount: number,
customerId: string
) {
const headers = getMedusaHeaders(["auctions"])

return fetch(`http://localhost:9000/store/auctions/${auctionId}/bids`, {
method: "POST",
headers,
body: JSON.stringify({ amount, customer_id: customerId }),
})
.then((response) => response.json())
.catch((err) => medusaError(err))
}

// Cart actions
export async function createCart(data = {}) {
const headers = getMedusaHeaders(["cart"])
Expand Down
18 changes: 18 additions & 0 deletions src/lib/util/time-ago.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function timeAgo(date: Date) {
const now = new Date()
const diff = Math.abs(now.getTime() - date.getTime())
const seconds = Math.floor(diff / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)

if (days > 0) {
return `${days} day${days > 1 ? "s" : ""} ago`
} else if (hours > 0) {
return `${hours} h ago`
} else if (minutes > 0) {
return `${minutes} min ago`
} else {
return `${seconds} s ago`
}
}
22 changes: 22 additions & 0 deletions src/modules/products/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use server"

import { createBid } from "@lib/data"
import { revalidateTag } from "next/cache"

export async function placeBid({
auctionId,
amount,
customerId,
}: {
auctionId: string
amount: number
customerId: string
}) {
try {
const res = await createBid(auctionId, amount, customerId)
revalidateTag("auctions")
return res
} catch (error: any) {
return error.toString()
}
}
194 changes: 194 additions & 0 deletions src/modules/products/components/auction-actions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
"use client"

import { Customer, Region } from "@medusajs/medusa"
import { PricedProduct } from "@medusajs/medusa/dist/types/pricing"
import { Button, Input, Text } from "@medusajs/ui"
import { FormEvent, Suspense, useRef, useState } from "react"

import { formatAmount } from "@lib/util/prices"
import Divider from "@modules/common/components/divider"
import { placeBid } from "@modules/products/actions"
import { Auction } from "types/global"
import AuctionBids from "../auction-bids"
import AuctionCountdown from "../auction-countdown"
import LocalizedClientLink from "@modules/common/components/localized-client-link"

type AuctionsActionsProps = {
product: PricedProduct
region: Region
auction: Auction
customer?: Omit<Customer, "password_hash"> | null
}

export type PriceType = {
calculated_price: string
original_price?: string
price_type?: "sale" | "default"
percentage_diff?: string
}

export default function AuctionsActions({
region,
auction,
customer,
}: AuctionsActionsProps) {
const [amount, setAmount] = useState<number | undefined>()
const [isloading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)

const maxBid =
auction?.bids.length > 0
? auction?.bids?.reduce((a, b) => {
if (a.amount > b.amount) return a
return b
})
: { amount: auction?.starting_price, customer_id: "" }

const currentBid = formatAmount({
amount: maxBid?.amount,
region,
})

const minNextBid = formatAmount({
amount: maxBid ? maxBid?.amount + 500 : auction?.starting_price + 500,
region,
})

const formRef = useRef<HTMLFormElement>(null)

const handlePlaceBid = async (e: FormEvent) => {
e.preventDefault()

setError(null)
setIsLoading(true)

if (!customer) {
setIsLoading(false)
setError("Please sign in to place a bid")
return
}

if (!amount) {
setIsLoading(false)
setError("Please enter a valid amount")
return
}

if (amount * 100 < (maxBid?.amount + 500 || auction.starting_price + 500)) {
setIsLoading(false)
setError("Please enter an amount higher than " + minNextBid)
return
}

await placeBid({
auctionId: auction.id,
amount: amount * 100 || 0,
customerId: customer.id,
})
.then((res) => {
if (res.message && res.highestBid) {
const message =
"Please enter an amount higher than " +
formatAmount({ amount: res.highestBid, region })
setError(message)
}
})
.catch((e) => {
setError(e)
})

setAmount(undefined)
formRef.current?.reset()
setIsLoading(false)
}

return (
<div className="flex flex-col small:sticky small:top-48 small:py-0 small:max-w-[300px] w-full py-8 gap-y-10">
{!auction && <p>No active auction. </p>}

{auction && (
<>
<div className="flex flex-col gap-2">
<Suspense>
<AuctionCountdown targetDate={new Date(auction.ends_at)} />
</Suspense>

<Divider />
</div>
<div className="flex flex-col gap-y-3">
<Text as="span" className="txt-compact-small text-ui-fg-subtle">
Current bid:
</Text>
<Text as="span" className="text-3xl ">
{currentBid}
</Text>
{maxBid.customer_id === customer?.id && (
<Text className="text-ui-fg-subtle txt-compact-xsmall">
You are the highest bidder!
</Text>
)}
</div>
{!customer ? (
<Text className="text-ui-fg-subtle txt-compact-medium">
<LocalizedClientLink
href="/account"
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
>
Sign in
</LocalizedClientLink>{" "}
to place a bid
</Text>
) : (
<form
className="flex flex-col gap-4"
onSubmit={handlePlaceBid}
ref={formRef}
>
<div>
<Input
min={minNextBid}
type="number"
placeholder={`Enter your bid (min: ${minNextBid})`}
value={amount}
onChange={(e) => setAmount(parseFloat(e.target.value))}
/>

{error && <Text className="text-ui-fg-error">{error}</Text>}
</div>
<Button
type="submit"
variant="primary"
className="w-full h-10"
isLoading={isloading}
>
Place bid
</Button>
</form>
)}
<div className="flex flex-col gap-y-2">
<div>
<div className="flex flex-col gap-y-4">
<Divider />
<Suspense>
<AuctionBids
bids={auction.bids}
region={region}
customer={customer}
/>
</Suspense>
</div>
</div>
<Suspense>
<Text
className="text-ui-fg-muted text-right"
suppressHydrationWarning
>
Ends at: {new Date(auction.ends_at).toDateString()}
</Text>
</Suspense>
</div>
</>
)}
</div>
)
}