Repository: Agilo/fashion-starter Branch: master Commit: a2c31cc781cb Files: 379 Total size: 1002.1 KB Directory structure: gitextract_9kshcczh/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ └── node.js.yml ├── .gitignore ├── LICENSE ├── README.md ├── medusa/ │ ├── .gitignore │ ├── .npmrc │ ├── .vscode/ │ │ └── settings.json │ ├── .yarnrc.yml │ ├── README.md │ ├── docker-compose.yml │ ├── instrumentation.js │ ├── integration-tests/ │ │ └── http/ │ │ ├── README.md │ │ └── health.spec.ts │ ├── jest.config.js │ ├── medusa-config.js │ ├── package.json │ ├── src/ │ │ ├── admin/ │ │ │ ├── README.md │ │ │ ├── components/ │ │ │ │ ├── EditMaterialDrawer.tsx │ │ │ │ ├── Form/ │ │ │ │ │ ├── Form.tsx │ │ │ │ │ ├── ImageField.tsx │ │ │ │ │ ├── InputField.tsx │ │ │ │ │ ├── SelectField.tsx │ │ │ │ │ ├── SubmitButton.tsx │ │ │ │ │ └── TextareaField.tsx │ │ │ │ └── QueryClientProvider.tsx │ │ │ ├── hooks/ │ │ │ │ ├── fashion.ts │ │ │ │ └── images.ts │ │ │ ├── routes/ │ │ │ │ └── fashion/ │ │ │ │ ├── [id]/ │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── tsconfig.json │ │ │ └── widgets/ │ │ │ ├── collection-details.tsx │ │ │ ├── product-fashion.tsx │ │ │ └── product-type-details.tsx │ │ ├── api/ │ │ │ ├── README.md │ │ │ ├── admin/ │ │ │ │ ├── custom/ │ │ │ │ │ ├── collections/ │ │ │ │ │ │ └── [collectionId]/ │ │ │ │ │ │ └── details/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── index-products/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── product-types/ │ │ │ │ │ └── [productTypeId]/ │ │ │ │ │ └── details/ │ │ │ │ │ └── route.ts │ │ │ │ ├── fashion/ │ │ │ │ │ ├── [id]/ │ │ │ │ │ │ ├── colors/ │ │ │ │ │ │ │ ├── [colorId]/ │ │ │ │ │ │ │ │ ├── restore/ │ │ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ ├── restore/ │ │ │ │ │ │ │ └── route.ts │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ └── products/ │ │ │ │ └── [id]/ │ │ │ │ └── fashion/ │ │ │ │ └── route.ts │ │ │ ├── middlewares.ts │ │ │ └── store/ │ │ │ └── custom/ │ │ │ ├── customer/ │ │ │ │ └── send-welcome-email/ │ │ │ │ └── route.ts │ │ │ ├── fashion/ │ │ │ │ └── [productHandle]/ │ │ │ │ └── route.ts │ │ │ ├── product-types/ │ │ │ │ ├── [id]/ │ │ │ │ │ └── route.ts │ │ │ │ ├── helpers.ts │ │ │ │ ├── middlewares.ts │ │ │ │ ├── query-config.ts │ │ │ │ ├── route.ts │ │ │ │ └── validators.ts │ │ │ └── stripe/ │ │ │ ├── get-payment-method/ │ │ │ │ └── [id]/ │ │ │ │ └── route.ts │ │ │ └── set-payment-method/ │ │ │ └── route.ts │ │ ├── jobs/ │ │ │ └── README.md │ │ ├── links/ │ │ │ └── README.md │ │ ├── modules/ │ │ │ ├── README.md │ │ │ ├── fashion/ │ │ │ │ ├── index.ts │ │ │ │ ├── migrations/ │ │ │ │ │ ├── .snapshot-medusa.json │ │ │ │ │ └── Migration20241002190028.ts │ │ │ │ ├── models/ │ │ │ │ │ ├── color.ts │ │ │ │ │ └── material.ts │ │ │ │ └── service.ts │ │ │ ├── meilisearch/ │ │ │ │ ├── index.ts │ │ │ │ ├── loader.ts │ │ │ │ ├── service.ts │ │ │ │ └── types.ts │ │ │ └── resend/ │ │ │ ├── emails/ │ │ │ │ ├── auth-email-confirm.tsx │ │ │ │ ├── auth-forgot-password.tsx │ │ │ │ ├── auth-password-reset.tsx │ │ │ │ ├── components/ │ │ │ │ │ └── EmailLayout.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── order-placed.tsx │ │ │ │ ├── order-update.tsx │ │ │ │ └── welcome.tsx │ │ │ ├── index.ts │ │ │ └── service.tsx │ │ ├── scripts/ │ │ │ ├── README.md │ │ │ ├── index-products.ts │ │ │ └── seed.ts │ │ ├── subscribers/ │ │ │ ├── README.md │ │ │ ├── auth-password-reset-notification.ts │ │ │ ├── customer-welcome-notification.ts │ │ │ ├── index-products.ts │ │ │ └── order-placed-notification.ts │ │ └── workflows/ │ │ ├── README.md │ │ ├── emit-customer-welcome-event.ts │ │ └── index-products.ts │ └── tsconfig.json └── storefront/ ├── .github/ │ ├── scripts/ │ │ └── medusa-config.js │ └── workflows/ │ └── test-e2e.yaml ├── .gitignore ├── .prettierrc ├── .yarnrc.yml ├── LICENSE ├── README.md ├── check-env-variables.js ├── e2e/ │ ├── README.md │ ├── data/ │ │ ├── reset.ts │ │ └── seed.ts │ ├── fixtures/ │ │ ├── account/ │ │ │ ├── account-page.ts │ │ │ ├── addresses-page.ts │ │ │ ├── index.ts │ │ │ ├── login-page.ts │ │ │ ├── modals/ │ │ │ │ └── address-modal.ts │ │ │ ├── order-page.ts │ │ │ ├── orders-page.ts │ │ │ ├── overview-page.ts │ │ │ ├── profile-page.ts │ │ │ └── register-page.ts │ │ ├── base/ │ │ │ ├── base-modal.ts │ │ │ ├── base-page.ts │ │ │ ├── cart-dropdown.ts │ │ │ ├── nav-menu.ts │ │ │ └── search-modal.ts │ │ ├── cart-page.ts │ │ ├── category-page.ts │ │ ├── checkout-page.ts │ │ ├── index.ts │ │ ├── modals/ │ │ │ └── mobile-actions-modal.ts │ │ ├── order-page.ts │ │ ├── product-page.ts │ │ └── store-page.ts │ ├── index.ts │ ├── tests/ │ │ ├── authenticated/ │ │ │ ├── address.spec.ts │ │ │ ├── orders.spec.ts │ │ │ └── profile.spec.ts │ │ ├── global/ │ │ │ ├── public-setup.ts │ │ │ ├── setup.ts │ │ │ └── teardown.ts │ │ └── public/ │ │ ├── cart.spec.ts │ │ ├── checkout.spec.ts │ │ ├── discount.spec.ts │ │ ├── giftcard.spec.ts │ │ ├── login.spec.ts │ │ ├── register.spec.ts │ │ └── search.spec.ts │ └── utils/ │ ├── index.ts │ └── locators.ts ├── eslint.config.cjs ├── next-env.d.ts ├── next-sitemap.js ├── next.config.js ├── package.json ├── playwright.config.ts ├── postcss.config.js ├── src/ │ ├── app/ │ │ ├── [countryCode]/ │ │ │ ├── (checkout)/ │ │ │ │ ├── checkout/ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ └── not-found.tsx │ │ │ └── (main)/ │ │ │ ├── about/ │ │ │ │ └── page.tsx │ │ │ ├── account/ │ │ │ │ ├── layout.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── my-orders/ │ │ │ │ │ ├── [orderId]/ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── auth/ │ │ │ │ ├── forgot-password/ │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── reset/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── login/ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── register/ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── reset-password/ │ │ │ │ └── page.tsx │ │ │ ├── cart/ │ │ │ │ ├── loading.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ └── page.tsx │ │ │ ├── collections/ │ │ │ │ └── [handle]/ │ │ │ │ └── page.tsx │ │ │ ├── cookie-policy/ │ │ │ │ └── page.tsx │ │ │ ├── inspiration/ │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── not-found.tsx │ │ │ ├── order/ │ │ │ │ └── confirmed/ │ │ │ │ └── [id]/ │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── privacy-policy/ │ │ │ │ └── page.tsx │ │ │ ├── products/ │ │ │ │ └── [handle]/ │ │ │ │ └── page.tsx │ │ │ ├── search/ │ │ │ │ └── page.tsx │ │ │ ├── store/ │ │ │ │ └── page.tsx │ │ │ └── terms-of-use/ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── not-found.tsx │ │ └── robots.ts │ ├── components/ │ │ ├── Button.tsx │ │ ├── Carousel.tsx │ │ ├── CartDrawer.tsx │ │ ├── CartIcon.tsx │ │ ├── CollectionsSection.tsx │ │ ├── Dialog.tsx │ │ ├── Drawer.tsx │ │ ├── Footer.tsx │ │ ├── Forms.tsx │ │ ├── Header.tsx │ │ ├── HeaderDrawer.tsx │ │ ├── HeaderWrapper.tsx │ │ ├── Icon.tsx │ │ ├── IconCircle.tsx │ │ ├── InputNumberField.tsx │ │ ├── Layout.tsx │ │ ├── Link.tsx │ │ ├── LocalizedLink.tsx │ │ ├── NewsletterForm.tsx │ │ ├── NumberField.tsx │ │ ├── ProductPageGallery.tsx │ │ ├── RegionSwitcher.tsx │ │ ├── SearchField.tsx │ │ ├── icons/ │ │ │ ├── ArrowLeft.tsx │ │ │ ├── ArrowRight.tsx │ │ │ ├── ArrowUpRight.tsx │ │ │ ├── Calendar.tsx │ │ │ ├── Case.tsx │ │ │ ├── Check.tsx │ │ │ ├── ChevronDown.tsx │ │ │ ├── ChevronLeft.tsx │ │ │ ├── ChevronRight.tsx │ │ │ ├── ChevronUp.tsx │ │ │ ├── Close.tsx │ │ │ ├── CreditCard.tsx │ │ │ ├── Heart.tsx │ │ │ ├── Info.tsx │ │ │ ├── Loader.tsx │ │ │ ├── MapPin.tsx │ │ │ ├── Menu.tsx │ │ │ ├── Minus.tsx │ │ │ ├── Package.tsx │ │ │ ├── Plus.tsx │ │ │ ├── Receipt.tsx │ │ │ ├── Search.tsx │ │ │ ├── Sliders.tsx │ │ │ ├── Trash.tsx │ │ │ ├── Truck.tsx │ │ │ ├── Undo.tsx │ │ │ └── User.tsx │ │ └── ui/ │ │ ├── Checkbox.tsx │ │ ├── Modal.tsx │ │ ├── Radio.tsx │ │ ├── Select.tsx │ │ ├── Skeleton.tsx │ │ ├── Slider.tsx │ │ ├── Tag.tsx │ │ └── TagList.tsx │ ├── hooks/ │ │ ├── cart.ts │ │ ├── country-code.tsx │ │ ├── customer.ts │ │ └── store.tsx │ ├── lib/ │ │ ├── config.ts │ │ ├── constants.tsx │ │ ├── data/ │ │ │ ├── cart.ts │ │ │ ├── categories.ts │ │ │ ├── collections.ts │ │ │ ├── cookies.ts │ │ │ ├── customer.ts │ │ │ ├── fulfillment.ts │ │ │ ├── orders.ts │ │ │ ├── payment.ts │ │ │ ├── product-types.ts │ │ │ ├── products.ts │ │ │ └── regions.ts │ │ ├── search-client.ts │ │ ├── util/ │ │ │ ├── collections.ts │ │ │ ├── compare-addresses.ts │ │ │ ├── enrich-line-items.ts │ │ │ ├── env.ts │ │ │ ├── get-precentage-diff.ts │ │ │ ├── get-product-price.ts │ │ │ ├── inventory.ts │ │ │ ├── isEmpty.ts │ │ │ ├── medusa-error.ts │ │ │ ├── money.ts │ │ │ ├── react-query.tsx │ │ │ ├── repeat.ts │ │ │ └── sort-products.ts │ │ └── webmcp/ │ │ ├── WebMCPProvider.tsx │ │ ├── is-supported.ts │ │ ├── register-tools.ts │ │ ├── tools/ │ │ │ ├── cart.ts │ │ │ ├── checkout.ts │ │ │ ├── products-search.ts │ │ │ └── promotion.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── middleware.ts │ ├── modules/ │ │ ├── account/ │ │ │ └── components/ │ │ │ ├── AddressMultiple.tsx │ │ │ ├── AddressSingle.tsx │ │ │ ├── DefaultBillingAddressSelect.tsx │ │ │ ├── DefaultShippingAddressSelect.tsx │ │ │ ├── DeleteAddressButton.tsx │ │ │ ├── PersonalInfoForm.tsx │ │ │ ├── RequestPasswordResetButton.tsx │ │ │ ├── SidebarNav.tsx │ │ │ ├── SignOutButton.tsx │ │ │ └── UpsertAddressForm.tsx │ │ ├── auth/ │ │ │ └── components/ │ │ │ ├── ForgotPasswordForm.tsx │ │ │ ├── LoginForm.tsx │ │ │ ├── ResetPasswordForm.tsx │ │ │ └── SignUpForm.tsx │ │ ├── cart/ │ │ │ ├── components/ │ │ │ │ ├── cart-totals/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── discount-code/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── empty-cart-message/ │ │ │ │ │ └── index.tsx │ │ │ │ └── item/ │ │ │ │ └── index.tsx │ │ │ ├── templates/ │ │ │ │ ├── index.tsx │ │ │ │ ├── items.tsx │ │ │ │ └── summary.tsx │ │ │ └── utils/ │ │ │ └── getCheckoutStep.tsx │ │ ├── checkout/ │ │ │ ├── components/ │ │ │ │ ├── addresses/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── billing_address/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── checkout-form/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── checkout-summary-wrapper/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── country-select/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── discount-code/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── email/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── error-message/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── mobile-checkout-summary-wrapper/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── payment/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── payment-button/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── payment-card-button/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── payment-container/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── payment-test/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── payment-wrapper/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── stripe-wrapper.tsx │ │ │ │ ├── review/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── shipping/ │ │ │ │ │ └── index.tsx │ │ │ │ └── shipping-address/ │ │ │ │ └── index.tsx │ │ │ └── templates/ │ │ │ ├── checkout-summary/ │ │ │ │ └── index.tsx │ │ │ └── mobile-checkout-summary/ │ │ │ └── index.tsx │ │ ├── collections/ │ │ │ └── templates/ │ │ │ └── index.tsx │ │ ├── common/ │ │ │ ├── components/ │ │ │ │ ├── cart-totals/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── delete-button/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── line-item-unit-price/ │ │ │ │ │ └── index.tsx │ │ │ │ └── submit-button/ │ │ │ │ └── index.tsx │ │ │ └── icons/ │ │ │ ├── bancontact.tsx │ │ │ ├── ideal.tsx │ │ │ ├── paypal.tsx │ │ │ ├── placeholder-image.tsx │ │ │ └── spinner.tsx │ │ ├── header/ │ │ │ └── components/ │ │ │ └── LoginLink.tsx │ │ ├── order/ │ │ │ ├── components/ │ │ │ │ ├── OrderTotals.tsx │ │ │ │ ├── item/ │ │ │ │ │ └── index.tsx │ │ │ │ └── payment-details/ │ │ │ │ └── index.tsx │ │ │ └── templates/ │ │ │ └── order-completed-template.tsx │ │ ├── products/ │ │ │ ├── components/ │ │ │ │ ├── image-gallery/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── product-actions/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── product-preview/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── product-price/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── related-products/ │ │ │ │ │ └── index.tsx │ │ │ │ └── thumbnail/ │ │ │ │ └── index.tsx │ │ │ └── templates/ │ │ │ ├── index.tsx │ │ │ ├── product-actions-wrapper/ │ │ │ │ └── index.tsx │ │ │ └── product-info/ │ │ │ └── index.tsx │ │ ├── skeletons/ │ │ │ ├── components/ │ │ │ │ ├── skeleton-button/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-cart-item/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-cart-totals/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-mobile-summary-trigger/ │ │ │ │ │ └── index.tsx │ │ │ │ ├── skeleton-order-summary/ │ │ │ │ │ └── index.tsx │ │ │ │ └── skeleton-product-preview/ │ │ │ │ └── index.tsx │ │ │ └── templates/ │ │ │ ├── skeleton-account-page/ │ │ │ │ └── index.tsx │ │ │ ├── skeleton-cart-page/ │ │ │ │ └── index.tsx │ │ │ ├── skeleton-checkout-summary/ │ │ │ │ └── index.tsx │ │ │ ├── skeleton-order-confirmed/ │ │ │ │ └── index.tsx │ │ │ ├── skeleton-product-grid/ │ │ │ │ └── index.tsx │ │ │ └── skeleton-related-products/ │ │ │ └── index.tsx │ │ └── store/ │ │ ├── components/ │ │ │ ├── collections-slider/ │ │ │ │ └── index.tsx │ │ │ ├── no-results.tsx/ │ │ │ │ └── index.tsx │ │ │ ├── pagination/ │ │ │ │ └── index.tsx │ │ │ └── refinement-list/ │ │ │ ├── category-filter/ │ │ │ │ └── index.tsx │ │ │ ├── collection-filter/ │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ ├── mobile-filters/ │ │ │ │ └── index.tsx │ │ │ ├── mobile-sort/ │ │ │ │ └── index.tsx │ │ │ ├── sort-products/ │ │ │ │ └── index.tsx │ │ │ └── type-filter/ │ │ │ └── index.tsx │ │ └── templates/ │ │ ├── index.tsx │ │ └── paginated-products.tsx │ ├── styles/ │ │ └── globals.css │ └── types/ │ └── icon.ts ├── tailwind.config.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/node.js.yml ================================================ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs name: PR test on: pull_request_target: types: [assigned, opened, synchronize, reopened] branches: [master] paths: ['storefront/**'] jobs: lint-storefront: runs-on: ubuntu-latest strategy: matrix: # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ node-version: [20.x, 22.x] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'yarn' cache-dependency-path: 'storefront/yarn.lock' - run: yarn install --frozen-lockfile working-directory: storefront - run: yarn lint working-directory: storefront env: NODE_ENV: production NEXT_PUBLIC_MEDUSA_BACKEND_URL: ${{ secrets.NEXT_PUBLIC_MEDUSA_BACKEND_URL }} NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY }} NEXT_PUBLIC_STRIPE_KEY: ${{ secrets.NEXT_PUBLIC_STRIPE_KEY }} REVALIDATE_SECRET: ${{ secrets.REVALIDATE_SECRET }} DISALLOW_ROBOTS: true NEXT_PUBLIC_DEFAULT_REGION: us NEXT_PUBLIC_FEATURE_SEARCH_ENABLED: false NEXT_PUBLIC_BASE_URL: https://fashion-starter.agilo.com ================================================ FILE: .gitignore ================================================ .DS_Store .agents/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 Agilo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

