├── .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 |
16 |
17 | 18 |
19 |
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 | 5 | 9 | 13 | 17 | 23 | 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 | 5 | Facebook 6 | 10 | 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 | 5 | LinkedIn 6 | 10 | 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 | 5 | Pinterest 6 | 10 | 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 | 5 | Snapchat 6 | 10 | 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 | 5 | TikTok 6 | 10 | 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 | 5 | Twitter 6 | 10 | 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 | 5 | YouTube 6 | 10 | 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) =>