diff --git a/.vscode/settings.json b/.vscode/settings.json index 7f0ef15..f743f25 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,7 +11,7 @@ // format on save // "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll": true, - "source.organizeImports": true + "source.fixAll": "explicit", + "source.organizeImports": "explicit" } } \ No newline at end of file diff --git a/packages/app/package.json b/packages/app/package.json index 51cae72..26fffcd 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -15,7 +15,7 @@ "prepare": "touch ./public/config.local.js" }, "dependencies": { - "@commercelayer/app-elements": "^1.3.0", + "@commercelayer/app-elements": "^1.9.7", "@commercelayer/sdk": "5.18.0", "@hookform/resolvers": "^3.3.2", "lodash": "^4.17.21", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 7d6caf4..7d2ac3b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -8,21 +8,31 @@ import { } from '@commercelayer/app-elements' import { Suspense, lazy } from 'react' import { SWRConfig } from 'swr' -import { Route, Router, Switch } from 'wouter' +import { Redirect, Route, Router, Switch } from 'wouter' import { appRoutes } from './data/routes' -const HomePage = lazy(async () => await import('#pages/HomePage')) +// const HomePage = lazy(async () => await import('#pages/HomePage')) +const PromotionListPage = lazy( + async () => await import('#pages/PromotionListPage') +) +const FiltersPage = lazy(async () => await import('#pages/FiltersPage')) +const PromotionDetailsPage = lazy( + async () => await import('#pages/PromotionDetailsPage') +) +const EditPromotionPage = lazy( + async () => await import('#pages/EditPromotionPage') +) const NewSelectTypePage = lazy( async () => await import('#pages/NewSelectTypePage') ) const NewPromotionPage = lazy( async () => await import('#pages/NewPromotionPage') ) -const NewPromotionRulesPage = lazy( - async () => await import('#pages/NewPromotionRulesPage') +const PromotionConditionsPage = lazy( + async () => await import('#pages/PromotionConditionsPage') ) -const NewPromotionRulesAddPage = lazy( - async () => await import('#pages/NewPromotionRulesAddPage') +const NewPromotionConditionPage = lazy( + async () => await import('#pages/NewPromotionConditionPage') ) const isDev = Boolean(import.meta.env.DEV) @@ -53,7 +63,26 @@ export function App(): JSX.Element { }> - + {/* */} + + + + + + + - diff --git a/packages/app/src/components/ListEmptyState.tsx b/packages/app/src/components/ListEmptyState.tsx new file mode 100644 index 0000000..aebd14a --- /dev/null +++ b/packages/app/src/components/ListEmptyState.tsx @@ -0,0 +1,84 @@ +import { A, EmptyState } from '@commercelayer/app-elements' + +interface Props { + scope?: 'history' | 'userFiltered' | 'presetView' | 'noSKUs' | 'noBundles' +} + +export function ListEmptyState({ scope = 'history' }: Props): JSX.Element { + if (scope === 'presetView') { + return ( + +

There are no promotions for the current list.