Fashion E-commerce Starter for Medusa 2.0

Figma Design Template

The **Fashion E-commerce Starter** is a modern, customizable e-commerce template built with **Medusa 2.0**. Designed around the concept of the sustainable furniture brand **Sofa Society**, this starter showcases the power of new Medusa 2.0 version. With its focus on cutting-edge design, sustainability, and personalization, Sofa Society offers users an elegant shopping experience where they can explore customizable collections, product options, and a streamlined checkout flow. This starter kit is an ideal solution for developers who need to set up a professional, feature-rich fashion e-commerce store quickly. It comes with a sleek and modern design, customizable collections, an Inspiration page, an About page, and a streamlined checkout process. The storefront is fully responsive and optimized for mobile, tablet, and desktop devices.

Table of Contents

- [Features](#features) - [Roadmap](#roadmap) - [Screenshots](#screenshots) - [Prerequisites](#prerequisites) - [Quickstart](#quickstart) - [Medusa](#medusa) - [Storefront](#storefront) - [Meilisearch](#meilisearch) ## Features - **Sleek, Modern Design**: The storefront boasts a minimalist, contemporary design that perfectly reflects **Sofa Society's** commitment to modern aesthetics and sustainability. - **Dynamic Materials and Colors**: Add richness to your product offerings by defining **materials** and **colors** for each product. Colors will be displayed using their corresponding hex codes, and each material can have multiple color options. Customers first select a material, then a color, with dynamic pricing based on their choices. - **Customizable Collections**: Easily customize the content and images for each collection. Each product page also features images and a CTA for the collection it belongs to, which can be personalized as well, creating a fully branded shopping experience. - **Premade Inspiration Page**: A beautiful, ready-to-use inspiration page helps customers explore the latest trends and styles, showcasing Sofa Society's furniture in real-world settings. - **About Page**: Share your brand’s story, values, and commitment to sustainability with a pre-built about page that captures the essence of **Sofa Society**. - **Streamlined Checkout Flow**: The checkout process is designed to be fast, intuitive, and frictionless, providing a seamless shopping experience for your customers from start to finish. - **Fully Responsive Design**: Optimized for mobile, tablet, and desktop devices, ensuring a smooth, consistent experience across all platforms. - **Stripe Integration for Payments**: Accept payments effortlessly by integrating **Stripe**. Simply add your Stripe API key to `medusa/.env` and the publishable key to `storefront/.env` to get started. - **Full E-commerce Functionality**: The starter includes all the essential e-commerce features you need, including product pages, a shopping cart, a checkout process, and order confirmation. - **Next.js and Tailwind CSS**: Built with **Next.js** v15 app router and **Tailwind CSS**, the starter is highly performant, customizable, and easy to extend with additional features. ## Roadmap - [x] **Figma Design Template**: This will enable you to easily customize the design of the storefront to match your brand. [View template](https://www.figma.com/community/file/1494273775050024009). - [x] **Search**: Integration with Meilisearch for a powerful search experience. - [x] **404 Page**: Custom 404 page for a better user experience. - [x] **Account Management**: Allow customers to create accounts, view order history, and manage their personal information. - [x] **Cart Drawer**: Cart drawer that slides in from the side where customers can view and edit their cart items. - [x] **Email Templates**: Customizable email templates for order confirmation, shipping updates, and more. - [x] **Infinite Scroll Pagination**: Improve the product discovery experience with infinite scroll pagination on store and collection pages. - [x] **Resend Integration**: Integration with Resend for sending transactional emails. ## Screenshots
Home ![Home Page](./media/home.jpeg)
About ![About Page](./media/about.jpeg)
Inspiration ![Inspiration Page](./media/inspiration.jpeg)
Collection ![Collection Page](./media/collection.jpeg)
Store ![Store Page](./media/store.jpeg)
Product ![Product Page](./media/product.jpeg)
Cart ![Cart Page](./media/cart.jpeg)
Checkout ![Checkout Page](./media/checkout.jpeg)
Checkout Review ![Checkout Review Page](./media/checkout-review.jpeg)
Order Confirmation ![Order Confirmation Page](./media/order-confirmation.jpeg)
Admin - Edit Collection ![Admin - Edit Collection](./media/admin-collection.jpeg)
Admin - Edit Product Type ![Admin - Edit Product Type](./media/admin-product-type.jpeg)
Admin - Materials ![Admin - Materials](./media/admin-materials.jpeg)
Admin - Colors ![Admin - Colors](./media/admin-colors.jpeg)
Admin - Edit Color ![Admin - Edit Color](./media/admin-edit-color.jpeg)
Admin - Product ![Admin - Product](./media/admin-product.jpeg)
Admin - Product Missing Color ![Admin - Product Missing Color](./media/product-missing-color.jpeg)
Admin - Product Add Missing Color ![Admin - Product Add Missing Color](./media/product-add-missing-color.jpeg)
## Prerequisites - Node >= 20 - Yarn >= 3.5 for Medusa, Yarn v1 for Storefront - Docker and Docker Compose - Stripe account (for payments) - httpie ## Quickstart ```bash git clone git@github.com:Agilo/fashion-starter.git ``` ### Medusa ```bash cd medusa # Create the .env file cp .env.template .env # Install dependencies yarn # Spin up the database and Redis docker-compose up -d # Build the project yarn build # Run the migrations yarn medusa db:migrate # Seed the database yarn seed # Create an user yarn medusa user -e "admin@medusa.local" -p "supersecret" # Start the development server yarn dev ``` At this point, you should be able to access the Medusa admin at http://localhost:9000/app with the credentials you just created. After logging in, you should go to http://localhost:9000/app/settings/publishable-api-keys, copy the publishable key, and paste it into the `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` env variable in the `storefront/.env.local` file. ### Storefront ```bash cd storefront # Create the .env.local file cp .env.template .env.local # Install dependencies yarn # Start the development server yarn dev ``` You should now be able to access the storefront at http://localhost:8000. ### Meilisearch ```bash # Get search api key http --auth "yoursecretmasterkey" --auth-type bearer GET http://localhost:7700/keys ``` You should go to `storefront/.env.local` file and paste obtained key into the `NEXT_PUBLIC_SEARCH_API_KEY` env variable. Also, go to the `backend/.env` file and paste admin key into `MEILISEARCH_API_KEY` ================================================ FILE: medusa/.gitignore ================================================ /dist .env .DS_Store /uploads /node_modules yarn-error.log .idea coverage !src/** ./tsconfig.tsbuildinfo package-lock.json medusa-db.sql build .cache .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/sdks !.yarn/versions .medusa /static ================================================ FILE: medusa/.npmrc ================================================ node-linker=hoisted ================================================ FILE: medusa/.vscode/settings.json ================================================ { } ================================================ FILE: medusa/.yarnrc.yml ================================================ nodeLinker: node-modules ================================================ FILE: medusa/README.md ================================================

Medusa logo

Medusa

Documentation | Website

Building blocks for digital commerce

PRs welcome! Product Hunt Discord Chat Follow @medusajs

## Compatibility This starter is compatible with versions >= 1.8.0 of `@medusajs/medusa`. ## Getting Started Visit the [Quickstart Guide](https://docs.medusajs.com/create-medusa-app) to set up a server. Visit the [Docs](https://docs.medusajs.com/development/backend/prepare-environment) to learn more about our system requirements. ## What is Medusa Medusa is a set of commerce modules and tools that allow you to build rich, reliable, and performant commerce applications without reinventing core commerce logic. The modules can be customized and used to build advanced ecommerce stores, marketplaces, or any product that needs foundational commerce primitives. All modules are open-source and freely available on npm. Learn more about [Medusa’s architecture](https://docs.medusajs.com/development/fundamentals/architecture-overview) and [commerce modules](https://docs.medusajs.com/modules/overview) in the Docs. ## Roadmap, Upgrades & Plugins You can view the planned, started and completed features in the [Roadmap discussion](https://github.com/medusajs/medusa/discussions/categories/roadmap). Follow the [Upgrade Guides](https://docs.medusajs.com/upgrade-guides/) to keep your Medusa project up-to-date. Check out all [available Medusa plugins](https://medusajs.com/plugins/). ## Community & Contributions The community and core team are available in [GitHub Discussions](https://github.com/medusajs/medusa/discussions), where you can ask for support, discuss roadmap, and share ideas. Join our [Discord server](https://discord.com/invite/medusajs) to meet other community members. ## Other channels - [GitHub Issues](https://github.com/medusajs/medusa/issues) - [Twitter](https://twitter.com/medusajs) - [LinkedIn](https://www.linkedin.com/company/medusajs) - [Medusa Blog](https://medusajs.com/blog/) ================================================ FILE: medusa/docker-compose.yml ================================================ services: postgres: image: postgres:16 ports: - 5432:5432 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: medusa volumes: - medusa-postgres-data:/var/lib/postgresql/data redis: image: redis ports: - 6379:6379 minio: image: minio/minio:RELEASE.2024-10-13T13-34-11Z ports: - 9090:9000 - 9001:9001 volumes: - medusa-minio-data:/data environment: MINIO_ROOT_USER: medusaminio MINIO_ROOT_PASSWORD: medusaminio command: server /data --console-address ":9001" healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live'] interval: 30s timeout: 20s retries: 3 createbuckets: image: minio/mc:RELEASE.2024-10-08T09-37-26Z depends_on: minio: condition: service_healthy restart: on-failure entrypoint: > /bin/sh -c " /usr/bin/mc alias set myminio http://minio:9000 medusaminio medusaminio; /usr/bin/mc mb myminio/medusa; /usr/bin/mc anonymous set public myminio/medusa; exit 0; " meilisearch: image: getmeili/meilisearch:v1.12 ports: - 7700:7700 volumes: - meili-data:/meili_data environment: MEILI_MASTER_KEY: ${MEILISEARCH_MASTER_KEY} volumes: medusa-postgres-data: medusa-minio-data: meili-data: ================================================ FILE: medusa/instrumentation.js ================================================ // Uncomment this file to enable instrumentation and observability using OpenTelemetry // Refer to the docs for installation instructions: https://docs.medusajs.com/v2/debugging-and-testing/instrumentation // const { registerOtel } = require("@medusajs/medusa") // // If using an exporter other than Zipkin, require it here. // const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin') // // If using an exporter other than Zipkin, initialize it here. // const exporter = new ZipkinExporter({ // serviceName: 'my-medusa-project', // }) // export function register() { // registerOtel({ // serviceName: 'medusajs', // // pass exporter // exporter, // instrument: { // http: true, // workflows: true, // remoteQuery: true // }, // }) // } ================================================ FILE: medusa/integration-tests/http/README.md ================================================ # Integration Tests The `medusa-test-utils` package provides utility functions to create integration tests for your API routes and workflows. For example: ```ts import { medusaIntegrationTestRunner } from "medusa-test-utils" medusaIntegrationTestRunner({ testSuite: ({ api, getContainer }) => { describe("Custom endpoints", () => { describe("GET /store/custom", () => { it("returns correct message", async () => { const response = await api.get( `/store/custom` ) expect(response.status).toEqual(200) expect(response.data).toHaveProperty("message") expect(response.data.message).toEqual("Hello, World!") }) }) }) } }) ``` Learn more in [this documentation](https://docs.medusajs.com/v2/debugging-and-testing/testing-tools/integration-tests). ================================================ FILE: medusa/integration-tests/http/health.spec.ts ================================================ import { medusaIntegrationTestRunner } from '@medusajs/test-utils'; jest.setTimeout(60 * 1000); medusaIntegrationTestRunner({ inApp: true, env: {}, testSuite: ({ api }) => { describe('Ping', () => { it('ping the server health endpoint', async () => { const response = await api.get('/health'); expect(response.status).toEqual(200); }); }); }, }); ================================================ FILE: medusa/jest.config.js ================================================ const { loadEnv } = require('@medusajs/utils') loadEnv('test', process.cwd()) module.exports = { transform: { "^.+\\.[jt]s$": [ "@swc/jest", { jsc: { parser: { syntax: "typescript", decorators: true }, }, }, ], }, testEnvironment: "node", moduleFileExtensions: ["js", "ts", "json"], modulePathIgnorePatterns: ["dist/"], } if (process.env.TEST_TYPE === "integration:http") { module.exports.testMatch = ["**/integration-tests/http/*.spec.[jt]s"] } else if (process.env.TEST_TYPE === "integration:modules") { module.exports.testMatch = ["**/src/modules/*/__tests__/**/*.[jt]s"] } else if (process.env.TEST_TYPE === "unit") { module.exports.testMatch = ["**/src/**/__tests__/**/*.unit.spec.[jt]s"] } ================================================ FILE: medusa/medusa-config.js ================================================ const { loadEnv, defineConfig } = require('@medusajs/framework/utils'); loadEnv(process.env.NODE_ENV, process.cwd()); module.exports = defineConfig({ admin: { backendUrl: process.env.BACKEND_URL ?? 'https://sofa-society-starter.medusajs.app', storefrontUrl: process.env.STOREFRONT_URL, }, projectConfig: { databaseUrl: process.env.DATABASE_URL, redisUrl: process.env.REDIS_URL, http: { storeCors: process.env.STORE_CORS, adminCors: process.env.ADMIN_CORS, authCors: process.env.AUTH_CORS, jwtSecret: process.env.JWT_SECRET || 'supersecret', cookieSecret: process.env.COOKIE_SECRET || 'supersecret', jwtExpiresIn: process.env.JWT_EXPIRES_IN || '24h', }, }, modules: [ { resolve: '@medusajs/medusa/payment', options: { providers: [ { id: 'stripe', resolve: '@medusajs/medusa/payment-stripe', options: { apiKey: process.env.STRIPE_API_KEY, webhookSecret: process.env.STRIPE_WEBHOOK_SECRET, }, }, ], }, }, { resolve: './src/modules/fashion', }, { resolve: '@medusajs/medusa/file', options: { providers: [ { resolve: '@medusajs/medusa/file-s3', id: 's3', options: { file_url: process.env.S3_FILE_URL, access_key_id: process.env.S3_ACCESS_KEY_ID, secret_access_key: process.env.S3_SECRET_ACCESS_KEY, region: process.env.S3_REGION, bucket: process.env.S3_BUCKET, endpoint: process.env.S3_ENDPOINT, additional_client_config: { forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true' ? true : undefined, }, }, }, ], }, }, { resolve: '@medusajs/medusa/notification', options: { providers: [ { resolve: './src/modules/resend', id: 'resend', options: { channels: ['email'], api_key: process.env.RESEND_API_KEY, from: process.env.RESEND_FROM, siteTitle: 'SofaSocietyCo.', companyName: 'Sofa Society', footerLinks: [ { url: 'https://agilo.com', label: 'Agilo', }, { url: 'https://www.instagram.com/agiloltd/', label: 'Instagram', }, { url: 'https://www.linkedin.com/company/agilo/', label: 'LinkedIn', }, ], }, }, ], }, }, { resolve: '@medusajs/medusa/event-bus-redis', options: { redisUrl: process.env.REDIS_URL, }, }, { resolve: '@medusajs/medusa/caching', options: { providers: [ { resolve: '@medusajs/caching-redis', id: 'caching-redis', is_default: true, options: { redisUrl: process.env.REDIS_URL, }, }, ], }, }, { resolve: '@medusajs/medusa/workflow-engine-redis', options: { redis: { redisUrl: process.env.REDIS_URL, }, }, }, { resolve: '@medusajs/medusa/locking', options: { providers: [ { resolve: '@medusajs/medusa/locking-redis', id: 'locking-redis', is_default: true, options: { redisUrl: process.env.REDIS_URL, }, }, ], }, }, { resolve: './src/modules/meilisearch', /** * @type {import('./src/modules/meilisearch/types').MeiliSearchPluginOptions} */ options: { config: { host: process.env.MEILISEARCH_HOST ?? 'https://fashion-starter-search.agilo.agency', apiKey: process.env.MEILISEARCH_API_KEY, }, settings: { products: { indexSettings: { searchableAttributes: [ 'title', 'subtitle', 'description', 'collection', 'categories', 'type', 'tags', 'variants', 'sku', ], displayedAttributes: [ 'id', 'title', 'handle', 'subtitle', 'description', 'is_giftcard', 'status', 'thumbnail', 'collection', 'collection_handle', 'categories', 'categories_handle', 'type', 'tags', 'variants', 'sku', ], }, primaryKey: 'id', /** * @param {import('@medusajs/types').ProductDTO} product */ transformer: (product) => { return { id: product.id, title: product.title, handle: product.handle, subtitle: product.subtitle, description: product.description, is_giftcard: product.is_giftcard, status: product.status, thumbnail: product.images?.[0]?.url ?? null, collection: product.collection.title, collection_handle: product.collection.handle, categories: product.categories?.map((category) => category.name) ?? [], categories_handle: product.categories?.map((category) => category.handle) ?? [], type: product.type?.value, tags: product.tags.map((tag) => tag.value), variants: product.variants.map((variant) => variant.title), sku: product.variants .filter( (variant) => typeof variant.sku === 'string' && variant.sku, ) .map((variant) => variant.sku), }; }, }, }, }, }, ], plugins: [ { resolve: '@agilo/medusa-analytics-plugin', options: {}, }, ], }); ================================================ FILE: medusa/package.json ================================================ { "name": "fashion-starter-medusa", "version": "2.0.0", "description": "A starter for Medusa projects.", "author": "Medusa (https://medusajs.com)", "license": "MIT", "keywords": [ "sqlite", "postgres", "typescript", "ecommerce", "headless", "medusa" ], "scripts": { "build": "medusa build", "seed": "medusa exec ./src/scripts/seed.ts", "start": "medusa start", "dev": "medusa develop", "emails:dev": "email dev --dir=src/modules/resend/emails", "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit", "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" }, "dependencies": { "@agilo/medusa-analytics-plugin": "^1.4.0", "@medusajs/admin-sdk": "2.13.1", "@medusajs/cli": "2.13.1", "@medusajs/framework": "2.13.1", "@medusajs/icons": "2.13.1", "@medusajs/medusa": "2.13.1", "@medusajs/types": "2.13.1", "@medusajs/ui": "4.1.1", "@react-email/components": "^1.0.7", "@tanstack/react-query": "5.64.2", "meilisearch": "0.55.0", "posthog-node": "^5.24.15", "react-dropzone": "^15.0.0", "resend": "^6.9.2" }, "devDependencies": { "@medusajs/test-utils": "2.13.1", "@react-email/preview-server": "^5.2.8", "@swc/core": "^1.15.11", "@swc/jest": "^0.2.39", "@types/jest": "^29.5.14", "@types/node": "^22.19.11", "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", "jest": "^29.7.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-email": "5.2.8", "ts-node": "^10.9.2", "typescript": "^5.9.3" }, "engines": { "node": ">=20" }, "packageManager": "yarn@4.7.0" } ================================================ FILE: medusa/src/admin/README.md ================================================ # Admin Customizations You can extend the Medusa Admin to add widgets and new pages. Your customizations interact with API routes to provide merchants with custom functionalities. ## Example: Create a Widget A widget is a React component that can be injected into an existing page in the admin dashboard. For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: ```tsx title="src/admin/widgets/product-widget.tsx" import { defineWidgetConfig } from "@medusajs/admin-sdk" // The widget const ProductWidget = () => { return (

Product Widget

) } // The widget's configurations export const config = defineWidgetConfig({ zone: "product.details.after", }) export default ProductWidget ``` This inserts a widget with the text “Product Widget” at the end of a product’s details page. ================================================ FILE: medusa/src/admin/components/EditMaterialDrawer.tsx ================================================ import * as React from 'react'; import { z } from 'zod'; import { Button, Drawer } from '@medusajs/ui'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Form } from './Form/Form'; import { InputField } from './Form/InputField'; export const materialFormSchema = z.object({ name: z.string(), }); export const EditMaterialDrawer: React.FC<{ id: string; initialValues: z.infer; children: React.ReactNode; }> = ({ id, initialValues, children }) => { const queryClient = useQueryClient(); const [isDrawerOpen, setIsDrawerOpen] = React.useState(false); const updateMaterialMutation = useMutation({ mutationKey: ['fashion', 'update'], mutationFn: async (values: z.infer) => { return fetch(`/admin/fashion/${id}`, { method: 'POST', body: JSON.stringify(values), credentials: 'include', }).then((res) => res.json()); }, onSuccess: async () => { await queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0] === 'fashion', }); }, }); return ( {children} Edit Material
{ await updateMaterialMutation.mutateAsync(values); setIsDrawerOpen(false); }} formProps={{ id: `edit-material-${id}-form`, }} defaultValues={initialValues} >
); }; ================================================ FILE: medusa/src/admin/components/Form/Form.tsx ================================================ import * as React from 'react'; import { FormProvider, useForm, UseFormProps, DefaultValues, UseFormReturn, } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; export type FormProps> = UseFormProps< z.infer > & { schema: T; onSubmit: ( values: z.infer, form: UseFormReturn>, ) => void | Promise; defaultValues?: DefaultValues>; children?: React.ReactNode; formProps?: Omit, 'onSubmit'>; }; export const Form = >({ schema, onSubmit, children, formProps, ...props }: FormProps) => { const form = useForm({ resolver: zodResolver(schema), ...props, }); const submitHandler = React.useCallback( (values: z.infer) => { return onSubmit(values, form); }, [onSubmit, form], ); const onFormSubmit: React.FormEventHandler = React.useCallback( (event) => { event.preventDefault(); event.stopPropagation(); form.handleSubmit(submitHandler)(event); }, [form, submitHandler], ); return (
{children}
); }; ================================================ FILE: medusa/src/admin/components/Form/ImageField.tsx ================================================ import { Label, Button, clx } from '@medusajs/ui'; import { DropzoneProps, useDropzone } from 'react-dropzone'; import { useController, useFormContext } from 'react-hook-form'; import { z } from 'zod'; import { useAdminUploadImage } from '../../hooks/images'; export interface ImageFieldProps { className?: string; name: string; label?: string; dropzoneProps?: Omit; dropzoneRootClassName?: string; sizeRecommendation?: React.ReactNode; isRequired?: boolean; } export interface ImageFieldValue { id: string; url: string; } export const imageFieldSchema = (params?: z.RawCreateParams) => z.object( { id: z.string(), url: z.string().url(), }, params, ); export const ImageField: React.FC = ({ className, name, label, dropzoneProps, dropzoneRootClassName, sizeRecommendation = '1200 x 1600 (3:4) recommended, up to 10MB each', isRequired, }) => { const form = useFormContext(); const { field, fieldState } = useController<{ __name__: { id: string; url: string; }; }>({ name: name as '__name__' }); const uploadFileMutation = useAdminUploadImage({ onSuccess: (data) => { field.onChange({ id: data.files[0].id, url: data.files[0].url, }); }, onError(error) { form.setError(name, { message: error.message, type: 'upload_error', }); }, }); const { getRootProps, getInputProps, open } = useDropzone({ accept: { 'image/*': ['.jpg', '.jpeg', '.png'], }, ...dropzoneProps, maxFiles: 1, onDropAccepted(files) { uploadFileMutation.mutate({ files, }); }, }); return (
{typeof label !== 'undefined' && ( )}
{field.value && typeof field.value !== 'string' ? ( ) : (

Drop your image here, or{' '} click to browse

{sizeRecommendation}
)}
{field.value && typeof field.value !== 'string' && (
)} {fieldState.error && (
{fieldState.error.message}
)}
); }; ================================================ FILE: medusa/src/admin/components/Form/InputField.tsx ================================================ import { Input, Label, clx } from '@medusajs/ui'; import { useController, ControllerRenderProps } from 'react-hook-form'; export interface InputFieldProps { className?: string; name: string; label?: string; type?: React.ComponentProps['type']; labelProps?: React.ComponentProps; inputProps?: Omit< React.ComponentProps, 'name' | 'id' | 'type' | keyof ControllerRenderProps >; isRequired?: boolean; suffix?: React.ReactNode; } export const InputField: React.FC = ({ className, name, label, type, labelProps, inputProps, isRequired, suffix, }) => { const { field, fieldState } = useController<{ __name__: string }, '__name__'>( { name: name as '__name__' } ); const inputEl = ( ); return (
{typeof label !== 'undefined' && ( )} {suffix ? (
{inputEl}
{suffix}
) : ( inputEl )} {fieldState.error && (
{fieldState.error.message}
)}
); }; ================================================ FILE: medusa/src/admin/components/Form/SelectField.tsx ================================================ import { Label, clx, Select } from '@medusajs/ui'; import { useController, ControllerRenderProps } from 'react-hook-form'; export interface SelectFieldProps { className?: string; name: string; label?: string; labelProps?: React.ComponentProps; selectProps?: Omit< React.ComponentProps, 'name' | 'id' | keyof ControllerRenderProps >; isRequired?: boolean; children?: React.ReactNode; } export const SelectField: React.FC = ({ className, name, label, labelProps, selectProps, isRequired, children, }) => { const { field, fieldState } = useController<{ __name__: string }, '__name__'>( { name: name as '__name__' }, ); return (
{typeof label !== 'undefined' && ( )} {fieldState.error && (
{fieldState.error.message}
)}
); }; ================================================ FILE: medusa/src/admin/components/Form/SubmitButton.tsx ================================================ import { Button } from '@medusajs/ui'; import { useFormState } from 'react-hook-form'; export const SubmitButton: React.FC> = ( props ) => { const { isSubmitting } = useFormState(); return (