Skip to content
This repository has been archived by the owner on Jul 10, 2023. It is now read-only.

Commit

Permalink
Feat: revamp discount list (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
pKorsholm authored Jan 27, 2022
1 parent ed87498 commit 3a72aed
Show file tree
Hide file tree
Showing 11 changed files with 384 additions and 331 deletions.
4 changes: 2 additions & 2 deletions src/assets/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,8 @@
@apply bg-yellow-40 bg-opacity-20 text-yellow-60;
}

.badge-denomination {
@apply bg-grey-10 inter-small-regular whitespace-nowrap;
.badge-default {
@apply bg-grey-10 text-grey-90 whitespace-nowrap;
}

.btn {
Expand Down
4 changes: 2 additions & 2 deletions src/components/fundamentals/badge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import clsx from "clsx"
import React from "react"

type BadgeProps = {
variant: "primary" | "danger" | "success" | "warning" | "denomination"
variant: "primary" | "danger" | "success" | "warning" | "default"
} & React.HTMLAttributes<HTMLDivElement>

const Badge: React.FC<BadgeProps> = ({
Expand All @@ -17,7 +17,7 @@ const Badge: React.FC<BadgeProps> = ({
["badge-danger"]: variant === "danger",
["badge-success"]: variant === "success",
["badge-warning"]: variant === "warning",
["badge-denomination"]: variant === "denomination",
["badge-default"]: variant === "default",
})

return (
Expand Down
7 changes: 5 additions & 2 deletions src/components/molecules/table/filtering-option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ const FilteringOptions: React.FC<FilteringOptionProps> = ({
opt.onClick()
setSelected(opt.title)
}}
disabled={opt.count < 1}
disabled={typeof opt.count !== "undefined" && opt.count < 1}
className={clsx(
"py-1.5 my-1 w-48 px-3 flex items-center rounded text-grey-90 hover:border-0 hover:outline-none inter-small-semibold",
{ "cursor-pointer hover:bg-grey-10": opt.count > 0 }
{
"cursor-pointer hover:bg-grey-10":
typeof opt.count === "undefined" || opt.count > 0,
}
)}
>
{selected === opt.title && (
Expand Down
19 changes: 10 additions & 9 deletions src/components/molecules/table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,10 @@ const Table: TableType = React.forwardRef(
return (
<div className="flex flex-col">
<div className="w-full flex justify-between">
{filteringOptions && (
<div className="flex mb-2 self-end">
{filteringOptions.map((fo) => (
<FilteringOptions {...fo} />
))}
</div>
)}
<div className="flex mb-2 self-end">
{filteringOptions &&
filteringOptions.map((fo) => <FilteringOptions {...fo} />)}
</div>
<div className="flex">
{enableSearch && (
<TableSearch
Expand Down Expand Up @@ -240,11 +237,15 @@ Table.Row = React.forwardRef(
{ "cursor-pointer": linkTo !== undefined }
)}
{...props}
{...(linkTo && { onClick: () => navigate(linkTo) })}
{...(linkTo && {
onClick: () => {
navigate(linkTo)
},
})}
>
{children}
{actions && (
<Table.Cell className="w-8">
<Table.Cell onClick={(e) => e.stopPropagation()} className="w-8 py-1">
<Actionables actions={actions} />
</Table.Cell>
)}
Expand Down
4 changes: 2 additions & 2 deletions src/components/molecules/tag-grid.tsx/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type GiftCardVariant = {

type TagGridProps = {
tags: string[]
badgeVariant: "primary" | "danger" | "success" | "warning" | "denomination"
badgeVariant: "primary" | "danger" | "success" | "warning" | "default"
}

const TagGrid: React.FC<TagGridProps> = ({ tags, badgeVariant }) => {
Expand All @@ -30,7 +30,7 @@ const TagGrid: React.FC<TagGridProps> = ({ tags, badgeVariant }) => {
</Badge>
)
})}
{remainder > 0 && <Badge variant="denomination">+{remainder}</Badge>}
{remainder > 0 && <Badge variant="default">+{remainder}</Badge>}
</div>
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/organisms/gift-card-banner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const GiftCardBanner: React.FC<GiftCardBannerProps> = ({
<BannerCard.Description>{description}</BannerCard.Description>
<BannerCard.Footer>
<div className="flex items-center justify-between">
<TagGrid tags={denominations} badgeVariant="denomination" />
<TagGrid tags={denominations} badgeVariant="default" />
<StatusIndicator
variant={status === "published" ? "success" : "danger"}
title={status === "published" ? "Published" : "Unpublished"}
Expand Down
264 changes: 264 additions & 0 deletions src/components/templates/discount-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import React, { useContext, useEffect, useState } from "react"
import EditIcon from "../fundamentals/icons/edit-icon"
import TrashIcon from "../fundamentals/icons/trash-icon"
import StatusDot from "../fundamentals/status-indicator"
import Table from "../molecules/table"
import Medusa from "../../services/api"
import DeletePrompt from "../organisms/delete-prompt"
import { navigate } from "gatsby"
import DuplicateIcon from "../fundamentals/icons/duplicate-icon"
import Badge from "../fundamentals/badge"
import Spinner from "../atoms/spinner"
import qs from "query-string"
import { InterfaceContext } from "../../context/interface"
import { parse, end } from "iso8601-duration"
import { displayAmount } from "../../utils/prices"
import { getErrorMessage } from "../../utils/error-messages"
import { useAdminCreateDiscount, useAdminDiscounts } from "medusa-react"
import useToaster from "../../hooks/use-toaster"

const getDiscountStatus = (discount) => {
console.log(discount)
if (!discount.is_disabled) {
const date = new Date()
if (new Date(discount.starts_at) > date) {
return <StatusDot title="Scheduled" variant="warning" />
} else if (
(discount.ends_at && new Date(discount.ends_at) < date) ||
(discount.valid_duration &&
date >
end(parse(discount.valid_duration), new Date(discount.starts_at))) ||
discount.usage_count === discount.usage_limit
) {
return <StatusDot title="Expired" variant="danger" />
} else {
return <StatusDot title="Active" variant="success" />
}
}
return <StatusDot title="Draft" variant="default" />
}

const getDiscountAmount = (discount) => {
switch (discount.rule.type) {
case "fixed":
if (!discount.regions?.length) {
return ""
}
return `${displayAmount(
discount.regions[0].currency_code,
discount.rule.value
)} ${discount.regions[0].currency_code.toUpperCase()}`
case "percentage":
return `${discount.rule.value}%`
case "free_shipping":
return "Free Shipping"
default:
return ""
}
}

const DiscountTable: React.FC = () => {
const toaster = useToaster()
const [deleteDiscount, setDeleteDiscount] = useState(undefined)
const createDiscount = useAdminCreateDiscount()
const filtersOnLoad = qs.parse(window.location.search)

if (!filtersOnLoad.offset) {
filtersOnLoad.offset = 0
}

if (!filtersOnLoad.limit) {
filtersOnLoad.limit = 14
}

const { discounts, refetch, isLoading, count } = useAdminDiscounts({
is_dynamic: false,
limit: 14,
offset: 0,
...filtersOnLoad,
})

const [loading, setLoading] = useState(false)
const [limit, setLimit] = useState(filtersOnLoad.limit || 14)
const [offset, setOffset] = useState(filtersOnLoad.offset || 0)

const searchQuery = (customQuery = {}) => {
setOffset(0)
const baseUrl = qs.parse(window.location.href).url

const queryParts = {
offset: 0,
limit: 14,
...customQuery,
}

const prepared = qs.stringify(queryParts, {
skipNull: true,
skipEmptyString: true,
})

window.history.replaceState(baseUrl, "", `?${prepared}`)
refetch({
...queryParts,
})
}

const { setOnSearch, onUnmount } = useContext(InterfaceContext)
useEffect(onUnmount, [])
useEffect(() => {
setOnSearch({ q: searchQuery })
}, [])

const duplicateDiscount = (discount) => {
setLoading(true)
const newRule = {
description: discount.rule.description,
type: discount.rule.type,
value: discount.rule.value,
allocation: discount.rule.allocation,
valid_for: discount.rule.valid_for.map((product) => product.id),
}
const newDiscount = {
code: `${discount.code} DUPLICATE`,
is_dynamic: discount.isDynamic,
rule: newRule,
starts_at: discount.starts_at,
ends_at: discount.ends_at,
regions: discount.regions.map((region) => region.id),
valid_duration: discount.valid_duration,
usage_limit: discount.usage_limit,
is_disabled: discount.is_disabled,
metadata: discount.metadata,
}

createDiscount
.mutateAsync(newDiscount)
.then(() => {
toaster("Successfully created discount", "success")
})
.catch((error) => {
toaster(getErrorMessage(error), "error")
})
.finally(() => setLoading(false))
}

const handleDiscountSearch = (q: string) => {
searchQuery({ q })
}

const getTableRow = (discount, index) => {
return (
<Table.Row
key={`discount-${index}`}
color={"inherit"}
linkTo={`/a/discounts/${discount.id}`}
actions={[
{
label: "Edit Discount",
onClick: () => navigate(`/a/discounts/${discount.id}`),
icon: <EditIcon size={20} />,
},
{
label: "Duplicate",
onClick: () => duplicateDiscount(discount),
icon: <DuplicateIcon size={20} />,
},
{
label: "Delete",
variant: "danger",
onClick: (e) => {
setDeleteDiscount(discount)
},
icon: <TrashIcon size={20} />,
},
]}
>
<Table.Cell>
<Badge variant="default">
<span className="inter-small-regular">{discount.code}</span>
</Badge>
</Table.Cell>
<Table.Cell>{discount.rule.description}</Table.Cell>
<Table.Cell>{getDiscountAmount(discount)}</Table.Cell>
<Table.Cell>{getDiscountStatus(discount)}</Table.Cell>
<Table.Cell>
{discount.rule.valid_for.length ? (
<span>
{discount.rule.valid_for[0].title}
<span className="text-grey-40">
{discount.rule.valid_for?.length > 1 &&
` + ${discount.rule.valid_for.length - 1} more`}
</span>
</span>
) : (
<span>All Products</span>
)}
</Table.Cell>
<Table.Cell className="text-right pr-4">
{discount.usage_count}
</Table.Cell>
</Table.Row>
)
}

return (
<div className="w-full h-full flex flex-col justify-between">
<div>
<Table
enableSearch
placeholder="Search Discounts"
handleSearch={handleDiscountSearch}
>
<Table.Head>
<Table.HeadRow>
<Table.HeadCell>Code</Table.HeadCell>
<Table.HeadCell>Description</Table.HeadCell>
<Table.HeadCell className="w-32">Amount</Table.HeadCell>
<Table.HeadCell>Status</Table.HeadCell>
<Table.HeadCell>Products</Table.HeadCell>
<Table.HeadCell className="text-right pr-4">
Redemptions
</Table.HeadCell>
<Table.HeadCell></Table.HeadCell>
</Table.HeadRow>
</Table.Head>
{!(isLoading || !discounts || loading) && (
<Table.Body>
{discounts.map((d, i) => getTableRow(d, i))}
</Table.Body>
)}
</Table>
{(isLoading || !discounts || loading) && (
<div className="w-full pt-2xlarge flex items-center justify-center">
<Spinner size={"large"} variant={"secondary"} />
</div>
)}
</div>

{deleteDiscount && (
<DeletePrompt
text={"Are you sure you want to remove this discount?"}
heading={"Remove discount"}
successText="Discount has been removed"
onDelete={() =>
Medusa.discounts.delete(deleteDiscount.id).then(() => {
searchQuery()
})
}
handleClose={() => setDeleteDiscount(undefined)}
/>
)}

<div className="flex w-full mt-8 justify-between inter-small-regular text-grey-50">
<span>{`${parseInt(offset) + 1} - ${
count > parseInt(offset) + parseInt(limit)
? parseInt(offset) + parseInt(limit)
: count
} of ${count} Discounts`}</span>
<span>{`${offset / limit + 1} of ${Math.ceil(count / limit)}`}</span>
</div>
</div>
)
}

export default DiscountTable
Loading

0 comments on commit 3a72aed

Please sign in to comment.