├── .cursor
├── README.md
└── rules
│ ├── medusa-development.mdc
│ ├── remix-hook-form-migration.mdc
│ ├── remix-storefront-components.mdc
│ ├── remix-storefront-optimization.mdc
│ ├── remix-storefront-routing.mdc
│ ├── testing-patterns-e2e.mdc
│ ├── testing-patterns-integration.mdc
│ ├── testing-patterns-unit.mdc
│ └── typescript-patterns.mdc
├── .github
├── composite
│ ├── docker
│ │ ├── medusa
│ │ │ └── action.yaml
│ │ └── storefront
│ │ │ └── action.yaml
│ └── monorepo-install
│ │ └── action.yml
└── workflows
│ ├── barrio.yaml
│ └── pr-checks.yaml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .vscode
└── settings.json
├── .yarn
└── releases
│ └── yarn-4.5.0.cjs
├── .yarnrc.yml
├── LICENSE
├── README.md
├── apps
├── medusa
│ ├── .dockerignore
│ ├── .env.template
│ ├── .gitignore
│ ├── Dockerfile
│ ├── README.md
│ ├── docker-compose.yaml
│ ├── integration-tests
│ │ └── http
│ │ │ ├── README.md
│ │ │ └── health.spec.ts
│ ├── jest.config.js
│ ├── medusa-config.ts
│ ├── package.json
│ ├── src
│ │ ├── admin
│ │ │ ├── README.md
│ │ │ └── tsconfig.json
│ │ ├── api
│ │ │ ├── README.md
│ │ │ ├── admin
│ │ │ │ └── custom
│ │ │ │ │ └── route.ts
│ │ │ └── store
│ │ │ │ └── custom
│ │ │ │ └── route.ts
│ │ ├── jobs
│ │ │ └── README.md
│ │ ├── links
│ │ │ └── README.md
│ │ ├── modules
│ │ │ └── README.md
│ │ ├── scripts
│ │ │ ├── README.md
│ │ │ ├── seed.ts
│ │ │ └── seed
│ │ │ │ ├── products.ts
│ │ │ │ └── reviews.ts
│ │ ├── subscribers
│ │ │ └── README.md
│ │ └── workflows
│ │ │ └── README.md
│ └── tsconfig.json
└── storefront
│ ├── .env.template
│ ├── .gitignore
│ ├── Dockerfile
│ ├── README.md
│ ├── app
│ ├── components
│ │ ├── LogoStoreName
│ │ │ └── LogoStoreName.tsx
│ │ ├── badges
│ │ │ ├── HasSaleBadge.tsx
│ │ │ └── SoldOutBadge.tsx
│ │ ├── cart
│ │ │ ├── CartDrawer.tsx
│ │ │ ├── CartDrawerDesignDoc.md
│ │ │ ├── CartDrawerItem.tsx
│ │ │ ├── index.ts
│ │ │ └── line-items
│ │ │ │ ├── LineItemQuantitySelect.tsx
│ │ │ │ └── index.ts
│ │ ├── checkout
│ │ │ ├── CheckoutAccountDetails.tsx
│ │ │ ├── CheckoutDeliveryMethod.tsx
│ │ │ ├── CheckoutFlow.tsx
│ │ │ ├── CheckoutOrderSummary
│ │ │ │ ├── CheckoutOrderSummary.tsx
│ │ │ │ ├── CheckoutOrderSummaryDiscountCode.tsx
│ │ │ │ ├── CheckoutOrderSummaryItems.tsx
│ │ │ │ ├── CheckoutOrderSummaryTotals.tsx
│ │ │ │ ├── RemoveDiscountCodeButton.tsx
│ │ │ │ └── index.ts
│ │ │ ├── CheckoutPayment.tsx
│ │ │ ├── CheckoutSectionHeader.tsx
│ │ │ ├── CheckoutSidebar.tsx
│ │ │ ├── CompleteCheckoutForm.tsx
│ │ │ ├── HiddenAddressGroup.tsx
│ │ │ ├── ManualPayment
│ │ │ │ └── ManualPayment.tsx
│ │ │ ├── MedusaStripeAddress
│ │ │ │ └── MedusaStripeAddress.tsx
│ │ │ ├── StripePayment
│ │ │ │ ├── StripeElementsProvider.tsx
│ │ │ │ ├── StripeExpressPayment.tsx
│ │ │ │ ├── StripeExpressPaymentForm.tsx
│ │ │ │ ├── StripePayment.tsx
│ │ │ │ ├── StripePaymentForm.tsx
│ │ │ │ └── index.ts
│ │ │ ├── address
│ │ │ │ └── AddressDisplay.tsx
│ │ │ ├── checkout-fields
│ │ │ │ └── ShippingOptionsRadioGroup
│ │ │ │ │ ├── ShippingOptionsRadioGroup.tsx
│ │ │ │ │ └── ShippingOptionsRadioGroupOption.tsx
│ │ │ ├── checkout-form-helpers.ts
│ │ │ └── index.ts
│ │ ├── collection
│ │ │ ├── collection-list-item.tsx
│ │ │ ├── collection-list.tsx
│ │ │ └── index.ts
│ │ ├── common
│ │ │ ├── Address
│ │ │ │ └── Address.tsx
│ │ │ ├── Empty
│ │ │ │ └── Empty.tsx
│ │ │ ├── ImageUpload
│ │ │ │ ├── ImageUploadWithPreview.tsx
│ │ │ │ └── ImageUploader.tsx
│ │ │ ├── Pagination
│ │ │ │ ├── Pagination.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── pagination-with-context.tsx
│ │ │ │ └── react-use-pagination
│ │ │ │ │ ├── Pagination.tsx
│ │ │ │ │ ├── getPaginationMeta.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── paginationStateReducer.ts
│ │ │ │ │ └── usePagination.ts
│ │ │ ├── actions-list
│ │ │ │ └── ActionList.tsx
│ │ │ ├── actions
│ │ │ │ ├── Actions.tsx
│ │ │ │ └── index.ts
│ │ │ ├── alert
│ │ │ │ ├── Alert.tsx
│ │ │ │ └── index.ts
│ │ │ ├── assets
│ │ │ │ ├── StripeBadge.png
│ │ │ │ └── icons
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── payment-methods
│ │ │ │ │ ├── AmericanExpressIcon.tsx
│ │ │ │ │ ├── DinersClubIcon.tsx
│ │ │ │ │ ├── DiscoverIcon.tsx
│ │ │ │ │ ├── EloCardIcon.tsx
│ │ │ │ │ ├── JCBIcon.tsx
│ │ │ │ │ ├── MasterCardIcon.tsx
│ │ │ │ │ ├── UnionPayIcon.tsx
│ │ │ │ │ ├── VisaIcon.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ │ └── social
│ │ │ │ │ ├── facebook.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── instagram.tsx
│ │ │ │ │ ├── linkedin.tsx
│ │ │ │ │ ├── pinterest.tsx
│ │ │ │ │ ├── snapchat.tsx
│ │ │ │ │ ├── tiktok.tsx
│ │ │ │ │ ├── twitter.tsx
│ │ │ │ │ └── youtube.tsx
│ │ │ ├── breadcrumbs
│ │ │ │ ├── Breadcrumbs.tsx
│ │ │ │ └── index.ts
│ │ │ ├── buttons
│ │ │ │ ├── Button.tsx
│ │ │ │ ├── ButtonBase.tsx
│ │ │ │ ├── ButtonLink.tsx
│ │ │ │ ├── IconButton.tsx
│ │ │ │ ├── ScrollArrowButtons.tsx
│ │ │ │ └── index.ts
│ │ │ ├── card
│ │ │ │ ├── Card.tsx
│ │ │ │ ├── CardBody.tsx
│ │ │ │ ├── CardContent.tsx
│ │ │ │ ├── CardDate.tsx
│ │ │ │ ├── CardExcerpt.tsx
│ │ │ │ ├── CardFooter.tsx
│ │ │ │ ├── CardGrid.tsx
│ │ │ │ ├── CardHeader.tsx
│ │ │ │ ├── CardLabel.tsx
│ │ │ │ ├── CardThumbnail.tsx
│ │ │ │ ├── CardTitle.tsx
│ │ │ │ └── index.ts
│ │ │ ├── container
│ │ │ │ ├── Container.tsx
│ │ │ │ └── index.ts
│ │ │ ├── forms
│ │ │ │ ├── fields
│ │ │ │ │ ├── Field.types.ts
│ │ │ │ │ ├── FieldError.tsx
│ │ │ │ │ ├── FieldGroup.tsx
│ │ │ │ │ ├── FieldInput.tsx
│ │ │ │ │ ├── FieldLabel.tsx
│ │ │ │ │ └── FieldWrapper.tsx
│ │ │ │ └── inputs
│ │ │ │ │ ├── Input.tsx
│ │ │ │ │ ├── InputCheckbox.tsx
│ │ │ │ │ ├── Select.tsx
│ │ │ │ │ └── Textarea.tsx
│ │ │ ├── grid
│ │ │ │ ├── Grid.tsx
│ │ │ │ ├── GridColumn.tsx
│ │ │ │ └── index.ts
│ │ │ ├── icons
│ │ │ │ ├── CreditCardIcon.tsx
│ │ │ │ └── index.ts
│ │ │ ├── images
│ │ │ │ ├── Image.tsx
│ │ │ │ ├── ImageBase.tsx
│ │ │ │ ├── LightboxGallery.tsx
│ │ │ │ └── brokenImgSrc.ts
│ │ │ ├── link
│ │ │ │ ├── URLAwareNavLink.tsx
│ │ │ │ └── index.ts
│ │ │ ├── menu
│ │ │ │ ├── Menu.tsx
│ │ │ │ ├── MenuButton.tsx
│ │ │ │ ├── MenuItem.tsx
│ │ │ │ ├── MenuItemIcon.tsx
│ │ │ │ ├── MenuItems.tsx
│ │ │ │ └── index.ts
│ │ │ ├── modals
│ │ │ │ ├── ConfirmModal.tsx
│ │ │ │ ├── Modal.tsx
│ │ │ │ └── index.ts
│ │ │ ├── remix-hook-form
│ │ │ │ ├── buttons
│ │ │ │ │ └── SubmitButton.tsx
│ │ │ │ ├── field-groups
│ │ │ │ │ ├── ConfirmPasswordFieldGroup.tsx
│ │ │ │ │ ├── QuantitySelector.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ └── forms
│ │ │ │ │ ├── FormError.tsx
│ │ │ │ │ ├── fields
│ │ │ │ │ ├── FieldError.tsx
│ │ │ │ │ ├── FieldGroup.tsx
│ │ │ │ │ ├── FieldLabel.tsx
│ │ │ │ │ ├── ProductOptionSelectorSelect.tsx
│ │ │ │ │ ├── StyledPassword.tsx
│ │ │ │ │ └── StyledTextField.tsx
│ │ │ │ │ └── inputs
│ │ │ │ │ ├── Input.tsx
│ │ │ │ │ ├── InputCheckbox.tsx
│ │ │ │ │ ├── Select.tsx
│ │ │ │ │ └── Textarea.tsx
│ │ │ └── status-indicators
│ │ │ │ ├── StatusIndicator.tsx
│ │ │ │ └── index.ts
│ │ ├── images
│ │ │ └── StripeSecurityImage.tsx
│ │ ├── layout
│ │ │ ├── Page.tsx
│ │ │ ├── footer
│ │ │ │ ├── Footer.tsx
│ │ │ │ └── SocialIcons.tsx
│ │ │ └── header
│ │ │ │ ├── Header.tsx
│ │ │ │ ├── HeaderSideNav.tsx
│ │ │ │ └── useActiveSection.ts
│ │ ├── newsletter
│ │ │ └── Newsletter.tsx
│ │ ├── product
│ │ │ ├── EmptyProductListItem.tsx
│ │ │ ├── ProductBadges.tsx
│ │ │ ├── ProductCarousel.tsx
│ │ │ ├── ProductCarouselSkeleton.tsx
│ │ │ ├── ProductCategoryTabs.tsx
│ │ │ ├── ProductCollectionTabs.tsx
│ │ │ ├── ProductGrid.tsx
│ │ │ ├── ProductGridSkeleton.tsx
│ │ │ ├── ProductImageGallery.tsx
│ │ │ ├── ProductListHeader.tsx
│ │ │ ├── ProductListItem.tsx
│ │ │ ├── ProductListWithPagination.tsx
│ │ │ ├── ProductOptionSelectorRadio.tsx
│ │ │ ├── ProductOptionSelectorSelect.tsx
│ │ │ ├── ProductPrice.tsx
│ │ │ ├── ProductPriceRange.tsx
│ │ │ ├── ProductTagsMenu.tsx
│ │ │ ├── ProductThumbnail.tsx
│ │ │ └── ProductVariantPrice.tsx
│ │ ├── reviews
│ │ │ ├── ProductReviewComponent.tsx
│ │ │ ├── ProductReviewForm.tsx
│ │ │ ├── ProductReviewList.tsx
│ │ │ ├── ProductReviewSection.tsx
│ │ │ ├── ProductReviewStars.tsx
│ │ │ ├── ProductReviewView.tsx
│ │ │ ├── ReviewImageThumbnailRow.tsx
│ │ │ ├── ReviewListWithPagination.tsx
│ │ │ ├── ReviewSummary.tsx
│ │ │ └── StarRating.tsx
│ │ ├── sections
│ │ │ ├── GridCTA.tsx
│ │ │ ├── Hero.tsx
│ │ │ ├── ListItems.tsx
│ │ │ ├── PageHeading.tsx
│ │ │ ├── ProductList.tsx
│ │ │ ├── SectionHeading.tsx
│ │ │ └── SideBySide.tsx
│ │ ├── share
│ │ │ ├── Share.tsx
│ │ │ ├── Share.types.tsx
│ │ │ ├── ShareButton.tsx
│ │ │ ├── ShareModal.tsx
│ │ │ └── index.tsx
│ │ └── tabs
│ │ │ ├── TabButton.tsx
│ │ │ ├── TabList.tsx
│ │ │ └── index.ts
│ ├── entry.client.tsx
│ ├── entry.server.tsx
│ ├── hooks
│ │ ├── useCart.ts
│ │ ├── useCheckout.tsx
│ │ ├── useCustomer.tsx
│ │ ├── useEnv.ts
│ │ ├── useKeypress.ts
│ │ ├── useLocalStorage.ts
│ │ ├── useLogin.ts
│ │ ├── useProductInventory.ts
│ │ ├── useRegion.ts
│ │ ├── useRegions.ts
│ │ ├── useRemoveCartItem.ts
│ │ ├── useRootLoaderData.tsx
│ │ ├── useScrollArrows.ts
│ │ ├── useSiteDetails.ts
│ │ └── useStorefront.ts
│ ├── providers
│ │ ├── checkout-provider.tsx
│ │ ├── root-providers.tsx
│ │ └── storefront-provider.tsx
│ ├── root.tsx
│ ├── routes.ts
│ ├── routes
│ │ ├── $.tsx
│ │ ├── [.well-known].apple-developer-merchantid-domain-association.tsx
│ │ ├── [favicon.ico].tsx
│ │ ├── [robots.txt].tsx
│ │ ├── [sitemap-collections.xml].tsx
│ │ ├── [sitemap-pages.xml].tsx
│ │ ├── [sitemap-products.xml].tsx
│ │ ├── [sitemap.xml].tsx
│ │ ├── _index.tsx
│ │ ├── about-us.tsx
│ │ ├── api.cart.line-items.create.ts
│ │ ├── api.cart.line-items.delete.ts
│ │ ├── api.cart.line-items.update.ts
│ │ ├── api.checkout.account-details.ts
│ │ ├── api.checkout.billing-address.ts
│ │ ├── api.checkout.complete.ts
│ │ ├── api.checkout.contact-info.ts
│ │ ├── api.checkout.discount-code.ts
│ │ ├── api.checkout.express.ts
│ │ ├── api.checkout.remove-discount-code.ts
│ │ ├── api.checkout.shipping-methods.ts
│ │ ├── api.health.live.ts
│ │ ├── api.newsletter-subscriptions.ts
│ │ ├── api.page-data.ts
│ │ ├── api.product-reviews.upsert.ts
│ │ ├── api.region.ts
│ │ ├── categories.$categoryHandle.tsx
│ │ ├── checkout._index.tsx
│ │ ├── checkout.success.tsx
│ │ ├── collections.$collectionHandle.tsx
│ │ ├── orders_.$orderId.reviews.tsx
│ │ ├── products.$productHandle.tsx
│ │ └── products._index.tsx
│ ├── styles
│ │ └── global.css
│ └── templates
│ │ └── ProductTemplate.tsx
│ ├── env.d.ts
│ ├── libs
│ ├── config
│ │ └── site
│ │ │ ├── navigation-items.ts
│ │ │ └── site-settings.ts
│ ├── types.ts
│ └── util
│ │ ├── addresses
│ │ ├── addressToMedusaAddress.ts
│ │ ├── compareAddresses.ts
│ │ ├── index.ts
│ │ └── medusaAddressToAddress.ts
│ │ ├── buildObjectFromSearchParams.ts
│ │ ├── buildSearchParamsFromObject.ts
│ │ ├── carts
│ │ ├── calculateEstimatedShipping.ts
│ │ └── index.ts
│ │ ├── checkout
│ │ ├── amountToStripeExpressCheckoutAmount.ts
│ │ ├── checkStepComplete.ts
│ │ ├── express-checkout-client.ts
│ │ ├── getShippingOptionsByProfile.ts
│ │ └── index.ts
│ │ ├── createReducer.ts
│ │ ├── fetcher-keys.ts
│ │ ├── formatters.ts
│ │ ├── forms
│ │ ├── formDataToObject.ts
│ │ ├── index.ts
│ │ ├── objectToFormData.ts
│ │ └── parseFormData.ts
│ │ ├── index.ts
│ │ ├── is-browser.ts
│ │ ├── medusaError.ts
│ │ ├── meta.ts
│ │ ├── page.ts
│ │ ├── phoneNumber.ts
│ │ ├── prices.ts
│ │ ├── products.ts
│ │ ├── server
│ │ ├── auth.server.ts
│ │ ├── cache-builder.server.ts
│ │ ├── client.server.ts
│ │ ├── config.server.ts
│ │ ├── cookies.server.ts
│ │ ├── data
│ │ │ ├── cart.server.ts
│ │ │ ├── categories.server.ts
│ │ │ ├── collections.server.ts
│ │ │ ├── customer.server.ts
│ │ │ ├── fulfillment.server.ts
│ │ │ ├── orders.server.ts
│ │ │ ├── payment.server.ts
│ │ │ ├── product-reviews.server.ts
│ │ │ ├── products.server.ts
│ │ │ └── regions.server.ts
│ │ ├── env.ts
│ │ ├── page.server.ts
│ │ ├── products.server.ts
│ │ └── root.server.ts
│ │ ├── withPaginationParams.ts
│ │ └── xml
│ │ └── sitemap-builder.ts
│ ├── package.json
│ ├── postcss.config.mjs
│ ├── public
│ ├── assets
│ │ └── images
│ │ │ ├── banner-coffee-shop.png
│ │ │ ├── barrio-banner.png
│ │ │ ├── benefit-1.png
│ │ │ ├── benefit-2.png
│ │ │ ├── benefit-3.png
│ │ │ ├── coffee-shop-2.png
│ │ │ ├── credit-card-icons
│ │ │ ├── amex.png
│ │ │ ├── diners.png
│ │ │ ├── discover.png
│ │ │ ├── jcb.png
│ │ │ ├── mastercard.png
│ │ │ ├── unionpay.png
│ │ │ ├── unknown.png
│ │ │ └── visa.png
│ │ │ ├── grid-cta-1.png
│ │ │ ├── grid-cta-2.png
│ │ │ ├── header-image-1.png
│ │ │ ├── header-image-2.png
│ │ │ ├── location-1.png
│ │ │ ├── location-2.png
│ │ │ └── location-3.png
│ └── favicon.svg
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── types
│ ├── global.ts
│ ├── index.ts
│ └── remix.ts
│ └── vite.config.ts
├── biome.json
├── helm-charts
├── medusa
│ ├── .helmignore
│ ├── Chart.yaml
│ ├── templates
│ │ ├── NOTES.txt
│ │ ├── _helpers.tpl
│ │ ├── deployment.yaml
│ │ ├── hpa.yaml
│ │ ├── ingress.yaml
│ │ ├── jobs
│ │ │ ├── migrate.yaml
│ │ │ └── seed.yaml
│ │ ├── service.yaml
│ │ ├── serviceaccount.yaml
│ │ └── tests
│ │ │ └── test-connection.yaml
│ └── values.yaml
└── storefront
│ ├── .helmignore
│ ├── Chart.yaml
│ ├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── service.yaml
│ └── serviceaccount.yaml
│ └── values.yaml
├── package.json
├── turbo.json
└── yarn.lock
/.cursor/rules/testing-patterns-e2e.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description: End-to-end testing patterns for Medusa applications including storefront testing, user workflows, and test utilities
3 | globs:
4 | - "**/*.test.ts"
5 | - "**/*.test.tsx"
6 | - "**/*.spec.ts"
7 | - "**/*.spec.tsx"
8 | - "**/test/**/*.ts"
9 | - "**/test/**/*.tsx"
10 | - "**/__tests__/**/*.ts"
11 | - "**/__tests__/**/*.tsx"
12 | - "**/e2e/**/*.ts"
13 | - "**/e2e/**/*.tsx"
14 | - "**/playwright/**/*.ts"
15 | - "**/playwright/**/*.tsx"
16 | alwaysApply: true
17 | ---
18 |
19 | # End-to-End Testing Patterns for Medusa Applications
20 |
21 | You are an expert in end-to-end testing TypeScript applications, focusing on testing complete user workflows and system behavior in Medusa and React applications.
22 |
23 | ## Core Testing Principles
24 |
25 | - Write tests that verify complete user workflows and system behavior
26 | - Test from the user's perspective using real browsers
27 | - Use descriptive test names that explain the user journey being tested
28 | - Focus on critical user paths and business scenarios
29 | - Use proper page object patterns for maintainability
30 | - Ensure tests are stable and reliable
31 | - Test across different browsers and devices when needed
32 |
--------------------------------------------------------------------------------
/.github/composite/docker/medusa/action.yaml:
--------------------------------------------------------------------------------
1 | name: 'Docker Build & Push for Medusa'
2 | description: 'Docker Build & Push for Medusa'
3 |
4 | inputs:
5 | GITHUB_TOKEN:
6 | description: 'github token needed for docker credentials'
7 | required: true
8 |
9 | DOCKER_REPOSITORY:
10 | description: 'docker image repository'
11 | required: true
12 |
13 | DOCKER_TAG:
14 | description: 'docker image tag'
15 | required: true
16 |
17 | runs:
18 | using: 'composite'
19 |
20 | steps:
21 | - name: Set up QEMU
22 | uses: docker/setup-qemu-action@v2
23 |
24 | - name: Set up Docker Buildx
25 | uses: docker/setup-buildx-action@v2
26 |
27 | - name: Login to Github actions
28 | uses: docker/login-action@v2
29 | with:
30 | registry: ghcr.io
31 | username: '${{ github.actor }}'
32 | password: '${{ inputs.GITHUB_TOKEN }}'
33 |
34 | - name: Build and push
35 | uses: docker/build-push-action@v4
36 | with:
37 | context: .
38 | push: true
39 | tags: ${{ inputs.DOCKER_REPOSITORY }}:${{ inputs.DOCKER_TAG }}
40 | file: apps/medusa/Dockerfile
41 |
--------------------------------------------------------------------------------
/.github/composite/docker/storefront/action.yaml:
--------------------------------------------------------------------------------
1 | name: 'Docker Build & Push for Storefront'
2 | description: 'Docker Build & Push for Storefront'
3 |
4 | inputs:
5 | GITHUB_TOKEN:
6 | description: 'github token needed for docker credentials'
7 | required: true
8 |
9 | DOCKER_REPOSITORY:
10 | description: 'docker image repository'
11 | required: true
12 |
13 | DOCKER_TAG:
14 | description: 'docker image tag'
15 | required: true
16 |
17 | runs:
18 | using: 'composite'
19 |
20 | steps:
21 | - name: Set up QEMU
22 | uses: docker/setup-qemu-action@v2
23 |
24 | - name: Set up Docker Buildx
25 | uses: docker/setup-buildx-action@v2
26 |
27 | - name: Login to Github actions
28 | uses: docker/login-action@v2
29 | with:
30 | registry: ghcr.io
31 | username: '${{ github.actor }}'
32 | password: '${{ inputs.GITHUB_TOKEN }}'
33 |
34 | - name: Build and push
35 | uses: docker/build-push-action@v4
36 | with:
37 | context: .
38 | push: true
39 | tags: ${{ inputs.DOCKER_REPOSITORY }}:${{ inputs.DOCKER_TAG }}
40 | file: apps/storefront/Dockerfile
41 |
--------------------------------------------------------------------------------
/.github/workflows/pr-checks.yaml:
--------------------------------------------------------------------------------
1 | name: Lint and Type Check
2 |
3 | on: [pull_request]
4 |
5 | concurrency:
6 | group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
7 | cancel-in-progress: true
8 |
9 | env:
10 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
11 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
12 | TURBO_CACHE: "remote:rw"
13 |
14 | jobs:
15 | lint:
16 | runs-on: warp-ubuntu-2404-x64-2x
17 | steps:
18 | - name: Checkout code
19 | uses: actions/checkout@v4
20 |
21 | - name: Setup Biome
22 | uses: biomejs/setup-biome@v2
23 |
24 | - name: Run Biome
25 | run: biome ci .
26 |
27 | typecheck:
28 | runs-on: warp-ubuntu-2404-x64-2x
29 | timeout-minutes: 15
30 | steps:
31 | - uses: actions/checkout@v4
32 | with:
33 | fetch-depth: 0
34 |
35 | - name: Cache turbo build setup
36 | if: env.skip == 'false'
37 | uses: WarpBuilds/cache@v1
38 | with:
39 | path: .turbo
40 | key: ${{ runner.os }}-turbo-${{ github.sha }}
41 | restore-keys: |
42 | ${{ runner.os }}-turbo-
43 |
44 | - name: Setup Node.js environment
45 | uses: WarpBuilds/setup-node@v4
46 | with:
47 | node-version-file: '.nvmrc'
48 | cache: 'yarn'
49 |
50 | - name: Install dependencies
51 | run: yarn install --immutable
52 |
53 | - name: Type-check
54 | if: ${{ needs.detect-changes.outputs.e2e == 'true' }}
55 | run: yarn typecheck
56 |
57 | - name: Build
58 | run: yarn build
59 |
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 |
4 |
5 | # Dependencies
6 | node_modules
7 | .pnp
8 | .pnp.js
9 |
10 | # Local env files
11 | .env
12 | .env.local
13 | .env.development.local
14 | .env.test.local
15 | .env.production.local
16 |
17 | # Testing
18 | coverage
19 |
20 | # Turbo
21 | .turbo
22 |
23 | # Vercel
24 | .vercel
25 |
26 | # Build Outputs
27 | .next/
28 | out/
29 | build
30 | dist
31 |
32 |
33 | # Debug
34 | npm-debug.log*
35 | yarn-debug.log*
36 | yarn-error.log*
37 |
38 | # Misc
39 | .DS_Store
40 | *.pem
41 |
42 | .yarn/install-state.gz
43 | *-values.yaml
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/.npmrc
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "biomejs.biome",
3 | "[typescript]": {
4 | "editor.defaultFormatter": "biomejs.biome"
5 | },
6 | "[typescriptreact]": {
7 | "editor.defaultFormatter": "biomejs.biome"
8 | },
9 | "editor.codeActionsOnSave": {
10 | "quickfix.biome": "explicit",
11 | },
12 | "editor.formatOnSave": true
13 | }
14 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | yarnPath: .yarn/releases/yarn-4.5.0.cjs
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021 LambdaCurry
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/apps/medusa/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | .medusa
4 | demo-values.yaml
5 | integration-tests
--------------------------------------------------------------------------------
/apps/medusa/.env.template:
--------------------------------------------------------------------------------
1 | DB_NAME=medusa2
2 | DATABASE_URL=postgresql://postgres:postgres@localhost:5432/${DB_NAME}
3 | POSTGRES_URL=postgresql://postgres:postgres@localhost:5432/${DB_NAME}
4 |
5 | STORE_CORS=http://localhost:8000,https://docs.medusajs.com
6 | ADMIN_CORS=http://localhost:7000,http://localhost:7001,https://docs.medusajs.com
7 | AUTH_CORS=http://localhost:7000,http://localhost:7001,https://docs.medusajs.com
8 | REDIS_URL=redis://localhost:6379
9 | JWT_SECRET=supersecret
10 | COOKIE_SECRET=supersecret
11 |
12 | ADMIN_BACKEND_URL=http://localhost:9000
13 |
14 | # Add your own Stripe secret key here
15 | STRIPE_API_KEY=
16 |
--------------------------------------------------------------------------------
/apps/medusa/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 | .env
3 | .DS_Store
4 | /uploads
5 | /node_modules
6 | yarn-error.log
7 |
8 | .idea
9 |
10 | coverage
11 |
12 | !src/**
13 |
14 | ./tsconfig.tsbuildinfo
15 | package-lock.json
16 | yarn.lock
17 | medusa-db.sql
18 | build
19 | .cache
20 |
21 | .yarn/*
22 | !.yarn/patches
23 | !.yarn/plugins
24 | !.yarn/releases
25 | !.yarn/sdks
26 | !.yarn/versions
27 |
28 | .medusa
29 |
30 | demo-values.yaml
31 |
32 | /.yalc
33 | yalc.lock
34 |
35 | static/*
--------------------------------------------------------------------------------
/apps/medusa/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20-alpine AS base
2 |
3 | RUN apk update && apk add --no-cache libc6-compat
4 |
5 | WORKDIR /app
6 |
7 | FROM base AS builder
8 |
9 | COPY . .
10 |
11 | RUN npx --yes turbo@2.1.2 prune --scope=medusa --docker
12 |
13 | FROM base AS installer
14 |
15 | COPY --from=builder /app/out/json/ .
16 | COPY --from=builder /app/out/yarn.lock yarn.lock
17 | COPY --from=builder /app/.yarnrc.yml .yarnrc.yml
18 | COPY --from=builder /app/.yarn .yarn
19 |
20 | RUN yarn install
21 |
22 | COPY --from=builder /app/out/full/ .
23 |
24 | RUN yarn turbo build --filter=medusa && \
25 | rm -rf node_modules/.cache .yarn/cache
26 |
27 |
28 | FROM base AS runner
29 |
30 | COPY --chown=node:node --from=installer /app/yarn.lock .
31 | COPY --chown=node:node --from=installer /app/.yarnrc.yml .yarnrc.yml
32 | COPY --chown=node:node --from=installer /app/.yarn .yarn
33 | COPY --chown=node:node --from=installer /app/apps/medusa/.medusa/server /app/server
34 | COPY --chown=node:node --from=installer /app/yarn.lock /app/server/yarn.lock
35 |
36 | RUN cd /app/server && yarn workspaces focus --production
37 |
38 | USER 1000
39 |
40 | WORKDIR /app/server
41 |
42 | ENV PORT=80
43 |
44 | CMD ["yarn", "start:prod"]
--------------------------------------------------------------------------------
/apps/medusa/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | name: medusa2-starter
3 | services:
4 | db:
5 | image: postgres:16
6 | container_name: medusa2-starter-postgres
7 | volumes:
8 | - database:/var/lib/postgresql/data
9 | ports:
10 | - '5432:5432'
11 | environment:
12 | POSTGRES_PASSWORD: 'postgres'
13 | redis:
14 | image: redis
15 | container_name: medusa2-starter-redis
16 | ports:
17 | - '6379:6379'
18 | volumes:
19 | - redis:/data
20 |
21 | volumes:
22 | database:
23 | redis:
24 |
--------------------------------------------------------------------------------
/apps/medusa/integration-tests/http/README.md:
--------------------------------------------------------------------------------
1 | # Integration Tests
2 |
3 | The `medusa-test-utils` package provides utility functions to create integration tests for your API routes and workflows.
4 |
5 | For example:
6 |
7 | ```ts
8 | import { medusaIntegrationTestRunner } from "medusa-test-utils"
9 |
10 | medusaIntegrationTestRunner({
11 | testSuite: ({ api, getContainer }) => {
12 | describe("Custom endpoints", () => {
13 | describe("GET /store/custom", () => {
14 | it("returns correct message", async () => {
15 | const response = await api.get(
16 | `/store/custom`
17 | )
18 |
19 | expect(response.status).toEqual(200)
20 | expect(response.data).toHaveProperty("message")
21 | expect(response.data.message).toEqual("Hello, World!")
22 | })
23 | })
24 | })
25 | }
26 | })
27 | ```
28 |
29 | Learn more in [this documentation](https://docs.medusajs.com/v2/debugging-and-testing/testing-tools/integration-tests).
--------------------------------------------------------------------------------
/apps/medusa/integration-tests/http/health.spec.ts:
--------------------------------------------------------------------------------
1 | import { medusaIntegrationTestRunner } from '@medusajs/test-utils';
2 | jest.setTimeout(60 * 1000);
3 |
4 | medusaIntegrationTestRunner({
5 | inApp: true,
6 | env: {},
7 | testSuite: ({ api }) => {
8 | describe('Ping', () => {
9 | it('ping the server health endpoint', async () => {
10 | const response = await api.get('/health');
11 | expect(response.status).toEqual(200);
12 | });
13 | });
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/apps/medusa/jest.config.js:
--------------------------------------------------------------------------------
1 | const { loadEnv } = require('@medusajs/utils');
2 | loadEnv('test', process.cwd());
3 |
4 | module.exports = {
5 | transform: {
6 | '^.+\\.[jt]s$': [
7 | '@swc/jest',
8 | {
9 | jsc: {
10 | parser: { syntax: 'typescript', decorators: true },
11 | },
12 | },
13 | ],
14 | },
15 | testEnvironment: 'node',
16 | moduleFileExtensions: ['js', 'ts', 'json'],
17 | modulePathIgnorePatterns: ['dist/'],
18 | };
19 |
20 | if (process.env.TEST_TYPE === 'integration:http') {
21 | module.exports.testMatch = ['**/integration-tests/http/*.spec.[jt]s'];
22 | } else if (process.env.TEST_TYPE === 'integration:modules') {
23 | module.exports.testMatch = ['**/src/modules/*/__tests__/**/*.[jt]s'];
24 | } else if (process.env.TEST_TYPE === 'unit') {
25 | module.exports.testMatch = ['**/src/**/__tests__/**/*.unit.spec.[jt]s'];
26 | }
27 |
--------------------------------------------------------------------------------
/apps/medusa/src/admin/README.md:
--------------------------------------------------------------------------------
1 | # Admin Customizations
2 |
3 | You can extend the Medusa Admin to add widgets and new pages. Your customizations interact with API routes to provide merchants with custom functionalities.
4 |
5 | ## Example: Create a Widget
6 |
7 | A widget is a React component that can be injected into an existing page in the admin dashboard.
8 |
9 | For example, create the file `src/admin/widgets/product-widget.tsx` with the following content:
10 |
11 | ```tsx title="src/admin/widgets/product-widget.tsx"
12 | import { defineWidgetConfig } from "@medusajs/admin-sdk"
13 |
14 | // The widget
15 | const ProductWidget = () => {
16 | return (
17 |
18 |
Product Widget
19 |
20 | )
21 | }
22 |
23 | // The widget's configurations
24 | export const config = defineWidgetConfig({
25 | zone: "product.details.after",
26 | })
27 |
28 | export default ProductWidget
29 | ```
30 |
31 | This inserts a widget with the text “Product Widget” at the end of a product’s details page.
--------------------------------------------------------------------------------
/apps/medusa/src/admin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "module": "esnext"
5 | },
6 | "include": ["."],
7 | "exclude": ["**/*.spec.js"]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/medusa/src/api/admin/custom/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from '@medusajs/framework';
2 |
3 | export async function GET(req: MedusaRequest, res: MedusaResponse): Promise {
4 | res.sendStatus(200);
5 | }
6 |
--------------------------------------------------------------------------------
/apps/medusa/src/api/store/custom/route.ts:
--------------------------------------------------------------------------------
1 | import { MedusaRequest, MedusaResponse } from '@medusajs/framework';
2 | import { ContainerRegistrationKeys } from '@medusajs/utils';
3 |
4 | export async function GET(req: MedusaRequest, res: MedusaResponse): Promise {
5 | const query = req.scope.resolve(ContainerRegistrationKeys.QUERY);
6 | // const orderService = req.scope.resolve(
7 | // ContainerRegistrationKeys.ORDER_SERVICE
8 | // );
9 | //test
10 |
11 | res.json({ message: 'Hello' });
12 | }
13 |
--------------------------------------------------------------------------------
/apps/medusa/src/jobs/README.md:
--------------------------------------------------------------------------------
1 | # Custom scheduled jobs
2 |
3 | A scheduled job is a function executed at a specified interval of time in the background of your Medusa application.
4 |
5 | A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory.
6 |
7 | For example, create the file `src/jobs/hello-world.ts` with the following content:
8 |
9 | ```ts
10 | import {
11 | IProductModuleService,
12 | MedusaContainer
13 | } from "@medusajs/types";
14 | import { ModuleRegistrationName } from "@medusajs/utils";
15 |
16 | export default async function myCustomJob(container: MedusaContainer) {
17 | const productService: IProductModuleService = container.resolve(ModuleRegistrationName.PRODUCT)
18 |
19 | const products = await productService.listAndCountProducts();
20 |
21 | // Do something with the products
22 | }
23 |
24 | export const config = {
25 | name: "daily-product-report",
26 | schedule: "0 0 * * *", // Every day at midnight
27 | };
28 | ```
29 |
30 | A scheduled job file must export:
31 |
32 | - The function to be executed whenever it’s time to run the scheduled job.
33 | - A configuration object defining the job. It has three properties:
34 | - `name`: a unique name for the job.
35 | - `schedule`: a [cron expression](https://crontab.guru/).
36 | - `numberOfExecutions`: an optional integer, specifying how many times the job will execute before being removed
37 |
38 | The `handler` is a function that accepts one parameter, `container`, which is a `MedusaContainer` instance used to resolve services.
39 |
--------------------------------------------------------------------------------
/apps/medusa/src/links/README.md:
--------------------------------------------------------------------------------
1 | # Module Links
2 |
3 | A module link forms an association between two data models of different modules, while maintaining module isolation.
4 |
5 | For example:
6 |
7 | ```ts
8 | import HelloModule from "../modules/hello"
9 | import ProductModule from "@medusajs/product"
10 | import { defineLink } from "@medusajs/utils"
11 |
12 | export default defineLink(
13 | ProductModule.linkable.product,
14 | HelloModule.linkable.myCustom
15 | )
16 | ```
17 |
18 | This defines a link between the Product Module's `product` data model and the Hello Module (custom module)'s `myCustom` data model.
19 |
20 | Learn more about links in [this documentation](https://docs.medusajs.com/v2/advanced-development/modules/module-links)
--------------------------------------------------------------------------------
/apps/medusa/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "esModuleInterop": true,
5 | "module": "NodeNext",
6 | "moduleResolution": "NodeNext",
7 | "emitDecoratorMetadata": true,
8 | "experimentalDecorators": true,
9 | "skipLibCheck": true,
10 | "skipDefaultLibCheck": true,
11 | "declaration": false,
12 | "sourceMap": false,
13 | "inlineSourceMap": true,
14 | "outDir": "./.medusa/server",
15 | "rootDir": "./",
16 | "baseUrl": ".",
17 | "jsx": "react-jsx",
18 | "forceConsistentCasingInFileNames": true,
19 | "resolveJsonModule": true,
20 | "checkJs": false,
21 | "strict": true,
22 | "incremental": true,
23 | "isolatedModules": true
24 | },
25 | "ts-node": {
26 | "swc": true
27 | },
28 | "include": ["**/*", ".medusa/types/*"],
29 | "exclude": ["node_modules", ".medusa/server", ".medusa/admin", ".cache", "public"]
30 | }
31 |
--------------------------------------------------------------------------------
/apps/storefront/.env.template:
--------------------------------------------------------------------------------
1 | STRIPE_PUBLIC_KEY='pk_'
2 | STRIPE_SECRET_KEY='sk_'
3 | MEDUSA_PUBLISHABLE_KEY='pk_'
--------------------------------------------------------------------------------
/apps/storefront/.gitignore:
--------------------------------------------------------------------------------
1 | .env.*
2 | !.env.template
3 | .cache
4 | build
5 | public/build
6 | app/tailwind.css
7 | prod.package.json
8 | vite.config.ts.timestamp-*.mjs
9 |
10 | .yalc
11 | yalc.lock
12 |
13 | .react-router
--------------------------------------------------------------------------------
/apps/storefront/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20-alpine AS base
2 |
3 | RUN apk update && apk add --no-cache libc6-compat
4 |
5 | WORKDIR /app
6 |
7 | FROM base AS builder
8 |
9 | COPY . .
10 |
11 | RUN npx --yes turbo@2.1.2 prune --scope=storefront --docker
12 |
13 | FROM base AS installer
14 |
15 | COPY --from=builder /app/out/json/ .
16 | COPY --from=builder /app/out/yarn.lock yarn.lock
17 | COPY --from=builder /app/.yarnrc.yml .yarnrc.yml
18 | COPY --from=builder /app/.yarn .yarn
19 |
20 | RUN yarn install
21 |
22 | COPY --from=builder /app/out/full/ .
23 |
24 | RUN yarn turbo build --filter=storefront && \
25 | yarn workspaces focus --all --production && \
26 | rm -rf node_modules/.cache .yarn/cache
27 |
28 | FROM base AS runner
29 |
30 | COPY --chown=node:node --from=installer /app/ .
31 |
32 | USER 1000
33 |
34 | WORKDIR /app/apps/storefront
35 |
36 | ENV PORT=80
37 |
38 | CMD ["yarn", "start"]
--------------------------------------------------------------------------------
/apps/storefront/README.md:
--------------------------------------------------------------------------------
1 | # MarketHaus Storefront
2 |
3 | - [Remix Docs](https://remix.run/docs)
4 |
5 | ## Development
6 |
7 | From your terminal:
8 |
9 | ```sh
10 | yarn dev
11 | ```
12 |
13 | This starts your app in development mode, rebuilding assets on file changes.
14 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/LogoStoreName/LogoStoreName.tsx:
--------------------------------------------------------------------------------
1 | import { Image } from '@app/components/common/images/Image';
2 | import { useSiteDetails } from '@app/hooks/useSiteDetails';
3 | import clsx from 'clsx';
4 | import type { FC, PropsWithChildren } from 'react';
5 | import { Link } from 'react-router';
6 |
7 | const LogoHeader: FC = ({
8 | primary,
9 | className,
10 | ...rest
11 | }) => (primary ? : );
12 |
13 | export const LogoStoreName: FC<{ primary?: boolean; className?: string }> = ({ primary, className }) => {
14 | const { store, settings } = useSiteDetails();
15 |
16 | if (!store || !settings) return null;
17 |
18 | return (
19 |
25 |
26 | {store?.name}
27 |
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/badges/HasSaleBadge.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 |
3 | export const HasSaleBadge = ({ className, endingSoon }: { className?: string; endingSoon?: boolean }) => {
4 | return (
5 |
11 | {endingSoon ? <>Sale ends soon!> : <>On sale!>}
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/badges/SoldOutBadge.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 |
3 | export const SoldOutBadge = ({ className }: { className?: string }) => {
4 | return (
5 |
11 | Sold out
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/cart/index.ts:
--------------------------------------------------------------------------------
1 | export * from './line-items';
2 | export * from './CartDrawer';
3 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/cart/line-items/index.ts:
--------------------------------------------------------------------------------
1 | export * from './LineItemQuantitySelect';
2 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/CheckoutFlow.tsx:
--------------------------------------------------------------------------------
1 | import { Alert } from '@app/components/common/alert/Alert';
2 | import { useCheckout } from '@app/hooks/useCheckout';
3 | import { useCustomer } from '@app/hooks/useCustomer';
4 | import { FC, useEffect } from 'react';
5 | import { CheckoutAccountDetails } from './CheckoutAccountDetails';
6 | import { CheckoutDeliveryMethod } from './CheckoutDeliveryMethod';
7 | import { CheckoutPayment } from './CheckoutPayment';
8 | import { StripeExpressCheckout } from './StripePayment/StripeExpressPayment';
9 |
10 | export const CheckoutFlow: FC = () => {
11 | const { customer } = useCustomer();
12 | const { goToNextStep, cart } = useCheckout();
13 | const isLoggedIn = !!customer?.id;
14 |
15 | if (!cart) return;
16 |
17 | useEffect(() => {
18 | if (isLoggedIn) goToNextStep();
19 | return () => goToNextStep();
20 | }, [isLoggedIn]);
21 |
22 | return (
23 | <>
24 |
25 | {isLoggedIn && (
26 |
27 | Checking out as:{' '}
28 |
29 | {customer.first_name} {customer.last_name} ({customer.email})
30 |
31 |
32 | )}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | >
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/CheckoutOrderSummary/CheckoutOrderSummary.tsx:
--------------------------------------------------------------------------------
1 | import { useCheckout } from '@app/hooks/useCheckout';
2 | import { PromotionDTO, StoreCart } from '@medusajs/types';
3 | import { FC, ReactNode } from 'react';
4 | import { CheckoutOrderSummaryItems } from './CheckoutOrderSummaryItems';
5 | import { CheckoutOrderSummaryTotals } from './CheckoutOrderSummaryTotals';
6 |
7 | export interface CheckoutOrderSummaryProps {
8 | submitButton?: ReactNode;
9 | name: string;
10 | }
11 |
12 | export const CheckoutOrderSummary: FC = ({ submitButton, name }) => {
13 | const { shippingOptions, cart } = useCheckout();
14 |
15 | if (!cart) return null;
16 |
17 | return (
18 |
19 |
Items in your cart
20 |
21 |
25 | {submitButton &&
{submitButton}
}
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/CheckoutOrderSummary/RemoveDiscountCodeButton.tsx:
--------------------------------------------------------------------------------
1 | import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
2 | import { FetcherKeys } from '@libs/util/fetcher-keys';
3 | import { StoreCart, StoreCartPromotion } from '@medusajs/types';
4 | import { FC } from 'react';
5 | import { useFetcher } from 'react-router';
6 |
7 | export interface RemoveDiscountCodeButtonProps {
8 | cart: StoreCart;
9 | promotion: StoreCartPromotion;
10 | }
11 |
12 | export const RemovePromotionCodeButton: FC = ({ cart, promotion }) => {
13 | const fetcher = useFetcher<{}>({ key: FetcherKeys.cart.removePromotionCode });
14 |
15 | if (['submitting', 'loading'].includes(fetcher.state)) return null;
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/CheckoutOrderSummary/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CheckoutOrderSummary';
2 | export * from './CheckoutOrderSummaryItems';
3 | export * from './CheckoutOrderSummaryTotals';
4 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/CheckoutSectionHeader.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@app/components/common/buttons/Button';
2 | import { CheckoutStep } from '@app/providers/checkout-provider';
3 | import CheckIcon from '@heroicons/react/24/solid/CheckIcon';
4 | import { FC, PropsWithChildren } from 'react';
5 |
6 | export const CheckoutSectionHeader: FC<
7 | PropsWithChildren<{
8 | completed: boolean;
9 | setStep: (step: CheckoutStep) => void;
10 | step: CheckoutStep;
11 | }>
12 | > = ({ completed, setStep, step, children }) => {
13 | return (
14 |
15 | {children}
16 | {completed && (
17 | <>
18 |
21 |
22 |
23 |
24 | >
25 | )}
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/CheckoutSidebar.tsx:
--------------------------------------------------------------------------------
1 | import { useCheckout } from '@app/hooks/useCheckout';
2 | import { CheckoutStep } from '@app/providers/checkout-provider';
3 | import clsx from 'clsx';
4 | import { FC } from 'react';
5 | import { CheckoutOrderSummary } from './CheckoutOrderSummary';
6 |
7 | export const CheckoutSidebar: FC = () => {
8 | const { step } = useCheckout();
9 |
10 | return (
11 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/HiddenAddressGroup.tsx:
--------------------------------------------------------------------------------
1 | import { TextField } from '@lambdacurry/forms/remix-hook-form';
2 | import { Address } from '@libs/types';
3 |
4 | interface HiddenAddressGroupProps {
5 | address: Address;
6 | prefix: 'shippingAddress' | 'billingAddress';
7 | }
8 |
9 | const HiddenAddressGroup: React.FC = ({ address, prefix }) => {
10 | return (
11 | <>
12 | {Object.keys(address).map((key: string) => {
13 | const castedKey = key as keyof Address;
14 | if (address[castedKey] == null) return;
15 |
16 | return (
17 |
23 | );
24 | })}
25 | >
26 | );
27 | };
28 |
29 | export default HiddenAddressGroup;
30 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/ManualPayment/ManualPayment.tsx:
--------------------------------------------------------------------------------
1 | import { CustomPaymentSession } from '@libs/types';
2 | import { FC, PropsWithChildren } from 'react';
3 | import { CompleteCheckoutForm } from '../CompleteCheckoutForm';
4 |
5 | export interface ManualPaymentProps extends PropsWithChildren {
6 | isActiveStep: boolean;
7 | paymentMethods: CustomPaymentSession[];
8 | }
9 |
10 | export const ManualPayment: FC = (props) => (
11 |
18 | );
19 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/StripePayment/StripeElementsProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useCheckout } from '@app/hooks/useCheckout';
2 | import { useEnv } from '@app/hooks/useEnv';
3 | import { Elements } from '@stripe/react-stripe-js';
4 | import { StripeElementsOptions, loadStripe } from '@stripe/stripe-js';
5 | import { FC, PropsWithChildren, useMemo } from 'react';
6 |
7 | export interface StripeElementsProviderProps extends PropsWithChildren {
8 | options?: StripeElementsOptions;
9 | }
10 |
11 | export const StripeElementsProvider: FC = ({ options, children }) => {
12 | const { env } = useEnv();
13 | const { cart } = useCheckout();
14 |
15 | const stripePromise = useMemo(() => (env.STRIPE_PUBLIC_KEY ? loadStripe(env.STRIPE_PUBLIC_KEY) : null), []);
16 |
17 | const stripeSession = useMemo(
18 | () => cart?.payment_collection?.payment_sessions?.find((s) => s.provider_id === 'pp_stripe_stripe'),
19 | [cart?.payment_collection?.payment_sessions],
20 | ) as unknown as {
21 | data: { client_secret: string };
22 | };
23 |
24 | const clientSecret = stripeSession?.data?.client_secret as string;
25 |
26 | if (!stripeSession || !stripePromise || !clientSecret) return null;
27 |
28 | return (
29 |
38 | {children}
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/StripePayment/StripeExpressPayment.tsx:
--------------------------------------------------------------------------------
1 | import { useCheckout } from '@app/hooks/useCheckout';
2 | import { amountToStripeExpressCheckoutAmount } from '@libs/util/checkout/amountToStripeExpressCheckoutAmount';
3 | import { StoreCart } from '@medusajs/types';
4 | import { StripeElementsOptionsMode } from '@stripe/stripe-js';
5 | import { type FC } from 'react';
6 | import { StripeElementsProvider } from './StripeElementsProvider';
7 | import { StripeExpressCheckoutForm } from './StripeExpressPaymentForm';
8 |
9 | interface StripeExpressCheckoutProps {
10 | cart: StoreCart;
11 | }
12 |
13 | export const StripeExpressCheckout: FC = ({ cart }) => {
14 | const { activePaymentSession } = useCheckout();
15 |
16 | const options: StripeElementsOptionsMode = {
17 | mode: 'payment',
18 | amount: amountToStripeExpressCheckoutAmount(cart.total),
19 | currency: cart?.currency_code || 'usd',
20 | capture_method: 'manual',
21 | };
22 |
23 | return (
24 |
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/StripePayment/StripePayment.tsx:
--------------------------------------------------------------------------------
1 | import type { CustomPaymentSession } from '@libs/types';
2 | import type { FC, PropsWithChildren } from 'react';
3 | import { StripeElementsProvider } from './StripeElementsProvider';
4 | import { StripePaymentForm } from './StripePaymentForm';
5 |
6 | export interface StripePaymentProps extends PropsWithChildren {
7 | isActiveStep: boolean;
8 | paymentMethods: CustomPaymentSession[];
9 | }
10 |
11 | export const StripePayment: FC = (props) => {
12 | const { isActiveStep, ...rest } = props;
13 | return (
14 |
15 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/StripePayment/index.ts:
--------------------------------------------------------------------------------
1 | export * from './StripePayment';
2 | export * from './StripePaymentForm';
3 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/address/AddressDisplay.tsx:
--------------------------------------------------------------------------------
1 | import type { Address } from '@libs/types';
2 |
3 | export const AddressDisplay: React.FC<{
4 | title?: string;
5 | address: Address;
6 | countryOptions: { value: string; label: string }[];
7 | }> = ({ title, address, countryOptions }) => (
8 |
9 | {title && {title}}
10 |
11 | {address?.company && (
12 | <>
13 | {address?.company}
14 |
15 | >
16 | )}
17 | {address?.address1}
18 |
19 | {address?.address2 && (
20 | <>
21 | {address?.address2}
22 |
23 | >
24 | )}
25 | {address?.city}, {address?.province} {address?.postalCode}
26 |
27 | {address?.countryCode && (
28 | <>
29 | {countryOptions.find(({ value }) => value === address?.countryCode)?.label}
30 |
31 | >
32 | )}
33 |
34 |
35 | );
36 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/checkout-form-helpers.ts:
--------------------------------------------------------------------------------
1 | import { StoreCart, StoreCartAddress, StoreCustomer } from '@medusajs/types';
2 |
3 | export const selectInitialShippingAddress = (cart: StoreCart, customer?: StoreCustomer) => {
4 | if (cart.shipping_address) return cart.shipping_address;
5 |
6 | if (!customer || !customer?.addresses?.length) return null;
7 |
8 | const customerAddress = customer?.default_shipping_address_id
9 | ? customer.addresses?.find((a) => a.id === customer?.default_shipping_address_id)
10 | : customer?.addresses?.[0];
11 |
12 | return (customerAddress as StoreCartAddress) || null;
13 | };
14 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/checkout/index.ts:
--------------------------------------------------------------------------------
1 | export * from './checkout-form-helpers';
2 | export * from './CheckoutAccountDetails';
3 | export * from './CheckoutDeliveryMethod';
4 | export * from './CheckoutFlow';
5 | export * from './CheckoutOrderSummary';
6 | export * from './CheckoutPayment';
7 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/collection/collection-list-item.tsx:
--------------------------------------------------------------------------------
1 | import { StoreCollection, StoreProduct } from '@medusajs/types';
2 | import { type FC, Suspense } from 'react';
3 | import { Await, Link } from 'react-router';
4 | import ProductCarousel from '../product/ProductCarousel';
5 | import { ProductCarouselSkeleton } from '../product/ProductCarouselSkeleton';
6 |
7 | export interface CollectionListItemContentProps {
8 | collection: StoreCollection;
9 | }
10 |
11 | export interface CollectionListItemProps extends CollectionListItemContentProps {
12 | className?: string;
13 | collection: StoreCollection;
14 | deferredProducts: Promise<{ products: StoreProduct[] }>;
15 | }
16 |
17 | export const CollectionListItem: FC = ({ collection, deferredProducts }) => {
18 | return (
19 | <>
20 |
24 | {collection.title}
25 |
26 |
27 | }>
28 | {({ products }) => }
29 |
30 | >
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/collection/collection-list.tsx:
--------------------------------------------------------------------------------
1 | import { StoreCollection, StoreProduct } from '@medusajs/types';
2 | import type { FC } from 'react';
3 | import type { CollectionListItemProps } from './collection-list-item';
4 |
5 | export const CollectionList: FC<{
6 | title?: string;
7 | collections: StoreCollection[];
8 | deferredProductsByCollection: Record<
9 | string,
10 | Promise<{
11 | products: StoreProduct[];
12 | }>
13 | >;
14 | renderCollectionListItem: FC;
15 | }> = ({ collections, title = 'Collections', deferredProductsByCollection, renderCollectionListItem }) => {
16 | const CollectionListItem = renderCollectionListItem;
17 |
18 | return (
19 |
20 |
21 |
22 |
{title}
23 |
24 |
25 |
26 |
27 | {collections.map((collection) => {
28 | const deferredProducts = deferredProductsByCollection[collection.handle!];
29 | return (
30 |
31 | );
32 | })}
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/collection/index.ts:
--------------------------------------------------------------------------------
1 | export * from './collection-list';
2 | export * from './collection-list-item';
3 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/Address/Address.tsx:
--------------------------------------------------------------------------------
1 | import { formatPhoneNumber } from '@libs/util';
2 | import { StoreCartAddress } from '@medusajs/types';
3 | import { FC } from 'react';
4 |
5 | export interface AddressProps {
6 | address: StoreCartAddress;
7 | }
8 |
9 | export const Address: FC = ({ address }) => {
10 | return (
11 |
12 | {address.address_1}
13 |
14 | {address.address_2 && (
15 | <>
16 | {address.address_2}
17 |
18 | >
19 | )}
20 | {address.city}, {address.province} {address.postal_code} {address.country_code}
21 | {address.phone && (
22 | <>
23 |
24 | {formatPhoneNumber(address.phone)}
25 | >
26 | )}
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/Empty/Empty.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, ReactNode, SVGAttributes } from 'react';
2 |
3 | export interface EmptyProps {
4 | icon?: FC>;
5 | action?: ReactNode;
6 | title: string;
7 | description: string;
8 | }
9 |
10 | export const Empty: FC = ({ icon: Icon, title, description, action }) => (
11 |
12 | {Icon && (
13 |
14 |
15 |
16 | )}
17 |
{title}
18 |
{description}
19 | {action &&
{action}
}
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/ImageUpload/ImageUploader.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { CSSProperties, FC } from 'react';
3 | import { FileUploader } from 'react-drag-drop-files';
4 |
5 | interface FileInputProps {
6 | id?: string;
7 | name: string;
8 | multiple?: boolean;
9 | label?: string;
10 | className?: string;
11 | encType?: string;
12 | required?: boolean;
13 | disabled?: boolean;
14 | hoverTitle?: string;
15 | fileOrFiles?: File[] | File | null;
16 | classes?: string;
17 | types?: string[];
18 | onTypeError?: (err: any) => void;
19 | children?: React.ReactNode;
20 | maxSize?: number;
21 | minSize?: number;
22 | onSizeError?: (file: File) => void;
23 | onDrop?: (file: File) => void;
24 | onSelect?: (file: File) => void;
25 | handleChange?: (files: File[]) => void;
26 | onDraggingStateChange?: (dragging: boolean) => void;
27 | dropMessageStyle?: CSSProperties;
28 | }
29 |
30 | // Note: this component is unstyled and meant to be used with children
31 | export const ImageUploader: FC = ({ className, ...props }) => {
32 | return (
33 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/Pagination/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Pagination';
2 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/Pagination/react-use-pagination/Pagination.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { usePagination } from './usePagination';
4 |
5 | type PaginationProps = {
6 | children: (arg0: ReturnType) => ReactNode;
7 | totalItems?: number;
8 | initialPage?: number;
9 | initialPageSize: number;
10 | };
11 |
12 | function Pagination({ children, totalItems = 0, initialPage = 0, initialPageSize }: PaginationProps) {
13 | return children(usePagination({ totalItems, initialPage, initialPageSize }));
14 | }
15 |
16 | Pagination.displayName = 'Pagination';
17 |
18 | export { Pagination };
19 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/Pagination/react-use-pagination/index.ts:
--------------------------------------------------------------------------------
1 | export * from './getPaginationMeta';
2 | export * from './Pagination';
3 | export * from './usePagination';
4 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/actions-list/ActionList.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@app/components/common/buttons';
2 | import { URLAwareNavLink } from '@app/components/common/link/URLAwareNavLink';
3 | import { type CustomAction } from '@libs/types';
4 | import clsx from 'clsx';
5 | import { FC, HTMLAttributes } from 'react';
6 |
7 | export interface ActionListProps extends HTMLAttributes {
8 | actions: CustomAction[];
9 | }
10 |
11 | export const ActionList: FC = ({ actions, className }) => (
12 |
13 | {actions.map(({ url, label, new_tab, style_variant }, index) => {
14 | if (!label) return null;
15 |
16 | return (
17 |
29 | );
30 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/actions/Actions.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface ActionsProps extends HTMLAttributes {
5 | className?: string;
6 | }
7 |
8 | export const Actions: FC = ({ className, ...props }) => (
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/actions/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Actions';
2 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/alert/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Alert';
2 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/assets/StripeBadge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/app/components/common/assets/StripeBadge.png
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/assets/icons/index.ts:
--------------------------------------------------------------------------------
1 | export * from './payment-methods';
2 | export * from './social';
3 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/assets/icons/payment-methods/MasterCardIcon.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes } from 'react';
2 |
3 | export default (props: HTMLAttributes) => (
4 |
24 | );
25 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/assets/icons/payment-methods/index.ts:
--------------------------------------------------------------------------------
1 | export { default as AmericanExpressIcon } from './AmericanExpressIcon';
2 | export { default as DinersClubIcon } from './DinersClubIcon';
3 | export { default as DiscoverIcon } from './DiscoverIcon';
4 | export { default as JCBIcon } from './JCBIcon';
5 | export { default as MasterCardIcon } from './MasterCardIcon';
6 | export { default as UnionPayIcon } from './UnionPayIcon';
7 | export { default as VisaIcon } from './VisaIcon';
8 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/assets/icons/social/facebook.tsx:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from 'react';
2 |
3 | export default ({ color = 'currentColor', ...props }: HTMLAttributes) => (
4 |
11 | );
12 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/assets/icons/social/index.ts:
--------------------------------------------------------------------------------
1 | export { default as InstagramIcon } from './instagram';
2 | export { default as FacebookIcon } from './facebook';
3 | export { default as TwitterIcon } from './twitter';
4 | export { default as YoutubeIcon } from './youtube';
5 | export { default as PinterestIcon } from './pinterest';
6 | export { default as LinkedinIcon } from './linkedin';
7 | export { default as SnapchatIcon } from './snapchat';
8 | export { default as TiktokIcon } from './tiktok';
9 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/assets/icons/social/linkedin.tsx:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from 'react';
2 |
3 | export default ({ color = 'currentColor', ...props }: HTMLAttributes) => (
4 |
11 | );
12 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/assets/icons/social/pinterest.tsx:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from 'react';
2 |
3 | export default ({ color = 'currentColor', ...props }: HTMLAttributes) => (
4 |
11 | );
12 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/assets/icons/social/snapchat.tsx:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from 'react';
2 |
3 | export default ({ color = 'currentColor', ...props }: HTMLAttributes) => (
4 |
11 | );
12 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/assets/icons/social/tiktok.tsx:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from 'react';
2 |
3 | export default ({ color = 'currentColor', ...props }: HTMLAttributes) => (
4 |
11 | );
12 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/assets/icons/social/twitter.tsx:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from 'react';
2 |
3 | export default ({ color = 'currentColor', ...props }: HTMLAttributes) => (
4 |
11 | );
12 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/assets/icons/social/youtube.tsx:
--------------------------------------------------------------------------------
1 | import type { HTMLAttributes } from 'react';
2 |
3 | export default ({ color = 'currentColor', ...props }: HTMLAttributes) => (
4 |
11 | );
12 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/breadcrumbs/Breadcrumbs.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, Fragment, ReactNode } from 'react';
3 | import { Link } from 'react-router';
4 | import { ButtonLink } from '../buttons/ButtonLink';
5 |
6 | export interface Breadcrumb {
7 | label: ReactNode;
8 | url?: string;
9 | }
10 |
11 | export interface BreadcrumbsProps {
12 | breadcrumbs: Breadcrumb[];
13 | className?: string;
14 | }
15 |
16 | export const Breadcrumbs: FC = ({ breadcrumbs, className }) => (
17 |
41 | );
42 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/breadcrumbs/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Breadcrumbs';
2 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/buttons/ButtonBase.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import {
3 | AnchorHTMLAttributes,
4 | ButtonHTMLAttributes,
5 | ForwardRefRenderFunction,
6 | ForwardedRef,
7 | HTMLAttributes,
8 | forwardRef,
9 | } from 'react';
10 |
11 | export type ButtonRef = HTMLButtonElement & HTMLAnchorElement;
12 |
13 | export type ButtonAs =
14 | | keyof Pick
15 | | ((props: ButtonHTMLAttributes & AnchorHTMLAttributes & HTMLAttributes) => JSX.Element);
16 |
17 | export type ButtonBaseProps = (ButtonHTMLAttributes &
18 | AnchorHTMLAttributes &
19 | HTMLAttributes) & {
20 | as?: ButtonAs;
21 | ref?: ForwardedRef;
22 | };
23 |
24 | const ButtonBaseInner: ForwardRefRenderFunction = (
25 | { as: T = 'button', className, disabled, ...props },
26 | ref,
27 | ) => {
28 | const type = T === 'button' ? 'button' : undefined;
29 |
30 | return (
31 |
42 | );
43 | };
44 |
45 | export const ButtonBase = forwardRef(ButtonBaseInner);
46 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/buttons/ButtonLink.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { Button, ButtonProps } from './Button';
3 |
4 | export interface ButtonLinkProps extends ButtonProps {}
5 |
6 | export const ButtonLink: FC = (props) => ;
7 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/buttons/IconButton.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, JSXElementConstructor, SVGAttributes } from 'react';
3 | import { ButtonBase, ButtonBaseProps } from './ButtonBase';
4 |
5 | export interface IconButtonProps extends ButtonBaseProps {
6 | icon: JSXElementConstructor;
7 | iconProps?: SVGAttributes;
8 | }
9 |
10 | export const IconButton: FC = ({ icon: Icon, className, iconProps, ...props }) => (
11 |
18 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/buttons/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Button';
2 | export * from './ButtonBase';
3 | export * from './ButtonLink';
4 | export * from './IconButton';
5 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/card/Card.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface CardProps extends HTMLAttributes {}
5 |
6 | export const Card: FC = ({ className, ...props }) => (
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/card/CardBody.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface CardBodyProps extends HTMLAttributes {}
5 |
6 | export const CardBody: FC = ({ className, ...props }) => (
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/card/CardContent.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface CardContentProps extends HTMLAttributes {}
5 |
6 | export const CardContent: FC = ({ className, ...props }) => (
7 |
11 | );
12 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/card/CardDate.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface CardDateProps extends HTMLAttributes {}
5 |
6 | export const CardDate: FC = ({ className, ...props }) => (
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/card/CardExcerpt.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface CardExcerptProps extends HTMLAttributes {}
5 |
6 | export const CardExcerpt: FC = ({ className, ...props }) => (
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/card/CardFooter.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface CardFooterProps extends HTMLAttributes {}
5 |
6 | export const CardFooter: FC = ({ className, ...props }) => (
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/card/CardGrid.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface CardGridProps extends HTMLAttributes {}
5 |
6 | export const CardGrid: FC = ({ className, ...props }) => (
7 |
14 | );
15 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/card/CardHeader.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface CardHeaderProps extends HTMLAttributes {}
5 |
6 | export const CardHeader: FC = ({ className, ...props }) => (
7 |
11 | );
12 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/card/CardLabel.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, PropsWithChildren } from 'react';
3 |
4 | export interface CardLabelProps {
5 | className?: string;
6 | }
7 |
8 | export const CardLabel: FC> = ({ className, ...props }) => (
9 |
16 | );
17 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/card/CardThumbnail.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { type FC } from 'react';
3 | import { Image } from '../images/Image';
4 |
5 | export interface CardThumbnailProps extends React.ImgHTMLAttributes {
6 | src?: string;
7 | className?: string;
8 | }
9 |
10 | export const CardThumbnail: FC = ({ className, ...props }) => {
11 | if (!props.src) return null;
12 |
13 | return (
14 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/card/CardTitle.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface CardTitleProps extends HTMLAttributes {}
5 |
6 | export const CardTitle: FC = ({ className, children, ...props }) => (
7 |
8 | {children}
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/card/index.ts:
--------------------------------------------------------------------------------
1 | export { Card } from './Card';
2 | export { CardBody } from './CardBody';
3 | export { CardContent } from './CardContent';
4 | export { CardDate } from './CardDate';
5 | export { CardExcerpt } from './CardExcerpt';
6 | export { CardFooter } from './CardFooter';
7 | export { CardGrid } from './CardGrid';
8 | export { CardHeader } from './CardHeader';
9 | export { CardLabel } from './CardLabel';
10 | export { CardThumbnail } from './CardThumbnail';
11 | export { CardTitle } from './CardTitle';
12 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/container/Container.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
3 |
4 | export const Container: FC & { className?: string }> = ({
5 | className,
6 | ...props
7 | }) => {
8 | return (
9 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/container/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Container';
2 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/forms/fields/FieldError.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes, ReactElement } from 'react';
3 |
4 | export type FieldErrorComponent = (
5 | errorProps: Omit,
6 | ) => ReactElement | null;
7 |
8 | export interface FieldErrorProps extends HTMLAttributes {
9 | error?: string;
10 | errorComponent?: FieldErrorComponent;
11 | }
12 |
13 | export const FieldError: FC = ({ className, errorComponent, ...props }) => {
14 | if (errorComponent) return errorComponent(props);
15 |
16 | if (!props.error) return null;
17 |
18 | return (
19 |
20 | {props.error}
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/forms/fields/FieldGroup.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface FieldGroupProps extends HTMLAttributes {}
5 |
6 | export const FieldGroup: FC = ({ className, ...props }) => (
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/forms/fields/FieldInput.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { type FC, type HTMLAttributes } from 'react';
3 |
4 | export interface FieldInputProps extends HTMLAttributes {
5 | type?: string;
6 | }
7 |
8 | export const FieldInput: FC = ({ type = '', className, ...props }) => (
9 |
20 | );
21 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/forms/fields/FieldLabel.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, LabelHTMLAttributes, ReactElement } from 'react';
3 |
4 | export type FieldLabelComponent = (
5 | labelProps: Omit,
6 | ) => ReactElement | null;
7 |
8 | export interface FieldLabelProps extends LabelHTMLAttributes {
9 | error?: string;
10 | optional?: boolean;
11 | labelComponent?: FieldLabelComponent;
12 | }
13 |
14 | export const FieldLabel: FC = ({ className, labelComponent, children, ...props }) => {
15 | if (!labelComponent && !children) return null;
16 |
17 | if (labelComponent) return labelComponent(props);
18 |
19 | return (
20 |
21 |
24 |
25 | {props.optional && Optional}
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/forms/fields/FieldWrapper.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { HTMLAttributes, forwardRef } from 'react';
3 |
4 | export interface FieldWrapperProps extends HTMLAttributes {
5 | name: string;
6 | type?: string;
7 | error?: string;
8 | }
9 |
10 | export const FieldWrapper = forwardRef(
11 | ({ name, type, className, error, ...props }, ref) => (
12 |
24 | ),
25 | );
26 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/forms/inputs/Input.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { type InputHTMLAttributes, forwardRef } from 'react';
3 |
4 | export interface InputProps extends InputHTMLAttributes {
5 | step?: string;
6 | error?: string | null;
7 | }
8 |
9 | export const Input = forwardRef(({ className, error, ...props }, ref) => (
10 |
20 | ));
21 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/forms/inputs/InputCheckbox.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { InputHTMLAttributes, forwardRef } from 'react';
3 |
4 | export interface InputCheckboxProps extends Omit, 'type'> {
5 | error?: string | null;
6 | }
7 |
8 | export const InputCheckbox = forwardRef(({ className, error, ...props }, ref) => (
9 |
20 | ));
21 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/forms/inputs/Select.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { type ReactNode, type SelectHTMLAttributes, forwardRef } from 'react';
3 |
4 | export interface SelectProps extends SelectHTMLAttributes {
5 | options?: { label: ReactNode; value: string; disabled?: boolean }[];
6 | error?: string | null;
7 | }
8 |
9 | export const Select = forwardRef(
10 | ({ options, children, className, error, ...props }, ref) => (
11 |
29 | ),
30 | );
31 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/forms/inputs/Textarea.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { type InputHTMLAttributes, forwardRef } from 'react';
3 |
4 | export interface TextareaProps extends InputHTMLAttributes {
5 | error?: string | null;
6 | }
7 |
8 | export const Textarea = forwardRef(({ className, error, ...props }, ref) => (
9 |
19 | ));
20 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/grid/Grid.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface GridProps extends HTMLAttributes {}
5 |
6 | export const Grid: FC = ({ className, ...props }) => (
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/grid/GridColumn.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface GridColumnProps extends HTMLAttributes {}
5 |
6 | export const GridColumn: FC = ({ className, ...props }) => (
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/grid/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Grid';
2 | export * from './GridColumn';
3 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/icons/CreditCardIcon.tsx:
--------------------------------------------------------------------------------
1 | import HeroCreditCardIcon from '@heroicons/react/24/solid/CreditCardIcon';
2 | import { type CreditCardBrand } from '@libs/types';
3 | import clsx from 'clsx';
4 | import { FC, HTMLAttributes } from 'react';
5 | import {
6 | AmericanExpressIcon,
7 | DinersClubIcon,
8 | DiscoverIcon,
9 | JCBIcon,
10 | MasterCardIcon,
11 | UnionPayIcon,
12 | VisaIcon,
13 | } from '../assets/icons';
14 |
15 | export interface CreditCardIconProps extends HTMLAttributes {
16 | brand: CreditCardBrand;
17 | }
18 |
19 | export const brandToIconMap = {
20 | amex: AmericanExpressIcon,
21 | diners: DinersClubIcon,
22 | discover: DiscoverIcon,
23 | jcb: JCBIcon,
24 | mastercard: MasterCardIcon,
25 | visa: VisaIcon,
26 | unionpay: UnionPayIcon,
27 | unknown: HeroCreditCardIcon,
28 | };
29 |
30 | export const CreditCardIcon: FC = ({ brand, className, ...props }) => {
31 | const Icon = brandToIconMap[brand as CreditCardBrand];
32 |
33 | return (
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/icons/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CreditCardIcon';
2 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/images/Image.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, ImgHTMLAttributes } from 'react';
3 | import { ImageBase } from './ImageBase';
4 |
5 | export interface ImageProps extends ImgHTMLAttributes {
6 | src?: string;
7 | sources?: {
8 | src: string;
9 | media?: string;
10 | }[];
11 | alt?: string;
12 | fallbackSrc?: string[];
13 | }
14 |
15 | export const Source = ({
16 | src,
17 | media,
18 | }: {
19 | src: string;
20 | media?: string;
21 | }) => {
22 | return ;
23 | };
24 |
25 | export const Image: FC = ({ src, sources, className, ...rest }) => {
26 | if (!src && !sources?.length) return null;
27 |
28 | const defaultSrc = src || (sources && sources[sources.length - 1].src);
29 |
30 | return (
31 |
32 | {sources?.map(({ src, media }) =>
33 | src && src !== defaultSrc ? : null,
34 | )}
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/images/ImageBase.tsx:
--------------------------------------------------------------------------------
1 | import { DOMAttributes, useCallback, useState } from 'react';
2 | import { brokenImgSrc } from './brokenImgSrc';
3 |
4 | interface ImageProps extends React.ImgHTMLAttributes {
5 | fallbackSrc?: string[];
6 | }
7 |
8 | export const ImageBase = ({ src, alt, className, fallbackSrc, ...props }: ImageProps) => {
9 | // Keep track of errors so we can try the next fallbackSrc.
10 | const [errorCount, setErrorCount] = useState(0);
11 | const [currentSrc, setCurrentSrc] = useState(src || brokenImgSrc);
12 |
13 | const handleBrokenImage: DOMAttributes['onError'] = useCallback(() => {
14 | // Allow the fallbackSrc to be an array of URLs to try in order.
15 | if (fallbackSrc && errorCount < fallbackSrc.length) {
16 | setCurrentSrc(fallbackSrc[errorCount]);
17 | setErrorCount(errorCount + 1);
18 | } else {
19 | setCurrentSrc(brokenImgSrc);
20 | }
21 | }, [fallbackSrc, errorCount, setCurrentSrc, setErrorCount]);
22 |
23 | return
;
24 | };
25 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/link/URLAwareNavLink.tsx:
--------------------------------------------------------------------------------
1 | import { FC, HTMLAttributes, PropsWithChildren } from 'react';
2 | import { NavLink } from 'react-router';
3 | import { NavLinkProps } from 'react-router';
4 |
5 | export interface AwareNavLinkProps {
6 | url: string;
7 | newTab?: boolean;
8 | prefetch?: NavLinkProps['prefetch'];
9 | preventScrollReset?: NavLinkProps['preventScrollReset'];
10 | className?: string | ((props: { isActive: boolean }) => string | undefined);
11 | }
12 |
13 | export const URLAwareNavLink: FC<
14 | PropsWithChildren, 'className'>>
15 | > = ({ url, newTab, prefetch = 'intent', preventScrollReset, className, children, ...rest }) => {
16 | const isExternal = url.startsWith('http') || url.startsWith('mailto') || url.startsWith('tel');
17 | const target = newTab ? '_blank' : '_self';
18 | const rel = newTab ? 'noopener noreferrer' : undefined;
19 |
20 | if (isExternal)
21 | return (
22 |
29 | {children}
30 |
31 | );
32 |
33 | return (
34 |
44 | {children}
45 |
46 | );
47 | };
48 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/link/index.ts:
--------------------------------------------------------------------------------
1 | export * from './URLAwareNavLink';
2 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/menu/Menu.tsx:
--------------------------------------------------------------------------------
1 | import { Menu as HeadlessMenu, type MenuProps as HeadlessMenuProps } from '@headlessui/react';
2 | import clsx from 'clsx';
3 | import type { ElementType, FC } from 'react';
4 |
5 | export type MenuProps = HeadlessMenuProps;
6 |
7 | export const Menu: FC = ({ className, ...props }) => (
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/menu/MenuButton.tsx:
--------------------------------------------------------------------------------
1 | import { Menu as HeadlessMenu, type MenuButtonProps as HeadlessMenuButtonProps } from '@headlessui/react';
2 | import type { ElementType, FC, PropsWithChildren } from 'react';
3 |
4 | export type MenuButtonProps = HeadlessMenuButtonProps;
5 |
6 | export const MenuButton: FC = (props) => ;
7 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/menu/MenuItem.tsx:
--------------------------------------------------------------------------------
1 | import { MenuItem as HeadlessMenuItem, type MenuItemProps as HeadlessMenuItemProps } from '@headlessui/react';
2 | import clsx from 'clsx';
3 | import type { ElementType, FC } from 'react';
4 |
5 | export interface MenuItemRenderProps {
6 | active: boolean;
7 | disabled: boolean;
8 | close: () => void;
9 | className?: string;
10 | }
11 |
12 | export type MenuItemProps = HeadlessMenuItemProps & {
13 | item: (menuItemProps: MenuItemRenderProps) => JSX.Element;
14 | };
15 |
16 | export const MenuItem: FC = ({ item, ...props }) => (
17 |
18 | {(menuItemProps: { active: boolean; disabled: boolean; close: () => void }) =>
19 | item({
20 | className: clsx('group flex gap-2 w-full items-center rounded-md p-2 text-sm', {
21 | 'bg-primary-50 text-primary-700': menuItemProps.active,
22 | 'text-gray-900': !menuItemProps.active,
23 | }),
24 | ...menuItemProps,
25 | })
26 | }
27 |
28 | );
29 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/menu/MenuItemIcon.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import type { FC, HTMLAttributes } from 'react';
3 |
4 | export interface MenuItemIconProps extends HTMLAttributes {
5 | icon: FC>;
6 | }
7 |
8 | export const MenuItemIcon: FC = ({ icon: Icon, className, ...props }) => (
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/menu/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Menu';
2 | export * from './MenuButton';
3 | export * from './MenuItem';
4 | export * from './MenuItemIcon';
5 | export * from './MenuItems';
6 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/modals/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ConfirmModal';
2 | export * from './Modal';
3 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/buttons/SubmitButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonProps } from '@app/components/common/buttons/Button';
2 | import { FC } from 'react';
3 | import { useRemixFormContext } from 'remix-hook-form';
4 |
5 | export interface SubmitButtonProps extends ButtonProps {}
6 |
7 | export const SubmitButton: FC = ({ children, ...props }) => {
8 | const { formState } = useRemixFormContext();
9 |
10 | return (
11 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/field-groups/ConfirmPasswordFieldGroup.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { FieldGroup, FieldGroupProps } from '../forms/fields/FieldGroup';
3 | import { StyledPassword } from '../forms/fields/StyledPassword';
4 |
5 | export interface ConfirmPasswordFieldGroupProps extends FieldGroupProps {}
6 |
7 | export const ConfirmPasswordFieldGroup: FC = (props) => (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/field-groups/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ConfirmPasswordFieldGroup';
2 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/forms/FormError.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 |
3 | import clsx from 'clsx';
4 | import { HTMLAttributes } from 'react';
5 | import { useRemixFormContext } from 'remix-hook-form';
6 | import { Alert } from '../../alert';
7 |
8 | export interface FormErrorProps extends HTMLAttributes {
9 | error?: string;
10 | onClearClick?: () => void;
11 | }
12 |
13 | export const FormError: FC = ({ className, error, onClearClick }) => {
14 | const { formState } = useRemixFormContext();
15 |
16 | if (!formState.errors?.root?.message && !error) return null;
17 |
18 | return (
19 |
20 | {error || formState.errors?.root?.message}
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/forms/fields/FieldError.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes, ReactElement } from 'react';
3 |
4 | export type FieldErrorComponent = (
5 | errorProps: Omit,
6 | ) => ReactElement | null;
7 |
8 | export interface FieldErrorProps extends HTMLAttributes {
9 | error?: string;
10 | errorComponent?: FieldErrorComponent;
11 | }
12 |
13 | export const FieldError: FC = ({ className, errorComponent, ...props }) => {
14 | if (errorComponent) return errorComponent(props);
15 |
16 | if (!props.error) return null;
17 |
18 | return (
19 |
20 | {props.error}
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/forms/fields/FieldGroup.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface FieldGroupProps extends HTMLAttributes {}
5 |
6 | export const FieldGroup: FC = ({ className, ...props }) => (
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/forms/fields/FieldLabel.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, LabelHTMLAttributes, ReactElement } from 'react';
3 |
4 | export type FieldLabelComponent = (
5 | labelProps: Omit,
6 | ) => ReactElement | null;
7 |
8 | export interface FieldLabelProps extends LabelHTMLAttributes {
9 | error?: string;
10 | optional?: boolean;
11 | labelComponent?: FieldLabelComponent;
12 | }
13 |
14 | export const FieldLabel: FC = ({ className, labelComponent, children, ...props }) => {
15 | if (!labelComponent && !children) return null;
16 |
17 | if (labelComponent) return labelComponent(props);
18 |
19 | return (
20 |
21 |
24 |
25 | {props.optional && Optional}
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/forms/fields/ProductOptionSelectorSelect.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, FC } from 'react';
2 | import { useRemixFormContext } from 'remix-hook-form';
3 |
4 | export interface ProductOptionSelectorProps {
5 | option: {
6 | id: string;
7 | title: string;
8 | product_id: string;
9 | values: { value: string; label: string; disabled?: boolean }[];
10 | };
11 | value: string;
12 | onChange?: (event: ChangeEvent) => void;
13 | }
14 |
15 | export const ProductOptionSelectorSelect: FC = ({ option, value, onChange }) => {
16 | const { register } = useRemixFormContext();
17 |
18 | const filteredValues = (option.values ?? []).filter(
19 | (optValue, index, self) => self.findIndex((item) => item.value === optValue.value) === index,
20 | );
21 |
22 | return (
23 |
24 |
27 |
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/forms/fields/StyledTextField.tsx:
--------------------------------------------------------------------------------
1 | import { TextField } from '@lambdacurry/forms/remix-hook-form';
2 | import clsx from 'clsx';
3 | import type { ComponentProps } from 'react';
4 |
5 | type StyledTextFieldProps = ComponentProps & {
6 | name: string;
7 | };
8 |
9 | export const StyledTextField = ({ className, name, ...props }: StyledTextFieldProps) => {
10 | return (
11 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/forms/inputs/Input.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { type InputHTMLAttributes, forwardRef } from 'react';
3 |
4 | export interface InputProps extends InputHTMLAttributes {
5 | step?: string;
6 | error?: string | null;
7 | }
8 |
9 | export const Input = forwardRef(({ className, error, ...props }, ref) => (
10 |
20 | ));
21 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/forms/inputs/InputCheckbox.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { type InputHTMLAttributes, forwardRef } from 'react';
3 |
4 | export interface InputCheckboxProps extends Omit, 'type'> {
5 | error?: string | null;
6 | }
7 |
8 | export const InputCheckbox = forwardRef(({ className, error, ...props }, ref) => (
9 |
20 | ));
21 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/forms/inputs/Select.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { type ReactNode, type SelectHTMLAttributes, forwardRef } from 'react';
3 |
4 | export interface SelectProps extends SelectHTMLAttributes {
5 | options?: { label: ReactNode; value: string; disabled?: boolean }[];
6 | error?: string | null;
7 | }
8 |
9 | export const Select = forwardRef(
10 | ({ options, children, className, error, ...props }, ref) => (
11 |
29 | ),
30 | );
31 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/remix-hook-form/forms/inputs/Textarea.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { type InputHTMLAttributes, forwardRef } from 'react';
3 |
4 | export interface TextareaProps extends InputHTMLAttributes {
5 | error?: string | null;
6 | }
7 |
8 | export const Textarea = forwardRef(({ className, error, ...props }, ref) => (
9 |
19 | ));
20 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/status-indicators/StatusIndicator.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import React from 'react';
3 |
4 | export type StatusIndicatorProps = {
5 | title?: string;
6 | variant: 'primary' | 'danger' | 'warning' | 'success' | 'active' | 'default';
7 | } & React.HTMLAttributes;
8 |
9 | export const StatusIndicator: React.FC = ({
10 | title,
11 | variant = 'success',
12 | className,
13 | ...props
14 | }) => {
15 | const dotClass = clsx({
16 | 'bg-teal-500': variant === 'success',
17 | 'bg-rose-400': variant === 'danger',
18 | 'bg-yellow-500': variant === 'warning',
19 | 'bg-violet-600': variant === 'primary',
20 | 'bg-emerald-400': variant === 'active',
21 | 'bg-gray-400': variant === 'default',
22 | });
23 | return (
24 |
25 |
26 | {title &&
{title}}
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/common/status-indicators/index.ts:
--------------------------------------------------------------------------------
1 | export * from './StatusIndicator';
2 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/images/StripeSecurityImage.tsx:
--------------------------------------------------------------------------------
1 | import StripeBadge from '@app/components/common/assets/StripeBadge.png';
2 |
3 | export interface StripeSecurityImageProps {
4 | className?: string;
5 | }
6 |
7 | export const StripeSecurityImage = ({ className }: StripeSecurityImageProps) => {
8 | return
;
9 | };
10 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/layout/Page.tsx:
--------------------------------------------------------------------------------
1 | import { CartDrawer } from '@app/components/cart/CartDrawer';
2 | import clsx from 'clsx';
3 | import type { FC, ReactNode } from 'react';
4 | import { Footer } from './footer/Footer';
5 | import { Header } from './header/Header';
6 | export interface PageProps {
7 | className?: string;
8 | children: ReactNode;
9 | }
10 |
11 | export const Page: FC = ({ className, children }) => {
12 | return (
13 |
14 |
15 |
16 |
17 | {children}
18 |
19 |
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/layout/footer/SocialIcons.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FacebookIcon,
3 | InstagramIcon,
4 | LinkedinIcon,
5 | PinterestIcon,
6 | SnapchatIcon,
7 | TiktokIcon,
8 | TwitterIcon,
9 | YoutubeIcon,
10 | } from '@app/components/common/assets/icons';
11 | import { IconButton } from '@app/components/common/buttons/IconButton';
12 | import { SiteSettings } from '@libs/types';
13 | import type { FC } from 'react';
14 |
15 | export const SocialIcons: FC<{ siteSettings?: SiteSettings }> = ({ siteSettings }) => {
16 | const socialLinks = [
17 | { icon: FacebookIcon, url: siteSettings?.social_facebook },
18 | { icon: InstagramIcon, url: siteSettings?.social_instagram },
19 | { icon: TwitterIcon, url: siteSettings?.social_twitter },
20 | { icon: YoutubeIcon, url: siteSettings?.social_youtube },
21 | { icon: LinkedinIcon, url: siteSettings?.social_linkedin },
22 | { icon: PinterestIcon, url: siteSettings?.social_pinterest },
23 | { icon: TiktokIcon, url: siteSettings?.social_tiktok },
24 | { icon: SnapchatIcon, url: siteSettings?.social_snapchat },
25 | ].filter((link) => !!link.url);
26 |
27 | if (socialLinks.length === 0) return null;
28 |
29 | return (
30 |
31 | {socialLinks.map(({ icon, url }) => (
32 |
}
35 | className="text-white hover:text-black"
36 | iconProps={{ fill: 'currentColor', width: '24' }}
37 | icon={icon}
38 | />
39 | ))}
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/EmptyProductListItem.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { HTMLAttributes } from 'react';
3 |
4 | export interface EmptyProductListItemProps extends HTMLAttributes {}
5 |
6 | export const EmptyProductListItem: React.FC = ({ className, ...props }) => (
7 |
8 |
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/ProductBadges.tsx:
--------------------------------------------------------------------------------
1 | import { SoldOutBadge } from '@app/components/badges/SoldOutBadge';
2 | import { useProductInventory } from '@app/hooks/useProductInventory';
3 | import { StoreProduct } from '@medusajs/types';
4 | import { FC, HTMLAttributes } from 'react';
5 |
6 | interface ProductBadgesProps extends HTMLAttributes {
7 | product: StoreProduct;
8 | className?: string;
9 | }
10 |
11 | export const ProductBadges: FC = ({ product, className }) => {
12 | const productInventory = useProductInventory(product);
13 | const isSoldOut = productInventory.averageInventory === 0;
14 |
15 | return {isSoldOut && }
;
16 | };
17 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/ProductCarouselSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { EmptyProductListItem } from './EmptyProductListItem';
2 |
3 | export const ProductCarouselSkeleton = ({ length }: { length: number }) => (
4 |
5 | {Array.from({ length }, (_, i) => (
6 |
7 | ))}
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/ProductCategoryTabs.tsx:
--------------------------------------------------------------------------------
1 | import { TabButton } from '@app/components/tabs/TabButton';
2 | import { TabList } from '@app/components/tabs/TabList';
3 | import { Tab } from '@headlessui/react';
4 | import { StoreProductCategory } from '@medusajs/types';
5 | import { type FC, Fragment } from 'react';
6 |
7 | export interface ProductCategoryTabsProps {
8 | categories: StoreProductCategory[];
9 | selectedIndex?: number;
10 | onChange?: (index: number) => void;
11 | }
12 |
13 | export const ProductCategoryTabs: FC = ({ categories, ...props }) => {
14 | if (!categories?.length) return null;
15 |
16 | return (
17 |
18 |
19 | {({ selected }) => All}
20 |
21 | {categories.map((category) => (
22 |
23 | {({ selected }) => {category.name}}
24 |
25 | ))}
26 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/ProductCollectionTabs.tsx:
--------------------------------------------------------------------------------
1 | import { TabButton } from '@app/components/tabs/TabButton';
2 | import { TabList } from '@app/components/tabs/TabList';
3 | import { Tab } from '@headlessui/react';
4 | import { StoreCollection } from '@medusajs/types';
5 | import { type FC, Fragment } from 'react';
6 |
7 | export interface ProductCollectionTabsProps {
8 | collections: StoreCollection[];
9 | selectedIndex?: number;
10 | onChange?: (index: number) => void;
11 | }
12 |
13 | export const ProductCollectionTabs: FC = ({ collections, ...props }) => {
14 | if (!collections?.length) return null;
15 |
16 | return (
17 |
18 |
19 | {({ selected }) => All}
20 |
21 | {collections.map((collection) => (
22 |
23 | {({ selected }) => {collection.title}}
24 |
25 | ))}
26 |
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/ProductGrid.tsx:
--------------------------------------------------------------------------------
1 | import { StoreProduct } from '@medusajs/types';
2 | import clsx from 'clsx';
3 | import type { FC } from 'react';
4 | import { NavLink, useNavigation } from 'react-router';
5 | import { ProductGridSkeleton } from './ProductGridSkeleton';
6 | import { ProductListHeader, type ProductListHeaderProps } from './ProductListHeader';
7 | import { ProductListItem } from './ProductListItem';
8 |
9 | export interface ProductListProps extends Partial {
10 | products?: StoreProduct[];
11 | className?: string;
12 | }
13 |
14 | export const ProductGrid: FC = ({
15 | heading,
16 | actions,
17 | products,
18 | className = 'grid grid-cols-1 gap-y-6 @md:grid-cols-2 gap-x-4 @2xl:!grid-cols-3 @4xl:!grid-cols-4 @4xl:gap-x-4',
19 | }) => {
20 | const navigation = useNavigation();
21 | const isLoading = navigation.state !== 'idle';
22 |
23 | if (!products) return ;
24 |
25 | return (
26 |
31 |
32 |
33 |
34 | {products?.map((product) => (
35 |
36 | {({ isTransitioning }) => }
37 |
38 | ))}
39 |
40 |
41 | );
42 | };
43 |
44 | // required for lazy loading this component
45 | export default ProductGrid;
46 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/ProductGridSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { EmptyProductListItem } from './EmptyProductListItem';
2 |
3 | export const ProductGridSkeleton = ({ length }: { length: number }) => (
4 |
5 | {Array.from({ length }, (_, i: number) => (
6 |
7 | ))}
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/ProductListItem.tsx:
--------------------------------------------------------------------------------
1 | import { useRegion } from '@app/hooks/useRegion';
2 | import { StoreProduct } from '@medusajs/types';
3 | import clsx from 'clsx';
4 | import type { FC, HTMLAttributes } from 'react';
5 | import { ProductBadges } from './ProductBadges';
6 | import { ProductPrice } from './ProductPrice';
7 | import { ProductThumbnail } from './ProductThumbnail';
8 |
9 | export interface ProductListItemProps extends HTMLAttributes {
10 | product: StoreProduct;
11 | isTransitioning?: boolean;
12 | }
13 |
14 | export const ProductListItem: FC = ({ product, className, isTransitioning, ...props }) => {
15 | const { region } = useRegion();
16 |
17 | return (
18 |
19 |
23 |
24 | {product.title}
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/ProductListWithPagination.tsx:
--------------------------------------------------------------------------------
1 | import type { PaginationConfig } from '@app/components/common/Pagination';
2 | import { PaginationWithContext } from '@app/components/common/Pagination/pagination-with-context';
3 | import { ProductGrid, type ProductListProps } from '@app/components/product/ProductGrid';
4 | import { StoreProduct } from '@medusajs/types';
5 | import type { FC } from 'react';
6 |
7 | export interface ProductListWithPaginationProps extends ProductListProps {
8 | products?: StoreProduct[];
9 | paginationConfig?: PaginationConfig;
10 | context: string;
11 | }
12 |
13 | export const ProductListWithPagination: FC = ({
14 | context,
15 | paginationConfig,
16 | ...props
17 | }) => (
18 |
19 |
20 | {paginationConfig &&
}
21 |
22 | );
23 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/ProductPrice.tsx:
--------------------------------------------------------------------------------
1 | import { type FC, useMemo } from 'react';
2 |
3 | import { getCheapestProductVariant } from '@libs/util/prices';
4 | import { StoreProduct, StoreProductVariant } from '@medusajs/types';
5 | import { ProductVariantPrice } from './ProductVariantPrice';
6 |
7 | export interface ProductPriceProps {
8 | product: StoreProduct;
9 | variant?: StoreProductVariant;
10 | currencyCode: string;
11 | }
12 |
13 | export const ProductPrice: FC = ({ product, currencyCode, ...props }) => {
14 | const variant = useMemo(
15 | () => props.variant || getCheapestProductVariant(product),
16 | [props.variant, product, currencyCode],
17 | );
18 |
19 | if (!variant) return null;
20 |
21 | return ;
22 | };
23 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/ProductPriceRange.tsx:
--------------------------------------------------------------------------------
1 | import { formatPrice, getVariantFinalPrice, sortProductVariantsByPrice } from '@libs/util/prices';
2 | import { StoreProduct } from '@medusajs/types';
3 | import { type FC, useMemo } from 'react';
4 | import { ProductVariantPrice } from './ProductVariantPrice';
5 |
6 | export interface ProductPriceRangeProps {
7 | product: StoreProduct;
8 | currencyCode: string;
9 | }
10 |
11 | export const ProductPriceRange: FC = ({ product, currencyCode }) => {
12 | const sortedVariants = useMemo(() => sortProductVariantsByPrice(product), [product, currencyCode]);
13 |
14 | const minVariant = sortedVariants[0];
15 | const maxVariant = sortedVariants[sortedVariants.length - 1];
16 |
17 | const minPrice = useMemo(() => getVariantFinalPrice(minVariant), [sortedVariants]);
18 | const maxPrice = useMemo(() => getVariantFinalPrice(maxVariant), [sortedVariants]);
19 |
20 | const hasPriceRange = minPrice !== maxPrice;
21 |
22 | return (
23 | <>
24 | {hasPriceRange ? (
25 | <>
26 | {formatPrice(minPrice, { currency: currencyCode })}
27 | {maxPrice && maxPrice > minPrice ? <>–{formatPrice(maxPrice, { currency: currencyCode })}> : ''}
28 | >
29 | ) : (
30 |
31 | )}
32 | >
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/ProductTagsMenu.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@app/components/common/buttons/Button';
2 | import { Menu } from '@app/components/common/menu/Menu';
3 | import { MenuButton } from '@app/components/common/menu/MenuButton';
4 | import { MenuItem } from '@app/components/common/menu/MenuItem';
5 | import { MenuItems } from '@app/components/common/menu/MenuItems';
6 | import ChevronDownIcon from '@heroicons/react/24/solid/ChevronDownIcon';
7 | import { StoreProductTag } from '@medusajs/types';
8 | import clsx from 'clsx';
9 | import type { FC } from 'react';
10 | import { NavLink } from 'react-router';
11 |
12 | export interface ProductTagsMenuProps {
13 | tags?: StoreProductTag[];
14 | }
15 |
16 | export const ProductTagsMenu: FC = ({ tags }) => {
17 | if (!tags?.length) return null;
18 |
19 | return (
20 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/product/ProductVariantPrice.tsx:
--------------------------------------------------------------------------------
1 | import { formatPrice, getVariantPrices } from '@libs/util/prices';
2 | import { StoreProductVariant } from '@medusajs/types';
3 | import isNumber from 'lodash/isNumber';
4 | import { type FC } from 'react';
5 |
6 | export interface ProductVariantPriceProps {
7 | variant: StoreProductVariant;
8 | currencyCode: string;
9 | }
10 |
11 | export const ProductVariantPrice: FC = ({ variant, currencyCode }) => {
12 | const { original, calculated } = getVariantPrices(variant);
13 |
14 | const hasSale = isNumber(calculated) && calculated < (original ?? 0);
15 |
16 | return (
17 | <>
18 | {hasSale ? (
19 |
20 | {formatPrice(calculated, { currency: currencyCode })}
21 | {formatPrice(original || 0, { currency: currencyCode })}
22 |
23 | ) : (
24 | formatPrice(original || 0, { currency: currencyCode })
25 | )}
26 | >
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/reviews/ProductReviewSection.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from 'react';
2 | import { useRouteLoaderData } from 'react-router';
3 | import { ProductPageLoaderData } from '../../routes/products.$productHandle';
4 | import { ProductReviewListWithPagination } from './ReviewListWithPagination';
5 | import ProductReviewSummary from './ReviewSummary';
6 |
7 | export const ProductReviewSection: FC = () => {
8 | const data = useRouteLoaderData('routes/products.$productHandle');
9 |
10 | if (!data) return null;
11 |
12 | const { product, productReviews, productReviewStats } = data;
13 |
14 | if (!productReviews.count || productReviewStats.count < 1) return null;
15 |
16 | return (
17 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/reviews/ProductReviewStars.tsx:
--------------------------------------------------------------------------------
1 | import { StoreProductReviewStats } from '@lambdacurry/medusa-plugins-sdk';
2 | import { FC } from 'react';
3 | import { Link } from 'react-router';
4 | import { StarRating } from './StarRating';
5 |
6 | interface ProductReviewStarsProps {
7 | reviewsCount?: number;
8 | reviewStats?: StoreProductReviewStats;
9 | }
10 |
11 | export const ProductReviewStars: FC = ({ reviewsCount, reviewStats }) => {
12 | if (!reviewsCount || reviewsCount < 1) return null;
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 | {' '}
21 | {reviewsCount} Review{reviewsCount > 1 ? 's' : ''}
22 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/reviews/ReviewListWithPagination.tsx:
--------------------------------------------------------------------------------
1 | import { StoreProductReview } from '@lambdacurry/medusa-plugins-sdk';
2 | import { FC } from 'react';
3 | import { PaginationConfig } from '../common/Pagination';
4 | import { PaginationWithContext } from '../common/Pagination/pagination-with-context';
5 | import { ProductReviewList, ProductReviewListProps } from './ProductReviewList';
6 |
7 | export interface ProductReviewListWithPaginationProps extends ProductReviewListProps {
8 | productReviews: StoreProductReview[];
9 | paginationConfig?: PaginationConfig;
10 | context: string;
11 | }
12 |
13 | export const ProductReviewListWithPagination: FC = ({
14 | context,
15 | paginationConfig,
16 | className,
17 | ...props
18 | }) => (
19 | <>
20 |
21 |
22 | {paginationConfig && (
23 |
24 | )}
25 |
26 | >
27 | );
28 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/reviews/StarRating.tsx:
--------------------------------------------------------------------------------
1 | import { Rating, RatingProps, RoundedStar } from '@smastrom/react-rating';
2 | import '@smastrom/react-rating/style.css';
3 | import { FC } from 'react';
4 |
5 | export const StarRating: FC<
6 | Omit & {
7 | value?: number | undefined;
8 | includeLink?: boolean;
9 | className?: string;
10 | }
11 | > = ({ value, ...props }) => {
12 | if (typeof value !== 'number') return null;
13 |
14 | return (
15 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/sections/PageHeading.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface PageHeadingProps extends HTMLAttributes {}
5 |
6 | export const PageHeading: FC = ({ className, ...props }) => (
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/sections/SectionHeading.tsx:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { FC, HTMLAttributes } from 'react';
3 |
4 | export interface SectionHeadingProps extends HTMLAttributes {}
5 |
6 | export const SectionHeading: FC = ({ className, ...props }) => (
7 |
8 | );
9 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/sections/SideBySide.tsx:
--------------------------------------------------------------------------------
1 | import { Container } from '@app/components/common/container';
2 | import clsx from 'clsx';
3 |
4 | interface SideBySideSectionProps {
5 | className?: string;
6 | title?: string;
7 | left?: React.ReactNode;
8 | right?: React.ReactNode;
9 | }
10 |
11 | export const SideBySide = ({
12 | title,
13 |
14 | className,
15 | left,
16 | right,
17 | }: SideBySideSectionProps) => {
18 | return (
19 |
20 | {title && {title}
}
21 | {(left || right) && (
22 |
23 |
{left}
24 |
{right}
25 |
26 | )}
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/share/Share.tsx:
--------------------------------------------------------------------------------
1 | import { type ButtonBaseProps, IconButton } from '@app/components/common/buttons';
2 | import ArrowUpOnSquareIcon from '@heroicons/react/24/outline/ArrowUpOnSquareIcon';
3 | import { type FC, useState } from 'react';
4 | import { ShareItemType } from './Share.types';
5 | import { ShareButton } from './ShareButton';
6 | import { ShareModal } from './ShareModal';
7 |
8 | export interface ShareProps {
9 | itemType?: ShareItemType;
10 | shareData: ShareData;
11 | onSuccess?: () => void;
12 | onError?: (error?: unknown) => void;
13 | onInteraction?: () => void;
14 | disabled?: boolean;
15 | ButtonComponent?: FC;
16 | }
17 |
18 | export const Share: FC = ({
19 | itemType = 'page',
20 | shareData,
21 | onInteraction,
22 | onSuccess,
23 | onError,
24 | disabled,
25 | ButtonComponent = (buttonProps) => ,
26 | }) => {
27 | const [isModalOpen, setIsModalOpen] = useState(false);
28 |
29 | const handleNonNativeShare = () => setIsModalOpen(true);
30 |
31 | return (
32 | <>
33 |
42 | setIsModalOpen(false)}
47 | />
48 | >
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/share/Share.types.tsx:
--------------------------------------------------------------------------------
1 | export type ShareItemType = 'product' | 'page';
2 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/share/ShareButton.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonBase, ButtonBaseProps } from '@app/components/common/buttons/ButtonBase';
2 | import { type FC } from 'react';
3 |
4 | export interface ShareButtonProps {
5 | shareData: ShareData;
6 | onSuccess?: () => void;
7 | onError?: (error?: unknown) => void;
8 | onNonNativeShare?: () => void;
9 | onInteraction?: () => void;
10 | disabled?: boolean;
11 | ButtonComponent?: FC;
12 | }
13 |
14 | export const ShareButton: FC = ({
15 | shareData,
16 | onInteraction,
17 | onSuccess,
18 | onError,
19 | onNonNativeShare,
20 | disabled,
21 | ButtonComponent = ButtonBase,
22 | }) => {
23 | const handleClick = async () => {
24 | onInteraction?.();
25 |
26 | if (navigator.share) {
27 | shareData.url = shareData.url || `${window.location.origin}${window.location.pathname}`;
28 | try {
29 | await navigator.share(shareData);
30 | onSuccess && onSuccess();
31 | } catch (err) {
32 | onError && onError(err);
33 | }
34 | } else {
35 | onNonNativeShare?.();
36 | }
37 | };
38 |
39 | return ;
40 | };
41 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/share/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './Share.types';
2 | export * from './Share';
3 | export * from './ShareButton';
4 | export * from './ShareModal';
5 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/tabs/TabButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonProps, ButtonRef } from '@app/components/common/buttons';
2 | import clsx from 'clsx';
3 | import { forwardRef } from 'react';
4 |
5 | export interface TabButtonProps extends ButtonProps {
6 | selected?: boolean;
7 | }
8 |
9 | export const TabButton = forwardRef(({ selected, className, ...props }, ref) => (
10 |
24 | ));
25 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/tabs/TabList.tsx:
--------------------------------------------------------------------------------
1 | import { TabList as HeadlessTabList } from '@headlessui/react';
2 | import clsx from 'clsx';
3 | import { FC, PropsWithChildren } from 'react';
4 |
5 | export interface TabListProps {
6 | className?: string;
7 | }
8 |
9 | export const TabList: FC> = ({ className, ...props }) => (
10 |
17 | );
18 |
--------------------------------------------------------------------------------
/apps/storefront/app/components/tabs/index.ts:
--------------------------------------------------------------------------------
1 | export * from './TabButton';
2 | export * from './TabList';
3 |
--------------------------------------------------------------------------------
/apps/storefront/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | // https://github.com/remix-run/remix/issues/2947
2 |
3 | import * as Sentry from '@sentry/remix';
4 | import { StrictMode, startTransition } from 'react';
5 | import { hydrateRoot } from 'react-dom/client';
6 | import { HydratedRouter } from 'react-router/dom';
7 |
8 | declare global {
9 | interface Window {
10 | ENV: any;
11 | }
12 | }
13 |
14 | if (window?.ENV?.SENTRY_DSN)
15 | Sentry.init({
16 | dsn: window?.ENV?.SENTRY_DSN,
17 | environment: window?.ENV?.SENTRY_ENVIRONMENT,
18 | integrations: [],
19 | });
20 |
21 | const hydrate = () =>
22 | startTransition(() => {
23 | hydrateRoot(
24 | document,
25 |
26 |
27 | ,
28 | );
29 | });
30 |
31 | if (window.requestIdleCallback) {
32 | window.requestIdleCallback(hydrate);
33 | } else {
34 | // Safari doesn't support requestIdleCallback
35 | // https://caniuse.com/requestidlecallback
36 | window.setTimeout(hydrate, 1);
37 | }
38 |
--------------------------------------------------------------------------------
/apps/storefront/app/hooks/useCheckout.tsx:
--------------------------------------------------------------------------------
1 | import { CheckoutContext, CheckoutContextValue, CheckoutStep, useNextStep } from '@app/providers/checkout-provider';
2 | import { FetcherCartKeyPrefix } from '@libs/util/fetcher-keys';
3 | import { useContext } from 'react';
4 | import { useFetchers } from 'react-router';
5 |
6 | const actions = ({ dispatch }: CheckoutContextValue) => ({
7 | setStep: (step: CheckoutStep) => dispatch({ name: 'setStep', payload: step }),
8 | });
9 |
10 | export const useCheckout = () => {
11 | const context = useContext(CheckoutContext);
12 | const nextStep = useNextStep(context.state);
13 | const { state } = context;
14 | const fetchers = useFetchers();
15 | const cartMutationFetchers = fetchers.filter((f) => f.key.startsWith(FetcherCartKeyPrefix));
16 |
17 | if (!state.step) throw new Error('useCheckout must be used within a CheckoutProvider');
18 |
19 | return {
20 | ...state,
21 | ...actions(context),
22 | goToNextStep: () => context.dispatch({ name: 'setStep', payload: nextStep }),
23 | isCartMutating: cartMutationFetchers.some((f) => ['loading', 'submitting'].includes(f.state)),
24 | };
25 | };
26 |
--------------------------------------------------------------------------------
/apps/storefront/app/hooks/useCustomer.tsx:
--------------------------------------------------------------------------------
1 | import { useRootLoaderData } from './useRootLoaderData';
2 |
3 | export const useCustomer = () => {
4 | const rootData = useRootLoaderData();
5 |
6 | return { customer: rootData?.customer };
7 | };
8 |
--------------------------------------------------------------------------------
/apps/storefront/app/hooks/useEnv.ts:
--------------------------------------------------------------------------------
1 | import { useRootLoaderData } from './useRootLoaderData';
2 |
3 | export const useEnv = () => {
4 | const data = useRootLoaderData();
5 | if (!data?.env) throw new Error('No env data found, this should be provided in the root loader');
6 | return { env: data?.env };
7 | };
8 |
--------------------------------------------------------------------------------
/apps/storefront/app/hooks/useKeypress.ts:
--------------------------------------------------------------------------------
1 | // adapted from: https://usehooks.com/useKeyPress/
2 |
3 | import { useEffect, useState } from 'react';
4 |
5 | export const useKeyPress = (targetKey: string | string[], callBack?: () => void) => {
6 | // State for keeping track of whether key is pressed
7 | const [keyPressed, setKeyPressed] = useState(false);
8 |
9 | useEffect(() => {
10 | if (keyPressed === true && typeof callBack === 'function') callBack();
11 | }, [keyPressed, callBack]);
12 |
13 | // Add event listeners
14 | useEffect(() => {
15 | // If pressed key is our target key then set to true
16 | const downHandler = ({ key }: KeyboardEvent) => {
17 | if (Array.isArray(targetKey)) {
18 | if (targetKey.includes(key)) setKeyPressed(true);
19 | } else if (key === targetKey) setKeyPressed(true);
20 | };
21 | // If released key is our target key then set to false
22 | const upHandler = ({ key }: KeyboardEvent) => {
23 | if (Array.isArray(targetKey)) {
24 | if (targetKey.includes(key)) setKeyPressed(false);
25 | } else if (key === targetKey) setKeyPressed(false);
26 | };
27 |
28 | window.addEventListener('keydown', downHandler);
29 | window.addEventListener('keyup', upHandler);
30 | // Remove event listeners on cleanup
31 | return () => {
32 | window.removeEventListener('keydown', downHandler);
33 | window.removeEventListener('keyup', upHandler);
34 | };
35 | }, [targetKey]); // Empty array ensures that effect is only run on mount and unmount
36 |
37 | return keyPressed;
38 | };
39 |
--------------------------------------------------------------------------------
/apps/storefront/app/hooks/useLogin.ts:
--------------------------------------------------------------------------------
1 | import { useStorefront } from './useStorefront';
2 |
3 | export const useLogin = () => {
4 | const { state, actions } = useStorefront();
5 |
6 | if (!state.login) throw new Error('useLogin must be used within the StorefrontContext.Provider');
7 |
8 | return { login: state.login, toggleLoginModal: actions.toggleLoginModal };
9 | };
10 |
--------------------------------------------------------------------------------
/apps/storefront/app/hooks/useProductInventory.ts:
--------------------------------------------------------------------------------
1 | import { StoreProduct } from '@medusajs/types';
2 | import { useMemo } from 'react';
3 |
4 | export const useProductInventory = (product: StoreProduct) => {
5 | return useMemo(() => {
6 | const totalInventory =
7 | product.variants?.reduce((total, variant) => {
8 | if (variant.allow_backorder || !variant.manage_inventory) return Infinity;
9 | return total + (variant.inventory_quantity || 0);
10 | }, 0) ?? 0;
11 | const averageInventory = totalInventory / (product?.variants?.length ?? 1);
12 |
13 | return { averageInventory, totalInventory };
14 | }, [product]);
15 | };
16 |
--------------------------------------------------------------------------------
/apps/storefront/app/hooks/useRegion.ts:
--------------------------------------------------------------------------------
1 | import { useRootLoaderData } from './useRootLoaderData';
2 |
3 | export const useRegion = () => {
4 | const data = useRootLoaderData();
5 | if (!data?.region) throw new Error('No region data found, this should be provided in the root loader');
6 | return { region: data?.region };
7 | };
8 |
--------------------------------------------------------------------------------
/apps/storefront/app/hooks/useRegions.ts:
--------------------------------------------------------------------------------
1 | import { useRootLoaderData } from './useRootLoaderData';
2 |
3 | export const useRegions = () => {
4 | const data = useRootLoaderData();
5 | return { regions: data?.regions };
6 | };
7 |
--------------------------------------------------------------------------------
/apps/storefront/app/hooks/useRemoveCartItem.ts:
--------------------------------------------------------------------------------
1 | import { FetcherKeys } from '@libs/util/fetcher-keys';
2 | import { StoreCart, StoreCartLineItem } from '@medusajs/types';
3 | import { useFetcher } from 'react-router';
4 |
5 | export const useRemoveCartItem = () => {
6 | const fetcher = useFetcher<{ cart: StoreCart }>({ key: FetcherKeys.cart.removeLineItem });
7 |
8 | const submit = ({ id: lineItemId }: StoreCartLineItem) => {
9 | fetcher.submit({ lineItemId }, { method: 'delete', action: '/api/cart/line-items/delete' });
10 | };
11 |
12 | return { fetcher, state: fetcher.state, submit };
13 | };
14 |
--------------------------------------------------------------------------------
/apps/storefront/app/hooks/useRootLoaderData.tsx:
--------------------------------------------------------------------------------
1 | import { RootLoaderResponse } from '@libs/util/server/root.server';
2 | import { UIMatch, useMatches } from 'react-router';
3 |
4 | export const useRootLoaderData = () => {
5 | const matches = useMatches();
6 | const rootMatch = matches[0] as UIMatch;
7 |
8 | return rootMatch.data;
9 | };
10 |
--------------------------------------------------------------------------------
/apps/storefront/app/hooks/useSiteDetails.ts:
--------------------------------------------------------------------------------
1 | import { useRootLoaderData } from './useRootLoaderData';
2 |
3 | export const useSiteDetails = () => {
4 | const data = useRootLoaderData();
5 |
6 | return data.siteDetails || {};
7 | };
8 |
--------------------------------------------------------------------------------
/apps/storefront/app/hooks/useStorefront.ts:
--------------------------------------------------------------------------------
1 | import {
2 | StorefrontAction,
3 | StorefrontActionNames,
4 | StorefrontContext,
5 | StorefrontContextValue,
6 | TogglePayload,
7 | ToggleableTargets,
8 | } from '@app/providers/storefront-provider';
9 | import { useContext } from 'react';
10 |
11 | const toggleActionDispatch: (
12 | target: ToggleableTargets,
13 | payload?: TogglePayload | boolean,
14 | ) => StorefrontAction = (target, payload?) => {
15 | return typeof payload === 'boolean'
16 | ? { name: StorefrontActionNames.toggle, payload: { open: payload, target } }
17 | : { name: StorefrontActionNames.toggle, payload: { ...payload, target } };
18 | };
19 |
20 | const actions = ({ dispatch }: StorefrontContextValue) => ({
21 | toggleCartDrawer: (payload?: TogglePayload | boolean) =>
22 | dispatch(toggleActionDispatch(ToggleableTargets.cart, payload)),
23 | toggleLoginModal: (payload?: TogglePayload | boolean) =>
24 | dispatch(toggleActionDispatch(ToggleableTargets.login, payload)),
25 | toggleSearchDrawer: (payload?: boolean) => dispatch(toggleActionDispatch(ToggleableTargets.search, payload)),
26 | });
27 |
28 | export const useStorefront = () => {
29 | const context = useContext(StorefrontContext);
30 |
31 | if (!context) throw new Error('useStorefront must be used within a StorefrontContext.Provider');
32 | return { ...context, actions: actions(context) };
33 | };
34 |
--------------------------------------------------------------------------------
/apps/storefront/app/providers/root-providers.tsx:
--------------------------------------------------------------------------------
1 | import { StorefrontProvider, storefrontInitialState } from '@app/providers/storefront-provider';
2 | import { FC, PropsWithChildren } from 'react';
3 |
4 | export const RootProviders: FC = ({ children }) => (
5 | {children}
6 | );
7 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes.ts:
--------------------------------------------------------------------------------
1 | import { flatRoutes } from '@react-router/fs-routes';
2 |
3 | export default flatRoutes();
4 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/$.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from 'react-router';
2 |
3 | export const loader = async () => {
4 | return redirect('/');
5 | };
6 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/[favicon.ico].tsx:
--------------------------------------------------------------------------------
1 | import { siteSettings } from '@libs/config/site/site-settings';
2 | import { redirect } from 'react-router';
3 |
4 | export const loader = async () => {
5 | return redirect(siteSettings.favicon, { status: 302 });
6 | };
7 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/[robots.txt].tsx:
--------------------------------------------------------------------------------
1 | import type { LoaderFunctionArgs } from 'react-router';
2 |
3 | export const loader = ({ request }: LoaderFunctionArgs) => {
4 | const host = request.headers.get('host');
5 | const baseUrl = `https://${host}`;
6 | const robotText = `
7 | User-agent: *
8 | Disallow: /orders
9 | Disallow: /orders/
10 | Disallow: /checkout
11 | Disallow: /checkout/
12 | Disallow: /preview/
13 | Disallow: /api/
14 | Disallow: /_auth/
15 | Sitemap: ${baseUrl}/sitemap.xml
16 | `;
17 | return new Response(robotText, {
18 | status: 200,
19 | headers: {
20 | 'Content-Type': 'text/plain',
21 | },
22 | });
23 | };
24 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/[sitemap-collections.xml].tsx:
--------------------------------------------------------------------------------
1 | import { sdk } from '@libs/util/server/client.server';
2 | import { SitemapUrl, buildSitemapUrlSetXML } from '@libs/util/xml/sitemap-builder';
3 | import { LoaderFunctionArgs } from 'react-router';
4 |
5 | export const loader = async ({ request }: LoaderFunctionArgs) => {
6 | const { collections } = await sdk.store.collection.list({
7 | limit: 1000,
8 | });
9 |
10 | const host = request.headers.get('host');
11 | const baseUrl = `https://${host}`;
12 |
13 | const collectionUrls: SitemapUrl[] = collections.map(({ handle, updated_at }) => ({
14 | loc: `${baseUrl}/collections/${handle}`,
15 | lastmod: updated_at.toString(),
16 | priority: 0.8,
17 | changefreq: 'weekly',
18 | }));
19 |
20 | const content = buildSitemapUrlSetXML(collectionUrls);
21 |
22 | return new Response(content, {
23 | status: 200,
24 | headers: {
25 | 'Content-Type': 'application/xml',
26 | 'xml-version': '1.0',
27 | encoding: 'UTF-8',
28 | },
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/[sitemap-pages.xml].tsx:
--------------------------------------------------------------------------------
1 | import { SitemapUrl, buildSitemapUrlSetXML } from '@libs/util/xml/sitemap-builder';
2 | import { LoaderFunctionArgs } from 'react-router';
3 |
4 | const pages = ['/', '/products'];
5 |
6 | export const loader = async ({ request }: LoaderFunctionArgs) => {
7 | const host = request.headers.get('host');
8 | const baseUrl = `https://${host}`;
9 | const urls: SitemapUrl[] = pages.map((handle) => ({
10 | loc: `${baseUrl}/${handle}`,
11 | priority: 0.8,
12 | changefreq: 'daily',
13 | }));
14 |
15 | const content = buildSitemapUrlSetXML(urls);
16 |
17 | return new Response(content, {
18 | status: 200,
19 | headers: {
20 | 'Content-Type': 'application/xml',
21 | 'xml-version': '1.0',
22 | encoding: 'UTF-8',
23 | },
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/[sitemap-products.xml].tsx:
--------------------------------------------------------------------------------
1 | import { sdk } from '@libs/util/server/client.server';
2 | import { SitemapUrl, buildSitemapUrlSetXML } from '@libs/util/xml/sitemap-builder';
3 | import { LoaderFunctionArgs } from 'react-router';
4 |
5 | export const loader = async ({ request }: LoaderFunctionArgs) => {
6 | const { products } = await sdk.store.product.list({
7 | limit: 1000,
8 | });
9 |
10 | const host = request.headers.get('host');
11 | const baseUrl = `https://${host}`;
12 |
13 | const urls: SitemapUrl[] = products.map(({ handle, updated_at }) => ({
14 | loc: `${baseUrl}/products/${handle}`,
15 | lastmod: updated_at?.toString(),
16 | priority: 0.8,
17 | changefreq: 'daily',
18 | }));
19 |
20 | const content = buildSitemapUrlSetXML(urls);
21 |
22 | return new Response(content, {
23 | status: 200,
24 | headers: {
25 | 'Content-Type': 'application/xml',
26 | 'xml-version': '1.0',
27 | encoding: 'UTF-8',
28 | },
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/[sitemap.xml].tsx:
--------------------------------------------------------------------------------
1 | import { SitemapUrl } from '@libs/util/xml/sitemap-builder';
2 | import { LoaderFunctionArgs } from 'react-router';
3 |
4 | export const loader = async ({ request }: LoaderFunctionArgs) => {
5 | const host = request.headers.get('host');
6 | const baseUrl = `https://${host}`;
7 |
8 | const urls: SitemapUrl[] = [
9 | { loc: `${baseUrl}/sitemap-products.xml` },
10 | { loc: `${baseUrl}/sitemap-collections.xml` },
11 | { loc: `${baseUrl}/sitemap-pages.xml` },
12 | ];
13 |
14 | const content = `
15 |
16 | ${urls.map(({ loc }) => `${loc}`).join('\n')}
17 | `;
18 |
19 | return new Response(content, {
20 | status: 200,
21 | headers: {
22 | 'Content-Type': 'application/xml',
23 | 'xml-version': '1.0',
24 | encoding: 'UTF-8',
25 | },
26 | });
27 | };
28 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/api.cart.line-items.delete.ts:
--------------------------------------------------------------------------------
1 | import { zodResolver } from '@hookform/resolvers/zod';
2 | import { deleteLineItem, retrieveCart } from '@libs/util/server/data/cart.server';
3 | import { StoreCart } from '@medusajs/types';
4 | import { type ActionFunctionArgs, data } from 'react-router';
5 | import { getValidatedFormData } from 'remix-hook-form';
6 | import { z } from 'zod';
7 |
8 | const deleteLineItemSchema = z.object({
9 | lineItemId: z.string().min(1, 'Line item ID is required'),
10 | });
11 |
12 | type DeleteLineItemFormData = z.infer;
13 |
14 | export async function action({ request }: ActionFunctionArgs) {
15 | const { errors, data: validatedFormData } = await getValidatedFormData(
16 | request,
17 | zodResolver(deleteLineItemSchema),
18 | );
19 |
20 | if (errors) {
21 | return data({ errors }, { status: 400 });
22 | }
23 |
24 | const { lineItemId } = validatedFormData;
25 |
26 | await deleteLineItem(request, lineItemId);
27 | const cart = (await retrieveCart(request)) as StoreCart;
28 |
29 | return data({ cart });
30 | }
31 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/api.cart.line-items.update.ts:
--------------------------------------------------------------------------------
1 | import { zodResolver } from '@hookform/resolvers/zod';
2 | import { updateLineItem } from '@libs/util/server/data/cart.server';
3 | import { type ActionFunctionArgs, data } from 'react-router';
4 | import { getValidatedFormData } from 'remix-hook-form';
5 | import { z } from 'zod';
6 |
7 | export const updateLineItemSchema = z.object({
8 | lineItemId: z.string().min(1, 'Line item ID is required'),
9 | quantity: z.coerce.number().int(),
10 | });
11 |
12 | type UpdateLineItemFormData = z.infer;
13 |
14 | export async function action({ request }: ActionFunctionArgs) {
15 | const { errors, data: validatedFormData } = await getValidatedFormData(
16 | request,
17 | zodResolver(updateLineItemSchema),
18 | );
19 |
20 | if (errors) {
21 | return data({ errors }, { status: 400 });
22 | }
23 |
24 | const { lineItemId, quantity } = validatedFormData;
25 |
26 | const response = await updateLineItem(request, {
27 | lineId: lineItemId,
28 | quantity,
29 | });
30 |
31 | return data(response);
32 | }
33 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/api.checkout.contact-info.ts:
--------------------------------------------------------------------------------
1 | import { zodResolver } from '@hookform/resolvers/zod';
2 | import { updateCart } from '@libs/util/server/data/cart.server';
3 | import { data as remixData } from 'react-router';
4 | import type { ActionFunctionArgs } from 'react-router';
5 | import { getValidatedFormData } from 'remix-hook-form';
6 | import { z } from 'zod';
7 |
8 | export const contactInfoSchema = z.object({
9 | cartId: z.string(),
10 | email: z.string().email('Please enter a valid email'),
11 | });
12 |
13 | export type ContactInfoFormData = z.infer;
14 |
15 | export async function action(actionArgs: ActionFunctionArgs) {
16 | const { errors, data } = await getValidatedFormData(
17 | actionArgs.request,
18 | zodResolver(contactInfoSchema),
19 | );
20 |
21 | if (errors) {
22 | return remixData({ errors }, { status: 400 });
23 | }
24 |
25 | const { cart } = await updateCart(actionArgs.request, {
26 | email: data.email,
27 | });
28 |
29 | return remixData({ cart });
30 | }
31 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/api.checkout.discount-code.ts:
--------------------------------------------------------------------------------
1 | import { zodResolver } from '@hookform/resolvers/zod';
2 | import { sdk } from '@libs/util/server/client.server';
3 | import type { ActionFunctionArgs } from 'react-router';
4 | import { data as remixData } from 'react-router';
5 | import { getValidatedFormData } from 'remix-hook-form';
6 | import { z } from 'zod';
7 |
8 | export const discountCodeSchema = z.object({
9 | cartId: z.string(),
10 | code: z.string().min(1, 'Discount code is required'),
11 | });
12 |
13 | export type DiscountCodeFormData = z.infer;
14 |
15 | export async function action(actionArgs: ActionFunctionArgs) {
16 | const { errors, data } = await getValidatedFormData(
17 | actionArgs.request,
18 | zodResolver(discountCodeSchema),
19 | );
20 |
21 | if (errors) {
22 | return remixData({ errors }, { status: 400 });
23 | }
24 |
25 | try {
26 | const { cart } = await sdk.store.cart.update(data.cartId, {
27 | promo_codes: [data.code],
28 | });
29 |
30 | if (cart.promotions.length)
31 | if (!cart) {
32 | return remixData(
33 | { errors: { root: { message: 'Cart could not be updated. Please try again.' } } },
34 | { status: 400 },
35 | );
36 | }
37 |
38 | return remixData({ cart });
39 | } catch (error) {
40 | console.error(error);
41 | return remixData({ errors: { root: { message: 'Discount code is invalid.' } } }, { status: 400 });
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/api.checkout.remove-discount-code.ts:
--------------------------------------------------------------------------------
1 | import { zodResolver } from '@hookform/resolvers/zod';
2 | import { sdk } from '@libs/util/server/client.server';
3 | import { retrieveCart } from '@libs/util/server/data/cart.server';
4 | import type { PromotionDTO } from '@medusajs/types';
5 | import type { ActionFunctionArgs } from 'react-router';
6 | import { data as remixData } from 'react-router';
7 | import { getValidatedFormData } from 'remix-hook-form';
8 | import { z } from 'zod';
9 |
10 | export const removeDiscountCodeSchema = z.object({
11 | cartId: z.string(),
12 | code: z.string().min(1, 'Discount code is required'),
13 | });
14 |
15 | export type RemoveDiscountCodeFormData = z.infer;
16 |
17 | export async function action(actionArgs: ActionFunctionArgs) {
18 | const { errors, data } = await getValidatedFormData(
19 | actionArgs.request,
20 | zodResolver(removeDiscountCodeSchema),
21 | );
22 |
23 | if (errors) {
24 | return remixData({ errors }, { status: 400 });
25 | }
26 |
27 | const cart = await retrieveCart(actionArgs.request);
28 | const promoCodes = (cart as any)?.promotions
29 | ?.filter((promo: PromotionDTO) => promo.code !== data.code)
30 | .map((promo: PromotionDTO) => promo.code) as string[];
31 |
32 | const { cart: updatedCart } = await sdk.store.cart.update(data.cartId, {
33 | promo_codes: promoCodes || [],
34 | });
35 |
36 | if (!updatedCart) {
37 | return remixData({ errors: { root: { message: 'Could not remove promo code.' } } }, { status: 400 });
38 | }
39 |
40 | return remixData({ cart: updatedCart });
41 | }
42 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/api.health.live.ts:
--------------------------------------------------------------------------------
1 | import { LoaderFunctionArgs } from 'react-router';
2 |
3 | export const loader = async ({ request }: LoaderFunctionArgs) => {
4 | return Response.json({ status: "It's alive!!!" });
5 | };
6 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/api.newsletter-subscriptions.ts:
--------------------------------------------------------------------------------
1 | import { zodResolver } from '@hookform/resolvers/zod';
2 | import { ActionFunctionArgs, data } from 'react-router';
3 | import { getValidatedFormData } from 'remix-hook-form';
4 | import { z } from 'zod';
5 |
6 | export const newsletterSubscriberSchema = z.object({
7 | email: z.string().email('Please enter a valid email address'),
8 | });
9 |
10 | export const action = async ({ request }: ActionFunctionArgs) => {
11 | const { data: validatedData, errors } = await getValidatedFormData(
12 | await request.formData(),
13 | zodResolver(newsletterSubscriberSchema),
14 | );
15 |
16 | if (errors) {
17 | return data({ errors }, { status: 400 });
18 | }
19 |
20 | const { email } = validatedData;
21 |
22 | // Implement newsletter subscription here!
23 |
24 | console.log('Subscribed to newsletter', email);
25 |
26 | return data({ success: true }, { status: 200 });
27 | };
28 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/api.page-data.ts:
--------------------------------------------------------------------------------
1 | import { buildObjectFromSearchParams } from '@libs/util/buildObjectFromSearchParams';
2 | import { getProductListData } from '@libs/util/server/page.server';
3 | import { LoaderFunctionArgs, data as remixData } from 'react-router';
4 |
5 | const productList = async ({ request }: Pick) => {
6 | const result = await getProductListData(request);
7 | return remixData(result, {});
8 | };
9 |
10 | const loaders = {
11 | productList,
12 | };
13 |
14 | export const loader = async ({ request }: LoaderFunctionArgs) => {
15 | const url = new URL(request.url);
16 |
17 | const { subloader, data } = buildObjectFromSearchParams<{
18 | subloader: keyof typeof loaders;
19 | data: string;
20 | }>(url.searchParams);
21 |
22 | const _loader = loaders[subloader];
23 |
24 | if (!_loader) throw new Error(`Action handler not found for "${subloader}" loader.`);
25 |
26 | const parsedData = JSON.parse(data);
27 |
28 | return await _loader({ request });
29 | };
30 |
--------------------------------------------------------------------------------
/apps/storefront/app/routes/api.region.ts:
--------------------------------------------------------------------------------
1 | import { zodResolver } from '@hookform/resolvers/zod';
2 | import { getCartId, setSelectedRegionId } from '@libs/util/server/cookies.server';
3 | import { updateCart } from '@libs/util/server/data/cart.server';
4 | import { retrieveRegion } from '@libs/util/server/data/regions.server';
5 | import { ActionFunctionArgs, data } from 'react-router';
6 | import { getValidatedFormData } from 'remix-hook-form';
7 | import { z } from 'zod';
8 |
9 | export const changeRegionSchema = z.object({
10 | regionId: z.string().min(1, 'Region ID is required'),
11 | });
12 |
13 | export const action = async ({ request }: ActionFunctionArgs) => {
14 | const { errors, data: formData } = await getValidatedFormData(request, zodResolver(changeRegionSchema));
15 |
16 | if (errors) {
17 | return data({ errors }, { status: 400 });
18 | }
19 |
20 | try {
21 | const { regionId } = formData;
22 |
23 | await retrieveRegion(regionId);
24 |
25 | const headers = new Headers();
26 |
27 | await setSelectedRegionId(headers, regionId);
28 |
29 | const cartId = await getCartId(request.headers);
30 |
31 | if (cartId) await updateCart(request, { region_id: regionId });
32 |
33 | return data({ success: true }, { headers });
34 | } catch (error: any) {
35 | return data(error.response.data, {
36 | status: error.response.status,
37 | });
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/apps/storefront/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/apps/storefront/libs/config/site/navigation-items.ts:
--------------------------------------------------------------------------------
1 | import { NavigationCollection, NavigationItemLocation } from '@libs/types';
2 |
3 | export const headerNavigationItems: NavigationCollection = [
4 | {
5 | id: 1,
6 | label: 'View our Blends',
7 | url: '/categories/blends',
8 | sort_order: 0,
9 | location: NavigationItemLocation.header,
10 | new_tab: false,
11 | },
12 | {
13 | id: 3,
14 | label: 'Our Story',
15 | url: '/about-us',
16 | sort_order: 1,
17 | location: NavigationItemLocation.header,
18 | new_tab: false,
19 | },
20 | {
21 | id: 2,
22 | label: 'Shop All',
23 | url: '/products',
24 | sort_order: 1,
25 | location: NavigationItemLocation.header,
26 | new_tab: false,
27 | },
28 | ];
29 |
30 | export const footerNavigationItems: NavigationCollection = [
31 | {
32 | id: 1,
33 | label: 'Shop All',
34 | url: '/products',
35 | location: NavigationItemLocation.footer,
36 | sort_order: 1,
37 | new_tab: false,
38 | },
39 | {
40 | id: 2,
41 | label: 'Light Roasts',
42 | url: '/collections/light-roasts',
43 | location: NavigationItemLocation.footer,
44 | sort_order: 1,
45 | new_tab: false,
46 | },
47 | {
48 | id: 3,
49 | label: 'Medium Roasts',
50 | url: '/collections/medium-roasts',
51 | location: NavigationItemLocation.footer,
52 | sort_order: 1,
53 | new_tab: false,
54 | },
55 | {
56 | id: 4,
57 | label: 'Dark Roasts',
58 | url: '/collections/dark-roasts',
59 | location: NavigationItemLocation.footer,
60 | sort_order: 1,
61 | new_tab: false,
62 | },
63 | ];
64 |
--------------------------------------------------------------------------------
/apps/storefront/libs/config/site/site-settings.ts:
--------------------------------------------------------------------------------
1 | import { SiteSettings } from '@libs/types';
2 | import { config } from '@libs/util/server/config.server';
3 |
4 | export const siteSettings: SiteSettings = {
5 | storefront_url: config.STOREFRONT_URL,
6 | description: '',
7 | favicon: '/favicon.svg',
8 | social_facebook: 'https://www.facebook.com/',
9 | social_instagram: 'https://www.instagram.com/',
10 | social_twitter: 'https://www.twitter.com/',
11 | };
12 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/addresses/addressToMedusaAddress.ts:
--------------------------------------------------------------------------------
1 | import { Address, MedusaAddress } from '@libs/types';
2 |
3 | export const addressToMedusaAddress = (address: Address): MedusaAddress => {
4 | if (!address) return {} as MedusaAddress;
5 |
6 | return {
7 | first_name: address.firstName || '',
8 | last_name: address.lastName || '',
9 | company: address.company || '',
10 | address_1: address.address1 || '',
11 | address_2: address.address2 || '',
12 | city: address.city || '',
13 | country_code: address.countryCode || '',
14 | phone: address.phone || '',
15 | postal_code: address.postalCode,
16 | province: address.province,
17 | } as MedusaAddress;
18 | };
19 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/addresses/compareAddresses.ts:
--------------------------------------------------------------------------------
1 | import isEqual from 'lodash.isequal';
2 | import pick from 'lodash.pick';
3 |
4 | export default function compareAddresses(address1: any, address2: any) {
5 | return isEqual(
6 | pick(address1, [
7 | 'first_name',
8 | 'last_name',
9 | 'address_1',
10 | 'company',
11 | 'postal_code',
12 | 'city',
13 | 'country_code',
14 | 'province',
15 | 'phone',
16 | ]),
17 | pick(address2, [
18 | 'first_name',
19 | 'last_name',
20 | 'address_1',
21 | 'company',
22 | 'postal_code',
23 | 'city',
24 | 'country_code',
25 | 'province',
26 | 'phone',
27 | ]),
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/addresses/index.ts:
--------------------------------------------------------------------------------
1 | export * from './addressToMedusaAddress';
2 | export * from './medusaAddressToAddress';
3 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/addresses/medusaAddressToAddress.ts:
--------------------------------------------------------------------------------
1 | import type { Address, MedusaAddress } from '@libs/types';
2 | import { StoreCartAddress, StoreCreateCustomerAddress } from '@medusajs/types';
3 |
4 | export const emptyAddress: Address = {
5 | firstName: '',
6 | lastName: '',
7 | company: '',
8 | address1: '',
9 | address2: '',
10 | city: '',
11 | province: '',
12 | postalCode: '',
13 | countryCode: '',
14 | phone: '',
15 | };
16 |
17 | export const medusaAddressToAddress = (address?: MedusaAddress | StoreCartAddress | null): Address => {
18 | if (!address) return emptyAddress;
19 |
20 | return {
21 | ...emptyAddress,
22 | firstName: address?.first_name || '',
23 | lastName: address?.last_name || '',
24 | company: address?.company || '',
25 | address1: address?.address_1 || '',
26 | address2: address?.address_2 || '',
27 | city: address?.city || '',
28 | countryCode: address?.country_code || '',
29 | phone: address?.phone || '',
30 | postalCode: address?.postal_code || '',
31 | province: address?.province || '',
32 | };
33 | };
34 |
35 | export const addressPayload = (address?: MedusaAddress | StoreCartAddress | null): StoreCreateCustomerAddress => {
36 | if (!address) return emptyAddress as StoreCreateCustomerAddress;
37 |
38 | return {
39 | first_name: address?.first_name || '',
40 | last_name: address?.last_name || '',
41 | company: address?.company || '',
42 | address_1: address?.address_1 || '',
43 | address_2: address?.address_2 || '',
44 | city: address?.city || '',
45 | country_code: address?.country_code || '',
46 | phone: address?.phone || '',
47 | postal_code: address?.postal_code || '',
48 | province: address?.province || '',
49 | };
50 | };
51 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/buildObjectFromSearchParams.ts:
--------------------------------------------------------------------------------
1 | export const buildObjectFromSearchParams = (searchParams: URLSearchParams): T => {
2 | let params: Record = {};
3 |
4 | searchParams.forEach((value, key) => {
5 | let decodedKey = decodeURIComponent(key);
6 | const decodedValue = decodeURIComponent(value);
7 |
8 | if (decodedKey.endsWith('[]')) {
9 | // This key is part of an array
10 | decodedKey = decodedKey.replace('[]', '');
11 | params[decodedKey] || (params[decodedKey] = []);
12 | params[decodedKey].push(decodedValue);
13 | } else {
14 | // Just a regular parameter
15 | params[decodedKey] = decodedValue;
16 | }
17 | });
18 |
19 | return params as T;
20 | };
21 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/buildSearchParamsFromObject.ts:
--------------------------------------------------------------------------------
1 | export const buildSearchParamsFromObject = (search: Record, prefix = '', isArray = false): string => {
2 | return Object.entries(search)
3 | .filter(([key, value]) => value)
4 | .map(([key, value]) =>
5 | typeof value === 'object'
6 | ? buildSearchParamsFromObject(value, key, Array.isArray(value))
7 | : `${prefix ? `${prefix}[${isArray ? '' : key}]` : `${key}`}=${value}`,
8 | )
9 | .join('&');
10 | };
11 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/carts/calculateEstimatedShipping.ts:
--------------------------------------------------------------------------------
1 | import { StoreCartShippingOption } from '@medusajs/types';
2 | import { getShippingOptionsByProfile } from '../checkout';
3 |
4 | export function calculateEstimatedShipping(shippingOptions: StoreCartShippingOption[]): number {
5 | if (shippingOptions?.length < 1) return 0;
6 |
7 | const shippingOptionsByProfile = getShippingOptionsByProfile(shippingOptions);
8 |
9 | return Object.values(shippingOptionsByProfile).reduce((acc, shippingOptions) => {
10 | const cheapestOption = shippingOptions.reduce((prev, curr) =>
11 | (prev.amount || 0) < (curr?.amount || 0) ? prev : curr,
12 | );
13 |
14 | return acc + (cheapestOption.amount || 0);
15 | }, 0);
16 | }
17 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/carts/index.ts:
--------------------------------------------------------------------------------
1 | export * from './calculateEstimatedShipping';
2 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/checkout/amountToStripeExpressCheckoutAmount.ts:
--------------------------------------------------------------------------------
1 | export const amountToStripeExpressCheckoutAmount = (amount: number) => {
2 | return (amount ?? 0) * 100;
3 | };
4 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/checkout/checkStepComplete.ts:
--------------------------------------------------------------------------------
1 | import { StoreCart, StoreCartShippingOption, StoreCustomer } from '@medusajs/types';
2 | import { getShippingOptionsByProfile } from './getShippingOptionsByProfile';
3 |
4 | export const checkContactInfoComplete = (cart: StoreCart, customer?: Pick) =>
5 | !!cart.email || !!customer?.email;
6 |
7 | export const checkAccountDetailsComplete = (cart: StoreCart) => !!cart.shipping_address?.address_1;
8 |
9 | export const checkDeliveryMethodComplete = (cart: StoreCart, shippingOptions: StoreCartShippingOption[]) => {
10 | const values = cart.shipping_methods?.map((sm) => sm.shipping_option_id) || [];
11 | const shippingOptionsByProfile = getShippingOptionsByProfile(shippingOptions);
12 |
13 | return Object.values(shippingOptionsByProfile).every((shippingOptions) =>
14 | shippingOptions.find((so) => values.includes(so.id)),
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/checkout/express-checkout-client.ts:
--------------------------------------------------------------------------------
1 | import { ExpressCheckoutFormData, ExpressCheckoutResponse } from 'storefront/app/routes/api.checkout.express';
2 | import { convertToFormData } from '../forms';
3 |
4 | export const expressCheckoutClient = {
5 | update: async (data: ExpressCheckoutFormData): Promise<[ExpressCheckoutResponse, null] | [null, Error]> => {
6 | const response = await fetch('/api/checkout/express', {
7 | method: 'POST',
8 | body: convertToFormData(data),
9 | });
10 |
11 | if (!response.ok) return [null, new Error('Failed to update shipping address')];
12 |
13 | return [(await response.json()) as ExpressCheckoutResponse, null];
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/checkout/getShippingOptionsByProfile.ts:
--------------------------------------------------------------------------------
1 | import { StoreCartShippingOption } from '@medusajs/types';
2 |
3 | export const getShippingOptionsByProfile = (shippingOptions: StoreCartShippingOption[]) => {
4 | const shippingOptionsByProfile = shippingOptions.reduce>(
5 | (acc, shippingOption) => {
6 | const profileId = shippingOption.shipping_profile_id;
7 |
8 | if (!profileId) return acc;
9 |
10 | if (!acc[profileId]) acc[profileId] = [];
11 |
12 | acc[profileId].push(shippingOption as any);
13 |
14 | return acc;
15 | },
16 | {},
17 | );
18 |
19 | Object.keys(shippingOptionsByProfile).forEach((profileId) =>
20 | shippingOptionsByProfile[profileId].sort((a, b) => (a.amount || 0) - (b.amount || 0)),
21 | );
22 |
23 | return shippingOptionsByProfile;
24 | };
25 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/checkout/index.ts:
--------------------------------------------------------------------------------
1 | export * from './checkStepComplete';
2 | export * from './getShippingOptionsByProfile';
3 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/createReducer.ts:
--------------------------------------------------------------------------------
1 | export interface ReducerAction {
2 | name: string;
3 | payload?: any;
4 | }
5 |
6 | export type ReducerActionHandler = (state: S, payload?: any) => S;
7 |
8 | export interface ReducerActionHandlers {
9 | [x: string]: ReducerActionHandler;
10 | }
11 |
12 | export interface CreateReducerConfig {
13 | actionHandlers: ReducerActionHandlers;
14 | middleware?: (state: S, action: A) => void;
15 | }
16 |
17 | export const createReducer = ({
18 | actionHandlers,
19 | middleware,
20 | }: CreateReducerConfig) => {
21 | return (state: S, action: A) => {
22 | const actionHandler = actionHandlers[action.name];
23 |
24 | if (!actionHandler) throw new Error(`Unhandled action type: ${action.name}`);
25 |
26 | if (middleware) middleware(state, action);
27 |
28 | const newState = actionHandler(state, action.payload);
29 |
30 | return newState;
31 | };
32 | };
33 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/fetcher-keys.ts:
--------------------------------------------------------------------------------
1 | export const FetcherCartKeyPrefix = 'cart:';
2 |
3 | export const FetcherKeys = {
4 | cart: {
5 | accountDetails: `${FetcherCartKeyPrefix}account-details`,
6 | removePromotionCode: `${FetcherCartKeyPrefix}remove-promotion-code`,
7 | createLineItem: `${FetcherCartKeyPrefix}create-line-item`,
8 | removeLineItem: `${FetcherCartKeyPrefix}remove-line-item`,
9 | updateLineItem: `${FetcherCartKeyPrefix}update-line-item`,
10 | addShippingMethods: `${FetcherCartKeyPrefix}add-shipping-methods`,
11 | completeCheckout: `${FetcherCartKeyPrefix}complete-checkout`,
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/formatters.ts:
--------------------------------------------------------------------------------
1 | export const formatDate = (date: Date, format = 'en-US') => {
2 | return new Intl.DateTimeFormat(format, { dateStyle: 'medium' }).format(date);
3 | };
4 |
5 | export const formatList = (list: string[]) => {
6 | return new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }).format(list);
7 | };
8 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/forms/formDataToObject.ts:
--------------------------------------------------------------------------------
1 | import set from 'lodash/set';
2 |
3 | export const formDataToObject: (formData: FormData) => Record = (formData: FormData) =>
4 | Array.from(formData.entries()).reduce(
5 | (acc, [key, value]) => {
6 | if (acc.hasOwnProperty(key)) {
7 | if (Array.isArray(acc[key])) {
8 | (acc[key] as T[]).push(value as T);
9 | } else {
10 | acc[key] = [acc[key], value] as T[];
11 | }
12 | } else {
13 | set(acc, key, value);
14 | }
15 | return acc;
16 | },
17 | {} as Record,
18 | );
19 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/forms/index.ts:
--------------------------------------------------------------------------------
1 | export * from './formDataToObject';
2 | export * from './parseFormData';
3 | export * from './objectToFormData';
4 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/forms/objectToFormData.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Converts javascript arrays and objects to FormData
3 | * @param data
4 | * @param formData
5 | * @param parentKey
6 | * @returns
7 | */
8 | export const convertToFormData = (data: any, formData: FormData = new FormData(), parentKey = ''): FormData => {
9 | if (data === null || data === undefined) return formData;
10 |
11 | if (typeof data === 'object' && !(data instanceof Date) && !(data instanceof File)) {
12 | Object.entries(data).forEach(([key, value]) => {
13 | convertToFormData(
14 | value,
15 | formData,
16 | !parentKey ? key : data[key] instanceof File ? parentKey : `${parentKey}.${key}`,
17 | );
18 | });
19 | return formData;
20 | }
21 |
22 | if (Array.isArray(data)) {
23 | data.forEach((value, index) => {
24 | convertToFormData(value, formData, `${parentKey}.${index}`);
25 | });
26 | return formData;
27 | }
28 |
29 | formData.append(parentKey, data);
30 |
31 | return formData;
32 | };
33 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/forms/parseFormData.ts:
--------------------------------------------------------------------------------
1 | import { formDataToObject } from './formDataToObject';
2 |
3 | export const parseFormData = (form?: HTMLFormElement | null) => {
4 | if (!form) return {};
5 |
6 | const data = new FormData(form);
7 |
8 | return formDataToObject(data);
9 | };
10 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/index.ts:
--------------------------------------------------------------------------------
1 | export * from './addresses';
2 | export * from './buildObjectFromSearchParams';
3 | export * from './buildSearchParamsFromObject';
4 | export * from './carts';
5 | export * from './checkout';
6 | export * from './createReducer';
7 | export * from './forms';
8 | export * from './phoneNumber';
9 | export * from './prices';
10 | export * from './is-browser';
11 | export * from './formatters';
12 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/is-browser.ts:
--------------------------------------------------------------------------------
1 | export const isBrowser = typeof window !== 'undefined';
2 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/medusaError.ts:
--------------------------------------------------------------------------------
1 | export function medusaError(error: any): never {
2 | console.error('~ medusaError ~ error:', error);
3 | if (error.response) {
4 | // The request was made and the server responded with a status code
5 | // that falls out of the range of 2xx
6 | const u = new URL(error.config.url, error.config.baseURL);
7 | console.error('Resource:', u.toString());
8 | console.error('Response data:', error.response.data);
9 | console.error('Status code:', error.response.status);
10 | console.error('Headers:', error.response.headers);
11 |
12 | // Extracting the error message from the response data
13 | const message = error.response.data.message || error.response.data;
14 |
15 | throw new Error(message.charAt(0).toUpperCase() + message.slice(1) + '.');
16 | } else if (error.request) {
17 | // The request was made but no response was received
18 | throw new Error('No response received: ' + error.request);
19 | } else {
20 | // Something happened in setting up the request that triggered an Error
21 | throw new Error('Error setting up the request: ' + error.message);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/page.ts:
--------------------------------------------------------------------------------
1 | import { getCommonMeta, getParentMeta, mergeMeta } from './meta';
2 |
3 | export const getMergedPageMeta = mergeMeta(getParentMeta, getCommonMeta);
4 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/phoneNumber.ts:
--------------------------------------------------------------------------------
1 | export type FormatPhoneNumberMask = 'default' | 'dots' | 'dashes' | 'uri';
2 |
3 | export type FormatPhoneNumberCountryCode = 'US';
4 |
5 | export interface FormatPhoneNumberOptions {
6 | format: FormatPhoneNumberMask;
7 | countryCode: FormatPhoneNumberCountryCode;
8 | customMask?: string;
9 | }
10 |
11 | /**
12 | * Assists with applying different formats dynamically to a given phone number.
13 | * We provide default masks which can be used via the `format` and `countryCode`
14 | * options, or a consumer may pass in a `customMask`.
15 | *
16 | * @param phoneNumber string
17 | * @param options object
18 | * @return string
19 | */
20 | export const formatPhoneNumber = (phoneNumber: string, options?: Partial) => {
21 | return phoneNumber;
22 | };
23 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/auth.server.ts:
--------------------------------------------------------------------------------
1 | import { createCookie } from 'react-router';
2 | import { config } from './config.server';
3 | import { getCookie } from './cookies.server';
4 |
5 | export const authCookie = createCookie(config.AUTH_COOKIE_NAME);
6 |
7 | type AuthHeaders = { authorization: string } | {};
8 |
9 | export const getAuthHeaders = async (request: Partial): Promise => {
10 | if (!request.headers) {
11 | throw Error('No request provided for getting auth headers');
12 | }
13 |
14 | const token = await getCookie(request.headers, authCookie);
15 |
16 | if (!token) {
17 | return {};
18 | }
19 |
20 | return { authorization: `Bearer ${token}` };
21 | };
22 |
23 | export const withAuthHeaders = = any[], TReturn = any>(
24 | asyncFn: (request: Request, authHeaders: AuthHeaders, ...args: TArgs) => TReturn,
25 | ) => {
26 | return async (request: Request, ...args: TArgs): Promise> => {
27 | const authHeaders = await getAuthHeaders(request);
28 |
29 | return await asyncFn(request, authHeaders, ...args);
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/cache-builder.server.ts:
--------------------------------------------------------------------------------
1 | import { Cache, CacheEntry, totalTtl } from '@epic-web/cachified';
2 | import LRUCache from 'lru-cache';
3 |
4 | export const buildNewLRUCache = ({ max }: { max: number }) => {
5 | const lruInstance = new LRUCache({ max });
6 | const lru: Cache = {
7 | set(key, value) {
8 | const ttl = totalTtl(value?.metadata);
9 | return lruInstance.set(key, value, {
10 | ttl: ttl === Infinity ? undefined : ttl,
11 | start: value?.metadata?.createdTime,
12 | });
13 | },
14 | get(key) {
15 | return lruInstance.get(key);
16 | },
17 | delete(key) {
18 | return lruInstance.delete(key);
19 | },
20 | };
21 |
22 | return lru;
23 | };
24 |
25 | export const MILLIS = {
26 | TEN_SECONDS: 10_000,
27 | ONE_MINUTE: 60_000,
28 | FIVE_MINUTES: 300_000,
29 | ONE_HOUR: 3_600_000,
30 | };
31 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/client.server.ts:
--------------------------------------------------------------------------------
1 | import { MedusaPluginsSDK } from '@lambdacurry/medusa-plugins-sdk';
2 | import { buildNewLRUCache } from './cache-builder.server';
3 | import { config } from './config.server';
4 |
5 | // Defaults to standard port for Medusa server
6 | let MEDUSA_BACKEND_URL = 'http://localhost:9000';
7 |
8 | if (process.env.INTERNAL_MEDUSA_API_URL) {
9 | MEDUSA_BACKEND_URL = process.env.INTERNAL_MEDUSA_API_URL;
10 | }
11 |
12 | if (process.env.PUBLIC_MEDUSA_API_URL) {
13 | MEDUSA_BACKEND_URL = process.env.PUBLIC_MEDUSA_API_URL;
14 | }
15 |
16 | export const baseMedusaConfig = {
17 | baseUrl: MEDUSA_BACKEND_URL,
18 | debug: process.env.NODE_ENV === 'development',
19 | publishableKey: config.MEDUSA_PUBLISHABLE_KEY,
20 | };
21 |
22 | export const sdk = new MedusaPluginsSDK({
23 | ...baseMedusaConfig,
24 | });
25 |
26 | export const sdkCache = buildNewLRUCache({ max: 1000 });
27 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/config.server.ts:
--------------------------------------------------------------------------------
1 | import { loadEnv } from './env';
2 |
3 | loadEnv();
4 |
5 | export const config = {
6 | NODE_ENV: process.env.NODE_ENV,
7 | ENVIRONMENT: process.env.ENVIRONMENT,
8 | STRIPE_PUBLIC_KEY: process.env.STRIPE_PUBLIC_KEY,
9 | PUBLIC_MEDUSA_API_URL: process.env.PUBLIC_MEDUSA_API_URL,
10 | STOREFRONT_URL: process.env.STOREFRONT_URL,
11 | SENTRY_DSN: process.env.SENTRY_DSN,
12 | SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT,
13 | EVENT_LOGGING: process.env.EVENT_LOGGING,
14 | AUTH_COOKIE_NAME: process.env.AUTH_COOKIE_NAME ?? '_medusa_jwt',
15 | MEDUSA_PUBLISHABLE_KEY: process.env.MEDUSA_PUBLISHABLE_KEY,
16 | };
17 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/data/categories.server.ts:
--------------------------------------------------------------------------------
1 | import cachified from '@epic-web/cachified';
2 | import { sdk, sdkCache } from '@libs/util/server/client.server';
3 | import { MILLIS } from '../cache-builder.server';
4 |
5 | export const listCategories = async function () {
6 | return cachified({
7 | key: 'list-categories',
8 | cache: sdkCache,
9 | staleWhileRevalidate: MILLIS.ONE_HOUR,
10 | ttl: MILLIS.TEN_SECONDS,
11 | async getFreshValue() {
12 | return _listCategories();
13 | },
14 | });
15 | };
16 |
17 | export const _listCategories = async function () {
18 | return sdk.store.category.list({ fields: '+category_children' }).then(({ product_categories }) => product_categories);
19 | };
20 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/data/collections.server.ts:
--------------------------------------------------------------------------------
1 | import cachified from '@epic-web/cachified';
2 | import { medusaError } from '@libs/util/medusaError';
3 | import { sdk, sdkCache } from '@libs/util/server/client.server';
4 | import { HttpTypes } from '@medusajs/types';
5 | import { MILLIS } from '../cache-builder.server';
6 |
7 | export const retrieveCollection = async function (id: string) {
8 | return sdk.store.collection.retrieve(id, {}).then(({ collection }) => collection);
9 | };
10 |
11 | export const fetchCollections = async function (
12 | offset: number = 0,
13 | limit: number = 100,
14 | ): Promise<{ collections: HttpTypes.StoreCollection[]; count: number }> {
15 | return cachified({
16 | key: `collections-${JSON.stringify({ offset, limit })}`,
17 | cache: sdkCache,
18 | staleWhileRevalidate: MILLIS.ONE_HOUR,
19 | ttl: MILLIS.TEN_SECONDS,
20 | async getFreshValue() {
21 | return _fetchCollections(offset, limit);
22 | },
23 | });
24 | };
25 |
26 | export const _fetchCollections = async function (
27 | offset: number = 0,
28 | limit: number = 100,
29 | ): Promise<{ collections: HttpTypes.StoreCollection[]; count: number }> {
30 | return sdk.store.collection
31 | .list({ limit, offset })
32 | .then(({ collections }) => ({ collections, count: collections.length }));
33 | };
34 |
35 | export const getCollectionByHandle = async function (handle: string): Promise {
36 | return sdk.store.collection
37 | .list({ handle })
38 | .then(({ collections }) => collections[0])
39 | .catch(medusaError);
40 | };
41 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/data/customer.server.ts:
--------------------------------------------------------------------------------
1 | import { medusaError } from '@libs/util/medusaError';
2 | import { sdk } from '@libs/util/server/client.server';
3 | import { HttpTypes } from '@medusajs/types';
4 | import { withAuthHeaders } from '../auth.server';
5 |
6 | export const getCustomer = withAuthHeaders(async (request, authHeaders) => {
7 | return await sdk.store.customer
8 | .retrieve({}, authHeaders)
9 | .then(({ customer }) => customer)
10 | .catch(() => null);
11 | });
12 |
13 | export const updateCustomer = withAuthHeaders(async (request, authHeaders, body: HttpTypes.StoreUpdateCustomer) => {
14 | const updateRes = await sdk.store.customer
15 | .update(body, {}, authHeaders)
16 | .then(({ customer }) => customer)
17 | .catch(medusaError);
18 |
19 | return updateRes;
20 | });
21 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/data/fulfillment.server.ts:
--------------------------------------------------------------------------------
1 | import { sdk } from '@libs/util/server/client.server';
2 | import { StoreCartShippingOption } from '@medusajs/types';
3 |
4 | export const listCartShippingOptions = async (cartId: string) => {
5 | return sdk.store.fulfillment
6 | .listCartOptions({ cart_id: cartId })
7 | .then(({ shipping_options }) => shipping_options)
8 | .catch(() => [] as StoreCartShippingOption[]);
9 | };
10 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/data/orders.server.ts:
--------------------------------------------------------------------------------
1 | import { medusaError } from '@libs/util/medusaError';
2 | import { sdk } from '@libs/util/server/client.server';
3 | import { withAuthHeaders } from '../auth.server';
4 |
5 | export const retrieveOrder = withAuthHeaders(async (request, authHeaders, id: string) => {
6 | return sdk.store.order
7 | .retrieve(id, { fields: '*payment_collections.payments' }, authHeaders)
8 | .then(({ order }) => order)
9 | .catch(medusaError);
10 | });
11 |
12 | export const listOrders = withAuthHeaders(async (request, authHeaders, limit: number = 10, offset: number = 0) => {
13 | return sdk.store.order
14 | .list({ limit, offset }, authHeaders)
15 | .then(({ orders }) => orders)
16 | .catch(medusaError);
17 | });
18 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/data/payment.server.ts:
--------------------------------------------------------------------------------
1 | import { sdk } from '@libs/util/server/client.server';
2 | import { StorePaymentProvider } from '@medusajs/types';
3 |
4 | export const listCartPaymentProviders = async (regionId: string) => {
5 | return sdk.store.payment
6 | .listPaymentProviders({ region_id: regionId })
7 | .then(({ payment_providers }) => payment_providers)
8 | .catch(() => [] as StorePaymentProvider[]);
9 | };
10 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/env.ts:
--------------------------------------------------------------------------------
1 | import dotenv from 'dotenv';
2 |
3 | export const loadEnv = () => {
4 | const envFile = process.env.NODE_ENV ? `.env.${process.env.NODE_ENV}` : '.env';
5 | dotenv.config({ path: envFile });
6 | dotenv.config({ path: '.env.local' });
7 | dotenv.config({ path: '.env' });
8 | };
9 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/page.server.ts:
--------------------------------------------------------------------------------
1 | import { HttpTypes, StoreCollection, StoreProductCategory } from '@medusajs/types';
2 | import { getSelectedRegion } from './data/regions.server';
3 | import { fetchProducts } from './products.server';
4 |
5 | export const getProductListData = async (request: Request) => {
6 | const region = await getSelectedRegion(request.headers);
7 |
8 | const productsQuery: HttpTypes.StoreProductParams = {
9 | limit: 10,
10 | offset: 0,
11 | };
12 |
13 | const { products } = await fetchProducts(request, {
14 | ...productsQuery,
15 | region_id: region.id,
16 | fields: 'id,title,handle,thumbnail,variants.*,variants.prices.*',
17 | });
18 | const collectionTabs = new Map();
19 | const categoryTabs = new Map();
20 |
21 | products.forEach((product) => {
22 | product?.categories?.forEach((category) => {
23 | categoryTabs.set(category.id, category);
24 | });
25 |
26 | if (product.collection) {
27 | collectionTabs.set(product.collection.id, product.collection);
28 | }
29 | });
30 |
31 | return {
32 | products,
33 | collection_tabs: [...collectionTabs.values()],
34 | category_tabs: [...categoryTabs.values()],
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/server/products.server.ts:
--------------------------------------------------------------------------------
1 | import cachified from '@epic-web/cachified';
2 | import { sdk, sdkCache } from '@libs/util/server/client.server';
3 | import { HttpTypes } from '@medusajs/types';
4 | import { MILLIS } from './cache-builder.server';
5 | import { getSelectedRegion } from './data/regions.server';
6 |
7 | export const fetchProducts = async (request: Request, { ...query }: HttpTypes.StoreProductListParams = {}) => {
8 | const region = await getSelectedRegion(request.headers);
9 |
10 | return await cachified({
11 | key: `products-${JSON.stringify(query)}`,
12 | cache: sdkCache,
13 | staleWhileRevalidate: MILLIS.ONE_HOUR,
14 | ttl: MILLIS.TEN_SECONDS,
15 | async getFreshValue() {
16 | return await sdk.store.product.list({
17 | ...query,
18 | region_id: region.id,
19 | });
20 | },
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/withPaginationParams.ts:
--------------------------------------------------------------------------------
1 | export const withPaginationParams = ({
2 | request,
3 | defaultPageSize = 10,
4 | prefix = '',
5 | }: {
6 | request: Request;
7 | defaultPageSize?: number;
8 | prefix?: string;
9 | }) => {
10 | const url = new URL(request.url);
11 | const searchTermKey = `${prefix}term`;
12 | const pageSizeKey = `${prefix}pageSize`;
13 | const pageKey = `${prefix}page`;
14 | const searchTerm = url.searchParams.get(searchTermKey);
15 | const pageSize = url.searchParams.get(pageSizeKey);
16 | const page = url.searchParams.get(pageKey);
17 | const limit = pageSize ? parseInt(pageSize) : defaultPageSize;
18 | const offset = page ? (parseInt(page) - 1) * limit : 0;
19 |
20 | return { url, searchTerm, pageSize, page, limit, offset, searchParams: url.searchParams };
21 | };
22 |
--------------------------------------------------------------------------------
/apps/storefront/libs/util/xml/sitemap-builder.ts:
--------------------------------------------------------------------------------
1 | export interface SitemapUrl {
2 | loc: string;
3 | lastmod?: string;
4 | changefreq?: string;
5 | priority?: number;
6 | }
7 |
8 | export const buildSitemapUrlSetXML = (urls: SitemapUrl[]) =>
9 | `
10 | ${urls.map((url) => buildSiteMapUrlXML(url)).join('\n')}
11 |
12 | `;
13 |
14 | export const buildSiteMapUrlXML = (url: SitemapUrl) =>
15 | `
16 | ${url.loc}
17 | ${url.lastmod ? `${url.lastmod}` : ''}
18 | ${url.changefreq ? `${url.changefreq}` : ''}
19 | ${url.priority ? `${url.priority}` : ''}
20 |
21 | `;
22 |
--------------------------------------------------------------------------------
/apps/storefront/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {
4 | config: "tailwind.config.js",
5 | },
6 | autoprefixer: {},
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/banner-coffee-shop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/banner-coffee-shop.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/barrio-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/barrio-banner.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/benefit-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/benefit-1.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/benefit-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/benefit-2.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/benefit-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/benefit-3.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/coffee-shop-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/coffee-shop-2.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/credit-card-icons/amex.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/credit-card-icons/amex.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/credit-card-icons/diners.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/credit-card-icons/diners.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/credit-card-icons/discover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/credit-card-icons/discover.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/credit-card-icons/jcb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/credit-card-icons/jcb.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/credit-card-icons/mastercard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/credit-card-icons/mastercard.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/credit-card-icons/unionpay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/credit-card-icons/unionpay.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/credit-card-icons/unknown.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/credit-card-icons/unknown.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/credit-card-icons/visa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/credit-card-icons/visa.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/grid-cta-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/grid-cta-1.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/grid-cta-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/grid-cta-2.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/header-image-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/header-image-1.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/header-image-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/header-image-2.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/location-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/location-1.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/location-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/location-2.png
--------------------------------------------------------------------------------
/apps/storefront/public/assets/images/location-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lambda-curry/medusa2-starter/333ce96c56044c54a574f3ae57a42f2aaf202392/apps/storefront/public/assets/images/location-3.png
--------------------------------------------------------------------------------
/apps/storefront/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | "env.d.ts",
4 | "app",
5 | "libs",
6 | "types",
7 | "**/*.ts",
8 | "**/*.tsx",
9 | "vite.config.ts"
10 | ],
11 | "compilerOptions": {
12 | "sourceMap": true,
13 | "declaration": false,
14 | "emitDecoratorMetadata": true,
15 | "experimentalDecorators": true,
16 | "importHelpers": true,
17 | "module": "esnext",
18 | "moduleResolution": "Bundler",
19 | "isolatedModules": true,
20 | "esModuleInterop": true,
21 | "skipLibCheck": true,
22 | "skipDefaultLibCheck": true,
23 | "lib": ["DOM", "DOM.Iterable", "ES2021"],
24 | "jsx": "react-jsx",
25 | "resolveJsonModule": true,
26 | "target": "ESNext",
27 | "strict": true,
28 | "allowJs": true,
29 | "baseUrl": ".",
30 | "allowSyntheticDefaultImports": true,
31 | "noImplicitOverride": true,
32 | "noPropertyAccessFromIndexSignature": false,
33 | "noImplicitReturns": true,
34 | "forceConsistentCasingInFileNames": true,
35 | "paths": {
36 | "@app/*": ["./app/*"],
37 | "@ui-components/*": ["./libs/ui-components/*"],
38 | "@libs/*": ["./libs/*"],
39 | },
40 | "noEmit": true
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/apps/storefront/types/global.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'react';
2 |
3 | export interface ContextValue {
4 | state: S;
5 | dispatch: Dispatch;
6 | }
7 |
--------------------------------------------------------------------------------
/apps/storefront/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './global';
2 |
--------------------------------------------------------------------------------
/apps/storefront/types/remix.ts:
--------------------------------------------------------------------------------
1 | import { LoaderFunction } from 'react-router';
2 |
3 | export type RemixLoaderResponse = Awaited>;
4 |
--------------------------------------------------------------------------------
/apps/storefront/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { reactRouter } from '@react-router/dev/vite';
2 | import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
3 | import { defineConfig } from 'vite';
4 | import tsconfigPaths from 'vite-tsconfig-paths';
5 | // import { remixDevTools } from 'remix-development-tools';
6 |
7 | export default defineConfig({
8 | server: {
9 | port: 3000,
10 | warmup: {
11 | clientFiles: ['./app/entry.client.tsx', './app/root.tsx', './app/routes/**/*'],
12 | },
13 | },
14 | ssr: {
15 | noExternal: ['@medusajs/js-sdk', '@lambdacurry/medusa-plugins-sdk'],
16 | },
17 | plugins: [reactRouter(), tsconfigPaths({ root: './' }), vanillaExtractPlugin()],
18 | build: {},
19 | });
20 |
--------------------------------------------------------------------------------
/helm-charts/medusa/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/helm-charts/medusa/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: medusa
3 | description: A Helm chart for Kubernetes
4 |
5 | # A chart can be either an 'application' or a 'library' chart.
6 | #
7 | # Application charts are a collection of templates that can be packaged into versioned archives
8 | # to be deployed.
9 | #
10 | # Library charts provide useful utilities or functions for the chart developer. They're included as
11 | # a dependency of application charts to inject those utilities and functions into the rendering
12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed.
13 | type: application
14 |
15 | # This is the chart version. This version number should be incremented each time you make changes
16 | # to the chart and its templates, including the app version.
17 | # Versions are expected to follow Semantic Versioning (https://semver.org/)
18 | version: 0.1.0
19 |
20 | # This is the version number of the application being deployed. This version number should be
21 | # incremented each time you make changes to the application. Versions are not expected to
22 | # follow Semantic Versioning. They should reflect the version the application is using.
23 | # It is recommended to use it with quotes.
24 | appVersion: '1.16.0'
25 |
--------------------------------------------------------------------------------
/helm-charts/medusa/templates/hpa.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.autoscaling.enabled }}
2 | apiVersion: autoscaling/v2
3 | kind: HorizontalPodAutoscaler
4 | metadata:
5 | name: {{ include "medusa.fullname" . }}
6 | labels:
7 | {{- include "medusa.labels" . | nindent 4 }}
8 | spec:
9 | scaleTargetRef:
10 | apiVersion: apps/v1
11 | kind: Deployment
12 | name: {{ include "medusa.fullname" . }}
13 | minReplicas: {{ .Values.autoscaling.minReplicas }}
14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }}
15 | metrics:
16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
17 | - type: Resource
18 | resource:
19 | name: cpu
20 | target:
21 | type: Utilization
22 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
23 | {{- end }}
24 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
25 | - type: Resource
26 | resource:
27 | name: memory
28 | target:
29 | type: Utilization
30 | averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
31 | {{- end }}
32 | {{- end }}
33 |
--------------------------------------------------------------------------------
/helm-charts/medusa/templates/ingress.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.ingress.enabled -}}
2 | apiVersion: networking.k8s.io/v1
3 | kind: Ingress
4 | metadata:
5 | name: {{ include "medusa.fullname" . }}
6 | labels:
7 | {{- include "medusa.labels" . | nindent 4 }}
8 | {{- with .Values.ingress.annotations }}
9 | annotations:
10 | {{- toYaml . | nindent 4 }}
11 | {{- end }}
12 | spec:
13 | {{- with .Values.ingress.className }}
14 | ingressClassName: {{ . }}
15 | {{- end }}
16 | {{- if .Values.ingress.tls }}
17 | tls:
18 | {{- range .Values.ingress.tls }}
19 | - hosts:
20 | {{- range .hosts }}
21 | - {{ . | quote }}
22 | {{- end }}
23 | secretName: {{ .secretName }}
24 | {{- end }}
25 | {{- end }}
26 | rules:
27 | {{- range .Values.ingress.hosts }}
28 | - host: {{ .host | quote }}
29 | http:
30 | paths:
31 | {{- range .paths }}
32 | - path: {{ .path }}
33 | {{- with .pathType }}
34 | pathType: {{ . }}
35 | {{- end }}
36 | backend:
37 | service:
38 | name: {{ include "medusa.fullname" $ }}
39 | port:
40 | number: {{ $.Values.service.port }}
41 | {{- end }}
42 | {{- end }}
43 | {{- end }}
44 |
--------------------------------------------------------------------------------
/helm-charts/medusa/templates/jobs/migrate.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.jobs.migrate.enabled }}
2 | apiVersion: batch/v1
3 | kind: Job
4 | metadata:
5 | name: medusa-migrate-database
6 | labels:
7 | app: medusa-migrate-database
8 | chart: {{ .Chart.Name }}-{{ .Chart.Version }}
9 | release: {{ .Release.Name }}
10 | heritage: {{ .Release.Service }}
11 | helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
12 | annotations:
13 | helm.sh/hook: "pre-install,pre-upgrade"
14 | helm.sh/hook-delete-policy: before-hook-creation
15 | spec:
16 | ttlSecondsAfterFinished: 5
17 | template:
18 | metadata:
19 | labels:
20 | app: medusa-migrate-database
21 | release: {{ .Release.Name }}
22 | app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
23 | app.kubernetes.io/instance: {{ .Release.Name | quote }}
24 | helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
25 | spec:
26 | restartPolicy: Never
27 | containers:
28 | - name: api
29 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}"
30 | imagePullPolicy: "{{ .Values.image.pullPolicy }}"
31 | resources:
32 | {{- toYaml .Values.jobs.migrate.resources | nindent 12 }}
33 | command: ["sh", "-c"]
34 | args: ["yarn migrate:prod"]
35 | env:
36 | {{- range .Values.env }}
37 | - name: {{ .name | quote }}
38 | value: {{ .value | quote }}
39 | {{- end }}
40 | {{- with .Values.imagePullSecrets }}
41 | imagePullSecrets:
42 | {{- toYaml . | nindent 8 }}
43 | {{- end }}
44 | {{- end }}
45 |
--------------------------------------------------------------------------------
/helm-charts/medusa/templates/jobs/seed.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.jobs.seed.enabled }}
2 | apiVersion: batch/v1
3 | kind: Job
4 | metadata:
5 | name: medusa-seed-db
6 | labels:
7 | app: medusa-seed-db
8 | chart: {{ .Chart.Name }}-{{ .Chart.Version }}
9 | release: {{ .Release.Name }}
10 | heritage: {{ .Release.Service }}
11 | helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
12 | annotations:
13 | helm.sh/hook: "pre-install"
14 | helm.sh/hook-weight: "0"
15 | helm.sh/hook-delete-policy: before-hook-creation
16 | spec:
17 | ttlSecondsAfterFinished: 5
18 | template:
19 | metadata:
20 | labels:
21 | app: medusa-seed-db
22 | release: {{ .Release.Name }}
23 | app.kubernetes.io/managed-by: {{ .Release.Service | quote }}
24 | app.kubernetes.io/instance: {{ .Release.Name | quote }}
25 | helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
26 | spec:
27 | restartPolicy: Never
28 | containers:
29 | - name: api
30 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}"
31 | imagePullPolicy: {{ .Values.image.pullPolicy }}
32 | resources:
33 | {{- toYaml .Values.resources | nindent 12 }}
34 | command: ["sh", "-c"]
35 | args: ["yarn seed:prod"]
36 | env:
37 | {{- range .Values.env }}
38 | - name: {{ .name | quote }}
39 | value: {{ .value | quote }}
40 | {{- end }}
41 | {{- with .Values.imagePullSecrets }}
42 | imagePullSecrets:
43 | {{- toYaml . | nindent 8 }}
44 | {{- end }}
45 | {{- end }}
--------------------------------------------------------------------------------
/helm-charts/medusa/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ include "medusa.fullname" . }}
5 | labels: {{- include "medusa.labels" . | nindent 4 }}
6 | spec:
7 | type: {{ .Values.service.type }}
8 | ports:
9 | - port: {{ .Values.service.port }}
10 | targetPort: http
11 | protocol: TCP
12 | name: http
13 | selector: {{- include "medusa.selectorLabels" . | nindent 4 }}
14 |
--------------------------------------------------------------------------------
/helm-charts/medusa/templates/serviceaccount.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.serviceAccount.create -}}
2 | apiVersion: v1
3 | kind: ServiceAccount
4 | metadata:
5 | name: {{ include "medusa.serviceAccountName" . }}
6 | labels:
7 | {{- include "medusa.labels" . | nindent 4 }}
8 | {{- with .Values.serviceAccount.annotations }}
9 | annotations:
10 | {{- toYaml . | nindent 4 }}
11 | {{- end }}
12 | automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
13 | {{- end }}
14 |
--------------------------------------------------------------------------------
/helm-charts/medusa/templates/tests/test-connection.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: "{{ include "medusa.fullname" . }}-test-connection"
5 | labels:
6 | {{- include "medusa.labels" . | nindent 4 }}
7 | annotations:
8 | "helm.sh/hook": test
9 | spec:
10 | containers:
11 | - name: wget
12 | image: busybox
13 | command: ['wget']
14 | args: ['{{ include "medusa.fullname" . }}:{{ .Values.service.port }}']
15 | restartPolicy: Never
16 |
--------------------------------------------------------------------------------
/helm-charts/storefront/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/helm-charts/storefront/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: medusa-storefront
3 | description: A Helm chart for Kubernetes
4 |
5 | # A chart can be either an 'application' or a 'library' chart.
6 | #
7 | # Application charts are a collection of templates that can be packaged into versioned archives
8 | # to be deployed.
9 | #
10 | # Library charts provide useful utilities or functions for the chart developer. They're included as
11 | # a dependency of application charts to inject those utilities and functions into the rendering
12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed.
13 | type: application
14 |
15 | # This is the chart version. This version number should be incremented each time you make changes
16 | # to the chart and its templates, including the app version.
17 | # Versions are expected to follow Semantic Versioning (https://semver.org/)
18 | version: 0.0.1
19 |
20 | # This is the version number of the application being deployed. This version number should be
21 | # incremented each time you make changes to the application. Versions are not expected to
22 | # follow Semantic Versioning. They should reflect the version the application is using.
23 | # It is recommended to use it with quotes.
24 | appVersion: '1.16.0'
25 |
--------------------------------------------------------------------------------
/helm-charts/storefront/templates/hpa.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.autoscaling.enabled }}
2 | apiVersion: autoscaling/v2beta1
3 | kind: HorizontalPodAutoscaler
4 | metadata:
5 | name: {{ include "storefront.name" . }}
6 | labels:
7 | {{- include "storefront.labels" . | nindent 4 }}
8 | spec:
9 | scaleTargetRef:
10 | apiVersion: apps/v1
11 | kind: Deployment
12 | name: {{ include "storefront.name" . }}
13 | minReplicas: {{ .Values.autoscaling.minReplicas }}
14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }}
15 | metrics:
16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
17 | - type: Resource
18 | resource:
19 | name: cpu
20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
21 | {{- end }}
22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
23 | - type: Resource
24 | resource:
25 | name: memory
26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
27 | {{- end }}
28 | {{- end }}
29 |
--------------------------------------------------------------------------------
/helm-charts/storefront/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ include "storefront.name" . }}
5 | labels: {{- include "storefront.labels" . | nindent 4 }}
6 | spec:
7 | type: {{ .Values.service.type }}
8 | ports:
9 | - port: {{ .Values.service.port }}
10 | targetPort: http
11 | protocol: TCP
12 | name: http
13 | selector: {{- include "storefront.selectorLabels" . | nindent 4 }}
14 |
--------------------------------------------------------------------------------
/helm-charts/storefront/templates/serviceaccount.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.serviceAccount.create -}}
2 | apiVersion: v1
3 | kind: ServiceAccount
4 | metadata:
5 | name: {{ include "storefront.serviceAccountName" . }}
6 | labels:
7 | {{- include "storefront.labels" . | nindent 4 }}
8 | {{- with .Values.serviceAccount.annotations }}
9 | annotations:
10 | {{- toYaml . | nindent 4 }}
11 | {{- end }}
12 | {{- end }}
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "private": true,
4 | "scripts": {
5 | "build": "turbo build",
6 | "start": "turbo start",
7 | "dev": "turbo dev",
8 | "lint": "turbo lint",
9 | "generate-env": "cp ./apps/medusa/.env.template ./apps/medusa/.env && cp ./apps/storefront/.env.template ./apps/storefront/.env",
10 | "medusa:init": "turbo run medusa:init --filter=medusa",
11 | "format": "turbo run format",
12 | "clean": "find . -name \"node_modules\" -type d -prune -exec rm -rf '{}'",
13 | "typecheck": "turbo run typecheck"
14 | },
15 | "devDependencies": {
16 | "@biomejs/biome": "1.9.3",
17 | "prettier": "^3.2.5",
18 | "turbo": "^2.1.2",
19 | "typescript": "^5.6.2"
20 | },
21 | "engines": {
22 | "node": ">=20"
23 | },
24 | "resolutions": {
25 | "@medusajs/js-sdk": "2.7.0",
26 | "@medusajs/types": "2.7.0"
27 | },
28 | "packageManager": "yarn@4.5.0",
29 | "workspaces": [
30 | "apps/*",
31 | "packages/*"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "ui": "stream",
4 | "tasks": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "inputs": ["$TURBO_DEFAULT$", ".env*"],
8 | "outputs": ["build", "dist"]
9 | },
10 | "lint": {
11 | "dependsOn": ["^lint"]
12 | },
13 | "format": {
14 | "dependsOn": ["^format"]
15 | },
16 | "start": {
17 | "dependsOn": ["^start"]
18 | },
19 | "dev": {
20 | "cache": false,
21 | "persistent": true
22 | },
23 | "medusa:init": {
24 | "cache": false
25 | },
26 | "typecheck": {
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------