+ + } + /> + ) + } + + if (scope === 'userFiltered') { + return ( + +

+ We didn't find any promotions matching the current filters + selection. +

+ + } + /> + ) + } + + if (scope === 'noSKUs') { + return ( + +

+ We didn't find any SKU matching the current filters selection. +

+ + } + /> + ) + } + + if (scope === 'noBundles') { + return ( + +

+ We didn't find any bundle matching the current filters selection. +

+ + } + /> + ) + } + + return ( + +

Add an order with the API, or use the CLI.

+ + View API reference. + + + } + /> + ) +} diff --git a/packages/app/src/components/ListItemPromotion.tsx b/packages/app/src/components/ListItemPromotion.tsx new file mode 100644 index 0000000..d17dfac --- /dev/null +++ b/packages/app/src/components/ListItemPromotion.tsx @@ -0,0 +1,34 @@ +import { makePercentageDiscountPromotion } from '#mocks' +import { ResourceListItem, navigateTo } from '@commercelayer/app-elements' +import type { Promotion } from '@commercelayer/sdk' +import { useLocation } from 'wouter' + +interface Props { + resource?: Promotion + isLoading?: boolean + delayMs?: number +} + +export function ListItemPromotion({ + resource = makePercentageDiscountPromotion() as unknown as Promotion, + isLoading, + delayMs +}: Props): JSX.Element { + const [, setLocation] = useLocation() + + return ( + + ) +} diff --git a/packages/app/src/components/PromotionForm.tsx b/packages/app/src/components/PromotionForm.tsx index 04929fc..dcd6a43 100644 --- a/packages/app/src/components/PromotionForm.tsx +++ b/packages/app/src/components/PromotionForm.tsx @@ -67,8 +67,7 @@ export function PromotionForm({ } setLocation( - appRoutes.newPromotionRules.makePath({ - promotionSlug, + appRoutes.promotionDetails.makePath({ promotionId: promotion.id }) ) @@ -94,7 +93,7 @@ export function PromotionForm({ diff --git a/packages/app/src/data/filters.ts b/packages/app/src/data/filters.ts new file mode 100644 index 0000000..9a60482 --- /dev/null +++ b/packages/app/src/data/filters.ts @@ -0,0 +1,91 @@ +import type { FiltersInstructions } from '@commercelayer/app-elements' + +export const instructions: FiltersInstructions = [ + { + label: 'Status', + type: 'options', + sdk: { + predicate: 'status_in', + defaultOptions: ['prospect', 'acquired', 'repeat'] + }, + render: { + component: 'inputToggleButton', + props: { + mode: 'multi', + options: [ + { value: 'prospect', label: 'Prospect' }, + { value: 'acquired', label: 'Acquired' }, + { value: 'repeat', label: 'Repeat' } + ] + } + } + }, + { + label: 'Type', + type: 'options', + sdk: { + predicate: 'password_present', + parseFormValue: (value) => + Array.isArray(value) && value.length === 1 + ? value[0] === 'registered' + : undefined + }, + render: { + component: 'inputToggleButton', + props: { + mode: 'multi', + options: [ + { value: 'guest', label: 'Guest' }, + { value: 'registered', label: 'Registered' } + ] + } + } + }, + { + label: 'Groups', + type: 'options', + sdk: { + predicate: 'customer_group_id_in' + }, + render: { + component: 'inputResourceGroup', + props: { + fieldForLabel: 'name', + fieldForValue: 'id', + resource: 'customer_groups', + searchBy: 'name_cont', + sortBy: { attribute: 'name', direction: 'asc' }, + previewLimit: 5 + } + } + }, + { + label: 'Tags', + type: 'options', + sdk: { + predicate: 'tags_id_in' + }, + render: { + component: 'inputResourceGroup', + props: { + fieldForLabel: 'name', + fieldForValue: 'id', + resource: 'tags', + searchBy: 'name_cont', + sortBy: { attribute: 'name', direction: 'asc' }, + previewLimit: 5, + showCheckboxIcon: false + } + } + }, + { + label: 'Search', + type: 'textSearch', + sdk: { + predicate: ['email', 'customer_group_name'].join('_or_') + '_cont' + }, + render: { + component: 'searchBar' + } + } +] diff --git a/packages/app/src/data/lists.ts b/packages/app/src/data/lists.ts new file mode 100644 index 0000000..993b24a --- /dev/null +++ b/packages/app/src/data/lists.ts @@ -0,0 +1,11 @@ +import type { FormFullValues } from '@commercelayer/app-elements/dist/ui/resources/useResourceFilters/types' + +export type ListType = 'all' + +export const presets: Record = { + all: { + customerGroup: [], + status: [], + type: [] + } +} diff --git a/packages/app/src/data/routes.ts b/packages/app/src/data/routes.ts index 71486b0..236d13f 100644 --- a/packages/app/src/data/routes.ts +++ b/packages/app/src/data/routes.ts @@ -7,11 +7,12 @@ export type AppRoute = keyof typeof appRoutes // and `makePath` method to be used to generate the path used in navigation and links export const appRoutes = { home: createRoute('/'), + list: createRoute('/list/'), + filters: createRoute('/filters/'), + promotionDetails: createRoute('/list/:promotionId/'), + editPromotion: createRoute('/list/:promotionId/edit/'), newSelectType: createRoute('/new/'), newPromotion: createRoute('/new/:promotionSlug/'), - newPromotionEdit: createRoute('/new/:promotionSlug/:promotionId/'), - newPromotionRules: createRoute('/new/:promotionSlug/:promotionId/rules/'), - newPromotionRulesAdd: createRoute( - '/new/:promotionSlug/:promotionId/rules/add/' - ) + promotionConditions: createRoute('/list/:promotionId/conditions/'), + newPromotionCondition: createRoute('/list/:promotionId/conditions/new/') } diff --git a/packages/app/src/pages/EditPromotionPage.tsx b/packages/app/src/pages/EditPromotionPage.tsx new file mode 100644 index 0000000..f9f63de --- /dev/null +++ b/packages/app/src/pages/EditPromotionPage.tsx @@ -0,0 +1,60 @@ +import { PromotionForm } from '#components/PromotionForm' +import { + promotionDictionary, + promotionToFormValues +} from '#data/dictionaries/promotion' +import { appRoutes } from '#data/routes' +import { usePromotion } from '#hooks/usePromotion' +import { + PageLayout, + Section, + SkeletonTemplate, + Spacer, + useTokenProvider +} from '@commercelayer/app-elements' +import { useLocation, type RouteComponentProps } from 'wouter' + +function Page( + props: RouteComponentProps<{ promotionId: string }> +): JSX.Element { + const { + settings: { mode } + } = useTokenProvider() + const [, setLocation] = useLocation() + + const { isLoading, promotion } = usePromotion(props.params.promotionId) + const promotionConfig = promotionDictionary[promotion.type] + + return ( + + + +
+ +
+
+
+
+ ) +} + +export default Page diff --git a/packages/app/src/pages/FiltersPage.tsx b/packages/app/src/pages/FiltersPage.tsx new file mode 100644 index 0000000..675e907 --- /dev/null +++ b/packages/app/src/pages/FiltersPage.tsx @@ -0,0 +1,38 @@ +import { instructions } from '#data/filters' +import { appRoutes } from '#data/routes' +import { PageLayout, useResourceFilters } from '@commercelayer/app-elements' +import { useLocation } from 'wouter' + +function Page(): JSX.Element { + const [, setLocation] = useLocation() + const { FiltersForm, adapters } = useResourceFilters({ + instructions + }) + + return ( + + { + setLocation(appRoutes.list.makePath(filtersQueryString)) + }} + /> + + ) +} + +export default Page diff --git a/packages/app/src/pages/HomePage.tsx b/packages/app/src/pages/HomePage.tsx index 187effa..950be31 100644 --- a/packages/app/src/pages/HomePage.tsx +++ b/packages/app/src/pages/HomePage.tsx @@ -18,9 +18,12 @@ function HomePage(): JSX.Element { title='Promotions' mode={mode} gap='only-top' - onGoBack={() => { - window.location.href = - dashboardUrl != null ? `${dashboardUrl}/hub` : '/' + navigationButton={{ + onClick: () => { + window.location.href = + dashboardUrl != null ? `${dashboardUrl}/hub` : '/' + }, + label: 'Hub' }} > diff --git a/packages/app/src/pages/LoadingPage.tsx b/packages/app/src/pages/LoadingPage.tsx index 743b20c..1c5e566 100644 --- a/packages/app/src/pages/LoadingPage.tsx +++ b/packages/app/src/pages/LoadingPage.tsx @@ -15,7 +15,6 @@ function LoadingPage(): JSX.Element { title={Promotions} mode={mode} gap='only-top' - onGoBack={() => {}} >
diff --git a/packages/app/src/pages/NewPromotionRulesAddPage.tsx b/packages/app/src/pages/NewPromotionConditionPage.tsx similarity index 92% rename from packages/app/src/pages/NewPromotionRulesAddPage.tsx rename to packages/app/src/pages/NewPromotionConditionPage.tsx index 8bab7f6..806d1b4 100644 --- a/packages/app/src/pages/NewPromotionRulesAddPage.tsx +++ b/packages/app/src/pages/NewPromotionConditionPage.tsx @@ -1,7 +1,4 @@ -import { - promotionDictionary, - type Promotion -} from '#data/dictionaries/promotion' +import { type Promotion } from '#data/dictionaries/promotion' import { appRoutes } from '#data/routes' import { matchers, ruleBuilderConfig } from '#data/ruleBuilder/config' import { usePromotion } from '#hooks/usePromotion' @@ -24,7 +21,7 @@ import { useLocation, type RouteComponentProps } from 'wouter' import { z } from 'zod' function Page( - props: RouteComponentProps + props: RouteComponentProps ): JSX.Element { const { settings: { mode } @@ -38,13 +35,15 @@ function Page( title='New condition' mode={mode} gap='only-top' - onGoBack={() => { - setLocation( - appRoutes.newPromotionRules.makePath({ - promotionSlug: promotionDictionary[promotion.type].slug, - promotionId: props.params.promotionId - }) - ) + navigationButton={{ + label: 'Back', + onClick() { + setLocation( + appRoutes.promotionConditions.makePath({ + promotionId: props.params.promotionId + }) + ) + } }} > @@ -54,8 +53,7 @@ function Page( promotion={promotion} onSuccess={() => { setLocation( - appRoutes.newPromotionRules.makePath({ - promotionSlug: promotionDictionary[promotion.type].slug, + appRoutes.promotionConditions.makePath({ promotionId: props.params.promotionId }) ) diff --git a/packages/app/src/pages/NewPromotionPage.tsx b/packages/app/src/pages/NewPromotionPage.tsx index f595362..d446682 100644 --- a/packages/app/src/pages/NewPromotionPage.tsx +++ b/packages/app/src/pages/NewPromotionPage.tsx @@ -35,8 +35,11 @@ function Page( title={`New ${promotionConfig.titleNew}`} mode={mode} gap='only-top' - onGoBack={() => { - setLocation(appRoutes.newSelectType.makePath({})) + navigationButton={{ + label: 'Back', + onClick() { + setLocation(appRoutes.newSelectType.makePath({})) + } }} > diff --git a/packages/app/src/pages/NewSelectTypePage.tsx b/packages/app/src/pages/NewSelectTypePage.tsx index ace1b2b..3dd7030 100644 --- a/packages/app/src/pages/NewSelectTypePage.tsx +++ b/packages/app/src/pages/NewSelectTypePage.tsx @@ -25,8 +25,12 @@ function Page(): JSX.Element { title='Select type' mode={mode} gap='only-top' - onGoBack={() => { - setLocation(appRoutes.home.makePath({})) + navigationButton={{ + label: 'Cancel', + icon: 'x', + onClick() { + setLocation(appRoutes.home.makePath({})) + } }} > @@ -58,7 +62,8 @@ function LinkTo({ return ( } + icon={} + // icon={} href={appRoutes.newPromotion.makePath({ promotionSlug: promotion.slug })} > {promotion.titleList} diff --git a/packages/app/src/pages/NewPromotionRulesPage.tsx b/packages/app/src/pages/PromotionConditionsPage.tsx similarity index 89% rename from packages/app/src/pages/NewPromotionRulesPage.tsx rename to packages/app/src/pages/PromotionConditionsPage.tsx index 10c3076..bff0e36 100644 --- a/packages/app/src/pages/NewPromotionRulesPage.tsx +++ b/packages/app/src/pages/PromotionConditionsPage.tsx @@ -1,7 +1,4 @@ -import { - promotionDictionary, - type PromotionRule -} from '#data/dictionaries/promotion' +import { type PromotionRule } from '#data/dictionaries/promotion' import { appRoutes } from '#data/routes' import { toFormLabels } from '#data/ruleBuilder/config' import { usePromotion } from '#hooks/usePromotion' @@ -22,7 +19,7 @@ import { useCallback, useMemo, useState } from 'react' import { Link, useLocation, type RouteComponentProps } from 'wouter' function Page( - props: RouteComponentProps + props: RouteComponentProps ): JSX.Element | null { const { settings: { mode } @@ -35,16 +32,20 @@ function Page( return ( { - setLocation( - appRoutes.newPromotionEdit.makePath({ - promotionSlug: promotionDictionary[promotion.type].slug, - promotionId: props.params.promotionId - }) - ) + navigationButton={{ + label: 'Cancel', + icon: 'x', + onClick() { + setLocation( + appRoutes.promotionDetails.makePath({ + promotionId: props.params.promotionId + }) + ) + } }} > @@ -63,8 +64,7 @@ function Page( ))} diff --git a/packages/app/src/pages/PromotionDetailsPage.tsx b/packages/app/src/pages/PromotionDetailsPage.tsx new file mode 100644 index 0000000..de62618 --- /dev/null +++ b/packages/app/src/pages/PromotionDetailsPage.tsx @@ -0,0 +1,161 @@ +import type { Promotion } from '#data/dictionaries/promotion' +import { appRoutes } from '#data/routes' +import { usePromotion } from '#hooks/usePromotion' +import { + Badge, + Button, + Card, + Dropdown, + DropdownItem, + Icon, + ListDetails, + ListDetailsItem, + PageLayout, + Section, + SkeletonTemplate, + Spacer, + Text, + useCoreSdkProvider, + useTokenProvider +} from '@commercelayer/app-elements' +import { useMemo } from 'react' +import { Link, useLocation, type RouteComponentProps } from 'wouter' + +function Page( + props: RouteComponentProps<{ promotionId: string }> +): JSX.Element { + const { + settings: { mode } + } = useTokenProvider() + + const [, setLocation] = useLocation() + + const { sdkClient } = useCoreSdkProvider() + const { promotion, isLoading, mutatePromotion } = usePromotion( + props.params.promotionId + ) + + return ( + + {promotion.name} + + } + actionButton={ + { + setLocation( + appRoutes.editPromotion.makePath({ + promotionId: props.params.promotionId + }) + ) + }} + /> + ]} + /> + } + mode={mode} + gap='only-top' + navigationButton={{ + label: 'All promotions', + onClick() { + setLocation(appRoutes.list.makePath({})) + } + }} + > + + + + Promotion is{' '} + + {promotion.active === true ? 'active' : 'disabled'} + + + + + + + + + + + + +
+ Edit + + } + > + ?? +
+
+
+
+ ) +} + +function Info({ promotion }: { promotion: Promotion }): JSX.Element { + const specificDetails = useMemo(() => { + switch (promotion.type) { + case 'percentage_discount_promotions': + return ( + <> + + {promotion.percentage}% + + + ) + default: + return null + } + }, [promotion]) + + return ( + <> + {specificDetails} + + ?? 15 ‒ 31 October 2023 + + ?? 32 / 100 + {promotion.exclusive === true && ( + + + + + + )} + + ) +} + +export default Page diff --git a/packages/app/src/pages/PromotionListPage.tsx b/packages/app/src/pages/PromotionListPage.tsx new file mode 100644 index 0000000..742193d --- /dev/null +++ b/packages/app/src/pages/PromotionListPage.tsx @@ -0,0 +1,104 @@ +import { ListEmptyState } from '#components/ListEmptyState' +import { ListItemPromotion } from '#components/ListItemPromotion' +import { instructions } from '#data/filters' +import { presets } from '#data/lists' +import { appRoutes } from '#data/routes' +import { + PageLayout, + Spacer, + useResourceFilters, + useTokenProvider +} from '@commercelayer/app-elements' +import { Link, useLocation } from 'wouter' +import { navigate, useSearch } from 'wouter/use-location' + +function Page(): JSX.Element { + const { + settings: { mode }, + canUser + } = useTokenProvider() + + const queryString = useSearch() + const [, setLocation] = useLocation() + + const { SearchWithNav, FilteredList, viewTitle, hasActiveFilter } = + useResourceFilters({ + instructions + }) + + const isUserCustomFiltered = + hasActiveFilter && viewTitle === presets.all.viewTitle + const hideFiltersNav = !( + viewTitle == null || viewTitle === presets.all.viewTitle + ) + + return ( + + { + navigate(`?${qs}`, { + replace: true + }) + }} + onFilterClick={(queryString) => { + setLocation(appRoutes.filters.makePath(queryString)) + }} + hideFiltersNav={hideFiltersNav} + /> + + + + } + actionButton={ + canUser('create', 'promotions') ? ( + + Add new + + ) : undefined + } + /> + + + ) +} + +export default Page diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 590d89b..3d3a225 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: packages/app: dependencies: '@commercelayer/app-elements': - specifier: ^1.3.0 - version: 1.7.0(@commercelayer/sdk@5.18.0)(query-string@8.1.0)(react-dom@18.2.0)(react-gtm-module@2.0.11)(react-hook-form@7.48.2)(react@18.2.0)(wouter@2.12.1) + specifier: ^1.9.7 + version: 1.9.7(@commercelayer/sdk@5.18.0)(query-string@8.1.0)(react-dom@18.2.0)(react-gtm-module@2.0.11)(react-hook-form@7.48.2)(react@18.2.0)(wouter@2.12.1) '@commercelayer/sdk': specifier: 5.18.0 version: 5.18.0 @@ -101,11 +101,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /@ac-dev/countries-service@1.2.0: - resolution: {integrity: sha512-9+LUUTALFa17EKL7l93ExcjYgHdkcHWlOyvAqMdrVWie6/105eGryQqaba0yIwM4rBYfvs/b/KJHkbcAdSFD2Q==} - engines: {node: '>=10'} - dev: false - /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} @@ -337,8 +332,8 @@ packages: dev: true optional: true - /@commercelayer/app-elements@1.7.0(@commercelayer/sdk@5.18.0)(query-string@8.1.0)(react-dom@18.2.0)(react-gtm-module@2.0.11)(react-hook-form@7.48.2)(react@18.2.0)(wouter@2.12.1): - resolution: {integrity: sha512-GYMLrxewUrqJpUgDzhqwWV3890oKSBC9BVXxuLKT1Hx2IiQGyOEdtybWdPvDfPefcnOMQkE/fSbq8iWud9TMzw==} + /@commercelayer/app-elements@1.9.7(@commercelayer/sdk@5.18.0)(query-string@8.1.0)(react-dom@18.2.0)(react-gtm-module@2.0.11)(react-hook-form@7.48.2)(react@18.2.0)(wouter@2.12.1): + resolution: {integrity: sha512-lA76Rgrq6C0WrMPUIA15kcfoRX+NwGoFfxXB09pveSKokdo/+SKqnxbspY8hp2ir8oQ8XwtNndOo0JSttKVWlA==} engines: {node: '>=18', pnpm: '>=7'} peerDependencies: '@commercelayer/sdk': ^5.x @@ -349,7 +344,6 @@ packages: react-hook-form: ^7.43.x wouter: ^2.x dependencies: - '@ac-dev/countries-service': 1.2.0 '@commercelayer/sdk': 5.18.0 '@types/lodash': 4.14.202 '@types/react': 18.2.38